suhock/php-dependency-injection

PHP 依赖注入库

dev-develop 2023-08-10 16:03 UTC

This package is auto-updated.

Last update: 2024-09-10 18:23:55 UTC


README

PHP 依赖注入库为在 PHP 8.1 或更高版本上运行的项目提供可定制的依赖注入框架。

$container = new Suhock\DependencyInjection\Container();
$container->addSingletonClass(MyApplication::class)
    // Add the rest of your dependencies...
    ->get(MyApplication::class)
    ->run();

默认情况下,此库提供了单例瞬态生命周期策略,以及多种方式来提供特定类型的实例,还可以为特定命名空间中的所有类或实现特定接口指定工厂。您可以通过自己的自定义生命周期策略、实例提供者或嵌套容器轻松扩展默认的Container实现以满足您的需求。

该库还提供了一个ContextContainer,用于在嵌套上下文层次结构中级联依赖解析,以及一个Injector,用于将依赖和显式参数注入到特定的函数或构造函数。

目录

安装

suhock/dependency-injection添加到项目composer.json文件的require部分。

{
    "require": {
        "suhock/dependency-injection": "^0.1"
    }
}

或者,从项目的根目录使用命令行。

composer require "suhock/dependency-injection"

基本用法

基本的Container类包含用于构建容器和检索实例的方法。首先构造一个实例。

use Suhock\DependencyInjection\Container;

$container = new Container();

接下来,构建您的容器,即告诉容器如何解决您的应用程序中特定的依赖。

$container
    // Autowire the constructor
    ->addSingletonClass(MyApplication::class)

    // Manually construct an instance with factory
    ->addSingletonFactory(Logger::class, fn () => new FileLogger('myapp.log'))

    // Alias an interface to an implementing type
    ->addTransientImplementation(HttpClient::class, CurlHttpClient::class)

    // Add optional values with a mutator after autowiring the constructor
    ->addTransientClass(
        CurlHttpClient::class,
        function (CurlHttpClient $client, Logger $logger): void {
            $client->addLogger($logger);
        }
    );

最后,在容器上调用get()方法以检索应用程序的实例并运行它。

$container
    ->get(MyApplication::class)
    ->handleRequest();

容器将自动装配类构造函数并提供实例给您的应用程序。

class MyApplication
{
    // The constructor arguments will be provided by the container
    public function __construct(
        private readonly HttpClient $client,
        private readonly Logger $logger
    ) {
    }
}

如果您的应用程序有其他入口点(例如控制器),将容器注入到调用这些入口点的应用程序部分(例如路由器)可能很有用。

$container->addSingletonInstance(Container::class, $container);

class MyRouter
{
    public function __construct(
        private readonly Container $container
    ) {
    }

    public function routeRequest(string $method, string $path): void
    {
        $controllerClassName = $this->getControllerClassName($path);
        $controller = $this->container->get($controllerClassName);
        $controller->handleRequest($method);
    }
}

实例生命周期

实例的生命周期决定了容器何时请求类的全新实例。对于类有两个内置的生命周期策略:单例和瞬态。您还可以添加自己的自定义生命周期策略

单例

单例实例的生存期与容器相同。当容器首次接收到对单例实例的请求时,它会调用您为该类指定的工厂,存储结果,然后返回它。每次容器接收到对该类后续的请求时,它将返回相同的实例。默认的Container提供了以addSingleton为前缀的方便方法来添加单例工厂。

瞬态

瞬态实例永远不会持久化,每次请求实例时,容器都会提供一个新值。每次容器接收到对瞬态实例的请求时,它将调用您为该类指定的工厂。默认的Container提供了以addTransient为前缀的方便方法来添加瞬态工厂。

向容器添加依赖

有几种内置方式可以指定如何创建新实例。

如果需要,还可以指定您自己的自定义实例提供者

本文档使用修改后的PHP语法来传达API信息。

自动装配类

容器将通过调用类的构造函数来构建类,并自动解析构造函数参数列表中的任何依赖项。

如果类有任何具有Autowire属性的函数,容器将调用这些函数,解析并注入参数列表中列出的任何依赖项。

可选的$mutator回调允许在容器初始化对象后进行额外的配置。回调必须将类的实例作为其第一个参数。其他参数将自动注入。

callable<TClass> Mutator(TClass $instance, [object|null ...]): void;

class Container
{
    function addSingletonClass<TClass>(
        string<TClass> $className,
        Mutator<TClass>|null $mutator = null
    ): static;

    function addTransientClass<TClass>(
        string<TClass> $className,
        Mutator<TClass>|null $mutator = null
    ): static;
}
示例
自动注入类构造函数

在以下示例中,当容器提供MyService的实例时,它将自动将其构造函数中的所有依赖项注入以创建一个实例。

$container->addSingletonClass(MyService::class);
使用修改器设置可选属性

当容器提供CurlHttpClient的实例后,在自动注入构造函数之后,它还会设置其logger属性。

$container->addTransientClass(
    CurlHttpClient::class,
    function (CurlHttpClient $obj, Logger $logger): void {
        $obj->setLogger($logger);
    }
);
使用属性设置可选属性

当容器提供CurlHttpClient的实例时,它将看到setLogger()具有Autowire属性,并调用它,将来自容器的Logger实例传递给它。

use Suhock\DependencyInjection\Autowire;

class CurlHttpClient
{
    #[Autowire]
    public function setLogger(Logger $logger): void {
        $this->logger = $logger;
    }
    
    // ...
}

将接口映射到实现

容器将通过使用指定实现子类的实例提供者来提供类。因此,您还必须将实现类添加到容器中。

class Container
{
    function addSingletonImplementation<TClass, TImpl of TClass>(
        string<TClass> $className,
        string<TImpl> $implementationClassName
    ): static;

    function addTransientImplementation<TClass, TImpl of TClass>(
        string<TClass> $className,
        string<IImpl> $implementationClassName
    ): static;
}
示例
将接口映射到具体实现
$container
    ->addSingletonImplementation(HttpClient::class, CurlHttpClient::class)
    ->addSingletonClass(CurlHttpClient::class);

当您的应用程序请求HttpClient的实例时,容器将看到它实际上应该提供CurlHttpClient的实例。然后它会自动注入CurlHttpClient的构造函数以提供实例。

实现链
$container
    ->addTransientImplementation(Throwable::class, Exception::class)
    ->addTransientImplementation(Exception::class, LogicException::class)
    ->addTransientClass(LogicException::class);

当您的应用程序请求Throwable的实例时,容器将看到它实际上应该提供Exception的实例。接下来,它将看到应该使用LogicException来创建Exception的实例。最后,它将通过自动注入构造函数来为Throwable提供LogicException的实例。如果您的应用程序请求一个Exception的实例,则容器也会提供一个LogicException的实例。

未解析的映射

容器必须知道如何提供实现,否则将抛出异常

$container->addSingletonClass(HttpClient::class, CurlHttpClient::class);

/*
 * The container will throw an UnresolvedDependencyException because it does
 * not know how to provide an instance of CurlHttpClient.
 */
$container->get(HttpClient::class);

调用工厂方法

容器将通过请求工厂方法来提供类实例。工厂方法中的任何参数都将自动注入。

callable<TClass> FactoryMethod([object|null ...]): TClass;

class Container
{
    function addSingletonFactory<TClass>(
        string<TClass> $className,
        FactoryMethod<TClass> $factory
    ): static;

    function addTransientFactory<TClass>(
        string<TClass> $className,
        FactoryMethod<TClass> $factory
    ): static;
}
示例
注入配置值
$container->addSingletonFactory(
    Mailer::class,
    fn (AppConfig $config) => new Mailer($config->mailerTransport)
);

当您的应用程序从容器请求Mailer的实例时,它将调用指定的工厂,注入AppConfig依赖项。然后工厂手动构建一个实例,指定来自配置的邮件传输方式。

内联类实现
$container->addTransientFactory(
    Logger::class,
    fn (FileWriter $writer) => new class($writer) implements Logger {
        public function __construct(private readonly FileWriter $writer)
        {
        }

        public function log(string $message): void
        {
            $this->writer->writeLine($message);
        }
    }
);

提供特定实例

容器将提供一个预构建的类实例。

class Container
{
    function addSingletonInstance<TClass>(
        string<TClass> $className,
        TClass $instance
    ): static;
}
示例
基本用法
$request = new Request($_SERVER, $_GET, $_POST, $_COOKIE);
$container->addSingletonInstance(Request::class, $request);

当您的应用程序需要 Request 对象时,容器将提供与通过 $request 变量传入的实例完全相同的实例。

嵌套容器

如果容器找不到提供特定类实例的方法,它将接着检查是否有嵌套容器可以提供该值。提供了两个内置的嵌套容器实现:命名空间和实现。您还可以使用 addContainer() 方法添加自定义容器,这些容器实现 ContainerInterface。嵌套容器将按添加顺序依次搜索。

命名空间容器

命名空间容器如果请求的类位于配置的命名空间中,将提供该类的实例。默认情况下,命名空间容器将自动注入命名空间中所有类的构造函数。

命名空间容器接受一个可选的 $factory 参数,该参数指定一个提供命名空间中类实例的方法。该工厂必须以被实例化的类的名称作为第一个参数。外部容器将提供任何额外的依赖项。

callable ClassFactory<TClass>(
    string<TClass> $className,
    object|null ...$dependencies
): TClass;

class Container
{
    function addSingletonNamespace(
        string $namespace,
        ClassFactory|null $factory = null
    ): static;

    function addTransientNamespace(
        string $namespace,
        ClassFactory|null $factory = null
    ): static;
}
示例
$container->addSingletonNamespace('Http');

/*
 * The container will provide an instance of CurlHttpClient by autowiring the
 * constructor because the class is in the Http namespace.
 */
$curlClient = $container->get(Http\CurlHttpClient::class);

$container->addSingletonImplementation(
    Http\HttpClient::class,
    Http\CurlHttpClient::class
);

/*
 * The container will know to autowire CurlHttpClient for HttpClient because we
 * specified the interface-implementation mapping.
 */
$httpClient = $container->get(Http\HttpClient::class);

接口容器

接口容器如果请求的类是特定接口或基类的子类,将提供该类的实例。实例从给定的工厂获取,如果没有提供工厂,则通过自动注入构造函数获取。工厂必须以类名作为第一个参数。外部容器将提供任何额外的依赖项。

callable<TClass> ImplementationFactory<TImpl of TClass>(
    string<TImpl> $className,
    [object|null ...]
): TImpl;

class Container
{
    function addSingletonInterface<TInterface>(
        string<TInterface> $className,
        ImplementationFactory<TClass>|null $factory = null
    ): static;

    function addTransientInterface<TInterface>(
        string<TInterface> $className,
        ImplementationFactory<TClass>|null $factory = null
    ): static;
}
示例

以下示例从第三方库的容器中检索存储库实例。

$container->addSingletonInterface(
    EntityNameProvider::class,
    /**
     * @template T of EntityNameProvider
     * @var class-string<T> $className
     * @return T
     */
    fn (string $className, EntityManager $em) =>
        $em->getRepository($className::getEntityName())
);

/*
 * The container will query the EntityManager for a UserRepository.
 */
$userRepository = $container->get(UserRepository::class);

class UserRepository extends EntityRepository implements EntityNameProvider
{
    public static function getEntityName(): string
    {
        return User::class;
    }
}

属性容器

属性容器将为具有指定属性的任何类提供实例。实例从给定的工厂获取,如果没有提供工厂,则通过自动注入构造函数获取。工厂必须以类名作为第一个参数,并以属性实例作为第二个参数。外部容器将提供任何额外的依赖项。

callable<TAttr> AttributeClassFactory<TClass>(
    string<TClass> $className,
    TAttr $attributeInstance,
    [object|null ...]
): TClass;

class Container
{
    function addSingletonAttribute<TAttr>(
        string<TAttr> $attributeName,
        AttributeClassFactory<TAttr>|null $factory = null
    ): static;

    function addTransientAttribute<TAttr>(
        string<TAttr> $attributeName,
        AttributeClassFactory<TAttr>|null $factory = null
    ): static;
}
示例

以下示例提供了一种替代方法,该方法在 接口容器部分 的示例中使用属性来指定元数据,而不是接口。

$container->addSingletonAttribute(
    EntityName::class,
    fn (string $className, EntityName $attribute, EntityManager $em) =>
        $em->getRepository($attribute->getName())
);

/*
 * The container will query the EntityManager for a UserRepository.
 */
$userRepository = $container->get(UserRepository::class);

#[EntityName(User::class)]
class UserRepository extends EntityRepository
{
}

#[Attribute(Attribute::TARGET_CLASS)]
class EntityName
{
    public function __construct(
        private readonly string $name
    ) {
    }

    public function getName(): string
    {
        return $this->name;
    }
}

自定义容器

自定义生命周期策略

实现 LifetimeStrategy,并可选地使用方便方法扩展 Container 以支持您的新生命周期策略。

自定义实例提供者

实现 InstanceProvider,并使用基本添加方法之一将其添加到您的容器中。您还可以扩展 Container 以添加使用您的新实例提供者的方便方法。

class Container
{
    public function add<TClass>(
        string<TClass> $className,
        LifetimeStrategy<TClass> $lifetimeStrategy,
        InstanceProvider $instanceProvider
    ): static;

    public function addSingleton<TClass>(
        string<TClass> $className,
        InstanceProvider $instanceProvider
    ): static;

    public function addTransient<TClass>(
        string<TClass> $className,
        InstanceProvider $instanceProvider
    ): static;
}

自定义嵌套容器

实现 ContainerInterface,并使用以下方法之一将其传递到容器中。如果您的自定义容器需要能够自动注入对象,您可以将外部容器传递给其构造函数。

callable LifetimeStrategyFactory<TClass>(
    string<TClass> $className
): LifetimeStrategy<TClass>;

class Container
{
    public function addContainer(
        ContainerInterface $container,
        LifetimeStrategyFactory $lifetimeStrategyFactory
    ): static;

    public function addSingletonContainer(
        ContainerInterface $container
    ): static;

    public function addTransientContainer(
        ContainerInterface $container
    ): static;
}

上下文容器

ContextContainer 类提供了一组命名容器(上下文),可用于在不同的应用程序部分为同一类提供不同的构造。上下文可以用字符串或枚举值命名。

上下文容器使用上下文堆栈来解析依赖关系。堆栈可以通过 push()pop() 方法来管理,或者使用类、函数或参数声明上的 Context 属性。

use Suhock\DependencyInjection\Context\ContextContainerFactory;
use Suhock\DependencyInjection\Context\Context;

/*
 * Strings or enums can be used as identifiers for contexts. To help ease
 * analysis and future refactorings, enums or string-typed constants are
 * recommended.
 */
enum MyContexts {
    case Default;
    case Admin;
}

$container = ContextContainerFactory::createForDefaultContainer();

/*
 * Build the Default context's container.
 */
$container->context(MyContexts::Default)
    ->addSingletonClass(MyApplication::class)
    ->addTransientImplementation(HttpClient::class, CurlHttpClient::class)
    ->addSingletonFactory(
        Settings::class,
        fn () => JsonSettings::fromFile('default.json')
    );

/*
 * Build the Admin context's container.
 */
$container->context(MyContexts::Admin)
    ->addSingletonFactory(
        Settings::class,
        fn () => JsonSettings::fromFile('admin.json')
    );

$container
    /*
     * Make Default the default, fallback context.
     * Stack: Default
     */
    ->push(MyContexts::Default)

    /*
     * Fetch the application and run it.
     */
    ->get(MyApplication::class)
    ->run();

/*
 * Stack: Default, Admin
 *
 * The container will search the Admin context then the Default context for
 * each dependency in the following class.
 */
#[Context(MyContexts::Admin)]
class AdminEditDefaultSettingsController {
    /*
     * Stack: Default, Admin
     *
     * Since no context is explicitly specified, the stack is inherited as-is
     * from the class.
     */
    public function __construct(
        /*
         * Stack: Default, Admin
         *
         * The container will resolve $settings using the Settings factory in
         * the Admin context, since Admin is at the top of the context stack.
         */
        private readonly Settings $settings,

        /*
         * Stack: Default, Admin, Default
         *
         * The container will resolve $defaultSettings using the Settings
         * factory in the Default context, since the attribute below will
         * place Default at the top of the context stack for this parameter.
         */
        #[Context(MyContexts::Default)]
        private readonly Settings $defaultSettings,

        /*
         * Stack: Default, Admin
         *
         * The container will first attempt to resolve $httpClient using the
         * Admin context. However, since HttpClient does not exist in the
         * the Admin context, the container will resolve it using the factory
         * in the Default context.
         */
        private readonly HttpClient $httpClient
    ) {
    }
}

依赖注入器

该库还提供了一个依赖注入器 Injector,可用于直接调用构造函数和函数,并从容器中注入任何依赖项。注入器还允许您直接注入特定值以供命名或索引参数使用。

callable<TResult of mixed> InjectableFunction([... mixed]): TResult;

class Injector
{
    public function call<TResult>(
        InjectableFunction<TResult> $function,
        array<int|string, mixed> $params = []
    ): TResult;
    
    public function instantiate<TClass>(
        string<TClass> $className,
        array<int|string, mixed> $params = []
    ): TClass;
}

示例

以下示例中,需要在控制器中的函数而不是构造函数中注入依赖项。

use Suhock\DependencyInjection\Container;
use Suhock\DependencyInjection\ContainerInjector;

// Create the container and build it
$container = new Container();
// ... build the container ...

// Create an injector backed by the container
$injector = new ContainerInjector($container);

// Fetch the application router from the container
$router = $container->get(Router::class);

// Get the appropriate controller from the request path
$controller = $router->getControllerFromRequest($_SERVER);

// Call the controller's handleGet() method, injecting the indicated parameter
// values in addition to any additional dependencies in the parameter list.
$page = $injector
    ->call(
        $controller->handleGet(...),
        map_query_to_assoc_param_array($_GET)
    )

// Then, call the render() function on the return value.
$page->render();

class ProjectListController
{
    public function handleGet(
        // Parameter below will be injected from the container.
        ProjectRepository $projectRepository,

        // Parameter below will be populated from the value provided in the
        // $injector->call() parameter array. The default value will be used if
        // the key 'filter' is not present in the array.
        string $filter = ''
    ): PageInterface {
        $projects = $projectRepository->query($filter);

        return new ProjectListPage($projects);
    }
}

interface PageInterface {
    public function render(): void;
}

指定依赖

函数通过在其参数列表中列出它们来指定其依赖项。类通过在其构造函数的参数列表中列出它们来指定其依赖项。依赖项必须指定为命名对象类型、联合类型或交集类型。

命名对象类型

如果依赖项指定为命名对象类型,容器只有在能够解析该类型的工厂时才会提供值。

class MyApplication
{
    public function __construct(
        private readonly HttpClient $httpClient
    ) {
    }
}

在上面的示例中,容器将尝试解析一个 HttpClient 的实例。如果无法解析 HttpClient,它将抛出 ParameterResolutionException

可空类型

如果容器无法解析依赖项,但该依赖项是可空的,则容器将提供一个空值。

class MyApplication
{
    public function __construct(
        private readonly ?HttpClient $httpClient
    ) {
    }
}

在上面的示例中,容器将尝试解析一个 HttpClient 的实例。如果无法解析 HttpClient,它将注入一个 null 值。

具有默认值的内置类型

容器无法解析内置类型。然而,如果函数或类接受一个内置类型,并且该参数指定了默认值,则将使用默认值。

class MyApplication
{
    public function __construct(
        private readonly HttpClient $httpClient,
        private readonly string $homeUrl = '',
        private readonly int $timeout = 0,
        private readonly array $otherOptions = []
    ) {
    }
}

在上面的示例中,尽管容器无法解析 stringintarray 类型,但它将自动使用指定的默认值将构造函数进行自连接。如果您需要为内置类型注入非默认值,请使用 工厂方法

联合类型

如果依赖项指定为联合类型,容器将按顺序搜索联合列表中所有命名的对象类型。它将使用能够解析的第一个类型提供值。内置类型将被忽略。

class MyApplication
{
    public function __construct(
        private readonly HttpClient|GopherClient|string $client
    ) {
    }
}

在上面的示例中,容器将首先尝试解析一个 HttpClient 的实例。如果无法解析 HttpClient,它将尝试解析一个 GopherClient 的实例。如果无法解析 GopherClient,它将忽略 string,然后抛出 ParameterResolutionException

交集类型

如果依赖项指定为交集类型,容器将尝试获取列表中的每个类型的实例,直到找到一个满足列表中所有类型的实例。由于必须检索实例以测试它是否匹配,因此使用交集类型可能会导致速度较慢,并且如果任何非匹配实例的构建有副作用,则可能产生意外的后果。

class MyApplication
{
    public function __construct(
        private readonly HttpClient&Serializable $httpClient
    ) {
    }
}

在上面的示例中,容器将首先尝试解析一个 HttpClient 的实例。如果成功,它将检查

附录

关于服务定位器模式的说明

前面的示例类似于服务定位器模式。请注意,虽然 Container 类在功能上等同于服务定位器,但通常最好避免服务定位器模式,因为它会使测试、重构和推理应用程序变得更加困难。

只有直接调用入口点的应用程序中的地方应该直接使用容器。如果您在运行时之前知道所需的具体对象类型,您应该依赖于容器的自动依赖注入功能,而不是直接调用容器。在先前的示例中,应用程序必须在收到实际请求之前知道要调用哪个容器,因此注入容器是必要的。

以下是一个不应该做的示例。

/* Do NOT do this! */

class MyApiCaller
{
    public function __construct(
        private readonly Container $container
    ) {
    }

    public function callApi(): HttpResponse
    {
        $httpClient = $this->container->get(HttpClient::class);
        return $httpClient->get('https://www.example.com/api');
    }
}

在上面的示例中,所需类型 HttpClient 在运行时之前是已知的,并且可以通过构造函数直接请求。此更改将使 MyClass 的实际依赖关系更加清晰,从而使测试和重构变得更加容易。

/* Do this instead! */

class MyApiCaller
{
    public function __construct(
        private readonly HttpClient $httpClient
    ) {
    }

    public function callApi(): HttpResponse
    {
        return $this->httpClient->get('https://www.example.com/api');
    }
}

重构到依赖注入模式

违反服务定位器模式规则的一个例外可能是如果您正在将遗留应用程序重构到依赖注入模式:您想尽可能多地重用容器的依赖项构建逻辑,但在某些地方仍然难以正确注入依赖项。

在这种情况下,应用程序容器可以基于单例实例构建,并作为中间步骤提供给遗留代码。一旦您最终将所有使用单例容器的使用情况重构为使用正确的依赖注入,则可以删除单例容器。

/* Refactor this! */
public function myFragileBloatedFunction(...$args)
{
    // ...

    // $httpClient = new CurlHttpClient();
    // $logger = new FileLogger('myapp.log');
    // $httpClient->setLogger($logger);

    // Replaced the above duplicated construction logic with a call to the
    // container
    $httpClient = getAppContainer()->get(HttpClient::class);

    $result = $httpClient->get('https://www.example.com/api/' . $args[42]);
    // ...
}

/* Eliminate all references to the singleton container and remove this! */
function getAppContainer(): Container
{
    static $container;
    return $container ??= new Container();
}

/*
 * Build your container from the singleton container for now.
 * Replace with direct construction once refactoring is complete.
 */
$container = getAppContainer();
$container->addSingletonClass(MyApplication::class);
// ...