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 loads, it needs to figure out all of the services that should be in the container. Most of the services come from external bundles. But we now know that we can add our own services, like MarkdownHelper
. We're unstoppable!
All of that happens in services.yaml
under the services
key:
... lines 1 - 4 | |
services: | |
... lines 6 - 32 |
This is our spot to add our services. And I want to demystify what the config in this file actually does:
... lines 1 - 4 | |
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. | |
public: false # Allows optimizing the container by removing unused services; this also means | |
# fetching services directly from the container via $container->get() won't work. | |
# The best practice is to be explicit about your dependencies anyway. | |
# 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/{Entity,Migrations,Tests}' | |
# 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 25 - 32 |
All of this - except for the MarkdownHelper
stuff we just added - comes standard with every new Symfony project.
Let's start with _defaults
:
... lines 1 - 4 | |
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. | |
public: false # Allows optimizing the container by removing unused services; this also means | |
# fetching services directly from the container via $container->get() won't work. | |
# The best practice is to be explicit about your dependencies anyway. | |
... lines 13 - 32 |
This is a special key that sets default config values that should be applied to all services that are registered in this file.
For example, autowire: true
means that any services registered in this file should have the autowiring behavior turned on:
... lines 1 - 4 | |
services: | |
# default configuration for services in *this* file | |
_defaults: | |
autowire: true # Automatically injects dependencies in your services. | |
... lines 9 - 32 |
Because yea, you can actually set autowiring to false
if you want. In fact, you could set autowiring
to false
on just one service to override these defaults:
services:
_defaults:
autowire: true
# ...
App\Service\MarkdownHelper:
autowire: false
# ...
The autoconfigure
option is something we'll talk about during the last chapter of this course - but it's not too important:
... lines 1 - 4 | |
services: | |
# default configuration for services in *this* file | |
_defaults: | |
... line 8 | |
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. | |
... lines 10 - 32 |
We'll also talk about public: false
even sooner:
... lines 1 - 4 | |
services: | |
# default configuration for services in *this* file | |
_defaults: | |
... lines 8 - 9 | |
public: false # Allows optimizing the container by removing unused services; this also means | |
# fetching services directly from the container via $container->get() won't work. | |
# The best practice is to be explicit about your dependencies anyway. | |
... lines 13 - 32 |
The point is: we've established a few default values for any services that this file registers. No big deal.
The real magic comes down here with this App\
entry:
... lines 1 - 4 | |
services: | |
... lines 6 - 13 | |
# 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/{Entity,Migrations,Tests}' | |
... lines 19 - 32 |
This says:
Make all classes inside
src/
available as services in the container.
You can see this in real life! Run:
php bin/console debug:autowiring
At the top, yep! Our controller and MarkdownHelper
appear in this list. And any future classes will also show up here, automatically.
But wait! Does that mean that all of our classes are instantiated on every single request? Because, that would be super wasteful!
Sadly... yes! Bah, I'm kidding! Come on - Symfony kicks way more but than that! No: this line simply tells the container to be aware of these classes. But services are never instantiated until - and unless - someone asks for them. So, if we didn't ask for our MarkdownHelper
, it would never be instantiated on that request. Winning!
Oh, and one important thing: each service in the container is instantiated a maximum of once per request. If multiple parts of our code ask for the MarkdownHelper
, it will be created just once, and the same instance will be passed each time. That's awesome for performance: we don't need multiple markdown helpers... even if we need to call parse()
multiple times.
... lines 1 - 4 | |
services: | |
... lines 6 - 13 | |
# 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\: | |
... line 17 | |
exclude: '../src/{Entity,Migrations,Tests}' | |
... lines 19 - 32 |
The exclude
key is not too important: if you know that some classes don't need to be in the container, you can exclude them for a small performance boost in the dev
environment only.
So between _defaults
and this App\
line - which we have given the fancy name - "service auto-registration" - everything just... works! New classes are added to the container and autowiring handles most of the heavy-lifting!
Oh, and this last App\Controller\
part is not important:
... lines 1 - 4 | |
services: | |
... lines 6 - 19 | |
# 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 25 - 32 |
The classes in Controller\
are already registered as services thanks to the App\
section. This adds a special tag
to controllers... which you just shouldn't worry about. Honestly.
Finally, at the bottom, if you need to configure one service, this is where you do it: put the class name, then the config below:
... lines 1 - 4 | |
services: | |
... lines 6 - 25 | |
App\Service\MarkdownHelper: | |
arguments: | |
$logger: '@monolog.logger.markdown' | |
... lines 29 - 32 |
And actually, this is not the class name of the service. It's really the service id... which happens to be equal to the class name. Run:
php bin/console debug:container --show-private
Most services in the container have a "snake case" service id. That's the best-practice for re-usable bundles. But thanks to service auto-registration, our service id's are equal to their class name. I just wanted to point that out.
Thanks to all of this config... well... we don't need to spend much time in this config file! We only need to configure the "special cases" - like we did for MarkdownHelper
.
And actually.. there's a much cooler way to do that! Copy the service id and delete the config:
... lines 1 - 4 | |
services: | |
... lines 6 - 25 | |
App\Service\MarkdownHelper: | |
arguments: | |
$logger: '@monolog.logger.markdown' | |
... lines 29 - 32 |
If we didn't do anything else, Symfony would once-again pass us the "main" Logger object.
Now, add a new key beneath _defaults
called bind
. Then add $markdownLogger
set to @monolog.logger.markdown
:
... lines 1 - 4 | |
services: | |
# default configuration for services in *this* file | |
_defaults: | |
... lines 8 - 13 | |
# setup special, global autowiring rules | |
bind: | |
$markdownLogger: '@monolog.logger.markdown' | |
... lines 17 - 32 |
Copy that argument name, open MarkdownHelper
, and rename the argument from $logger
to $markdownLogger
. Update it below too:
... lines 1 - 8 | |
class MarkdownHelper | |
{ | |
... lines 11 - 14 | |
public function __construct(AdapterInterface $cache, MarkdownInterface $markdown, LoggerInterface $markdownLogger) | |
{ | |
... lines 17 - 18 | |
$this->logger = $markdownLogger; | |
} | |
... lines 21 - 35 | |
} |
Ok: markdown.log
still only has one line. And... refresh! Check the file... hey! It worked!
I love bind
: it says:
If you find any argument named
$markdownLogger
, pass this service to it.
And because we added it to _defaults
, it applies to all our services. Instead of configuring our services one-by-one, we're creating project-wide conventions. Next time you need this logger? Yep, just name it $markdownLogger
and keep coding.
Next! In addition to services, the container can also hold flat configuration: called parameters.
Got the same issue. The markdown_logging
handler is only logging the markdown
channel but the main
handler is logging anything except the event
channel. You have to exclude the markdown
channel from the main
handler within the monolog.yaml
.
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event", "!markdown"]
Hey NickReynke!
Ah, great work! This was on oversight on my part! And your solution is perfect.
Cheers!
Thanks for reply. I also found this solution but it looks like its a bad design. Because when I am specifying a channel to log, why it should also publish it on main log file by default.
The `markdown` channel, when created within a new handler (`markdown_logging` in this case), is also logged by the `main` handler because the `main` handler is by default only excluding the `event` channel, but not the new `markdown` channel.
I just read the monolog docs here: https://symfony.com/doc/4.4...
And it seems bind is obsolete in this case.
Just the parameter name hinting is sufficient since 3.5. No change to services.yaml required.
I removed the bind, cleared the cache and ... it works.
Did I miss something?
Hey Richard
Cool! So the binding it's now done by the bundle automatically, you just need to remember the convention. That feature didn't exist when this tutorials was written :)
Cheers!
I added Service to the excludes to test in services.yaml:
exclude: '../src/{Service,Entity,Migrations,Tests}'
And it still found MarkdownHelp in the Service directory. Bug? (I cleared the cache too).
Hey Richard
I believe you still have the custom configuration for the MarkdownHelp class. Could you double check it?
Cheers!
I believe you may have been right.. but worse than that... I had "Services" and not "Service" in the exclude. So ignore the noise...
Hello!
I am trying to use lazy loading, symfony 4.3,
configuring in services.yaml
services:
# Default configuration for all services.
_defaults:
# Automatically injects dependencies in the container.
autowire: false
# Automatically registers the services as commands, event subscribers, etc.
autoconfigure: true
# Prevents access to the injected files to be called from the container.
public: false
# Injects whether the application runs in debug mode. (use bool $isDebug in constructor)
bind:
$isDebug: '%kernel.debug%'
# Configuration for the dependency injection for App namespace.
App\:
lazy: true
public: false
autowire: true
resource: '../common/App/*'
#exclude: '../common/App/{Entity}'
App\Helpers\:
lazy: true
public: true
autowire: true
resource: '../common/App/Helpers/*'
When trying to call:
$container->get("App\Helper\Language")
I get error:
UndefinedMethodException
Attempted to call an undefined method named "staticProxyConstructor" of class "Language_2c90222"
Any idea?
Thanks
Hey Avraham,
Are you sure you need to lazy loading *everything* in your application? It's not a good practice, actually, I think it's just overkill. Well, except service you also have entities, models, forms, etc - they probably don't need to be lazy loaded. I'd recommend you to start configuring lazy loading for 1 service where you think lazy loading is the most important, get it working, then switch to another class that also should be lazy loaded, and so one. Try to configure it class by class - it would be easier to debug things I think.
About the error, first of all, try to clear the cache the the specific environment where you see the error, and try again. Do you see the same error? Do you have a staticProxyConstructor() method call somewhere in *your* code? If no, probably some version incompatibility, could you try to update Composer dependencies, clear the cache, and try again?
Cheers!
Hey Viktor,
Thanks for your advice.
Yes, I turn off lazy option.I succeed to obtain $entityManager = $this->getDoctrine()->getManager();
Yet still MappingException: Class "App\Entity\Patient" is not a valid entity or mapped super class.<br />$patient = new Patient();<br />$patient->setFname('Alex1');<br />// FAILS<br />// MappingException: Class "App\Entity\Patient" is not a valid entity or mapped super class.<br />$entityManager->persist($patient); <br />
Great, let's follow the second part of your comment in another thread to avoid duplications: https://symfonycasts.com/sc...
Cheers!
I have a question about 'Services are only Instantiated Once'.
What if we need to process some data in our service (for example some Converter) and we don't want to pass all the data (some arrays for example) to each method so we make this class keep the state in properties (but also uses some services from container)?
I saw that there is an option: shared: false
to Define Non Shared Services so it will create a new instance every time and I also found on the stackoverflow similar question: https://stackoverflow.com/questions/28511553/symfony2-services-with-or-without-state but there were just two answers.
So how should we do something like that? Use Non Shared Services; create stateless services, inject dependencies and pass them to statefull classes created by 'new' keyword inside service or some other way?
Hey Agata,
Sorry for the long reply! Yes, using "shared: false" is a valid way to achieve what you need, and it sounds OK to me. I don't know your design to much, but if you think there's no other way and you do need to use different objects in different places instead of one shared - go for it.
Cheers!
Hey,
When i try to test if it wil log but i get this weird error <br />invalidArgumentException<br />Invalid service "App\Service\MarkdownHelper": method "__construct()" has no argument named "$logger". Check your service definition.
even though i follow exactly like the tutorials and i can't seem to figure out where the problem lies.
Any solution why?
UPDATE
Found the solution i still had some code in the services.yaml after i quoted that out it works :)
`
App\Service\MarkdownHelper:
arguments:
$logger: '@monolog.logger.markdown'
`
Forgot this bit now it's disabled.
Hi.
I had used bind configuration con my project. I am using Symfony 4.1 and when I add more than one I get this error:
```
Unused binding "$variableName" in service ".abstract.instanceof.App\Twig\AppExtension".
```
And that is right, I don't inject this variable into that twig extension because I do not use it there
I found this question but without a correct answer: https://openclassrooms.com/...
Did you now something about this?
Side note: if I remove the bind variable or inject this variable into my AppExtension constructor it works. But this not make any sense
Hey micayael!
Ah yes, I can answer this! My guess is that, you have some configuration that looks like this:
# config/services.yaml
services:
_defaults:
# ...
# actually, in your code, this may appear beneath an _instanceof config, instead of _defaults. But,
# the situation is still the same
bind:
$variableName: 'someValue'
When you use bind, to help make sure you don't have a typo, you MUST have a $variableName
argument (with that name) in the constructor of one of your services. If you have ZERO arguments with this name, then, because this might be a typo, Symfony throws an exception. However, there is a small bug in the exception message. Really, the message should say:
Hey! You have a "bind" configured for an argument named $variableName. But, I didn't find this argument in any of your services. Do you maybe have a typo? Or is that bind unused?
The bug is that, internally, Symfony "attaches" the error to one specific service - in your case some internal ".abstract.instanceof.App\Twig\AppExtension".
So, the fix is to either (A) make sure that each bind is used on at least one service that it's bound to or (B) remove the bind because it's unused.
Let me know if that helps!
Cheers!
I have this configuration
~~~
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.
public: false # Allows optimizing the container by removing unused services; this also means
# fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway.
bind:
$variableName1: '%param1%'
$variableName2: '%param2%'
$variableName3: '@csa_guzzle.client.api'
~~~
The 3 are in at least one constructor. The one that gives me problems is injected into a command (is a parameter). Could this be a problem?
I got it
There was the old configuration that I was using before trying with your tutorial
App\Command\GreatCommand:
arguments:
$variableName2: '%param2%'
tags:
- { name: 'console.command' }
I deleted it and it worked. Thanks
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.1.4
"symfony/asset": "^4.0", // v4.0.4
"symfony/console": "^4.0", // v4.0.14
"symfony/flex": "^1.0", // v1.17.6
"symfony/framework-bundle": "^4.0", // v4.0.14
"symfony/lts": "^4@dev", // dev-master
"symfony/twig-bundle": "^4.0", // v4.0.4
"symfony/web-server-bundle": "^4.0", // v4.0.4
"symfony/yaml": "^4.0" // v4.0.14
},
"require-dev": {
"easycorp/easy-log-handler": "^1.0.2", // v1.0.4
"symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
"symfony/dotenv": "^4.0", // v4.0.14
"symfony/maker-bundle": "^1.0", // v1.0.2
"symfony/monolog-bundle": "^3.0", // v3.1.2
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.0.4
}
}
Its logging on markdown.log and dev.log.
Any solution?