依赖注入

CakePHP 服务容器使您能够通过依赖注入管理应用程序服务的类依赖关系。依赖注入会通过构造函数自动“注入”对象的依赖关系,而无需手动实例化它们。

您可以使用服务容器来定义“应用程序服务”。这些类可以使用模型并与其他对象(如记录器和邮件程序)进行交互,以构建可重用的工作流和应用程序的业务逻辑。

CakePHP 将在以下情况下使用 DI 容器

  • 构造控制器。

  • 在您的控制器上调用操作。

  • 构造组件。

  • 构造控制台命令。

  • 通过类名构造中间件。

控制器示例

// In src/Controller/UsersController.php
class UsersController extends AppController
{
    // The $users service will be created via the service container.
    public function ssoCallback(UsersService $users)
    {
        if ($this->request->is('post')) {
            // Use the UsersService to create/get the user from a
            // Single Signon Provider.
            $user = $users->ensureExists($this->request->getData());
        }
    }
}

// In src/Application.php
public function services(ContainerInterface $container): void
{
    $container->add(UsersService::class);
}

在本例中,UsersController::ssoCallback() 操作需要从单点登录提供者获取用户,并确保它存在于本地数据库中。由于此服务被注入到我们的控制器中,因此我们在测试时可以轻松地用模拟对象或虚拟子类替换实现。

命令示例

// In src/Command/CheckUsersCommand.php
use Cake\Console\CommandFactoryInterface;

class CheckUsersCommand extends Command
{
    public function __construct(protected UsersService $users, ?CommandFactoryInterface $factory = null)
    {
        parent::__construct($factory);
    }

    public function execute(Arguments $args, ConsoleIo $io)
    {
        $valid = $this->users->check('all');
    }

}

// In src/Application.php
public function services(ContainerInterface $container): void
{
    $container
        ->add(CheckUsersCommand::class)
        ->addArgument(UsersService::class)
        ->addArgument(CommandFactoryInterface::class);
    $container->add(UsersService::class);
}

这里的注入过程略有不同。我们不是将 UsersService 添加到容器中,而是首先需要将命令作为一个整体添加到容器中,并将 UsersService 添加为参数。这样,您就可以在命令的构造函数中访问该服务。

组件示例

// In src/Controller/Component/SearchComponent.php
class SearchComponent extends Component
{
    public function __construct(
        ComponentRegistry $registry,
        private UserService $users,
        array $config = []
    ) {
        parent::__construct($registry, $config);
    }

    public function something()
    {
        $valid = $this->users->check('all');
    }
}

// In src/Application.php
public function services(ContainerInterface $container): void
{
    $container->add(SearchComponent::class)
        ->addArgument(ComponentRegistry::class)
        ->addArgument(UsersService::class);
    $container->add(UsersService::class);
}

添加服务

为了让容器创建服务,您需要告诉它可以创建哪些类以及如何构建这些类。最简单的定义是通过类名

// Add a class by its name.
$container->add(BillingService::class);

您的应用程序和插件在 services() 钩子方法中定义它们拥有的服务

// in src/Application.php
namespace App;

use App\Service\BillingService;
use Cake\Core\ContainerInterface;
use Cake\Http\BaseApplication;

class Application extends BaseApplication
{
    public function services(ContainerInterface $container): void
    {
        $container->add(BillingService::class);
    }
}

您可以为应用程序使用的接口定义实现

use App\Service\AuditLogServiceInterface;
use App\Service\AuditLogService;

// in your Application::services() method.

// Add an implementation for an interface.
$container->add(AuditLogServiceInterface::class, AuditLogService::class);

如果需要,容器可以利用工厂函数来创建对象

$container->add(AuditLogServiceInterface::class, function (...$args) {
    return new AuditLogService(...$args);
});

工厂函数将接收类所有已解析依赖项作为参数。

定义类后,您还需要定义它所需的依赖项。这些依赖项可以是对象或基本值

// Add a primitive value like a string, array or number.
$container->add('apiKey', 'abc123');

$container->add(BillingService::class)
    ->addArgument('apiKey');

您的服务可以依赖于控制器操作中的 ServerRequest,因为它将被自动添加。

添加共享服务

默认情况下,服务不会共享。每次从容器中获取对象(和依赖项)时都会创建它。如果您希望重复使用单个实例(通常称为单例),您可以将服务标记为“共享”

// in your Application::services() method.

$container->addShared(BillingService::class);

扩展定义

定义服务后,您可以通过扩展它们来修改或更新服务定义。这使您可以为其他地方定义的服务添加额外的参数

// Add an argument to a partially defined service elsewhere.
$container->extend(BillingService::class)
    ->addArgument('logLevel');

标记服务

通过标记服务,您可以获得所有已解析的服务。这可以用于构建将其他服务集合组合在一起的服务,例如在报告系统中

$container->add(BillingReport::class)->addTag('reports');
$container->add(UsageReport::class)->addTag('reports');

$container->add(ReportAggregate::class, function () use ($container) {
    return new ReportAggregate($container->get('reports'));
});

使用配置数据

通常,您需要在服务中使用配置数据。虽然您可以将服务所需的所有配置键添加到容器中,但这可能很繁琐。为了使配置更容易使用,CakePHP 包含一个可注入的配置读取器

use Cake\Core\ServiceConfig;

// Use a shared instance
$container->addShared(ServiceConfig::class);

ServiceConfig 类提供了对 Configure 中所有可用数据的只读视图,因此您不必担心意外更改配置。

服务提供者

服务提供者使您能够将相关服务分组在一起,帮助您组织服务。服务提供者可以帮助提高应用程序的性能,因为定义的服务在首次使用后才懒加载注册。

创建服务提供者

服务提供者的示例如下

namespace App\ServiceProvider;

use Cake\Core\ContainerInterface;
use Cake\Core\ServiceProvider;
// Other imports here.

class BillingServiceProvider extends ServiceProvider
{
    protected $provides = [
        StripeService::class,
        'configKey',
    ];

    public function services(ContainerInterface $container): void
    {
        $container->add(StripeService::class);
        $container->add('configKey', 'some value');
    }
}

服务提供者使用它们的 services() 方法来定义它们将提供的所有服务。此外,这些服务必须$provides 属性中定义。如果未将服务包含在 $provides 属性中,则无法从容器中加载该服务。

使用服务提供者

要加载服务提供者,请使用 addServiceProvider() 方法将其添加到容器中

// in your Application::services() method.
$container->addServiceProvider(new BillingServiceProvider());

可启动服务提供者

如果您的服务提供者需要在添加到容器时运行逻辑,您可以实现 bootstrap() 方法。当您的服务提供者需要加载额外的配置文件、加载额外的服务提供者或修改应用程序其他地方定义的服务时,这种情况可能会发生。可启动服务的示例是

namespace App\ServiceProvider;

use Cake\Core\ServiceProvider;
// Other imports here.

class BillingServiceProvider extends ServiceProvider
{
    protected $provides = [
        StripeService::class,
        'configKey',
    ];

    public function bootstrap($container)
    {
        $container->addServiceProvider(new InvoicingServiceProvider());
    }
}

在测试中模拟服务

在使用 ConsoleIntegrationTestTraitIntegrationTestTrait 的测试中,您可以用模拟或存根替换通过容器注入的服务

// In a test method or setup().
$this->mockService(StripeService::class, function () {
    return new FakeStripe();
});

// If you need to remove a mock
$this->removeMockService(StripeService::class);

在测试期间,任何定义的模拟都将在您的应用程序容器中被替换,并自动注入到您的控制器和命令中。模拟将在每次测试结束后被清理。

自动连接

自动连接默认情况下处于关闭状态。要启用它

// In src/Application.php
public function services(ContainerInterface $container): void
{
    $container->delegate(
        new \League\Container\ReflectionContainer()
    );
}

虽然您的依赖项现在将被自动解析,但这种方法不会缓存解析,这会对性能产生负面影响。要启用缓存

$container->delegate(
     // or consider using the value of Configure::read('debug')
    new \League\Container\ReflectionContainer(true)
);

阅读有关自动连接的更多信息,请参阅 PHP League Container 文档