Decoupling Silex Applications from the Service Container Implementation
Direct access to the service container is convenient. But if your service container is omnipresent throughout your application, it is hard to switch to a different container implementation. In order to prepare your application for a switch to the Symfony Framework, we need to decouple it from the container.
Application instance as a service locator is especially popular when it comes to controllers, as the following example shows.
The framework will automatically inject the
Application instance when calling the action, thus giving us full access to the service container. In the example above, we access the Twig template engine to render the page that should be displayed to the user.
Another example of direct container access are services that have the application as a dependency. If we register our controller as a service, it could look like this.
What’s so problematic about this approach if we want to migrate the application to Symfony? Symfony uses a different service container implementation, so the code above will not work anymore. There are several approaches to tackle this issue.
Prefer Dependency Injection over Service Location
Injecting only the services that our class needs instead of giving it access to the whole container is what I would consider to be best practice for building large decoupled applications. And this example shows why: service location always couples our application logic to at least some kind of lookup mechanism. Moreover, our application logic needs to know under which name the desired service is registered with the service locator.
With dependency injection on the other hand, the class defines its dependencies and the container has to figure out how to fulfill them. Let’s revisit our controller.
The controller class above is completely unaware of the container that is being used to construct it. There is no need to modify the class when switching to a different container implementation. Of course, if the block that is labelled with
// Insert controller logic here. requires more services (it most likely does), those have to be injected into the constructor as well.
OpenCFP used application-aware controllers extensively, but the developers were already aware of the problems this might cause, so refactoring the controllers to container-independent services was already on their agenda. The change has been prepared in PR #855 by loading controllers as services. In subsequent PRs (all linked to PR #855), the container dependency is removed and replaced by constructor injection.
However, switching from service location to dependency injection requires large and time-consuming restructurings of the affected classes that are not always feasible. So let’s have a look at an alternative approach.
Decoupling Service Location via PSR-11
PSR-11 is a standard for a minimal interface of a service locator. The benefit for our application is that if we use that interface for service lookups, we can easily switch the implementation behind the interface. The piece of code that consumes the interface does not care about the container implementation anymore as long as we make sure that the service IDs remain the same.
In OpenCFP, we use that approach for integration testing. The integration tests used the container to construct the services that should be tested. By introducing PSR-11 here, the test cases could be reused after switching to Symfony.
But we can also use PSR-11 to decouple controllers and services. I have created a small service provider that enables PSR-11 for Silex applications. This allows us to write controllers like this.
As you can see, the controller still uses the container for service lookups, but it does not care about its implementation anymore. We can now use any container we want as long as we have a PSR-11 adapter for it.
The service provider also registers the PSR-11 container as service
service_container, so we can inject that instead of the
In both examples, we needed to change all service lookup calls, but the impact of the change is probably lower than it is when switching all classes from service location to dependency injection. Moreover, changing the lookup calls could be done semi-automatically in a modern code editor with a regex search and replace operation.
This article showed how to decouple application logic from the
Application object for service location. Unfortunately, the
Application object is sometimes also used as a carrier for utility logic. This use-case is of course not covered by PSR-11 and I’m going to deal with this issue in a later article.
Also, the controller above has another issue, when it comes to a Symfony migration: it returns a string instead of a
Response object. I’m planning another article on how to prepare controllers where I will deal with this problem.
Application logic that is tightly coupled to a specific container implementation needs to be refactored if the container is to be replaced. We can do this by either building the affected objects with dependency injection or by using the abstract
ContainerInterface defined by the PSR-11 standard to make service lookups independent from the actual container.