The Silex Sunset
The Symfony-based micro-framework Silex will reach its end of life soon. If a Silex application should be maintained beyond this point, a migration to Symfony might be an option worth considering.
Note: This is part one of a series on articles on migrating Silex applications to Symfony. Other parts:
Silex, the micro-framework side-project of Symfony, will reach its end of life soon. Symfony 4 feels a lot more “micro” than Symfony 2 did back then, so in a way, Silex has fulfilled its mission.
However, there are still many applications out there that have been built against Silex and that need to be maintained beyond the EOL of Silex. If we maintain such an application, we basically have the following options:
- Continue to use Silex. This won’t be a problem at first, but sooner or later, we will face incompatibilities with newer releases of other libraries or php versions. If we’re lucky, somebody has started to maintain an unofficial fork by then.
- Fork Silex. This will solve our compatibility problem, but maintaining our own framework is a time-consuming task. That’s probably the reason why we haven’t built our own framework for this application in the first place.
- Migrate to a maintained framework. Right now, this way might look like the most expensive option, but it enables us to maintain our application for a longer period of time.
As you might have guessed, I would pick option three in most situations. And since Silex uses Symfony components under the hood for most tasks, Symfony would be the obvious choice for such a migration.
In fact, I would make a bold statement: Any Silex application can be refactored to a state in which there are equal configurations for Silex and Symfony for that application.
In this article, I want to focus on the migration path from Silex to Symfony. A migration to a different framework might work as well, but it’s probably a more complex task and is considered out of scope for this article.
I’ve always liked Silex for its simplicity and I’ve used it several times for small prototypes or for partly modernizing legacy codebases. However, this simplicity fires back as soon as an application grows and needs to be maintained for a longer period of time.
The framework is designed around a class called Application, that acts as a DI container and as a façade to configure framework features such as events and routing. On top of that, it also acts as the main integration point for framework extensions (so-called service providers) and as the main entry point to your application. And as the icing on the cake, the class also brings some utility functions like redirection or file streaming.
This might sound like one big violation of the SRP (because it is), but it makes bootstrapping a new application really easy.
Unfortunately, this framework design encourages developers to extend the Application class and give it even more responsibilities it shouldn’t have. Many Silex applications have fat Application classes and a lot of application logic actually depends on that class. Since this class will eventually go away, one of our main tasks will be to reduce and eliminate those dependencies.
The largest difference between Silex and Symfony is the service container being used. While Silex uses the simplistic Pimple container, Symfony relies on its own Dependency Injection component. The follow very different approaches: A service configuration for pimple is written in pure php using an anonymous function that acts as a constructor. The container can be modified until the first service has been constructed.
Symfony DI on the other hand works with a compiled container. It is configured with semantic configuration files (usually in YAML or XML) which is used to dump a compiled container to disk. A side effect of this is that Symfony requires a cache directory for compiled code, a requirement that Silex didn’t have. Once the container is compiled, we cannot modify the service definitions which is more restrictive than Pimple’s behavior.
In general, a Pimple container configuration is incompatible with Symfony DI and it cannot easily be converted. Because of that, the service configuration is probably the one part that you have to rewrite from scratch. Fortunately, the autowiring capabilities of Symfony DI can smoothen this process.
Like with every refactoring, we should focus on performing small steps. After each step, our application is deployable and fully functional. It should even be possible to continue feature development during the refactoring, as long as the new features don’t use parts of our application that we decided to discontinue or framework features that we’re migrating away from.
The team behind the open-source application OpenCFP allowed me to help with the migration of their code from Silex to Symfony. They also challenged me to make my pull requests as small as possible, so they can be reviewed and merged easily. Although not all potential pitfalls of a Silex application apply for them, I’m going to use some of those pull requests as an example of the strategy I’m proposing.
Phase 1: Refactoring the Silex Application
The general idea is to not use any functionality that is original to Silex and interact with the underlying Symfony components instead. This way, the application can easily be switched to Symfony.
- Upgrade the application to Silex 2.2 and Symfony 3.4.
- Remove utility functionality from the Application class and its service providers by refactoring into helper services.
- Refactor controller actions and event listeners from anonymous functions into controller classes and event subscribers.
- Turn middlewares into event subscribers.
- Reduce container-awareness or decouple service lookups via PSR-11.
- Rewrite tests that modify the container.
- Modify your directory structure to match the Flex layout (optional).
Phase 2: Switching Over
After completing these refactoring tasks, we should be able to configure a Symfony kernel for our application.
- Bootstrap Flex from symfony/skeleton (optional).
- Find and configure bundles to replace service providers.
- Write a service configuration for application services and controllers.
- Alias framework services to the service IDs of their Silex counterpart.
- Dump Silex routes to YAML and load them into the Symfony kernel.
Now, we can modify the front controller to use the Symfony kernel instead of the Silex application and test it. Once all problems discovered during the tests have been eliminated, we can make the switch permanent and remove Silex from the project dependencies. In OpenCFP, pull request #895 performed that switch.
Phase 3: Further Refactorings
Afterwards, we can cleanup compatibility code like the service aliases and adopt framework functionality that was unavailable under Silex, like configuration via annotations for example. You can also upgrade to Symfony 4 now, if no other reasons prevent you from doing so.
A Word on Flex
Symfony Flex is a new Composer plugin that applies changes to your framework configuration when installing certain libraries or bundles. It is not part of Symfony itself, but starting with Symfony 3.4/4.0, it is the recommended way of composing and structuring a Symfony application.
Flex enforces a highly opinionated application layout. While this usually makes it easier to bootstrap new applications, it takes some effort to restructure existing applications to use it. Silex application developers on the other hand often have implemented their own strong opinions on how the application should be structured. After all, having the freedom to do this might have been one of the reasons we chose Silex over Symfony when starting the application.
As a consequence, the opinions behind Flex and our application will probably clash. We can now decide if we want to give up our own structure and adopt the one enforced by Flex or if we continue without it. The consequence of not using Flex will be more manual configuration effort, but as Silex developers, we’re probably used to that.
To Be Continued
As a follow-up, I’m planning a series of articles that take a deeper look at each of the refactorings. I will update this article as the other parts are published.
That being said, I’d love to receive feedback on my strategy and the individual refactoring steps. Also, if you have performed such a migration yourself and stepped into pitfalls I haven’t thought of, feel free to leave a comment or contact me on Twitter.
In order to prepare a Silex application for maintenance beyond the announced end of life of the Silex framework, we’re refactoring it to Symfony. This can be done by decoupling the application from Silex-only features and the façades that the Application class provides. The biggest challenge is bridging the differences between the service containers of the two frameworks. We refactor the application to a state in which we can configure a Symfony kernel to run it.