Simpla 框架的容器 IoC,实现了依赖注入

v1.3.0 2018-03-22 16:58 UTC

This package is not auto-updated.

Last update: 2024-09-29 05:20:59 UTC


README

服务容器

服务容器的概念本质上是与对象和面向对象编程(OOP)相关联的。实际上,每个服务都可以是一个对象,但并不是每个对象都是一个服务。在现代化的应用程序中,我们有各种对象或对象集合,每个对象都有其特定的功能。因此,我们有帮助我们发送电子邮件、访问和注册数据库信息等的对象。那么,什么是服务呢?

服务

Symfony 的文档对服务进行了相当清晰的定义

服务是任何执行某种类型“全局”任务的 PHP 对象。这是一个在计算机科学中使用的通用术语,用于描述为特定目的而创建的对象(例如,发送电子邮件)。每个服务在其应用中的任何地方都需要该特定功能时都会使用。您不需要做任何事情来构建一个服务:只需编写一个执行特定任务的 PHP 类即可。恭喜,您刚刚创建了一个服务!

这个概念存在于面向服务的架构中,其中每个“服务”都可以轻松访问和使用,而不会干扰其他服务或应用程序的运行。

服务容器

服务容器是一个负责管理和实例化服务的 PHP 对象(对象或元素)。

多个框架,如 Laravel、Zend、Symfony、Silex 和 Slim 也实现了服务容器。

Simpla 使用由 Symfony 开发和维护的 Pimple 作为其基础,它是 Silex 使用的,并使用了实现 PSR11 的版本。

为服务容器构建项目结构

在此示例中,我们将创建一个用于使用服务容器的标准结构。这是 Simpla 框架使用的相同结构,但我们将关注下面的示例。

  • 以下定义了“Core”位置,其中将存储所有创建的服务。

  • 在“Http/Controllers”中,我们将有一个可以访问该服务的类。

  • 在“Providers”中,我们将有服务提供者。

  • 在“bootstrap”中,我们将有一个服务自动初始化器。

      /__app
      |    |__Core
      |    |   |__ [Services.php]
      |    |__Http
      |    |   |__Controllers
      |    |       |__ [UsingServices.php]
      |    |__Providers
      |
      |__bootstrap
      |    |__providers.php
      |
      |__index.php
    

创建面向接口的服务

服务不过是一个对象,对象由一个类定义。然而,在我们构建一个类之前,我们可以为对象定义一个接口。

在这个意义上,我们将定义一个简单的计算器服务如下

    namespace App\Classes;

    interface CalculatorInterface
    {
        public function sum($a, $b);
        public function subtract($a, $b);    
        public function multiply($a, $b);    
        public function divide($a, $b);    
    }

接口就像对外部环境提供的必需行为合同,其中声明了公共方法和常量,此外,接口在面向对象应用程序的解耦中扮演着重要的角色。

接口定义了必须由实现它的类以强制方式实现的哪些方法。

这种做法的好处是代码耦合度更低。当应用程序需要执行求和计算时,它不会成为“CalculadoraDoFulano”的奴隶,我们可以用“CalculadorMelhorDoCiclano”替换它,而无需在整个应用程序中更换“CalculadoraDoFulano”中求和的实现,因为“sum”被实现为sum($a, $b = null),而“CalculadorMelhorDoCiclano”实现了add($a, $b)

因此,让我们实现接口

    namespace App\Classes;

    class Calculator implements CalculatorInterface
    {
        public function sum($a, $b)
        {
            return $a + $b;
        }

        public function subtract($a, $b)
        {
            return $a - $b;
        }

        public function multiply($a, $b)
        {
            return $a * $b;
        }

        public function divide($a, $b)
        {
            return $a / $b;
        }
    }

将服务添加到容器中

一旦定义了服务,我们就可以简单地使用$calc = new Calculator()来使用它。然而,有时从应用程序管理的角度来看,这可能并不总是可行的。应用程序中存在多少个实例化的对象?我们应该定义服务为单例(Singleton)吗?

为了简化对象管理,我们可以使用以下选项来使用服务容器

Make

让我们在index.php文件中加载Simpla容器并“注入”Pimple容器,如下所示

    use Simpla\Container\Container;
    use Xtreamwayz\Pimple\Container as Pimple;


    /* @var $app Simpla\Container\Container */
    $app = Container::instance(new Pimple);

容器是一个单例,它确保在整个应用程序中只有一个实例,这样我们就保证了整个应用程序中只有一个控制器。

可选地,我们可以将$app定义为一个全局对象,以便在整个应用程序中访问

    global $app;

我们可以使用make方法添加一个服务

    $app->make("calc", function(){
         return new \App\Core\Calculator();
    });

如果由于某种原因对象已经被定义,我们可以简单地包含它,而不是闭包

    $calc = new \App\Classes\Calculator();
    $app->make("calc", $calc);

    // ou diretamente

    $app->make("calc", new \App\Classes\Calculator());

我们也可以通过类签名信息添加一个服务

    $app->make("calc", \App\Classes\Calculator::class);

如果我们不想为服务定义一个名称,我们只需简单地提供类签名为唯一参数。

    $app->make(\App\Classes\Calculator::class);

这种选项的缺点是需要提供\App\Classes\Calculator::class作为服务名称,这可能会有些繁琐。

数组

通过实现ArrayAccess接口,Simpla容器允许我们以添加数组值的方式添加服务

    $app['calc1'] = new \App\Classes\Calculator();

    // Ou ainda

    $app['calc2'] = function(){
        return new \App\Classes\Calculator();    
    };

单例

使用singleton方法确保一个类只有一个实例(对象)存在。

    $app->singleton("calc", function(){
        return new Calculator();
    });

    $calc1 = $app["calc"];
    $calc2 = $app["calc"];
    $calc3 = $app["calc"];

    // $calc1 = $calc2 = $calc3

Pimple的一个特点,适用于这种情况的是将服务作为数组添加。

    $app['calc'] = new \App\Classes\Calculator();

    $calc1 = $app["calc"];
    $calc2 = $app["calc"];
    $calc3 = $app["calc"];

    // $calc1 = $calc2 = $calc3

恢复服务

服务可以通过get方法检索。

    $calc = $app->get("calc");
    $calc=>sum(34,54);
    
    //Usando diretamente

    $app->get("calc")->subtract(32,12);

我们也可以像访问数组一样检索一个服务

    $calc = $app['calc'];
    $calc=>sum(34,54);
    

向容器添加数据

容器还可以包含任何其他类型的非对象信息。这对于包含可以在整个应用程序中使用的全局信息非常有用。

    $app['tz.br.spo'] = "America/Sao_Paulo";
    $app['tz'] = new DateTimeZone($app['tz.br.spo']);

    $app->make("today", new DateTime("now", $app['tz']));

调用服务(依赖注入)

call方法允许使用“service@method”的语法调用预定义服务的某个方法,传递一个包含所有参数的数组。

    $show = $app->call("today@format", ['d/m/Y H:i:s']); // 02/10/2017 21:11:25
    

这样,我们就向服务的方法中注入了一个依赖。

添加接口

我们可以使用接口名称来定义一个服务。

    $app->make(App\Classes\CalculatorInterface::class, \App\Classes\Calculator::class);

    var_dump($app->get());

    $calcs = $app[App\Classes\CalculatorInterface::class];

为了简化,我们可以为该接口建立一个标签,就像一个名称一样。

    $app->tagged("ICalc", App\Classes\CalculatorInterface::class);

    $app->make("ICalc", \App\Classes\Calculator::class);

    // também podemos definir como array ou singleton

    $app["ICalc"] = new \App\Classes\Calculator;
    
    $app->singleton("ICalc", \App\Classes\Calculator::class);

添加闭包

我们可以在容器中添加闭包作为服务。

 
    $app["sum"] = $app->closure(function ($a, $b) {
        return $a + $b;
    });

    $rand = $app["sum"];
    
    var_dump($rand(4,12)); // 16

获取服务创建函数

当你访问一个对象时,Pimple会自动调用你定义的匿名函数,为你创建服务对象。如果你想获得对这个函数的原始访问权限,你可以使用raw()方法。

 
    $calc = $app->raw('calc');

    var_dump($calc()->sum(5,65)); // 70

这样,$calc就获得了包含服务实现的一个匿名函数。当我们像函数一样调用$calc时,服务就被创建了(创建了对象)。

定义后修改服务

在某些情况下,你可能想在服务定义之后修改服务的定义。你可以使用extend方法来定义在服务创建后要执行的额外代码。

    class Car
    {
        private $placa;
        private $ano; 

        function getPlaca()
        {
            return $this->placa;
        }

        function getAno()
        {
            return $this->ano;
        } 

        function setPlaca($placa)
        {
            $this->placa = $placa;
        }

        function setAno($ano)
        {
            $this->ano = $ano;
        }  
    }

    $app['car'] = function(){
        return new Car();
    };

    $app['car'] = $app->extend('car', function($car){
        $car->setPlaca("HNT-2299");
        $car->setAno(2010);

        return $car;
    });


    var_dump($app['car']);

    /*
        object(Car)#37 (2) {
            ["placa":"Car":private]=>
                string(8) "HNT-2299"
            ["ano":"Car":private]=>
                int(2010)
        }
    */

第一个参数是要扩展的服务名称,第二个参数是一个函数,该函数可以访问对象实例和容器。

服务提供者

在直接翻译中,Service provider是指服务提供商,也就是说:提供商:提供某物的人或机构。 服务:提供帮助或支持的行为或结果。服务提供商:提供一项服务的人或机构。

Service provider补充了容器(Container)的使用,因为它充当服务启动器,为服务提供所有必需的模块,以便正确启动。在面向对象(Orientação a Objetos)的上下文中,这就像我设置了类构造器启动所需的全部参数。

namespace App\Providers;
 
use Simpla\Contracts\ServiceProviderInterface;
use Simpla\Contracts\ContainerInterface;

class TodayServiceProvider implements ServiceProviderInterface
{ 
		/**
		 * Register the services 
		 *
		 * @return void
		 */	
    	public function register(ContainerInterface $serviceContainer)
    	{ 
    		$tz = new \DateTimeZone("America/Sao_Paulo");
    		
        	$serviceContainer->make("today", function(){
	        	   return  new DateTime("now");
        	});         
    	} 
}
 

Register 方法

请注意,我们的Service Provider被命名为TodayServiceProvider,并有一个注册(register)方法,其中我们定义了我们的服务。

为了定义服务,我们使用定义了接口ContainerInterface的容器作为参数,这样我们就可以使用容器并注册服务。

为了更好地理解,我们将在我们的结构中添加一个提供者。

    /__app
    |    |__Core
    |    |   |__ Calculator.php
    |    |__Http
    |    |   |__Controllers
    |    |       |__ Home.php
    |    |__Providers
    |           |__ CalculatorServiceProvider.php
    |__bootstrap
    |    |__providers.php
    |    |__bootstrap.php
    |
    |__index.php

Simpla中的service provider的工作方式与Laravel非常相似。

以下是一个服务提供商的另一个示例:

// CalculatorServiceProvider.php
namespace App\Providers;
 
use Simpla\Contracts\ServiceProviderInterface;
use Simpla\Contracts\ContainerInterface;

class CalculatorServiceProvider implements ServiceProviderInterface
{ 
		/**
		 * Register the services 
		 *
		 * @return void
		 */	
    	public function register(ContainerInterface $serviceContainer)
    	{ 
        	$serviceContainer->make("calc", function(){
	        	   return  new \App\Classes\Calculator();
        	});         
    	} 
}
 

该提供者已被创建,但只有在将其添加到我们的容器中时才能使用。为此,我们使用容器的register方法。

	$app->register(new \App\Providers\CalculatorServiceProvider());
	
	$calc = $app['calc'];
	
	$calc->sum(13.5,432.3) // 445.8

如果我们需要将所有服务提供商添加到容器中,这可能有些费时,但当我们使用Bootstrap时,这种方法的实用性会变得非常明显。

Boot 方法

我们还可以在Service Provider中添加一个boot方法。该方法在所有服务注册后调用,允许访问在此之前启动的所有服务。

boot方法应用于启动服务。在这种情况下,我们可以与服务交互,初始化其依赖项以及应在服务创建/调用之前执行的事件。

// CalculatorServiceProvider.php
namespace App\Providers;
 
use Simpla\Contracts\ServiceProviderInterface;
use Simpla\Contracts\ContainerInterface;

class TestServiceProvider implements ServiceProviderInterface
{ 
		/**
		 * Bootstrap the services 
		 *
		 * @return void
		 */	
    	public function boot(ContainerInterface $app)
    	{
    		echo "Isso é uma mensagem de inicialização";
    		
    		//Imprimindo serviços disponíveis
    		print_r($app->get());
    	}
		/**
		 * Register the services 
		 *
		 * @return void
		 */	
    	public function register(ContainerInterface $serviceContainer)
    	{ 
        	// code    
    	} 
}

Bootstrap

为了更自动化地创建我们的服务,我们可以使用一个初始化器。这样,我们可以使用包含我们希望调用的所有提供者的文件。

    /__app
    |    |__Core
    |    |   |__ [Services.php]
    |    |__Http
    |    |   |__Controllers
    |    |       |__ [UsingServices.php]
    |    |__Providers
    |
    |__bootstrap
    |    |__providers.php
    |    |__bootstrap.php
    |
    |__index.php

采用上述结构,我们可以在providers.php文件中的数组中定义我们希望调用的所有服务。

<?php

    $providers = [
    			App\Providers\CalculatorServiceProvider::class,
                       App\Providers\CarServiceProvider::class,
                       App\Providers\HelloServiceProvider::class
                   ];
                   
	 $aliases = [
	     'calc' => App\Facades\CalculatorFacade::class,
	     'hello' => App\Facades\HelloFacade::class,
	     'car' => \App\Facades\CarServiceProvider::class
	 ];

我们还应在同一文件中添加每个提供者的“别名”,这允许使用**门面**(如果存在)。如果不存在门面,我们应在“别名”中添加类或提供者的签名,如上例中的\App\Facades\CarServiceProvider::class

有关门面的更多信息。

我们还可以在bootstrap.php文件中定义我们的容器。

	require __DIR__.'/../bootstrap/providers.php';

	use Simpla\Container\Container;
	use Xtreamwayz\Pimple\Container as Pimple;
	 
	global $app;

	/* @var $app Simpla\Container\Container */
	$app = Container::instance(new Pimple);

	$app->createAlias($aliases);
	$app->registerProviders($providers); 

我们可以在index.php中加载我们的bootstrap.php文件并使用所有可用的服务。

Defer 选项

defer(延迟)选项使Service Provider仅在需要时注册。也就是说,容器仅在需要时注册服务。

这个选项非常有用,因为它可以防止在应用程序中自动加载不必要的服务。

您可以使用容器的getDeferredServices()命令获取“延迟”服务的列表。

为了定义将延迟加载的服务,我们必须将$defer属性设置为true,并在provider方法中返回一个包含服务名称的数组。

class CalculatorServiceProvider implements ServiceProviderInterface
{
	    protected $defer = true;
	  
	    public function register(ContainerInterface $serviceContainer)
	    {
			$calc = new \App\Classes\Calculator();

			$serviceContainer->make("calc", $calc);
			$serviceContainer->make("calcSum",  
				$serviceContainer->closure(function ($a, $b) {
				    return $a + $b;
				})
			);
	    }

	    public static function providers()
	    {
			return ['calc','callSum'];
	    }
}

外观

Simpla的Facades与Laravel框架的Facades非常相似。

在这个上下文中,Facades允许我们以简化的方式访问服务,就像它们是具有静态方法的类一样。这并不意味着这些服务实际上是静态的,门面创建了一种新的访问服务的方法,一种“门面”。

外观如何工作

为了使Facades起作用,我们需要定义并添加它们到我们的启动文件。

<?php

    $providers = [
   			App\Providers\CalculatorServiceProvider::class,
                       App\Providers\CarServiceProvider::class,
                       App\Providers\HelloServiceProvider::class
                   ];
                   
	 $aliases = [
	     'calc' => App\Facades\CalculatorFacade::class,
	     'hello' => App\Facades\HelloFacade::class,
	     'car' => \App\Facades\CarServiceProvider::class
	 ];

$aliases变量负责存储我们的门面的别名。

对于每个服务,我们都需要创建一个外观文件,在我们的结构中,它被添加到了 App\Facades,如下例所示:

// CalculatorFacade.php

namespace App\Facades;                  
 
class CalculatorFacade extends Simpla\Container\Facade
{
    public static function createService()
    {
        return 'calc';
    }
}

这样我们就可以像这样调用服务 Calculator

	use App\Facades\CalculatorFacade;

	calc::sum(43,21); // 63

createService() 方法中定义的名称和在 bootstrap 文件中定义的名称必须一致,否则将抛出 NotFoundException