Symfony2: Manipulating Service Parameters and Definitions

In my previous post I looked at Symfony2 Service Container Compiler Passes, in this post I will look at how to manipulate service container parameters and definitions from within a compiler pass.

So the important method of the compiler pass which gets called when the container is being built is the process method which looks like this:

In this method we can work with the passed in container which has been set up with parameters and service definitions from the various configuration files.

Getting and Setting Container Parameters

Working with container parameters is straight forward using the container’s accessor methods for parameters. You can check if a parameter has been defined in the container with

You can retrieve parameters set in the container with:

and set a parameter in the container with:

Getting and Setting Service Definitions

There are also some helpful methods of the passed in container builder for working with the service definitions.

To find out if there is a definition for a service id:

This is useful if you only want to do something if a particular definition exists. In my previous post I looked at an example from the AsseticBundle where a parameter was required only if a particular service has defined so hasDefinition() was used to check for the service definition before checking if the parameter was set. Here it is again:

You can retrieve a definition with

or

which unlike getDefinition() also resolves aliases so if the $serviceId argument is an alias you will get the underlying definition.

The service definitions themselves are objects so if you retrieve a definition with these methods and make changes to it these will be reflected in the container. If, however, you are creating a new definition then you can add it to the container using:

Working with a definition

Creating a new definition

If you need to create a new definition rather than manipulate one retrieved from then container then the definition class is SymfonyComponentDependencyInjectionDefinition.

Class

First up is the class of a definition, this is the class of the object returned when the service is requested from the container.

You may want to change the class used by a definition, if for example there is functionality which can only be used if a service from another bundle exists then you may have a class which make use of that other service and one that does not. The one that does not could be used for the service and then the one with the extra functionality swapped in using a compiler pass if the other service is available.

To find out what class is set for a definition:

and to set a different class:

Constructor Arguments

To get an array of the constructor arguments for a definition you can use

or to get a single argument by its position

You can add a new argument to the end of the arguments array using

The argument can be a string, an array, a service parameter by using '%paramater_name%' or a service id by using

In a similar way you can replace an already set argument by index using:

You can also replace all the arguments (or set some if there are none) with an array of arguments

Method Calls

If the service you are working with uses setter injection then you can manipulate any method calls in the definitions as well.

You can get an array of all the method calls with:

Add a method call with:

Where $method is the method name and $arguments is an array of the arguments to call the method with. The arguments can be strings, arrays, parameters or service ids as with the constructor arguments.

You can also replace any existing method calls with an array of new ones with:

Wrapping Up

The methods available then are the same ones that can be used to configure the container in the first place and which are shown in the Symfony2 documentation along with the XML and YAML ways you may be more familiar with. You would usually want to use config files rather than directly creating the definitions when setting the container up. In compiler passes you need to use these methods for making changes to the service definitions.

I have only looked at the more common methods of the definition object here, you can see some more available methods, by looking at the relevant docs e.g. How to Use a Factory to Create Services and looking at the PHP tab in the examples. Bear in mind the place of compiler passes in the process of building the service container though, for example, if you add a tag to a definition in a compiler pass, but the pass that looks for those tags has already run, then it will have no effect.

If there is one thing to take away from this post, it is that if you dislike writing YAML or XML configs for services then just be glad you can and you do not have to write all the definitions this way 🙂

Symfony2: Service Container Compiler Passes

In this post I am going to look at compiler passes. This is not something that you will often need to worry about when making an app with Symfony2. They do come in useful for some needs though and in particular for releasable bundles.

What are Compiler Passes

The lifecycle of Symfony2 service container is roughly along these lines. The configuration is loaded from the various bundles configurations and the app level config files are processed by the Extension classes in each bundle. Various compiler passes are then run against this configuration before it is all cached to file. This cached configuration is then used on subsequent requests. To read more about the loading of config files by a bundle’s extension class and processing the configuration see my posts Symfony2: Controller as Service and Symfony2: Writing a Dependency Injection Extension Configuration.

The compilation of the configuration is itself done first using a compiler pass. Further compiler passes are then used for various tasks to optimise the configuration before it is cached. For example, private services and abstract services are removed, and aliases are resolved.

Many of these compiler passes are part of the Dependency Injection component but individual bundles can register their own compiler passes. A common use is to inject tagged services into that bundle’s services. This functionality allows for services to be defined outside of a bundles config but still be used by that bundle. The bundle is not aware of these services in its own static config and a fresh container is passed to the Extension class meaning it does not have access to any other services, so compiler passes which are executed after all the configuration has been loaded are neccessary to inject tagged services. By using a compiler pass you know that all the other service config files have been already been processed.

Creating a Compiler Pass

To create a compiler pass it needs to implements the SymfonyComponentDependencyInjectionCompilerCompilerPassInterface interface.

It is standard practice to put the compiler passes in the DependencyInjection/Compiler folder of a bundle. They are not automatically registered though, to add the pass to the container, override the build method of the bundle definition class:

Uses for Compiler Passes

You can implement tags for your own services to allow other people to create services to be injected into your bundle. This of course only make sense for shared bundles which are released for other people to use. For bundles only used in a single application you have full control over its config and can just inject the services in there. There is a cookbook article in the Symfony docs on writing a compiler pass that makes use of tags.

Basically the compiler gives you an opportunity to manipulate the service definitions that have been compiled. Hence this being not something needed in everyday use. In most cases the service definitions in the config can be changed.

There are other uses such as providing more complicated checks on the configuration than is possible during configuration processing. For example, the configuration builder does not provide a way of checking that a value is required only when a certain service is present. The following example from AsseticBundle shows checking if a parameter has been set in the container if a particular service has been defined:

In my next post on this topic I will look at how to manipulate service container definitions from within a compiler pass.