跳转到内容

依赖注入 / 服务定位器


概览

Phalcon\Di\Di是一个存储服务或组件(类)的容器。这些服务在整个应用程序中可用,并简化了开发过程。假设我们正在开发一个名为InvoiceComponent的组件,用于为客户账单执行一些计算。它需要一个数据库连接以检索Invoice数据库中的记录。

我们的组件可以如下实现:

<?php

use Phalcon\Db\Adapter\Mysql;

class InvoiceComponent
{
    public function calculate()
    {
        $connection = new Mysql(
            [
                'host'     => 'localhost',
                'username' => 'root',
                'password' => 'secret',
                'dbname'   => 'tutorial',
            ]
        );

        $invoice = $connection->exec(
            'SELECT * FROM Invoices WHERE inv_id = 1'
        );

        // ...
    }
}

$invoice = new InvoiceComponent();
$invoice->calculate();

我们使用calculate方法获取数据。在方法内部,我们创建了一个带有设定凭据的新 MySQL 数据库连接,之后执行查询。虽然这确实是一个有效的实现,但这是不切实际的,并且将来会阻碍我们应用程序的维护,因为我们的连接参数或数据库类型是硬编码在组件中。如果将来我们需要更改这些内容,我们必须在此组件以及任何以相同方式设计的其他组件中进行修改。

<?php

use Phalcon\Db\Adapter\Mysql;

class InvoiceComponent
{
    private $connection;

    public function calculate()
    {
        $invoice = $this
            ->connection
            ->exec(
                'SELECT * FROM Invoices WHERE inv_id = 1'
            )
        ;

        // ...
    }

    public function setConnection(
        Mysql $connection
    ): InvoiceComponent {
        $this->connection = $connection;

        return $this;
    }
}

$connection = new Mysql(
    [
        'host'     => 'localhost',
        'username' => 'root',
        'password' => 'secret',
        'dbname'   => 'tutorial',
    ]
);

$invoice = new InvoiceComponent();
$invoice
    ->setConnection($connection)
    ->calculate()
;

为了提高灵活性,我们可以在组件外部创建数据库连接并在InvoiceComponent中通过 setter 设置。采用这种方法,我们可以注入任意需要它的组件的数据库连接,通过 setter 注入。同样,这也是一个完全有效的实现,但有一些缺点。例如,每次我们需要使用任何需要数据库连接的组件时,都必须构造一次数据库连接。

为集中管理此功能,我们可以实现一个全局注册模式并将连接对象存储在那里。然后我们就可以在需要的地方重复使用它。

<?php

use Phalcon\Db\Adapter\Mysql;

class Registry
{
    public static function getConnection(): Mysql
    {
        return new Mysql(
            [
                'host'     => 'localhost',
                'username' => 'root',
                'password' => 'secret',
                'dbname'   => 'tutorial',
            ]
        );
    }
}

class InvoiceComponent
{
    private $connection;

    public function calculate()
    {
        $invoice = $this
            ->connection
            ->exec(
                'SELECT * FROM Invoices WHERE inv_id = 1'
            )
        ;

        // ...
    }

    public function setConnection(
        Mysql $connection
    ): InvoiceComponent {
        $this->connection = $connection;

        return $this;
    }
}

$invoice = new InvoiceComponent();
$invoice
    ->setConnection(Registry::getConnection())
    ->calculate()
;

上述实现将在每次调用getConnection在Registry组件时创建一个新连接。解决这个问题,我们可以修改Registry类来存储数据库连接并重用它。

<?php

use Phalcon\Db\Adapter\Mysql;

class Registry
{
    protected static $connection;

    public static function getNewConnection(): Mysql
    {
        return self::createConnection();
    }

    public static function getSharedConnection(): Mysql
    {
        if (self::$connection === null) {
            self::$connection = self::createConnection();
        }

        return self::$connection;
    }

    protected static function createConnection(): Mysql
    {
        return new Mysql(
            [
                'host'     => 'localhost',
                'username' => 'root',
                'password' => 'secret',
                'dbname'   => 'tuturial',
            ]
        );
    }
}


class InvoiceComponent
{
    private $connection;

    public function calculate()
    {
        $invoice = $this
            ->connection
            ->exec(
                'SELECT * FROM Invoices WHERE inv_id = 1'
            )
        ;

        // ...
    }

    public function setConnection(
        Mysql $connection
    ): InvoiceComponent {
        $this->connection = $connection;

        return $this;
    }
}

$invoice = new InvoiceComponent();
$invoice
    ->setConnection(Registry::getSharedConnection())
    ->calculate()
;

$invoice = new InvoiceComponent();
$invoice
    ->setConnection(Registry::getNewConnection())
    ->calculate()
;

在上面的例子中,我们修改了Registry类,暴露了getNewConnection创建全新数据库连接的方法。它也暴露了getSharedConnection该方法将把连接存储在内部并为其所需的任何其他组件重新使用。

将依赖项注入到我们的组件中可以解决上述问题。将依赖项作为参数传递而不是在方法内部创建它们可以让我们的代码更具可维护性和解耦性。然而,从长远来看,这种形式的依赖注入也有一些缺点。例如,如果组件有很多依赖项,我们需要创建多个 setter 参数来传递这些依赖项,或者创建一个构造函数,将所有必需的依赖项作为参数传递。我们还需要在使用组件之前创建这些依赖项。这使得我们的代码不像我们希望的那样易于维护:

<?php

$connection = new Connection();
$fileSystem = new FileSystem();
$filter     = new Filter();
$selector   = new Selector();
$session    = new Session();

$invoice =  new InvoiceComponent(
    $connection, 
    $session, 
    $fileSystem, 
    $filter, 
    $selector
);

$invoice
    ->setConnection($connection)
    ->setFileSystem($fileSystem)
    ->setFilter($filter)
    ->setSelector($selector)
    ->setSession($session)
;

可维护性问题在这里产生了。如果我们必须在应用程序的许多地方创建这个对象,我们就需要执行相同的初始化操作,注入所有的依赖项。如果将来我们需要更改组件,使其需要额外的依赖项,我们必须去检查所有使用过该组件或其他组件的区域来调整我们的代码。为了解决这个问题,我们将使用全局注册类来创建该组件。但是,这种方法在创建对象之前又增加了一层抽象:

<?php

class InvoiceComponent
{
    private $connection;
    private $fileSystem;
    private $filter;
    private $selector;
    private $session;

    public function __construct(
        Connection $connection,
        FileSystem $fileSystem,
        Filter $filter,
        Selector $selector,
        Session $session

    ) {
        $this->connection = $connection;
        $this->fileSystem = $fileSystem;
        $this->filter     = $filter;
        $this->selector   = $selector;
        $this->session    = $session;
    }

    public static function factory()
    {
        $connection = new Connection();
        $fileSystem = new FileSystem();
        $filter     = new Filter();
        $selector   = new Selector();
        $session    = new Session();

        return new self(
            $connection, 
            $fileSystem, 
            $filter, 
            $selector,
            $session 
        );
    }
}

我们现在又回到了起点,在组件内部实例化依赖项。为了解决这个问题,我们将使用一个可以存储我们所有依赖项的容器。这是一种实用而优雅的方式。容器将作为我们前面探讨过的全局注册表。使用此容器作为桥接器来检索任何依赖项,使我们能够降低组件的复杂度:

<?php

use Phalcon\Db\Adapter\Mysql;
use Phalcon\Di\Di;
use Phalcon\Di\DiInterface;

class InvoiceComponent
{
    protected $container;

    public function __construct(
        DiInterface $container
    ) {
        $this->container = $container;
    }

    public function calculate()
    {
        $connection = $this
            ->container
            ->get('db')
        ;
    }

    public function view($id)
    {
        $filter = $this
            ->container
            ->get('filter')
        ;

        $id = $filter->sanitize($id, null, 'int');

        $connection = $this
            ->container
            ->getShared('db')
        ;
    }
}

$container = new Di();
$container->set(
    'db',
    function () {
        return new Mysql(
            [
                'host'     => 'localhost',
                'username' => 'root',
                'password' => 'secret',
                'dbname'   => 'tutorial',
            ]
        );
    }
);

$container->set(
    'filter',
    function () {
        return new Filter();
    }
);

$container->set(
    'session',
    function () {
        return new Session();
    }
);

$invoice =  new InvoiceComponent($container);
$invoice->calculate();

现在组件可以在需要时简单地访问它所需的依赖项。如果不需要某个依赖项,则不会初始化它,确保内存使用的最小化。我们的组件现在已经高度解耦。例如,如果我们以任何方式更改数据库连接,组件不会受到影响,而在维护方面,我们只需要在一个位置更改代码即可。

Phalcon\Di\Di是一个实现了依赖注入和服务定位器的组件。由于 Phalcon 高度解耦,Phalcon\Di\Di对于集成框架的不同组件至关重要。开发人员还可以使用此组件注入依赖项并管理应用程序中使用的不同类的全局实例。它还实现了控制反转模式。正因为如此,对象不是通过 setter 或构造函数接收其依赖项,而是请求一个服务依赖注入器。这减少了整体复杂性,因为在组件内只有一种方式获取所需的依赖项。

此外,这种模式增加了代码的可测试性,从而降低了错误发生的可能性。

方法

public function __call(
    string $method, 
    array $arguments = []
): mixed | null
魔术方法,用于通过setter/getter获取或设置服务。

public function attempt(
    string $name, 
    mixed definition, 
    bool shared = false
): ServiceInterface | bool
尝试在服务容器中注册一个服务。只有当之前未使用相同名称注册过服务时,该操作才会成功

public function get(
    string $name, 
    mixed parameters = null
): mixed
根据配置解析服务。

public static function getDefault(): DiInterface | null
返回最后创建的DI。

public function getInternalEventsManager(): ManagerInterface
返回内部事件管理器

public function getRaw(string $name): mixed
返回服务定义而不进行解析。

public function getService(string $name): ServiceInterface
返回一个Phalcon\Di\Service实例

public function getServices(): ServiceInterface[]
返回在DI中注册的服务。

public function getShared( 
    string $name, 
    mixed parameters = null
): mixed
返回一个共享的服务。首先解析该服务,然后将解析后的服务存储在DI中。后续对该服务的请求将返回相同的实例

public function loadFromPhp(string $filePath)
从php配置文件加载服务。

// /app/config/services.php
return [
     'myComponent' => [
         'className' => '\Acme\Components\MyComponent',
         'shared'    => true,
     ],
     'group'       => [
         'className' => '\Acme\Group',
         'arguments' => [
             [
                 'type'    => 'service',
                 'service' => 'myComponent',
             ],
         ],
     ],
     'user'        => [
         'className' => '\Acme\User',
     ],
];

$container->loadFromPhp("/app/config/services.php");

public function loadFromYaml(
    string $filePath, 
    array $callbacks = null
)
从yaml文件加载服务。

// /app/config/services.yml
myComponent:
    className: \Acme\Components\MyComponent
    shared: true

group:
    className: \Acme\Group
    arguments:
        - type: service
          name: myComponent

user:
   className: \Acme\User


$container->loadFromYaml(
    "/app/config/services.yaml",
    [
        "!approot" => function ($value) {
            return dirname(__DIR__) . $value;
        }
    ]
);

public function has(string $name): bool
检查DI是否包含某个名称的服务。

public function offsetGet(mixed $name): mixed
使用数组语法获取一个共享服务

var_dump($container["request"]);

public function offsetExists(mixed $name): bool
使用数组语法检查服务是否已注册。

public function offsetSet(mixed $name, mixed $definition)
允许使用数组语法注册共享服务。

$container["request"] = new \Phalcon\Http\Request();

public function offsetUnset(mixed $name)
使用数组语法从服务容器中移除服务。

public function register(ServiceProviderInterface $provider)
注册一个服务提供者

use Phalcon\Di\DiInterface;
use Phalcon\Di\ServiceProviderInterface;

class SomeServiceProvider implements ServiceProviderInterface
{
    public function register(DiInterface $container)
    {
        $container->setShared(
            'service',
            function () {
                // ...
            }
        );
    }
}

public function remove(string $name)
删除服务容器中的一个服务。它还会删除为此服务创建的任何共享实例

public static function reset()
重置内部默认的DI。

public function set(
    string $name, 
    mixed $definition, 
    bool $shared = false
): ServiceInterface
在服务容器中注册服务。

public static function setDefault(<DiInterface> container)
设置默认依赖注入容器

public function setInternalEventsManager(
    ManagerInterface $eventsManager
)
设置内部事件管理器。

public function setService(
    string $name, 
    ServiceInterface $rawDefinition
): ServiceInterface
使用原始Phalcon\Di\Service定义设置服务。

public function setShared(
    string $name, 
    mixed $definition
): ServiceInterface
注册一个始终共享在服务容器中注册服务

注册服务

框架本身或开发者可以注册服务。当组件A需要组件B(或其类的一个实例)才能运行时,它可以向容器请求组件B,而不是创建一个新的组件B实例。

这种方法具有以下优点:* 我们可以轻松地替换为由我们自己或第三方创建的组件。* 我们完全控制对象初始化过程,可以在将这些对象交付给组件之前按需设置它们。* 我们可以以结构化和统一的方式获取组件的全局实例。

可以使用多种类型的定义来注册服务。下面我们将探讨可以注册服务的不同方式:

字符串

此类型期望一个有效类的名称,如果该类已加载则返回指定类的对象,如果该类未加载,则会使用自动加载器实例化该类。这种类型的定义不允许为类构造函数或参数指定参数:

<?php

use Phalcon\Http\Request;

$container->set(
    'request',
    Request::class
);

类实例

此类型期望一个对象。由于该对象已经是对象,无需解析,可以说这并不是真正意义上的依赖注入,但是如果希望强制返回的依赖始终是同一个对象/值时,这种方式很有用:

<?php

use Phalcon\Http\Request;

$container->set(
    'request',
    new Request()
);

闭包

这种方法提供了更大的自由度来自定义构建依赖项,但是如果不完全更改依赖项的定义,就很难从外部更改某些参数:

<?php

use Phalcon\Db\Adapter\Pdo\Mysql;

$container->set(
    'db',
    function () {
        return new Mysql(
            [
                'host'     => 'localhost',
                'username' => 'root',
                'password' => 'secret',
                'dbname'   => 'tutorial',
            ]
        );
    }
);

通过向闭包的环境传递额外变量,可以克服一些限制:

<?php

use Phalcon\Config;
use Phalcon\Db\Adapter\Pdo\Mysql;

$config = new Config(
    [
        'host'     => 'localhost',
        'username' => 'user',
        'password' => 'pass',
        'dbname'   => 'tutorial',
    ]
);

$container->set(
    'db',
    function () use ($config) {
        return new Mysql(
            [
                'host'     => $config->host,
                'username' => $config->username,
                'password' => $config->password,
                'dbname'   => $config->name,
            ]
        );
    }
);

您也可以使用get()方法:

<?php

use Phalcon\Config\Config;
use Phalcon\Db\Adapter\Pdo\Mysql;

$container->set(
    'config',
    function () {
        return new Config(
            [
                'host'     => 'localhost',
                'username' => 'user',
                'password' => 'pass',
                'dbname'   => 'tutorial',
            ]
        );
    }
);

$container->set(
    'db',
    function () {
        $config = $this->get('config');

        return new Mysql(
            [
                'host'     => $config->host,
                'username' => $config->username,
                'password' => $config->password,
                'dbname'   => $config->name,
            ]
        );
    }
);

注意

$this可以在闭包内部使用

复杂注册

如果需要在不实例化/解析服务的情况下更改服务的定义,则需要使用数组语法定义服务。使用数组定义注册服务可能会稍微冗长一些:

<?php

use Phalcon\Annotations\Adapter\Apcu;

$container->set(
    'annotations',
    [
        'className' => Apcu::class,
        'arguments' => [
            [
                'type'  => 'parameter',
                'name'  => 'prefix',
                'value' => 'my-prefix',
            ],
            [
                'type'  => 'parameter',
                'name'  => 'lifetime',
                'value' => 3600,
            ],
        ],
    ]
);


$container->set(
    'annotations',
    function () {
        return new Apcu(
            [
                'prefix'   => 'my-prefix',
                'lifetime' => 3600,
            ]
        );
    }
);

上面两种服务注册产生的结果相同。然而,数组定义允许您在需要时更改服务参数:

<?php

use Phalcon\Annotations\Adapter\Memory;

$container
    ->getService('annotations')
    ->setClassName(Memory::class)
;

$container
    ->getService('annotations')
    ->setParameter(
        1,
        [
            'type'  => 'parameter',
            'name'  => 'lifetime',
            'value' => 7200,
        ]
    );

注入

此外,使用数组语法还可以使用三种类型的依赖注入:

构造函数注入

这种注入类型将依赖项/参数传递给类构造函数。假设我们有以下组件:

<?php

namespace MyApp\Http;

use Phalcon\Http\Response;

class Responder
{
    /**
     * @var Response
     */
    protected $response;

    /**
     * @var string
     */
    protected $contentType;

    public function __construct(Response $response, string $contentType)
    {
        $this->response    = $response;
        $this->contentType = $contentType;
    }
}

服务可以如下注册:

<?php

use MyApp\Http\Responder;
use Phalcon\Http\Response;

$container->set(
    'response',
    [
        'className' => Response::class
    ]
);

$container->set(
    'my-responder',
    [
        'className' => Responder::class,
        'arguments' => [
            [
                'type' => 'service',
                'name' => 'response',
            ],
            [
                'type'  => 'parameter',
                'value' => 'application/json',
            ],
        ]
    ]
);

该服务response (Phalcon\Http\Response被解析后作为构造函数的第一个参数传入,而第二个是一个string直接传递的值。

设值方法注入

类可能有设值方法来注入可选依赖项,我们前面的类可以通过设值方法接受依赖项:

<?php

namespace MyApp\Http;

use Phalcon\Http\Response;

class Responder
{
    /**
     * @var Response
     */
    protected $response;

    /**
     * @var string
     */
    protected $contentType;

    public function setResponse(Response $response)
    {
        $this->response = $response;
    }

    public function setContentType($contentType)
    {
        $this->contentType = $contentType;
    }
}

上述类可以使用getter和setter注册为服务:

<?php

use MyApp\Http\Responder;
use Phalcon\Http\Response;

$container->set(
    'response',
    [
        'className' => Response::class,
    ]
);

$container->set(
    'my-responder',
    [
        'className' => Responder::class,
        'calls'     => [
            [
                'method'    => 'setResponse',
                'arguments' => [
                    [
                        'type' => 'service',
                        'name' => 'response',
                    ]
                ]
            ],
            [
                'method'    => 'setContentType',
                'arguments' => [
                    [
                        'type'  => 'parameter',
                        'value' => 'application/json',
                    ]
                ]
            ]
        ]
    ]
);

属性注入

一种不太常见的策略是将依赖项或参数直接注入到类的公共属性中:

<?php

namespace MyApp\Http;

use Phalcon\Http\Response;

class Responder
{
    /**
     * @var Response
     */
    public $response;

    /**
     * @var string
     */
    public $contentType;
}

具有属性注入的服务可以如下注册:

<?php

use MyApp\Http\Responder;
use Phalcon\Http\Response;

$container->set(
    'response',
    [
        'className' => Response::class,
    ]
);

$container->set(
    'my-responder',
    [
        'className'  => Responder::class,
        'properties' => [
            [
                'name'  => 'response',
                'value' => [
                    'type' => 'service',
                    'name' => 'response',
                ],
            ],
            [
                'name'  => 'contentType',
                'value' => [
                    'type'  => 'parameter',
                    'value' => 'application/json',
                ],
            ]
        ]
    ]
);

支持的参数类型包括以下内容:

类型 描述 示例
instance 表示必须动态构建的对象 ['type' => 'instance', 'className' => \DateTime::class, 'arguments' => ['now']]
parameter 表示作为参数传递的字面值 ['type' => 'parameter', 'value' => 1234]
service 表示服务容器中的另一个服务 ['type' => 'service', 'name' => 'request']

解析定义复杂的服务可能比前面提到的简单定义稍慢一些。然而,这些方式提供了更健壮的方法来定义和注入服务。允许多种定义类型混合使用,您可以根据应用程序需求决定哪种方式最适合您注册服务。

数组语法

数组语法也可用于注册服务:

<?php

use Phalcon\Di\Di;
use Phalcon\Http\Request;

$container = new Di();

$container['request'] = Request::class;

$container['request'] = function () {
    return new Request();
};

$container['request'] = new Request();

$container['request'] = [
    'className' => Request::class,
];

在上面的例子中,当框架需要访问请求数据时,它会在容器中请求标识为request的服务。容器将返回所需服务的一个实例。如有需要,该组件可以轻松替换为不同的类。

如上例所示,每种设置/注册服务的方式都有其优缺点。具体选择哪一种取决于开发人员和特定需求。通过字符串设置服务很简单,但缺乏灵活性。使用数组设置服务提供了更多的灵活性,但会使代码更复杂。Lambda函数在这两者之间取得了良好的平衡,但可能会带来比预期更多的维护工作。

注意

Phalcon\Di\Di为其存储的每个服务提供延迟加载功能。除非开发人员选择直接实例化一个对象并将其存储在容器中,否则通过数组、字符串等方式存储的任何对象都将被延迟加载,即仅在请求时才进行实例化。

从配置中加载

YAML

此功能将通过解析一个YAML文件来加载服务:

; /app/config/services.yml

config:
  className: \Phalcon\Config\Config
  shared: true
<?php

use Phalcon\Di\Di;

$container = new Di();
$container->loadFromYaml('services.yml');
$container->get('/app/config/services.yml');

注意

此方法要求安装了Yaml模块。请参考此文档以获取更多信息。

PHP

您也可以使用PHP数组加载服务:

// /app/config/services.php

use Phalcon\Config\Config;

return [
    'config' => [
        'className' => Config::class,
        'shared'    => true,
    ],
];
<?php

use Phalcon\Di\Di;

$container = new Di();
$container->loadFromPhp('/app/config/services.php');
$container->get('config');

解析服务

从容器中获取服务只需简单调用“get”方法即可。每次都会返回该服务的一个新实例:

$request = $container->get('request');

或者通过魔术方法调用:

$request = $container->getRequest();

或使用数组访问语法:

$request = $container['request'];

可以通过向“get”方法添加一个数组参数,将参数传递给构造函数:

<?php

use Phalcon\Annotations\Adapter\Stream;

$annotations = $container->get(
    Stream::class,
    [
        ['annotationsDir' => 'storage/cache/annotations'],
    ]
);

事件

Phalcon\Di\Di能够将事件发送到EventsManager如果其存在的话。事件使用类型di.

事件名称 触发时机
afterServiceResolve 在解析服务后触发。监听器接收服务名称、实例以及传递给它的参数。
beforeServiceResolve 在解析服务前触发。监听器接收服务名称和传递给它的参数。

共享服务

服务可以被注册为shared服务,这意味着它们始终会表现为 [单例][singletons]。一旦服务第一次被解析,每次从容器中检索该服务时都会返回相同的实例:

<?php

use Phalcon\Session\Manager;
use Phalcon\Session\Adapter\Stream;

$container->setShared(
    'session',
    function () {
        $session = new Manager();
        $files = new Stream(
            [
                'savePath' => '/tmp',
            ]
        );
        $session->setAdapter($files);
        $session->start();

        return $session;
    }
);

$session = $container->get('session');

$session = $container->getSession();

第一次调用get在容器中解析服务并返回对象。随后的调用getSession将返回同一个对象。

注册共享服务的另一种方式是传递true作为set:

<?php

use Phalcon\Session\Manager;
use Phalcon\Session\Adapter\Stream;

$container->set(
    'session',
    function () {
        $session = new Manager();
        $files = new Stream(
            [
                'savePath' => '/tmp',
            ]
        );
        $session->setAdapter($files);
        $session->start();

        return $session;
    },
    true
);

$session = $container->get('session');

$session = $container->getSession();

注意

如果某个服务未被注册为共享服务,并且您希望确保每次从DI中检索该服务时都获取共享实例,您可以使用getShared方法可以实现反向操作。

$request = $container->getShared('request');

操作服务

一旦服务在服务容器中注册,您可以单独获取它以便进行操作:

<?php

use Phalcon\Http\Request;

$container->set('request', 'Phalcon\Http\Request');

$requestService = $container->getService('request');

$requestService->setDefinition(
    function () {
        return new Request();
    }
);

$requestService->setShared(true);

$request = $requestService->resolve();

实例化类

当你请求容器中的一个服务时,如果无法通过相同名称找到它,它会尝试加载一个同名的类。此行为允许您通过简单地注册一个同名服务来替换任意服务:

<?php

$container->set(
    'IndexController',
    function () {
        return new Component();
    },
    true
);

$container->set(
    'IndexController',
    function () {
        return new AnotherComponent();
    }
);

$component = $container->get('IndexController');
在上面的例子中我们替换超全局变量。如果cookie存在于任一集合中,则返回IndexController为您选择的其他组件。此外,您可以调整代码始终使用服务容器实例化您的类,即使它们未被注册为服务。容器会回退到您定义的自动加载器来加载类本身。通过使用此技术,您可以在将来通过实现不同的定义来替换任意类。

自动注入

如果某个类或组件需要DI本身来定位服务,则DI可以自动将自身注入它创建的实例中。要利用此功能,您所需要做的是在您的类中实现Phalcon\Di\InjectionAwareInterface接口:

<?php

use Phalcon\Di\DiInterface;
use Phalcon\Di\InjectionAwareInterface;

class InvoiceComponent implements InjectionAwareInterface
{
    /**
     * @var DiInterface
     */
    protected $container;

    public function setDi(DiInterface $container)
    {
        $this->container = $container;
    }

    public function getDi(): DiInterface
    {
        return $this->container;
    }
}

然后,一旦服务被解析,$containerDIsetDi()将会被自动传入:

<?php

$container->set('inv-component', InvoiceComponent::class);

$invoiceComponent = $container->get('inv-component');

注意

$invoiceComponent->setDi($container)会自动调用

为了方便起见,您还可以继承Phalcon\Di\AbstractInjectionAware类,它包含了上述代码并暴露了受保护的$container属性供您使用。

<?php

use Phalcon\Di\DiInterface;
use Phalcon\Di\AbstractInjectionAware;

class InvoiceComponent extends AbstractInjectionAware
{

}

将服务组织在文件中

通过将服务注册移动到单独的文件中而不是在应用程序引导程序中注册所有内容,您可以更好地组织您的应用程序:

<?php

$container->set(
    'router',
    function () {
        return include '/app/config/routes.php';
    }
);

然后在文件('/app/config/routes.php')中返回解析后的对象:

<?php

use Phalcon\Mvc\Router();

$router = new Router(false);

$router->post('/login');

return $router;

静态访问

The Phalcon\Di\Di提供了便捷的getDefault()静态方法,返回最近创建的容器。这允许您甚至从静态类中访问容器:

<?php

use Phalcon\Di\Di;

class InvoicesComponent
{
    public static function calculate()
    {
        $connection = Di::getDefault()->getDb();
    }
}

服务提供者

另一种注册服务的方法是将每个服务放在自己的文件中,并通过简单的循环依次注册所有服务。每个文件将包含一个类或Provider实现Phalcon\Di\ServiceProviderInterface接口。您可能想这么做的原因是获得更小的文件,每个文件处理一个服务注册,从而提供极大的灵活性、简短的代码,最终您可以随时添加/删除服务,而无需翻阅像引导程序那样的大文件。

示例

app/config/providers.php

<?php

return [
    MyApp\Providers\ConfigProvider::class,
    MyApp\Providers\RegistryProvider::class,
    MyApp\Providers\LoggerProvider::class,
];    

app/library/Providers/ConfigProvider.php

<?php

namespace MyApp\Providers;

use Phalcon\Config\Config;
use Phalcon\Di\ServiceProviderInterface;
use Phalcon\Di\DiInterface;

class ConfigProvider implements ServiceProviderInterface
{
    /**
     * @param DiInterface $container
     */
    public function register(DiInterface $container)
    {
        $container->setShared(
            'config',
            function () {
                $data = require 'app/config/config.php';

                return new Config($data);
            }
        );
    }
}

app/library/Providers/RegistryProvider.php

<?php

namespace MyApp\Providers;

use Phalcon\Config\Config;
use Phalcon\Di\ServiceProviderInterface;
use Phalcon\Di\DiInterface;
use Phalcon\Registry;

use function microtime;

class RegistryProvider implements ServiceProviderInterface
{
    /**
     * {@inheritdoc}
     *
     * @param DiInterface $container
     */
    public function register(DiInterface $container)
    {
        /** @var Config $config */
        $config  = $container->getShared('config');
        $devMode = $config->path('app.devMode', false);

        $container->setShared(
            'registry',
            function () use ($devMode) {
                $registry = new Registry();
                $registry->offsetSet('devMode', $devMode);
                $registry->offsetSet('execution', microtime(true));

                return $registry;
            }
        );
    }
}

app/library/Providers/LoggerProvider.php```php <?php

namespace MyApp\Providers;

use Phalcon\Di\DiInterface; use Phalcon\Di\ServiceProviderInterface; use Phalcon\Logger\Logger; use Phalcon\Logger\Adapter\Stream;

class LoggerProvider implements ServiceProviderInterface { use LoggerTrait;

/**
 * @param DiInterface $container
 *
 * @throws \Exception
 */
public function register(DiInterface $container)
{
    $container->setShared(
        'logger', 
        function () {
            $adapter = new Stream('/storage/logs/main.log');

            return new Logger(
                'messages',
                [
                    'main' => $adapter,
                ]
            );
        }
    );
}

}

Now we can register all the services with a simple loop:

```php
<?php

use Phalcon\Di\Di;

$services = include('app/config/providers.php');

$container = new Di();

foreach ($services as $service) {
    $container->register(new $service());
}

默认工厂

为了开发者的便利,Phalcon\Di\FactoryDefault已经提供了几个预设服务供您使用。没有什么阻止您逐个注册应用程序所需的所有服务。不过,您可以使用Phalcon\Di\FactoryDefault,它包含一组准备就绪的服务。注册的服务列表允许您拥有适合全栈应用的容器。

注意

由于服务总是延迟加载的,实例化Phalcon\Di\FactoryDefault容器不会比Phalcon\Di\Di容器消耗更多内存。

<?php

use Phalcon\Di\FactoryDefault;

$container = new FactoryDefault();

在Phalcon\Di\FactoryDefault是:

名称 对象 共享 描述
annotations Phalcon\Annotations\Adapter\Memory 是 注解解析器
assets [Phalcon\Assets\Manager][assets] 是 资源管理器
crypt Phalcon\Encryption\Crypt 是 加密/解密
cookies Phalcon\Http\Response\Cookies 是 HTTP Cookie 管理器
dispatcher Phalcon\Mvc\Dispatcher 是 分派器
escaper Phalcon\Html\Escaper 是 转义器
eventsManager Phalcon\Events\Manager 是 事件管理器
flash Phalcon\Flash\Direct 是 闪现消息
flashSession Phalcon\Flash\Session 是 会话闪现消息
filter Phalcon\Filter\Filter 是 过滤 / 清理
helper Phalcon\Support\HelperFactory 是 字符串、数组等辅助工具
modelsManager Phalcon\Mvc\Model\Manager 是 模型管理
modelsMetadata Phalcon\Mvc\Model\MetaData\Memory 否 模型元数据
request Phalcon\Http\Request 是 HTTP 请求
response Phalcon\Http\Response 是 HTTP 响应
router Phalcon\Mvc\Router 是 路由器
security Phalcon\Security 是 安全
tag Phalcon\Html\TagFactory 是 HTML 标签辅助工具
transactionManager Phalcon\Mvc\Model\Transaction\Manager 是 数据库事务管理器
url Phalcon\Mvc\Url 是 URL 生成

如果某些组件已被注册(例如数据库连接),它们将在内部以下列名称使用:

名称 对象 共享 描述
db Phalcon\Db 是 数据库连接
modelsCache 模型缓存后端
session 会话服务
sessionBag [Phalcon\Session\Bag][session-bag] 是 会话Bag服务

上述名称在整个框架中都会被使用。例如,db服务会在transactionManager服务内部使用。你可以通过以与上述名称相同的名称注册自己的组件来替换这些组件。

异常

在DI容器中抛出的任何异常都将是Phalcon\Di\Exception或或 Phalcon\Di\ServiceResolutionException。你可以使用此异常选择性地捕获仅从此组件抛出的异常。

<?php

use Phalcon\Di\Di;
use Phalcon\Di\Exception;

try {
    $container = new Di();
    $component = $container->get('unknown-service');
} catch (Exception $ex) {
    echo $ex->getMessage();
}

自定义

The 要创建自己的DI来替换Phalcon提供的DI或者扩展当前的DI,必须实现接口。你还可以使用Phalcon\Di\ServiceInterface来创建你自己对服务的实现以及它们在DI容器中的解析方式。