为Yii1应用程序提供依赖注入支持

1.0.4 2023-10-13 15:44 UTC

This package is auto-updated.

Last update: 2024-09-13 18:21:12 UTC


README

Yii1依赖注入扩展


本扩展为Yii1应用程序提供依赖注入支持。

有关许可证信息,请检查LICENSE文件。

Latest Stable Version Total Downloads Build Status

安装

安装此扩展的首选方式是通过composer

运行以下命令:

php composer.phar require --prefer-dist yii1tech/di

或添加以下内容到您的composer.json文件中的"require"部分:

"yii1tech/di": "*"

使用方法

此扩展使用兼容PSR-11的容器为Yii1应用程序提供依赖注入支持。

此扩展引入了一个静态外观类\yii1tech\di\DI,它提供了对PSR容器的全局访问和注入器。本扩展提供的所有依赖注入功能都依赖于此外观类。它提供了一个容器实体检索和依赖注入的简单方法。例如

<?php

use yii1tech\di\DI;

class Foo
{
    /**
     * @var CDbConnection
     */
    public $db;
    /**
     * @var string 
     */
    public $name = 'default';
    
    public function __construct(CDbConnection $db)
    {
        $this->db = $db;
    }
    
    public function format(CFormatter $formatter, string $value): string
    {
        return $formatter->formatDate($value);
    }
}

$psrContainer = DI::container(); // retrieve related PSR compatible container
var_dump($psrContainer instanceof \Psr\Container\ContainerInterface); // outputs `true`

$db = DI::get(CDbConnection::class); // retrieve entity from PSR compatible container

$object = DI::make(Foo::class); // instantiates object, resolving constructor arguments from PSR compatible container based on type-hints
var_dump($object->db instanceof CDbConnection); // outputs `true`

$date = DI::invoke([$object, 'format'], ['value' => time()]); // invokes given callback, resolving its arguments from PSR compatible container based on type-hints
var_dump($date); // outputs '2023/07/28'

$object = DI::create([ // instantiates object from Yii-style configuration, resolving constructor arguments from PSR compatible container based on type-hints
    'class' => Foo::class,
    'name' => 'custom',
]);
var_dump($object->db instanceof CDbConnection); // outputs `true`
var_dump($object->name); // outputs `custom`

容器设置

实际的依赖注入容器应通过\yii1tech\di\DI::setContainer()方法设置。这应该在创建应用程序之前在Yii引导阶段完成。例如

<?php
// file '/public/index.php'
require __DIR__ . '../vendor/autoload.php';
// ...

use yii1tech\di\Container;
use yii1tech\di\DI;

// setup DI container:
DI::setContainer(
    Container::new()
        ->config(CDbConnection::class, [
            'connectionString' => 'sqlite::memory:',
        ])
        ->lazy(ICache::class, function (Container $container) {
            $cache = new CDbCache();
            $cache->setDbConnection($container->get(CDbConnection::class))
            
            $cache->init();
            
            return $cache;
        })
        // ...
);

// create and run Yii application:
Yii::createWebApplication($config)->run();

而不是立即创建容器实例,您可能指定一个PHP回调,该回调将实例化它。例如

<?php
// file '/public/index.php'
require __DIR__ . '../vendor/autoload.php';
// ...
use yii1tech\di\Container;
use yii1tech\di\DI;

// setup DI container:
DI::setContainer(function () {
    return ContainerFactory::create();
});

// create and run Yii application:
Yii::createWebApplication($config)->run();

// ...
class ContainerFactory
{
    public static function create(): \Psr\Container\ContainerInterface
    {
        $container = Container::new();
        // fill up container
        
        return $container;
    }
}

应用程序组件的依赖注入

此扩展允许通过DI容器配置应用程序(服务定位器)组件。为了使其正常工作,您需要使用此扩展中提供的应用程序类之一 - 该功能不会自动在标准类上工作。

以下类被提供

  • \yii1tech\di\web\WebApplication - 具有DI意识的Web应用程序。
  • \yii1tech\di\console\ConsoleApplication - 具有DI意识的控制台应用程序。
  • \yii1tech\di\base\Module - 具有DI意识的模块。
  • \yii1tech\di\web\WebModule - 具有DI意识的Web模块。

如果您使用自己的自定义应用程序类,您可以使用\yii1tech\di\base\ResolvesComponentViaDI特质将其应用于DI组件解析。

例如

<?php
// file '/public/index.php'
require __DIR__ . '../vendor/autoload.php';
// ...
use yii1tech\di\DI;
use yii1tech\di\web\WebApplication;

// setup DI container:
DI::setContainer(/* ... */);

// create and run Yii DI-aware application:
Yii::createApplication(WebApplication::class, $config)->run();

由于是DI意识,应用程序将根据其配置的类通过DI容器解析配置的组件。这允许您将配置从服务定位器移动到DI容器而不会破坏任何东西。例如

<?php
// ...
use yii1tech\di\Container;
use yii1tech\di\DI;
use yii1tech\di\web\WebApplication;

// setup DI container:
DI::setContainer(
    Container::new()
        ->lazy(CDbConnection::class, function () {
            $db = new CDbConnection();
            $db->connectionString = 'mysql:host=127.0.0.1;dbname=example';
            $db->username = 'container_user';
            $db->password = 'secret';
            
            $db->init();
            
            return $db;
        })
        ->lazy(ICache::class, function (Container $container) {
            $cache = new CDbCache();
            $cache->setDbConnection($container->get(CDbConnection::class));
            
            $cache->init();
            
            return $cache;
        })
);

$config = [
    'components' => [
        'db' => [ // component 'db' will be fetched from container using ID 'CDbConnection'
            'class' => CDbConnection::class,
        ],
        'cache' => [ // component 'cache' will be fetched from container using ID 'ICache'
            'class' => ICache::class,
        ],
        'format' => [ // if component has no matching definition in container - it will be resolved in usual way
            'class' => CFormatter::class,
            'dateFormat' => 'Y/m/d',
        ],
    ],
];

// create and run Yii DI-aware application:
Yii::createApplication(WebApplication::class, $config)->run();
//...
$db = Yii::app()->getComponent('db');
var_dump($db->username); // outputs 'container_user'

$cache = Yii::app()->getComponent('cache');
var_dump(get_class($cache)); // outputs 'CDbCache'

注意! 在将Yii组件定义移动到DI容器时,请小心:请记住手动调用其上的init()方法。Yii经常将关键初始化逻辑放置在组件的init()中,如果您省略其调用,则在从容器中检索时可能会收到损坏的组件实例。

注意! 如果您将应用程序组件配置移动到DI容器,请确保在服务定位器中清理其原始配置。任何剩余的"属性值"定义都将应用于从容器中检索的对象。当您通过工厂定义容器实体时,这可能很有用,但在大多数情况下,它可能会给您带来麻烦。

提示:通过DI容器放置到Yii应用程序(服务定位器)中的组件不需要实现\IApplicationComponent接口,因为不会自动调用其上的init()方法。

Web应用程序中的依赖注入

此扩展允许根据参数的类型提示将 DI 容器实体注入到控制器构造函数和动作方法中。然而,此功能在直接从 CController 扩展的标准控制器上无法工作 - 您需要扩展 \yii1tech\di\web\Controller 类或使用 \yii1tech\di\web\ResolvesActionViaDI 特性。例如

<?php

use yii1tech\di\web\Controller;

class ItemController extends Controller
{
    /**
     * @var CDbConnection 
     */
    protected $db;
    
    // injects `CDbConnection` from DI container at constructor level
    public function __construct(CDbConnection $db, $id, $module = null)
    {
        parent::__construct($id, $module); // do not forget to invoke parent constructor
        
        $this->db = $db;
    }
    
    // injects `ICache` from DI container at action level
    public function actionIndex(ICache $cache)
    {
        // ...
    }
    
    // injects `ICache` from DI container at action level, populates `$id` from `$_GET`
    public function actionView(ICache $cache, $id)
    {
        // ...
    }
}

注意!在声明您自己的控制器构造函数时,请确保它接受父类的参数 $id$module 并将它们传递给父构造函数。否则,控制器可能无法正常工作。

控制台应用程序中的依赖注入

此扩展允许根据参数的类型提示将 DI 容器实体注入到控制台命令的构造函数和动作方法中。然而,此功能在直接从 CConsoleCommand 扩展的标准控制台命令上无法工作 - 您需要扩展 \yii1tech\di\console\ConsoleCommand 类或使用 \yii1tech\di\console\ResolvesActionViaDI 特性。例如

<?php

use yii1tech\di\console\ConsoleCommand;

class ItemCommand extends ConsoleCommand
{
    /**
     * @var CDbConnection 
     */
    protected $db;
    
    // injects `CDbConnection` from DI container at constructor level
    public function __construct(CDbConnection $db, $name, $runner)
    {
        parent::__construct($name, $runner); // do not forget to invoke parent constructor

        $this->db = $db;
    }
    
    // injects `ICache` from DI container at action level
    public function actionIndex(ICache $cache)
    {
        // ...
    }
    
    // injects `CFormatter` from DI container at action level, populates `$date` from shell arguments
    public function actionFormat(CFormatter $formatter, $date)
    {
        // ...
    }
}

注意!在声明您自己的控制台命令构造函数时,请确保它接受父类的参数 $name$runner 并将它们传递给父构造函数。否则,控制台命令可能无法正常工作。

外部(第三方)容器使用

容器 \yii1tech\di\Container 非常基本,您可能希望使用更复杂的东西,如 PHP-DI,而不是它。任何 PSR-11 兼容的容器都可以在此扩展中使用。您只需要将容器传递给 \yii1tech\di\DI::setContainer()。例如

<?php
// file '/public/index.php'
require __DIR__ . '../vendor/autoload.php';
// ...
use DI\ContainerBuilder;
use yii1tech\di\DI;
use yii1tech\di\web\WebApplication;

// use 'PHP-DI' for the container:
DI::setContainer(function () {
    $builder = new ContainerBuilder();
    
    $builder->useAutowiring(true);
    // ...
    
    return $builder->build();
});

// create and run Yii DI-aware application:
Yii::createApplication(WebApplication::class, $config)->run();

许多现有的 PSR-11 兼容解决方案都内置了注入机制,您可能希望利用它而不是此扩展提供的注入机制。为了做到这一点,您应该创建自己的注入器,实现 \yii1tech\di\InjectorContract 接口,并将其传递给 \yii1tech\di\DI::setInjector()

许多现有的 PSR-11 容器(包括 'PHP-DI')已经实现了依赖注入方法 make()call()。您可以使用 \yii1tech\di\external\ContainerBasedInjector 来利用这些方法。例如

<?php
// file '/public/index.php'
require __DIR__ . '../vendor/autoload.php';
// ...
use DI\ContainerBuilder;
use yii1tech\di\DI;
use yii1tech\di\external\ContainerBasedInjector;
use yii1tech\di\web\WebApplication;

// use 'PHP-DI' for the container:
DI::setContainer(function () {
    $builder = new ContainerBuilder();
    // ...
    
    return $builder->build();
})
    ->setInjector(new ContainerBasedInjector()); // use `\DI\Container::make()` and `\DI\Container::call()` for dependency injection

// create and run Yii DI-aware application:
Yii::createApplication(WebApplication::class, $config)->run();

注意!许多现有的提供内置注入方法的 PSR-11 容器在实现 \Psr\Container\ContainerInterface::has() 方法时存在不一致性。标准要求:只有在容器内部存在显式绑定时才应返回 true - 许多解决方案即使在绑定不存在但请求的类 可以 解决的情况下也返回 true,例如,其构造函数参数可以使用容器中的其他绑定填充。这种检查总是伴随着额外的类和方法反射创建,这会降低整体性能。这可能在通过 DI 容器解析 Yii 应用程序组件时产生重大影响。

建议在使用此扩展的容器之前解决 \Psr\Container\ContainerInterface::has() 实现中的任何不一致性。这可以通过使用 \yii1tech\di\external\ContainerProxy 类轻松实现。例如

<?php

// file '/public/index.php'
require __DIR__ . '../vendor/autoload.php';
// ...

use DI\Container;
use DI\ContainerBuilder;
use yii1tech\di\DI;
use yii1tech\di\external\ContainerProxy;

// use 'PHP-DI' for the container:
DI::setContainer(function () {
    $builder = new ContainerBuilder();
    // ...
    
    return ContainerProxy::new($builder->build())
        ->setCallbackForHas(function (Container $container, string $id) {
            return in_array($id, $container->getKnownEntryNames(), true);
        });
});
// ...