swiftly/dependency

简洁的依赖注入。

dev-main 2024-04-07 21:43 UTC

This package is auto-updated.

Last update: 2024-09-07 22:38:59 UTC


README

PHP Version CircleCI Coverage Status

轻松构建对象层次结构。

主要设计用于SwiftlyPHP项目,此组件提供依赖容器和注入器的轻量级实现,允许您轻松配置和构建复杂对象树,而无需太多麻烦。

虽然不如Symfony或Laravel等容器的功能全面,但此组件提供了一种最小化、易于使用的界面来管理依赖。

安装

要安装库,请使用Composer

composer require swiftly/dependency

用法

基础

如果您曾经参与过一个大型项目,您将知道将代码拆分为明确定义的类的益处。虽然这在使文件系统(和项目结构)更干净方面具有实际好处,但它还允许我们分离出相关的关注点,以易于推理和组合的方式将相关的功能组合在一起。

然而,随着时间的推移,您可能会拥有大量的类。对象将开始依赖其他对象。您会得到需要两个或三个对象构造函数的对象,而这些对象的构造函数又需要相同的对象,很快就会形成一个难以管理的复杂层次结构。

进入服务容器

<?php

use Swiftly\Dependency\Container;

$container = new Container();

服务容器是一个注册表,您可以将其中的应用程序中的类详细信息输入其中。

例如,假设我们有一个如下所示的类

<?php // MyClass.php

class MyClass
{
    public function __construct(
        private string $name,
        private int $age
    ) {}

    public function speak()
    {
        return "Hi, my name is " . $this->name . " and I am " . $this->age;
    }
}

我们可以这样将其注册到容器中

<?php

use Swiftly\Dependency\Container;

$container = new Container();
$container->register(MyClass::class)
    ->setArguments([
        'name' => 'John',
        'age' => 42
    ]);

在这里,我们已经让容器知道我们的自定义类,并提供了构建它所需的参数。现在,当我们需要MyClass的实例时,我们可以调用容器的get()方法,它会为我们创建一个副本。

<?php
// ... continued from above ...

$myclass = $container->get(MyClass::class);

// "Hi, my name is John and I am 42"
echo $myclass->speak();

这种方法的优点是,我们的对象仅在显式请求时才构造。如果我们从未从容器中请求它,它就永远不会被创建,从而节省了可能昂贵的实例化。

对象层次结构

现在我们已经解决了基础知识,让我们看看如何使用容器使创建对象层次结构更容易。

想象以下类

<?php // classes.php

class Person
{
    public function __construct(
        public string $name
    ) {}
}

class Salary
{
    public function __construct(
        public int $yearly
    ) {}
}

class Job
{
    public function __construct(
        public string $title,
        protected Salary $salary
    ) {}
}

class Employee
{
    public function __construct(
        Person $person,
        Job $role
    ) {}

    public function speak()
    {
        return "I'm " . $this->person->name ", I am a " . $this->job->title;
    }
}

这里有4个类,其中Employee的构建需要对PersonJob的引用,而Job有自己的依赖,形式为Salary对象。

传统上构建一个Employee可能如下所示

<?php

$person = new Person("Jim");
$salary = new Salary(42_000);
$job = new Job("Developer", $salary);
$employee = new Employee($person, $job);

// "I'm Jim, I am a Developer"
echo $employee->speak();

而使用我们的容器,它看起来像这样

<?php

use Swiftly\Dependency\Container;

$container = new Container();
$container->register(Person::class)->setArguments(['name' => 'Jim']);
$container->register(Salary::class)->setArguments(['yearly' => 42_000]);
$container->register(Job::class)->setArguments(['title' => 'Developer']);
$container->register(Employee:class);

$employee = $container->get(Employee:class);

// "I'm Jim, I am a Developer"
echo $employee->speak();

我们不需要每次需要Employee的实例时都构建一个新层次结构,只需注册我们的类一次,然后在需要时调用get()。容器检查每个类的构造函数,沿着层次结构向下,根据需要解决每个类的要求。(如果不能解决,则抛出一个有用的异常)。

工厂

在创建对象时,您可能需要执行一些额外的设置,例如打开数据库连接或读取配置文件。这就是工厂发挥作用的地方。

在本质上,工厂只是创建和返回对象的函数。

<?php

use Swiftly\Dependency\Container;

$container = new Container();
$container->register(Person::class, function () {
    return new Person("Jill");
});

$person = $container->get(Person::class);

// "Jill"
echo $person->name;

此代码的功能与上面的Person示例相同,但与使用setArguments()实用工具不同,值已在工厂中硬编码。

然而,一个更实际的例子可能如下所示

<?php

use Swiftly\Dependency\Container;

$container = new Container();
$container->register(Database::class, function () {
    $database = new Database(...);
    $database->open();
    return $database;
});

现在每次我们get()一个Database的副本时,我们可以确信它已经打开并准备好使用。

太好了!但如果您的工厂也需要传入值呢?

<?php

use Swiftly\Dependency\Container;

$container = new Container();
$container->register(HttpTransport::class, function (string $user_agent) {
    new CurlTransport($user_agent);
})->setArguments([
    'user_agent' => 'PHP/Curl'
]);
$container->register(
    HttpClient::class,
    function (HttpTransport $transport) {
        $client = new HttpClient($transport);
        $client->setTimeout(2000);
        $client->setBlocking(true);
        return $client;
    }
);

这里我们发现了2个关键点

  1. 您提供给setArguments()的参数会被转发到工厂
  2. 容器会尽可能解析类型提示的工厂参数。

标签

标签允许您将自定义字符串标签应用于服务,让您可以使用tagged()方法收集应用了特定标签的所有服务。

假设您有一系列任务对象,每个都实现了如下TaskInterface

<?php // tasks.php

interface TaskInterface
{
    public function execute(): void;
}

class EmailTask implements TaskInterface
{
    public function execute(): void
    {
        // ... send an email
    }
}

class NotificationTask implements TaskInterface
{
    public function execute(): void
    {
        // ... send push notification
    }
}

class LogTask implements TaskInterface
{
    public function execute(): void
    {
        // ... write log data
    }
}

作为开发者,我们可以看到这些类是相关的,并且每个都符合TaskInterface。对每个调用execute()会运行相应的逻辑。然而,容器对此关系却视而不见。

为了将它们分组,我们可以使用标签

<?php

use Swiftly\Dependency\Container;

$container = new Container();
$container->register(EmailTask::class)
    ->setTags(['task']);
$container->register(NotificationTask::class)
    ->setTags(['task']);
$container->register(LogTask::class)
    ->setTags(['task']);

foreach ($container->tagged('task') as $task) {
    $task->execute();
}

这里我们做了一些事情

  1. 我们将电子邮件、通知和日志任务类注册到了容器中
  2. 为每个类添加了自定义的task标签
  3. 调用了tagged()方法,该方法返回所有具有给定标签的类

如果您对类型安全感兴趣,也可以将类型约束作为第二个参数传递,这样您可以确保所有带标签的服务都是给定类(或实现了给定接口)。这还带来了一些好处,比如可以被您的IDE用于自动完成,以及像PsalmPHPStan这样的静态分析工具检测到。

<?php

foreach ($container->tagged('task', TaskInterface::class) as $task) {
    // IDE and static analysis can now infer type of `$task`
    $task->execute();
}

如果返回的服务中的任何一个不匹配TaskInterface,将抛出一个异常并解释原因。