If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWhen Symfony creates its container, it needs to get a big list of every service that should be in the container: each service's id, class name and the arguments that should be passed to its constructor. It gets this big list from exactly two places. The first - and the biggest - is from bundles.
If we run:
php bin/console debug:container
The vast majority of these services come from bundles. Each bundle has a list of the services it provides, which includes the id, class name and arguments for each one.
The second place the container goes to complete its list of services is our src/
directory. We already know that MarkdownHelper
is in the service container because we've been able to autowire it to our controller.
So when the container is being created, it asks each bundle for its service list and then - to learn about our services - it reads services.yaml
.
When Symfony starts parsing this file, nothing in the src/
directory has been registered as a service in the container. Adding our classes to the container is, in fact, the job of this file. And the way it does it is pretty amazing.
_defaults
KeyThe first thing under services
is a special key called _defaults
:
... lines 1 - 8 | |
services: | |
# default configuration for services in *this* file | |
_defaults: | |
autowire: true # Automatically injects dependencies in your services. | |
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. | |
... lines 14 - 33 |
This defines default options that should be applied to each service that's added to the container in this file. Every service registered here will have an option called autowire
set to true
and another called autoconfigure
set to true
.
Let me... say that a different way. When you configure a single service - like we're doing for MarkdownHelper
- it's totally legal to say autowire: true
or autowire: false
. That's an option that you can configure on any service. The _defaults
sections says:
Hey! Don't make me manually add
autowire: true
to every service - make that the default value.
What does the autowire
option mean? Simply, it tells Symfony's container:
Please try to guess the arguments to my constructor by reading their type-hints.
We like that feature, so it will be "on" automatically for all of our services. The other option - autoconfigure
- is more subtle and we'll talk about it later.
So these 3 lines don't do anything: they just set up default config.
The next section - these 3 lines starting with App\
- are the key to everything:
... lines 1 - 8 | |
services: | |
... lines 10 - 14 | |
# makes classes in src/ available to be used as services | |
# this creates a service per class whose id is the fully-qualified class name | |
App\: | |
resource: '../src/*' | |
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' | |
... lines 20 - 33 |
This says:
Hey container! Please look at my
src/
directory and register every class you find as a service in the container.
And when it does this, thanks to _defaults
, every service will have autowire
and autoconfigure
enabled. This is why MarkdownHelper
was instantly available as a service in the container and why its arguments are being autowired. This is called "service auto-registration".
But remember, every service in the container needs to have a unique id. When you auto-register services like this, the id matches the class name. We can see this! The vast majority of the services in debug:container
have a snake-case id. But if you go all the way to the top, our services are also in this list each service ID is identical to its class name.
This is done to keep life simple... but also because it powers autowiring. If we try to autowire App\Service\MarkdownHelper
into our controller or another service, in order to figure out what to pass to that argument, autowiring looks in the container for a service whose id exactly matches the type-hint: App\Service\MarkdownHelper
.
Anyways, back in services.yaml
, after the _defaults
section and this App\
block, we have now registered every class in the src/
directory as a service and told Symfony to autowire each one.
But do we really want every class in src/
to be a service? Actually, no. Not all classes are services and that's what the exclude:
key helps with. For example, the Entity/
directory will eventually store database model classes, which are not services: they're just classes that hold some data.
So we register everything in src/
as a service, except for things in these directories. And actually, the exclude
key is not that important. Heck, you could delete it! If you accidentally registered something as a service that is not a service, Symfony will realize that when you never use it, and remove it automatically from the container. No big deal.
The point is: everything in src/
is automatically available as a service in the container without you needing to think about it.
And... that's really it for the important stuff! The next section registers everything in src/Controller
as a service:
... lines 1 - 8 | |
services: | |
... lines 10 - 20 | |
# controllers are imported separately to make sure services can be injected | |
# as action arguments even if you don't extend any base controller class | |
App\Controller\: | |
resource: '../src/Controller' | |
tags: ['controller.service_arguments'] | |
... lines 26 - 33 |
But wait... didn't the section above already do that? Totally! This overrides those in order to add this "tag" thing. This is here to cover an "edge case" that doesn't apply to us. If we deleted this, everything would keep working. So... ignore it.
Now that we understand how our services are being added to the container, the config that we added to the bottom of this file will make more sense. Let's talk about it next and then leverage our new knowledge to learn a way cooler way to pass the $isDebug
flag to MarkdownHelper
.
Hey MattWelander!
Oh... timezones :p. I am far from an expert on these, but I believe that DateTime
objects create their timezone based on the php setting, independent of the locale
parameter. I'm not aware of these two things playing together in any way, but I could be wrong.
It also seems like a browser will actually pass timezone information, because my server has been set to UTC, but when users in my timezone post a form with a date (from the html5 datepicker) it seems that the timezone comes in correctly.
Hmm. Even if the locale were somehow picked up from a request header or something similar (this is possible - you could, for example, have some code that looks for the Accept
language header and calls $request->setLocale()
based on this), that shouldn't affect the DateTime objects. My guess is that this info is being passed to your form someone from the Datepicker. I'd look closely at the raw POST parameters that are sent when you submit the form to see if the submitted datetime string has some sort of timezone in it.
Sorry I can't give you more concrete answers - this isn't an area that I'm an expert in!
Cheers!
// composer.json
{
"require": {
"php": "^7.3.0 || ^8.0.0",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"sensio/framework-extra-bundle": "^6.0", // v6.2.1
"sentry/sentry-symfony": "^4.0", // 4.0.3
"symfony/asset": "5.0.*", // v5.0.11
"symfony/console": "5.0.*", // v5.0.11
"symfony/debug-bundle": "5.0.*", // v5.0.11
"symfony/dotenv": "5.0.*", // v5.0.11
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/framework-bundle": "5.0.*", // v5.0.11
"symfony/monolog-bundle": "^3.0", // v3.6.0
"symfony/profiler-pack": "*", // v1.0.5
"symfony/routing": "5.1.*", // v5.1.11
"symfony/twig-pack": "^1.0", // v1.0.1
"symfony/var-dumper": "5.0.*", // v5.0.11
"symfony/webpack-encore-bundle": "^1.7", // v1.8.0
"symfony/yaml": "5.0.*" // v5.0.11
},
"require-dev": {
"symfony/maker-bundle": "^1.15", // v1.23.0
"symfony/profiler-pack": "^1.0" // v1.0.5
}
}
Hey, I have a question regarding locale set in the services.yaml file.
parameters:
Does this in any way affect how datetime objects are handled by symfony, or is that purely determined by the server php setting?
It also seems like a browser will actually pass timezone information, because my server has been set to UTC, but when users in my timezone post a form with a date (from the html5 datepicker) it seems that the timezone comes in correctly. Don't know whether this is from the browser's datetime-picker or whether the application picks it up from the request headers locale?