Live Case Study: Legacy Project Update
A legacy project using Symfony, the Sonata Admin bundle, and PHP, required an upgrade of Symfony from version 3.3 to version 4.4 to support PHP version 7.2. The project consisted of six independent bundles. In addition to updating package versions, there was a requirement to adopt the project skeleton to Symfony 4.4 style.
Project Update Steps
- Update Symfony to version 4.4
- Update Sonata Admin bundle and other packages to the maximum version that supports PHP 7.2
- Test and verify project is functional by executing tests, which cover approximately 30% of the code base
- Refactor each bundle configuration
- Revival
- Remove unneeded bundles and restructure source code to match the Symfony 4.4 style
- Revival
- Fix bugs and issues found during testing
How the Project Was Updated
Symfony
After registering version 4.4 in all Symfony packages and writing a composer update hoping that everything would be updated, we started to get conflicts within the internal bundles. There were about 25 internal packages within the project that required Symfony 4.4 support to be added to composer.json.
After all the above updates had been completed, we then updated the rest of the packages referenced in the composer.json file. Various small packages caused either non-critical problems or no problems at all. Documentation has also been updated well.
Sonata Admin
The next step was to upgrade the Sonata Admin Bundle from version 3.13 to 3.24, which is the latest version of Sonata that supports PHP 7.2 . We saw at once that the Sonata Core Bundle helper package had been removed from the 3.24 version dependencies. We started searching for the corresponding namespace in the project and unfortunately there were overlaps. In each case, we needed to figure out what new services/ classes to use instead of old classes. Sonata developers had spread some functionality to other bundles that needed to be connected separately, therefore Sonata Exporter Bundle, Sonata Twig Bundle, and Sonata Form Bundle were added to our project. After all the packages were updated, the next step was to evaluate the health of the project.
First Revival
We tried to launch the project in vain of course. Slowly we began to fix the bugs we were getting. After a couple of minutes of fixes, we started getting different but similar types of errors. The errors stated that we were trying to access a private service directly from the container. Apparently, something had been changed in the new Symfony version.
We proceeded to read about the changes and found an article, which declared that services in versions 3.4 and later are no longer public. Public versions of the services are deprecated in version 3.4 but would be removed entirely in version 4.
Recall that we have a legacy project using legacy bundles, which supports but does not use dependency injection. Throughout the code, there are $ container-> get () statements that no longer worked, so we created public aliases. If the service is used in one or two places, we try to convert the service to use dependency injection. Unfortunately, we had 25 more internal packages, and each of them had its own configuration. One would need to go into the packages, make edits there, and then update all those dependencies. In a word this is pain. There would be 5 hours, if not more, of very exciting (not exciting at all) work ahead.
Eventually the project started and the admin page opened. Whether we turned left or right, we encountered a 500 error: No access to private service. We ran our unit tests which passed, but integration, functional tests were down.
We spent so much time on targeted troubleshooting on the homepage that we can’t even imagine how much time we would have to spend on everything else. The project is huge with lots of services and the test coverage was only 30%. If we focused our troubleshooting on each service, it could take far too long. Therefore, we decided to write a global “public: true” statement for services in all config files. There were a lot of config files because there were six bundles in the project, and each bundle had its own configuration and related packages. There was still much work ahead.
After all this work, most of the tests came to life, and all that remained was to solve certain problems.
Configuration Rework
Now that we’ve made the first revival, we can proceed to the next part of the update. The configuration looked as follows.
Let us remind you that we had six bundles. One of them is the Main Bundle that contains the main functionality. It was decided that everything from the Main Bundle configuration goes to the config folder, and then in the config folder, each bundle would have its own subfolder. We also needed to remove the configurations for packages from the config.yml file and place them in a separate file for each package in the config/packages directory and replace yml => yaml.
Below is the result.
All these config files were loaded into the kernels of their bundles. Since we needed to get rid of bundles, we created one kernel in src and loaded all the config files into it. We also remembered to register all the compiler passes.
Let’s have a look at the project.
Right from the start, we got errors. They were all the same type: incorrect relative paths. We searched through the folder with the config for relative paths and corrected everything. Then we reran the tests. A couple of them failed, but they were fixed and targeted without problems. This was the most painless stage.
Rework of Structure in Src
We had six bundles in src. Like the configuration, everything went from Main Bundle to src. Let us explain how we will deal with the rest of the bundles using the controller’s example. In the Main Bundle, we have the Controller folder. Therefore, now the controllers are in src/Controller. Inside this folder, we created a folder with the name of the bundle. We took all the controllers from ApiBundle and put them in src/Controller/Api. We repeated the same with all folders.
We end up with the following:
Next, we needed to get rid of the bundles namespaces. It wasn’t difficult to replace namespaces, including service definitions, throughout the project using search and replace.
We should separately mention templates. Now they are in the root of the project in the templates folder. But in admins/controllers, the path to these templates was almost everywhere described like in bundles: AppBundle: Admin: list.html.twig. Therefore, we had to search for “.html.twig” within the project and deal with each case separately.
Even in Sonata Admin Bundle, URLs are generated based on the namespace if they are not explicitly specified. And they were used in the templates in many places. We needed to replace all those with new ones.
Basically, this is it.
After that, all that we had to do was to fix the tests and get a bunch of bugs from testers for the pieces not covered by tests.