In the previous article, I explained what a legacy system is and how you can migrate it to the modern one using Strangler Pattern. In this article, I’ll show you the use case of migration legacy system to the modern one based on a Symfony framework.
Assume you have an application built using Symfony 1.4 framework. It’s still in active development but uses a legacy PHP 5.* version which is no longer supported. It’s still maintained but was implemented several years ago. As we know what the current situation is we can go through requirements. Requirements:
- The system should be running on the newer, continuously supported PHP version.
- Both legacy and the modern systems should be transparent for the users, so the UI is supposed to be the same.
- A modern system should be implemented using the Symfony 4+ framework version.
- A client wants to have Symfony 4 web profiler available for both legacy and modern application.
Now you know the current situation and what the requirements are. It means that you are ready to start migrating the system.
Choose a migration strategy
First, you need to choose the migration strategy. As you could read in the previous article there are several different ways to do this. In this case, the best approach is Legacy Route Loader. As a PHP is a scripting language there is no problem running external scripts in the other one. Thanks to that we can check if a requested route exists in one app and if it doesn’t, we can run the legacy script instead.
Investigate how the legacy system works
We’ve chosen a migration strategy. Now we need to look at the Symfony 1.4 and get to know how it actually works. This is an old framework and there’s no general entry point. It consists of several separate modules called applications. Every application has its own front controller. Each one gets the request, then runs some business logic and finally returns a response as an output.
But it's not all you need to do. There’s a problem with the legacy front controller. Symfony 1.4 writes a response to the output. To capture this response, we have to capture this output. PHP comes with output buffering that you can use. If you don't buffer this response, you won't be able to catch one and pass to Symfony 4 response object. In the worst case, it can end up with a double response from the server. The double response will display two copies of the same page on the browser screen.
Thanks to an output buffering you are able to store responses in an internal buffer. Then you can get buffered content and pass it to the response variable which is a part of sfContext object in Symfony 1.4.
Implement Route Loader
Now, you have some more knowledge of Symfony 1.4. You can go back to Symfony 4 to implement a dedicated route loader for generating appropriate routes to legacy scripts. Route loader extends internal Symfony Loader service. It’s used to generate routes that are understandable for the Symfony framework. In this case, the route loader should load all the front controller scripts of the legacy application with appropriate routes. For example, it will take a legacy/web/app.php and make a route /app that points to LegacyController with proper parameters. The code below is an exemplary implementation I made for the article.
Let’s go through the implementation of a route loader step by step. In the beginning, you need to find all the script files in order to set their pathnames to the Route object. You can do this using the Finder service. In this case, legacy scripts are placed in the legacy/web directory.
Then, as you have a collection of script files you need to create a route name for each one (Route object requires a unique route name).
Going forward you need to create a Route object itself and add it to the RouteCollection object.
The last thing you need to add to your route loader is the supports method. This method checks if the current loader supports the given resource. You can name the resource type legacy_routes.
In the end, you need to edit config/routes.yaml file to add a piece of information about legacy_routes and define the loader which generates them (the one we created above). It’s worth mentioning that the order of defined routes in the configuration implies the order of executing them. It means that if both applications have the same route, the earlier one will have priority.
Prepare Legacy Controller
To use routes generated by route loader, you will have to create a controller that handles these routes. Route loader will use this controller during routes creation (you might have noticed the _controller attribute in the previous example). Before you start to implement the controller, you need to think about what responsibility this controller should have. First of all, you'd like to run a proper legacy front controller script related to an incoming request. To do this you need to have a request path which is a relative pathname to target script. You also need a piece of information about the root folder of this script. You need this data to simulate a direct call to the legacy application.
At the beginning of an implementation, you need to create a controller method. This method will accept two arguments that were defined when creating the Route object.
It’ll help the legacy front controller to properly process an incoming request and return the response.
You’re almost done. Almost because Symfony 4.0 doesn’t understand Symfony 1.4 response object, so you have to create a new one using the old one. You have to remember to set the information like status code and a content type to keep consistency between old and modern ones.
An exemplary implementation with some refactoring and extra method for removing trailing slashes (for the URLs backward compatibility) of the controller should look like the one below
Once you have implemented the controller and route loader, it’s time to adjust the configuration of the framework. You need to edit config/services.yaml and configure a tag for route loader service to run it during route generation.
What about a user session?
Demo application
I created a git repository with the complete solution described in this article. The project is available at GitHub (link). Feel free to ask any questions you have. You can contact me on GitHub. I'm happy to talk about your approach to this topic and we can try to solve your problem together.