# A Migration Tale: from our custom PHP framework to Symfony

​​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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677660668726/ee4c896f-9b2e-4e53-b763-f87937466517.svg align="center")

* 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
    

```yaml
# 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
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1677267178011/311cda5f-7e53-4d81-b570-aec185df6fed.svg align="center")

---

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 🤩
