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
,因为它将被自动添加。
定义服务后,您可以通过扩展它们来修改或更新服务定义。这使您可以为其他地方定义的服务添加额外的参数
// 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());
}
}
在使用 ConsoleIntegrationTestTrait
或 IntegrationTestTrait
的测试中,您可以用模拟或存根替换通过容器注入的服务
// 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 文档。