Photo by Jesse Bowser on Unsplash
A Migration Tale: from our custom PHP framework to Symfony
Key points and strategy to migrate from a proprietary framework to Symfony, discussing routing, service injection, and database entities.
โโWe decided to code our own PHP framework when founding AssoConnect many years ago. That was a great yet challenging way to learn how many critical parts of a framework work ๐ค
But maintaining it was also too intense so we decided to migrate to the Symfony framework backed by a strong community.
We aimed to get as many end-to-end legacy-free flows as possible to lay down strong foundations to build modern code ASAP.
This means Symfony must be exposed and visible to the world: it can't start as a small piece within our old framework.
So we first changed the routing: Symfony will be from now on the entry point
If a modern controller supports the requested route, then it serves it
If not, then the request is routed to our legacy framework which handles it
# routes.yaml
legacy_all:
path: /{path}
controller: App\Controller\LegacyController::index
Running php bin/console debug:router
shows that the routing rule comes last โ
Then we established a base rule for dependency injection:
โ Old code can depend on modern code
โ Modern code cannot depend on old code
The container is passed along the Request object so old code can access modern dependencies through public aliases.
This makes it possible to have an end-to-end flow without a line of old code ๐
And old code doesn't stain the new code โ
It also designs a strategy to migrate the codebase:
Draw the dependency tree
Start with the leaves until there is no more old code
Our proprietary ORM uses entities close to Doctrine 1 ones (they persist themselves) which is very different from the Doctrine 2 logic (the ORM is in charge of persisting entities).
Our entities also have built-in lifecycle events and validation.
So migrating overnight wasn't an option ๐ฅ
We set a second rule for migrating entities in line with the main plan:
โ Old tables can sync (create, update, delete) modern tables
โ Modern tables cannot sync back to old tables
โ ๏ธ This creates a strong constraint: a modern table (and the related entity) is read-only as long as the old table exists.
It wasn't obvious at first sight but it actually makes sense when migrating data
You first expose a new interface (modern tables and entities).
Then you migrate read services.
Then you migrate write services (you should only have a few ones anyway or you have duplicate code or bad responsibilities segregation).
We are still migrating and our strategy is sometimes frustrating: rules may require us to migrate a part that is not a business priority.
But it helps to keep a clean modern codebase ๐ซ
And it has a rewarding upside: the more code you migrate, the more code turns migratable ๐คฉ