支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
如果你在Symfony 3.3的一个新项目中看一看 services.yml
文件,你会发现有一些重大改变: _defaults
, autowiring
, autoconfigure
还有其他更多。这些功能被设计为 automate configuration(自动配置)以令开发更快速,并且未牺牲可预测性,此点非常重要!另一个目标,是要让控制器和服务牢不可破。在In Symfony 3.3中,控制器默认 即是
服务。
文档已经更新,并且假设你已开启了这些新功能。如果你是一个Symfony老用户,十分希望了解这些改变背后的“为什么”和“是什么”,那么本文非常适合你!
最重要的一点, 你现在就可以升到Symfony 3.3而毋须对自己的程序做出任何改变。Symfony有很严格的 backwards compatibility promise(向下兼容承诺),可以确保你在微小版本的升级过程中万无一失。
所有新功能都是 optional(可选): 它们默认并未开启,因此你需要在配置文件中做出改变方可使用它们。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | services:
# default configuration for services in *this* file
# 作用于 *本文件* 之内的服务的默认配置
_defaults:
# automatically injects dependencies in your services
# 在你的服务中自动注入依赖
autowire: true
# automatically registers your services as commands, event subscribers, etc.
# 将服务自动注册为命令,事件订阅器,等等
autoconfigure: true
# this means you cannot fetch services directly from the container via $container->get()
# if you need to do this, you can override this setting on individual services
# 表示不可以通过 $container->get() 直接从容器中取出服务
# 如果你需要这样配置,你可以在单一服务的配置中覆写此项
public: false
# makes classes in src/AppBundle available to be used as services
# this creates a service per class whose id is the fully-qualified class name
# 让 src/AppBundle 中的所有类变成服务
# 以这种方式创建服务时使用类的FQCN作为服务id
AppBundle\:
resource: '../../src/AppBundle/*'
# you can exclude directories or files
# but if a service is unused, it's removed anyway
# 你可以排除目录或文件,但如果某个服务未被使用,它会被移除
exclude: '../../src/AppBundle/{Entity,Repository}'
# controllers are imported separately to make sure they're public
# and have a tag that allows actions to type-hint services
# 逐控制器地分别地导入服务,以确保其是公有的
# 并且拥有一个标签,以允许actions能够对服务进行type-hint(类型提示)
AppBundle\Controller\:
resource: '../../src/AppBundle/Controller'
public: true
tags: ['controller.service_arguments']
# add more services, or override services that need manual wiring
# 添加更多服务,或覆写那些需要手动关联的服务
# AppBundle\Service\ExampleService:
# arguments:
# $someArgument: 'some_value' |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults autowire="true" autoconfigure="true" public="false" />
<prototype namespace="AppBundle\" resource="../../src/AppBundle/*" exclude="../../src/AppBundle/{Entity,Repository}" />
<prototype namespace="AppBundle\Controller" resource="../../src/AppBundle/Controller" public="true">
<tag name="controller.service_arguments" />
</prototype>
</services>
</container> |
1 2 3 4 5 6 7 8 9 10 | // app/config/services.php
// _defaults and loading entire directories is not possible with PHP configuration
// you need to define your services one-by-one
use AppBundle\Controller\DefaultController;
$container->autowire(DefaultController::class)
->setAutoconfigured(true)
->addTag('controller.service_arguments')
->setPublic(true); |
这一小段配置,包含了“Symfony中的服务是如何配置的”之范本级(写法)改变。
第一个重大改变是,服务不需要再逐个定义了,这得益于以下配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # app/config/services.yml
services:
# ...
# makes classes in src/AppBundle available to be used as services
# this creates a service per class whose id is the fully-qualified class name
# 让 src/AppBundle 下面的类可以作为服务来使用
# 这把每一个类都创建为服务,该服务的id就是类的FQCN(完整类名)
AppBundle\:
resource: '../../src/AppBundle/*'
# you can exclude directories or files
# but if a service is unused, it's removed anyway
# 你可以排除目录或文件,但如果某个服务未被使用,它会被移除
exclude: '../../src/AppBundle/{Entity,Repository}' |
1 2 3 4 5 6 7 8 9 10 11 12 13 | <!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- ... -->
<prototype namespace="AppBundle\" resource="../../src/AppBundle/*" exclude="../../src/AppBundle/{Entity,Repository}" />
</services>
</container> |
1 2 3 4 5 | // app/config/services.php
// services cannot be automatically loaded with PHP configuration
// you need to define your services one-by-one
// PHP配置方式,服务无法被自动加载,你需要手动逐个定义服务 |
这意味着,在 src/AppBundle/
之下的每一个类都 可以 当成一个服务来使用。多亏了配置文件顶部的 _defaults
区段,所有这些服务,都是 autowired(自动关联)以及 private (私有,即 public: false
) 的。
它们的服务id,等同于类名 (即 AppBundle\Service\InvoiceGenerator
)。在Symfony 3.3中你也许注意到另一项改变: 我们推荐你使用类名作为服务id,除非你有 同一个类作为多个服务 这种情况。
但是这如何能让容器获知我的服务的参数?
由于服务已经 自动关联,容器能够自动确定多数参数。但是你始终可以覆写某个服务,并且能够 手动配置参数,或是(手动配置)服务中的任何特别之处。
但是慢着,如果我在
src/AppBundle/
目录下有一些model类(非服务),这不就把它们也都给注册成服务了吗?这难道不是问题吗?
这真的 不是 问题。由于所有的新服务都是 private (这得益于 _defaults
),如果任何一个服务 没有 被你的代码使用,它们会自动地从已编译的容器中移除。这意味着你的容器中的服务之数量,和你显式配置每一个服务或通过此种方式加载进全部服务是 相同 的。
Ok,但是我是否可以排除一些我知道的不含服务的路径?
是的! exclude
键接收一个glob pattern,可用于那些你 不希望 被当作服务的 blacklist (黑名单)路径。但是,由于未使用的服务会从容器中自动删除, exclude
也就不那么重要了。最大的好处是,那些路径将不会被容器所 跟踪,这样一来可能导致容器在 dev
环境下不是那么频繁地被重新构建。
第二个大改变是,对于你注册的所有服务,自动关联被开启了(通过 _defaults
)。 这也意味着现在的服务id 不太 重要了,而 "类型(types)" (即,类或接口的名称)变得 更加 重要。
例如,在Symfony 3.3之前 (现在仍然可以),你可以把一个服务作为参数传到另一个服务中,像下面这样的配置:
1 2 3 4 5 6 7 8 9 | # app/config/services.yml
services:
app.invoice_generator:
class: AppBundle\Service\InvoiceGenerator
app.invoice_mailer:
class: AppBundle\Service\InvoiceMailer
arguments:
- '@app.invoice_generator' |
1 2 3 4 5 6 7 8 9 | <!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
TODO
</container> |
1 2 3 | // app/config/services.php
TODO |
To pass the InvoiceGenerator
as an argument to InvoiceMailer
, you needed
to specify the service's id as an argument: app.invoice_generator
. Service
id's were the main way that you configured things.
But in Symfony 3.3, thanks to autowiring, all you need to do is type-hint the
argument with InvoiceGenerator
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // src/AppBundle/Service/InvoiceMailer.php
// ...
class InvoiceMailer
{
private $generator;
public function __construct(InvoiceGenerator $generator)
{
$this->generator = $generator
}
// ...
} |
That's it! Both services are automatically registered
and set to autowire. Without any configuration, the container knows to pass the
auto-registered AppBundle\Service\InvoiceGenerator
as the first argument. As
you can see, the type of the class - AppBundle\Service\InvoiceGenerator
- is
what's most important, not the id. You request an instance of a specific type and
the container automatically passes you the correct service.
Isn't that magic? How does it know which service to pass me exactly? What if I have multiple services of the same instance?
The autowiring system was designed to be super predictable. It first works by looking for a service whose id exactly matches the type-hint. This means you're in full control of what type-hint maps to what service. You can even use service aliases to get more control. If you have multiple services for a specific type, you choose which should be used for autowiring. For full details on the autowiring logic, see Autowiring Logic Explained.
But what if I have a scalar (e.g. string) argument? How does it autowire that?
If you have an argument that is not an object, it can't be autowired. But that's ok! Symfony will give you a clear exception (on the next refresh of any page) telling you which argument of which service could not be autowired. To fix it, you can manually configure *just* that one argument. This is the philosophy of autowiring: only configure the parts that you need to. Most configuration is automated.
Ok, but autowiring makes your applications less stable. If you change one thing or make a mistake, unexpected things might happen. Isn't that a problem?
Symfony has always valued stability, security and predictability first. Autowiring was designed with that in mind. Specifically:
Autowiring aims to automate configuration without magic.
The third big change is that, in a new Symfony 3.3 project, your controllers are services:
1 2 3 4 5 6 7 8 9 10 | # app/config/services.yml
services:
# ...
# controllers are imported separately to make sure they're public
# and have a tag that allows actions to type-hint services
AppBundle\Controller\:
resource: '../../src/AppBundle/Controller'
public: true
tags: ['controller.service_arguments'] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- ... -->
<prototype namespace="AppBundle\Controller" resource="../../src/AppBundle/Controller" public="true">
<tag name="controller.service_arguments" />
</prototype>
</services>
</container> |
1 2 3 4 5 6 7 8 9 10 | // app/config/services.php
// loading entire directories is not possible with PHP configuration
// you need to define your services one-by-one
use AppBundle\Controller\DefaultController;
$container->autowire(DefaultController::class)
->setAutoconfigured(true)
->addTag('controller.service_arguments')
->setPublic(true); |
But, you might not even notice this. First, your controllers can still extend
the same base Controller
class or a new AbstractController.
This means you have access to all of the same shortcuts as before. Additionally,
the @Route
annotation and _controller
syntax (e.g.``AppBundle:Default:homepage``)
used in routing will automatically use your controller as a service (as long as its
service id matches its class name, which it does in this case). See How to Define Controllers as Services
for more details. You can even create invokable controllers
In other words, everything works the same. You can even add the above configuration to your existing project without any issues: your controllers will behave the same as before. But now that your controllers are services, you can use dependency injection and autowiring like any other service.
To make life even easier, it's now possible to autowire arguments to your controller action methods, just like you can with the constructor of services. For example:
1 2 3 4 5 6 7 8 9 | use Psr\Log\LoggerInterface;
class InvoiceController extends Controller
{
public function listAction(LoggerInterface $logger)
{
$logger->info('A new way to access services!');
}
} |
This is only possible in a controller, and your controller service must be tagged
with controller.service_arguments
to make it happen. This new feature is used
throughout the documentation.
In general, the new best practice is to use normal constructor dependency injection
(or "action" injection in controllers) instead of fetching public services via
$this->get()
(though that does still work).
The last big change is the autoconfigure
key, which is set to true
under
_defaults
. Thanks to this, the container will auto-tag services registered in
this file. For example, suppose you want to create an event subscriber. First, you
create the class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // src/AppBundle/EventSubscriber/SetHeaderSusbcriber.php
// ...
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class SetHeaderSusbcriber implements EventSubscriberInterface
{
public function onKernelResponse(FilterResponseEvent $event)
{
$event->getResponse()->headers->set('X-SYMFONY-3.3', 'Less config');
}
public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => 'onKernelResponse'
];
}
} |
Great! In Symfony 3.2 or lower, you would now need to register this as a service
in services.yml
and tag it with kernel.event_subscriber
. In Symfony 3.3,
you're already done! The service is automatically registered.
And thanks to autoconfigure
, Symfony automatically tags the service because
it implements EventSubscriberInterface
.
That sounds like magic - it automatically tags my services?
In this case, you've created a class that implements EventSubscriberInterface
and registered it as a service. This is more than enough for the container to know
that you want this to be used as an event subscriber: more configuration is not needed.
And the tags system is its own, Symfony-specific mechanism. And of course, you can
always default autowire
to false in services.yml
, or disable it for a specific
service.
Does this mean tags are dead? Does this work for all tags?
This does not work for all tags. Many tags have required attributes, like event listeners, where you also need to specify the event name and method in your tag. Autoconfigure works only for tags without any required tag attributes, and as you read the docs for a feature, it'll tell you whether or not the tag is needed. You can also look at the extension classes (e.g. FrameworkExtension for 3.3.0) to see what it autoconfigures.
What if I need to add a priority to my tag?
Many autoconfigured tags have an optional priority. If you need to specify a priority (or any other optional tag attribute), no problem! Just manually configure your service and add the tag. Your tag will take precedent over the one added by auto-configuration.
Symfony is unique because it has a compiled container. This means that there is no runtime performance impact for using any of these features. That's also why the autowiring system can give you such clear errors.
However, there is some performance impact in the dev
environment. Most importantly,
your container will likely be rebuilt more often when you modify your service classes.
This is because it needs to rebuild whenever you add a new argument to a service,
or add an interface to your class that should be autoconfigured.
In very big projects, this may be a problem. If it is, you can always opt to not use autowiring. If you think the cache rebuilding system could be smarter in some situation, please open an issue!
Ready to upgrade your existing project? Great! Suppose you have the following configuration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # app/config/services.yml
services:
app.github_notifier:
class: AppBundle\Service\GitHubNotifier
arguments:
- '@app.api_client_github'
markdown_transformer:
class: AppBundle\Service\MarkdownTransformer
app.api_client_github:
class: AppBundle\Service\ApiClient
arguments:
- 'https://api.github.com'
app.api_client_sl_connect:
class: AppBundle\Service\ApiClient
arguments:
- 'https://connect.sensiolabs.com/api' |
It's optional, but let's upgrade this to the new Symfony 3.3 configuration step-by-step, without breaking our application.
Start by adding a _defaults
section with autowire
and autoconfigure
.
1 2 3 4 5 6 7 | # app/config/services.yml
services:
+ _defaults:
+ autowire: true
+ autoconfigure: true
# ... |
This step is easy: you're already explicitly configuring all of your services.
So, autowire
does nothing. You're also already tagging your services, so
autoconfigure
also doesn't change any existing services.
You have not added public: false
yet. That will come in a minute.
Right now, the service ids are machine names - e.g. app.github_notifier
. To
work well with the new configuration system, your service ids should be class names,
except when you have multiple instances of the same service.
Start by updating the service ids to class names:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # app/config/services.yml
services:
# ...
- app.github_notifier:
- class: AppBundle\Service\GitHubNotifier
+ AppBundle\Service\GitHubNotifier:
arguments:
- '@app.api_client_github'
- markdown_transformer:
- class: AppBundle\Service\MarkdownTransformer
+ AppBundle\Service\MarkdownTransformer: ~
# keep these ids because there are multiple instances per class
app.api_client_github:
# ...
app.api_client_sl_connect:
# ... |
But, this change will break our app! The old service ids (e.g. app.github_notifier
)
no longer exist. The simplest way to fix this is to find all your old service ids
and update them to the new class id: app.github_notifier
to AppBundle\Service\GitHubNotifier
.
In large projects, there's a better way: create legacy aliases that map the old id
to the new id. Create a new legacy_aliases.yml
file:
1 2 3 4 5 6 7 | # app/config/legacy_aliases.yml
services:
# aliases so that the old service ids can still be accessed
# remove these if/when you are not fetching these directly
# from the container via $container->get()
app.github_notifier: '@AppBundle\Service\GitHubNotifier'
markdown_transformer: '@AppBundle\Service\MarkdownTransformer' |
Then import this at the top of services.yml
:
1 2 3 4 5 | # app/config/services.yml
+ imports:
+ - { resource: legacy_aliases.yml }
# ... |
That's it! The old service ids still work. Later, (see the cleanup step below), you can remove these from your app.
Now you're ready to default all services to be private:
1 2 3 4 5 6 7 8 | # app/config/services.yml
# ...
services:
_defaults:
autowire: true
autoconfigure: true
+ public: false |
Thanks to this, any services created in this file cannot be fetched directly from
the container. But, since the old service id's are aliases in a separate file (legacy_aliases.yml
),
these are still public. This makes sure the app keeps working.
If you did not change the id of some of your services (because there are multiple instances of the same class), you may need to make those public:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # app/config/services.yml
# ...
services:
# ...
app.api_client_github:
# ...
+ # remove this if/when you are not fetching this
+ # directly from the container via $container->get()
+ public: true
app.api_client_sl_connect:
# ...
+ public: true |
This is to guarantee that the application doesn't break. If you're not fetching these services directly from the container, this isn't needed. In a minute, you'll clean that up.
You're now ready to automatically register all services in src/AppBundle/
(and/or any other directory/bundle you have):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # app/config/services.yml
services:
_defaults:
# ...
+ AppBundle\:
+ resource: '../../src/AppBundle/*'
+ exclude: '../../src/AppBundle/{Entity,Repository}'
+
+ AppBundle\Controller\:
+ resource: '../../src/AppBundle/Controller'
+ public: true
+ tags: ['controller.service_arguments']
# ... |
That's it! Actually, you're already overriding and reconfiguring all the services
you're using (AppBundle\Service\GitHubNotifier
and AppBundle\Service\MarkdownTransformer
).
But now, you won't need to manually register future services.
Once again, there is one extra complication if you have multiple services of the same class:
1 2 3 4 5 6 7 8 9 10 11 12 13 | # app/config/services.yml
services:
# ...
+ # alias ApiClient to one of our services below
+ # app.api_client_github will be used to autowire ApiClient type-hints
+ AppBundle\Service\ApiClient: '@app.api_client_github'
app.api_client_github:
# ...
app.api_client_sl_connect:
# ... |
This guarantees that if you try to autowire an ApiClient
instance, the app.api_client_github
will be used. If you don't have this, the auto-registration feature will try to
register a third ApiClient
service and use that for autowiring (which will fail,
because the class has a non-autowireable argument).
To make sure your application didn't break, you did some extra work. Now it's time
to clean things up! First, update your application to not use the old service id's (the
ones in legacy_aliases.yml
). This means updating any service arguments (e.g.
@app.github_notifier
to @AppBundle\Service\GitHubNotifier
) and updating your
code to not fetch this service directly from the container. For example:
1 2 3 4 5 6 7 8 9 | - public function indexAction()
+ public function indexAction(GitHubNotifier $gitHubNotifier, MarkdownTransformer $markdownTransformer)
{
- // the old way of fetching services
- $githubNotifier = $this->container->get('app.github_notifier');
- $markdownTransformer = $this->container->get('markdown_transformer');
// ...
} |
As soon as you do this, you can delete legacy_aliases.yml
and remove its import.
You should do the same thing for any services that you made public, like
app.api_client_github
and app.api_client_sl_connect
. Once you're not fetching
these directly from the container, you can remove the public: true
flag:
1 2 3 4 5 6 7 8 9 10 11 | # app/config/services.yml
services:
# ...
app.api_client_github:
# ...
- public: true
app.api_client_sl_connect:
# ...
- public: true |
Finally, you can optionally remove any services from services.yml
whose arguments
can be autowired. The final configuration looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | services:
_defaults:
autowire: true
autoconfigure: true
public: false
AppBundle\:
resource: '../../src/AppBundle/*'
exclude: '../../src/AppBundle/{Entity,Repository}'
AppBundle\Controller\:
resource: '../../src/AppBundle/Controller'
public: true
tags: ['controller.service_arguments']
AppBundle\Service\GitHubNotifier:
# this could be deleted, or I can keep being explicit
arguments:
- '@app.api_client_github'
# alias ApiClient to one of our services below
# app.api_client_github will be used to autowire ApiClient type-hints
AppBundle\Service\ApiClient: '@app.api_client_github'
# keep these ids because there are multiple instances per class
app.api_client_github:
class: AppBundle\Service\ApiClient
arguments:
- 'https://api.github.com'
app.api_client_sl_connect:
class: AppBundle\Service\ApiClient
arguments:
- 'https://connect.sensiolabs.com/api' |
You can now take advantage of the new features going forward.
本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。