In Symfony 3.0 (and I'm sure 2.X as well), if I want to make a custom constraint validator with a dependency, I have to register that validator as a service in the dependency injection container (which is described by default in project_directory/app/config/services.yml) using a special tag ( as described here).
This means that the Validator component must know where to look for the service container. This issue also comes up for the ControllerResolver. Since controllers can be registered as services, the ControllerResolver must know where the service container is.
How do the Symfony components know where to look for the service container, and how can I configure this? I ask because I want to build a custom framework using the Symfony components, which means I'll be making my own service container, and I'd like to be able to point the Validator and the ControllerResolver to that service container.
In Symfony Standard Edition, the container is initialised by the Kernel. Have a look at the AppKernel and its parent class.
The kernel loads a configuration file in the registerContainerConfiguration() method:
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
}
This code will load an environment specific configuration file (config_prod.yml, config_dev.yml etc). In a standard setup that file imports the main config.yml file.
The services.yml file is loaded with an import in config.yml:
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }
If you're thinking of building your own framework (which is a good learning experience), you'll need to read more code of existing frameworks first. Also, it's worth to read the excellent Create your own PHP framework.
This means that the Validator component must know where to look for the service container
This is scary. I see no connection between validation and the service container. I'd think of a better design.
P.S. Everyone should write their own framework once. The next step should be deleting it.
Related
I thought I fully understood Symfony's autowiring behaviour, but there must be something I'm missing and I'm hoping someone can fill in the blanks.
Three questions to begin with:
Is there a difference in the Symfony autowire behaviour in the main application vs in a bundle?
If you ask Symfony to autowire bundle services, does it completely ignore service definitions in the bundles services.yaml file?
Are there known issues with the setter injection functionality in Symfony 4?
I have a service definition within a bundle that uses setter injection but it seems to be completely ignored by Symfony, when I've asked Symfony to autowire my bundle services and even when I've asked Symfony to exclude services from the autowiring.
My application is using Symfony v4.1.3.
I have included my Bundle in my applications bundles.php file.
<?php
return [
//... core bundles,
Acme\\Symfony\\AcmeCustomBundle\\AcmeCustomBundle::class => ['all' => true]
];
In the default Symfony application services.yaml file, I have asked Symfony to autowire my bundles services with the following:
Acme\Symfony\AcmeCustomBundle\:
resource: '../vendor/acme-symfony/custom-bundle/*'
exclude: '../vendor/acme-symfony/custom-bundle/{Model,Tests}'
In my bundles services.yaml file located in ../vendor/acme-symfony/custom-bundle/Resources/config/services.yaml, I have the following:
parameters:
services:
Acme\Symfony\AcmeCustomBundle\Search\ConfigurationReader:
calls:
- method: setIndex
arguments:
$index: '%elasticsearch.index%'
- method: setSchema
arguments:
$schema: '%elasticsearch.schema%'
The parameters are set in my bundle extension class (extends configurable extension), and I have already verified that the parameters do exist and are getting set using the following method:
$container->setParameter('elasticsearch.index', $mergedConfigs['elasticsearch']['index']);
$container->setParameter('elasticsearch.schema', $mergedConfigs['elasticsearch']['schema']);
Now back to the problem. Symfony is not performing the setter injection, even when I tell Symfony to not autowire the above service by doing the following:
Acme\Symfony\AcmeCustomBundle\:
resource: '../vendor/acme-symfony/custom-bundle/*'
exclude: '../vendor/acme-symfony/custom-bundle/{Model,Tests,Search}'
I did however get Symfony to configure my service when I
Turned off the autowiring for the above service
Used the factory method of service creation
This kind of answered my second question above, but I'm not 100% sure of it. Nonetheless, I would much rather not have to use a factory class just to get around what may be an issue with Symfony, or my lack of understanding of how the setter injection / autowiring really work.
Can anyone see something obvious that I'm missing?
You can autowire other methods (e.g. Setters) if you want, just by using the #required annotation in your service:
/**
* #required
*/
public function setFoo(FooInterface $foo)
{
$this->foo = $foo;
}
Autowiring other methods
First, autowiring in bundles that are meant to be shared is not part of the best practices. You must explicitly define the services of a bundle you are going to share, and doing it in xml.
Now, that being said, what autowire does is that wires up the services in the constructor of a class, trying to autoresolve them. It does not perform setter injection.
What you need is to do is to configure an auto-cofiguration entry. In your bundle extension class, you can do:
$container->registerForAutoconfiguration(ConfigurationReader::class)
->addMethodCall('setIndex', ['%elasticsearch.index%'])
->addMethodCall('setSchema', ['%elasticsearch.schema%']);
Now, this is really meant to be used with interfaces or abstract classes, not really with concrete implementations. But is what you can do if you want to autoconfigure your service.
My suggestion is, if you are going to reuse this bundle, to define the services explicitly and in xml. Autowiring is for your business logic and services.
What is the point of using compiler passes in Symfony?
When we should use Extension class and when Compiler Passes in Symfony?
They come with services definition.
By creating a compiler pass, you are able to update the arguments passed to services.
It's most often done with tagged services.
Also, It can be used for :
Creating new services that require information about other defined services before being defined.
Swapping or adding arguments to a service that you did not write.
Creating and modifying parameters in the container.
I used a compiler pass to register a Factory that make me able to override the doctrine Repository.
You can see the code for more comprehension of how it works:
https://gist.github.com/chalasr/77be8eee5e3ecd3c06ec
Update
Thank's to #Sruj, I added the part I've forgotten about the Extension
Extension are part of dependency injection too, especially of the configuration.
Its primary role is to load the configuration of services across the bundles of your application.
Instead of load your configuration manually by using imports, you can create an extension that does it for you. All your services configuration are registered from your bundle and shared in your whole application.
When you register a vendor in your app configuration, the service container extension of the vendor is invoked.
See "Importing configuration via container extensions" part of the documentation
Currently I'm having one global assets.yml file in my project, where I define my assets and I use them throughout the whole application. I have several bundles and all their assets are defined in this global assets.yml which is not clear and not good.
I'd like to define one assets.yml with corresponding assets per each bundle. They would lie in the bundle configuration.
I don't know how to make them accessible for the entire application. Should I use import somehow or does the framework load files (which name follow certain convention)? I'd appreciate hints how can I achieve the above.
edit: I should explain a bit more... In my config.yml I import assets.yml as a resource:
imports:
- { resource: assets.yml }
and the assets.yml looks like the following:
assetic:
use_controller: false
filters:
cssrewrite: ~
yui_js:
jar: %kernel.root_dir%/Resources/java/yui-compressor.jar
#apply_to: "\.js$"
yui_css:
jar: %kernel.root_dir%/Resources/java/yui-compressor.jar
#apply_to: "\.css$"
assets:
jquery:
inputs:
- '%kernel.root_dir%/Resources/public/js/jquery-1.8.0.js'
- '%kernel.root_dir%/Resources/public/js/jquery.sizes.js'
- '%kernel.root_dir%/Resources/public/js/jquery.form.js'
- '%kernel.root_dir%/Resources/public/js/jquery.validate.js'
- '%kernel.root_dir%/Resources/public/js/jquery.metadata.js'
[...]
Check the official documentation for How to expose a Semantic Configuration for a Bundle
Some refs:
When you create a bundle, you have two choices on how to handle configuration:
Normal Service Configuration (easy):
You can specify your services in a configuration file (e.g. services.yml) that lives in your bundle and then import it from your main application configuration. This is really easy, quick and totally effective. If you make use of parameters, then you still have the flexibility to customize your bundle from your application configuration. See "Importing Configuration with imports" for more details.
Exposing Semantic Configuration (advanced):
This is the way configuration is done with the core bundles (as described above). The basic idea is that, instead of having the user override individual parameters, you let the user configure just a few, specifically created options. As the bundle developer, you then parse through that configuration and load services inside an "Extension" class. With this method, you won't need to import any configuration resources from your main application configuration: the Extension class can handle all of this.
So the main idea is to create Extension class in each bundle and load bundle specific configuration files.
You can also check core symfony or third-party bundles for some examples.
I would like to do the following:
Inside a bundle through dependency injection add propel connection.
So every bundle can have a connection that on load will be merged all together.
Is this possible?
I have achieved this via
- { resource: #XXXX/Resources/config/databases.yml }
- { resource: #XXXXX/Resources/config/databases.yml }
but would like to do it via Dependency Injection as it will be best practice in my opinion.
Thanks a lot.
Your question is not fully clear to me. I only understood the part with loading additional configuration files in your bundles.
Bundle configurations can be loaded automatically with DIC extensions. That's what happens with the Resources/config/services.xml file.
Read more on Loading External Configuration Resources.
I read the article yesterday: https://igor.io/2012/11/09/scaling-silex.html
And another one http://davedevelopment.co.uk/2012/10/03/Silex-Controllers-As-Services.html
So a conceptual question rised in my head:
Currently I do have a lot of controllers in separate classes. I overwrite controller_resolver to create a controller class instance and inject $app into contoller's constructor.
I define routes like this $app->get('/hello', 'HelloController::indexAction') <- my controller resolver will create new HelloController($app); - so far so good.
But to be honest it became a ServiceLocator pattern, not a DependencyInjection, because I do inject whe whole $app which looks like ServiceLocator usage.
Now I am in doubt: should I leave it as is (because it works well) or try "controllers as services" to inject only those sevices on which my controller really depends on? May be my SeviceLocator approach will hit me some day? (people say that DI is better for tests).
I have also looked into Symfony Framework Bundle: class Controller extends abstract class ContainerAware which also have the whole $container injected! ServiceLocator approach in full stack framework?
Any recomendation? Pros/cons?
The symfony2 full-stack framework
The framework uses the Dependency Injection pattern and not the Service Locator pattern.
All controllers aren't services by default. The ContainerAware class includes methods to get access to the service container, so you can call Services inside the Controller.
If you are going to use a Controller as a Service you need to remove the Controller extend. All dependencies you want to use inside the controller needs to be injected by the Service Container.
Read more about this in a blogpost by richard miller, one of the core contributors of Symfony2.
The Silex micro-framework
The Silex micro-framework provides the bare bones of a framework, it's up to you how the architecture looks and which patterns you use.
The Silex documentation uses Controllers that aren't Services. It injects the complete Service Container inside very Controller:
$app->post('/post/{id}-{slug}', function($id, $slug) use ($app) {
// ...
});
If you want to use controllers as service, you should only inject the services you want to use inside the controller.
EDIT: The Controller::action syntax refers also to a Controller that isn't a Service. The Controller:action notation is used to refer to Controllers as Services.
There's lot's of personal preference involved here. What you've done already is a good (enough) step to organising your code base. Some people like myself take things a step further and move our controllers to the container, rather than injecting it in to some kind of BaseController. This happens in both Silex and the full stack Symfony framework.
My advice would be to leave everything you have as is, then try defining your next controller as a service, by practising BDD.
For example, the behaviour of a UserController view action:
It should retrieve the user from a database
It should render the user with a template
Not once does it mention retrieving the database or the template renderer from a container. I'm comfortable not injecting the container, so I'll only inject what I'm led to believe I need by BDD.