How we Migrated from Nette to Symfony in 3 Weeks - Part 1

On the break of January/February 2019, we migrated whole Entrydo project from Nette to Symfony. It was API backend with no templates, but still, it wasn't as easy as I expected.

Many โ˜• and ๐Ÿบ were drunk during this migration. 0 programmers were too frustrated to give up.
Yet, you'd laugh if you knew what took us the most time.

And when I write we and 3 weeks, I mean myself and Jan Mikeลก and 3 weeks of occasional codding, not full-time. In total, we spent around 30-40 hours each on this migration.

How did that happen? One day I met with Honza to cowo-talk. No big topics, you know:

"Why don't you try Symfony?"
"I don't have time to play with it. There's already pressure for new features, damn Tom!"
"I'm pretty sure it's easier than you think. Let's give it 1 week and you'll see."

And that's how it started. In this short series, we'll share our short story about nights without sleep, PHP code and Neon parsing and short-term pain of having no clue what that code does. Maybe even... happy ending?

1. Getting Ready ๐Ÿค”

A year ago we tried to migrate from unmaintained Kdyby and Zenify packages to Contributte and other implementations. And we ended licking our burns.

That's why planning the migration is much more important than the migration itself. You have to talk about resources - how much energy and time are we both willing to invest. The problem with migration (or coding in general) is that there is a chance that 80 % will be done in 2 days, but then there is this 1 bug that makes most of the database tests fail. You try and try and fail and fail... and after a week you have depression and after 2 weeks you give up and never go back.

2. Making a Commitment ๐Ÿ’

Also, we had to decide to give this a priority. I was planning to put up new website with training admin and new design and Honza had to deliver the feature. We had a time span of 3 weeks, month top, to do this. Everything else is secondary.

When one sleeps, the other codes. We called each other at 3 AM in the morning to talk about frustration, we shared the joy when one solved the issue. Sometimes we slept for 2 hours, then coded some more because we felt we are very close and this must be the day we finish it. In 20 cases it wasn't, but we persisted. We persisted because we decided to.

If one would decide in the middle of a migration to work for a week on another project, the mutual motivation could go to garbage very quickly. No way!

3. Automated Migration > Manual Changes ๐Ÿค–

The basic idea was to do automated instant migration. Anything manually changes on more than 1 place is a potential future black hole.

We quickly discovered, it's better to use PHP factories over config coding and kill all parents we could (except our own ones of course).

Use Rector for PHP Changes

In the start, we run Rector with generic rules with brute-force way. Don't think, just try it. That gave us more idea about the code - we started to spot places we can write in Rector rule.

In the end, Rector helped us with many following changes:

The tricky part was to discover differences and create the bridge between both frameworks - "In Nette, you use this, in Symfony you'd use this."

Now it's done, so you can use them in NETTE_TO_SYMFONY set Rector set to migrate your code.

We didn't have to change any of these parts, because the code didn't use them:

Rector can automate them when some project will need them. Maybe your project :)

Non-PHP Migrations

Coding Standards

After many many changes in the code, we didn't care about spaces, tabs or where the { is. That's the job of EasyCodingStandard. We had to focus full attention on code structure, not to play with style.

Actually, Nette uses tabs and Symfony spaces, so ECS actually helped with migration a lot too.

4. WTFs Everywhere! ๐Ÿคฆ

This code worked in Nette:


$this->translation->translate('Hi, my name is %name%', [
    'name' => 'Tom'

In Nette:

Hy, my name is Tom.

After migration tests started to fail:

Hy, my name is %Tom%.

Damn! Why it's broken?

In this case, we had to look to the contents of the translate method of the previous service. Kdyby/Translation actually automatically wraps key name to %, while Symfony doesn't:


 $this->translation->translate('Hi, my name is %name%', [
-    'name' => 'Tom'
+    '%name%' => 'Tom'

Problem solved in 20 minutes, doh! Now it's part of Rector set, so you can actually forget this paragraph. But be ready for such issues. The hardest problems are usually behind simple differences - like extra %.

Luckily, Honza is xdebug expert, so he deep dives into the code to find the spots where code failed. It took him some time, but in the end, he disclosed magic and fixed all the issues we had. You can read more about that, Doctrine events and HttpRequest as a services migration in the next post.

There are no Happy Endings...

...or are they?

The project was published in staging for 2 days. The answer to the last question is Happy Valentine. That's the day the Symfony application was published to production.

Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!