4.3. PHP definitions
PHP definitions
除了 自动装配 跟 注解, 你还可以使用 PHP 配置格式 来定义注入
你可以将配置注册为数组:
$containerBuilder->addDefinitions([
// 将定义写在这里
]);
或将其放入返回数组的文件中
<?php
return [
// 将定义写在这里
];
$containerBuilder->addDefinitions('config.php');
关于懒加载的一句话
PHP-DI 加载你编写的定义,并将其用作关于如何创建对象的说明。
但是,仅当从容器中请求了这些对象,例如通过 $container->get(…)
或需要将它们注入另一个对象时才创建这些对象。这意味着你可以拥有大量的定义,在需要的时候,PHP-DI才会创建对象。
唯一的例外是将对象定义为值 ,但不建议这样做 (在 "值" 部分对此进行了说明)。
语法
PHP-DI的定义使用 DSL (领域特定语言,用PHP并基于辅助函数编写) 来编写。
此页面上显示的所有示例均使用与PHP 7.0兼容的语法。鼓励您使用以下功能:
-
PHP 5.5
::class
魔术常数:use Psr\Log\LoggerInterface; use Monolog\Logger; return [ LoggerInterface::class => DI\create(Logger::class) ];
-
PHP 5.6 函数导入:
use function DI\create; use function DI\get; return [ 'Foo' => create() ->constructor(get('Bar')), ];
注意: 请记住辅助函数 (例如DI\create()
) 是命名空间函数,而不是类。不要使用 new
(例如 new DI\create()
) ,否则会出现致命错误"Class 'DI\create' not found"
。
定义类型
此定义格式是所有功能中最强大的。您可以定义以下几种 条目类型 :
值
值 (即 Symfony中的parameters ) 是简单的PHP值。
return [
'database.host' => 'localhost',
'database.port' => 5000,
'report.recipients' => [
'bob@example.com',
'alice@example.com',
],
];
您还可以通过直接创建对象来定义它们:
return [
'Foo' => new Foo(),
];
但是 不建议这样做,因为即使没有使用该对象,也会为每个PHP请求创建该对象(不会像本节顶部所述那样被懒加载)。这也将阻止容器被编译。
您应该改用下面的方法之一。
工厂
工厂是可返回实例的PHP可调用对象。 它允许轻松地定义对象, 即仅在实际需要时才创建每个对象(因为可调用对象将在实际需要时被调用)。
就像任何其他定义一样,工厂只需要被调用一次,以后每次需要解析工厂时都会返回相同的结果。
可以使用 DI\factory()
辅助函数来定义工厂, 但可以使用闭包函数来作为快捷方式:
use Psr\Container\ContainerInterface;
use function DI\factory;
return [
'Foo' => function (ContainerInterface $c) {
return new Foo($c->get('db.host'));
},
// 相当于
'Foo' => DI\factory(function (ContainerInterface $c) {
return new Foo($c->get('db.host'));
}),
];
可以通过类型提示来注入其他服务 (只要它们在容器中注册或启用自动装配即可):
return [
'LoggerInterface' => DI\create('MyLogger'),
'Foo' => function (LoggerInterface $logger) {
return new Foo($logger);
},
];
DI\factory()
提供了一个 parameter()
方法, 使您可以指定无法通过类型提示自动插入的条目(例如值)。
return [
'Database' => DI\factory(function ($host) {...})
->parameter('host', DI\get('db.host')),
];
如第一个示例所示,这也可以通过注入容器本身来完成。当注入容器时,应该针对接口 Psr\Container\ContainerInterface
而不是实现 DI\Container
进行类型提示。
工厂可以是任何 PHP callable, 因此它们也可以是类方法:
class FooFactory
{
public function create(Bar $bar)
{
return new Foo($bar);
}
}
您可以在定义时加载 FooFactory
:
return [
// 不推荐!
Foo::class => DI\factory([new FooFactory, 'create']),
];
但是即使没有使用,工厂也会在每个请求上创建(new FooFactory
) 。此外,使用这种方法很难在工厂中传递依赖关系。
推荐的解决方案是让容器创建工厂:
return [
Foo::class => DI\factory([FooFactory::class, 'create']),
// 替代语法:
Foo::class => DI\factory('Namespace\To\FooFactory::create'),
];
上面的配置等效于以下代码:
$factory = $container->get(FooFactory::class);
return $factory->create(...);
如果工厂是静态方法,则非常简单:
class FooFactory
{
public static function create()
{
return ...
}
}
return [
Foo::class => DI\factory([FooFactory::class, 'create']),
];
请注意:
factory([FooFactory::class, 'build'])
: 如果build()
是 static 方法, 则不会创建该对象:FooFactory::build()
将会被静态调用 (正如你想到的一样)- 您可以在数组中设置任何容器条目名称,例如
DI\factory(['foo_bar_baz', 'build'])
(或者:DI\factory('foo_bar_baz::build')
),与其他任何对象一样,允许你配置foo_bar_baz
及其依赖项 - 因为工厂可以是任何PHP callable,所以您也可以使用可调用对象:
DI\factory(InvokableFooFactory::class)
(或者:DI\factory('invokable_foo_factory')
,前提已经在容器中定义。) - 所有闭包都将被PHP-DI视为 工厂,即使它们嵌套在其他定义中,例如
create()
,env()
, 等。 (请参阅 Nesting definitions 部分)
检索请求条目的名称
如果要重复使用同一工厂来创建不同的条目,则可能要检索当前正在解析的条目的名称。你可以通过类型提示注入DI\Factory\RequestedEntry
对象来做到这一点:
use DI\Factory\RequestedEntry;
return [
'Foo' => function (RequestedEntry $entry) {
// $entry->getName() 包含请求的名称
$class = $entry->getName();
return new $class();
},
];
由于 RequestedEntry
是使用类型提示进行注入的,因此可以将其与注入容器或任何其他服务结合使用。工厂参数的顺序无关紧要。
装饰器
当使用具有多个定义文件的系统时,可以使用装饰器覆盖先前的条目:
return [
// 装饰先前在另一个文件中定义的条目
'WebserviceApi' => DI\decorate(function ($previous, ContainerInterface $c) {
return new CachedApi($previous, $c->get('cache'));
}),
];
请阅读 definition overriding guide 以了解有关此内容的更多信息。
对象
使用工厂创建对象非常强大(因为我们可以使用PHP进行任何操作), 但是 DI\create()
有时会更简单。一些例子:
return [
// 实例化Logger类以创建对象
'Logger' => DI\create(),
// 将接口映射到实现
'LoggerInterface' => DI\create('MyLogger'),
// 对条目使用任意名称
'logger.for.backend' => DI\create('Logger'),
];
DI\create()
可用于定义构造函数参数:
return [
'Logger' => DI\create()
->constructor('app.log', DI\get('log.level'), DI\get('FileWriter')),
];
setter/method 注入:
return [
'Database' => DI\create()
->method('setLogger', DI\get('Logger')),
// 您可以两次调用一个方法
'Logger' => DI\create()
->method('addBackend', 'file')
->method('addBackend', 'syslog'),
];
属性注入:
return [
'Foo' => DI\create()
->property('bar', DI\get('Bar')),
];
每个条目只需解析一次,即会在任何使用到的地方注入相同的实例。 就像其他定义一样, 使用 create()
进行对象定义将在需要的时候才加载 (懒加载)。
自动装配对象
如果你启用了 自动装配 ,你可以使用 DI\autowire()
来定制自动装配的方式。
除了不是从头开始配置对象的构建方式,DI\autowire()
的行为与 DI\create()
相似, 我们仅覆盖自动装配所需的内容。
return [
// 当不使用任何选项时,无需将其写入配置文件
'MyLogger' => DI\autowire(),
// 将接口映射到实现(自动装配MyLogger类)
'LoggerInterface' => DI\autowire('MyLogger'),
// 对条目使用任意名称
'logger.for.backend' => DI\autowire('MyLogger'),
];
就像 DI\create()
一样,你可以显示设置构造函数参数:
return [
'Logger' => DI\autowire()
->constructor('app.log', DI\get('log.level'), DI\get('FileWriter')),
];
setter/method 注入:
return [
'Database' => DI\autowire()
->method('setLogger', DI\get('Logger')),
];
属性注入:
return [
'Foo' => DI\autowire()
->property('bar', DI\get('Bar')),
];
您也可以只定义特定的参数: 它允许使用类型提示来定义自动装配无法猜测的参数。
return [
'Logger' => DI\autowire()
// 设置$ filename参数
->constructorParameter('filename', 'app.log')
// 设置$ handler参数
->methodParameter('setHandler', 'handler', DI\get('SyslogHandler')),
];
别名
您可以使用 DI\get()
为另一个条目设置别名:
return [
'doctrine.entity_manager' => DI\get('Doctrine\ORM\EntityManager'),
];
环境变量
你可以通过使用 DI\env()
来获取环境变量的值:
return [
'db1.url' => DI\env('DATABASE_URL'),
// 具有默认值
'db2.url' => DI\env('DATABASE_URL', 'postgresql://user:pass@localhost/db'),
// 默认值为另一个条目
'db2.host' => DI\env('DATABASE_HOST', DI\get('db.host')),
];
字符串表达式
你可以使用 DI\string()
来连接字符串条目:
return [
'path.tmp' => '/tmp',
'log.file' => DI\string('{path.tmp}/app.log'),
];
数组
条目可以是包含简单值或其他条目的数组:
return [
'report.recipients' => [
'bob@example.com',
'alice@example.com',
],
'log.handlers' => [
DI\get('Monolog\Handler\StreamHandler'),
DI\get('Monolog\Handler\EmailHandler'),
],
];
如果您有多个定义文件,则数组还具有其他功能:详情请阅读 definition overriding 。
通配符
你可以使用通配符批量定义条目。将接口绑定到实现可能非常有用:
return [
'Blog\Domain\*RepositoryInterface' => DI\create('Blog\Architecture\*DoctrineRepository'),
];
在我们的示例中,通配符将匹配 Blog\Domain\UserRepositoryInterface
, 并将其映射到
Blog\Architecture\UserDoctrineRepository
.
您最好清楚并了解:
- 通配符在命名空间之间不匹配
- 优先选择完全匹配(即不使用
*
),而不是通配符的匹配(首先,PHP-DI查找完全匹配,然后在通配符中搜索) - 如果出现“冲突” (即2个不同的通配符匹配),则以第一个匹配为准
嵌套定义
您可以将定义嵌套在其他内部,以避免不必要的条目污染容器。例如:
return [
'Foo' => DI\create()
->constructor(DI\string('{root_directory}/test.json'), DI\create('Bar')),
];
请记住,闭包等效于“工厂”定义。 因此, 闭包始终会解释为工厂,即使嵌套在其他定义中也是如此。 如果将匿名函数用于工厂以外的其他内容,则需要将其包装在 DI\value()
中:
return [
'router' => DI\create(Router::class)
->method('setErrorHandler', DI\value(function () {
...
})),
];
当然,这仅适用于配置文件中的闭包。
直接在容器中设置定义
除了在数组中定义条目外,还可以直接在容器中设置它们,如下所示。
$container->set('db.host', 'localhost');
$container->set('My\Class', \DI\create()
->constructor('some raw value')));
但是建议使用数组定义,因为它允许 编译容器. 所有 Container::set()
配置的条目将不会被编译。
另请注意,使用编译容器时,不可能在运行中向容器中添加定义:
$builder = new ContainerBuilder();
$builder->setDefinitionCache(new ApcCache());
$container = $builder->build();
// 您可以设置值
$container->set('foo', 'hello');
$container->set('bar', new MyClass());
// 报错: 使用缓存时,无法使用-> set()设置定义
$container->set('foo', DI\create('MyClass'));
这样做的原因是定义被缓存(而不是值)。如果动态设置定义,则会将其缓存,,这可能会导致非常奇怪的错误(因为动态定义当然是动态的,因此不应缓存它们)。
在这种情况下,将您的定义放在如本文上面所示的数组或文件中。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。