CakePHP 5 upgrade guide

CakePHP 5 has just been released as 5.0.0.
As it stabilizes, here already some tips and helpful tools to quickly update your app or plugin code.
This aims to be a living doc on the process, so I will update it from time to time when needed.

First steps: Preparation

Make sure your app/plugin is at latest 4.x code at that time.
That includes the removal of already "known" deprecations.

Your goal should be to have a state-of-the-art Cake 4 app that has the minimum amount of changes needed to get to 5.x.
This can be done well in advance, this can be covered with some functional and integration tests.
With this you get a high confidence that the actual upgrade then can be planned and executed within the planned timeframe.

Check all plugins and dependencies

A very quick check could be looking at the awesome list.
But especially in the early phase of a new major things change fast, so the info here might not be fully up to date yet.

You can already use this little composer tool to check if every plugin has matching versions or branches for CakePHP 5 in real time: dereuromark.de/utilities/upgrade
The demo here shows the sandbox example.

Just enter your own composer file if you’d like and let it generate the CakePHP 5 compatible version.
It will also tell you which plugins cannot (yet) be found in any compatible version.

For any blockers, communicate to the plugin owner that you seek a compatible version or better yet: Make a PR and propose the matching changes.
This way they can more easily accept and release the new major.
Forking and maintaining your own fork should only be a last resort if the maintainer does not do active maintenance anymore – or as a quick-fix for the time being, so only as a temporary solution to unblock your upgrading process.

If you do not find any further blockers, you can now move forward to the actual task of upgrading.

Upgrade

First you should reference the official docs on this:

I started to adjust composer.json and made sure at least CLI and basic homepage is working again to some extend.
Once this is done, I committed already the intermediate state.

Again: Make sure to commit before doing the automated tooling changes below, as well. This way you can revert what was wrongly changed or just see the actual changes done for easier review and confirmation + commit.

Now you can run the upgrade tool once you have a somewhat working code base (without any fatal errors) again to finalize the changes needed:

bin/cake upgrade rector --rules cakephp50 /path/to/your/app/src/

For me it mainly fixed up the changes around arguments/params/returns and more strict typing, but also:

  • FrozenTime to DateTime
  • Custom finders syntax

Note: It is important to link to the src/ (and tests and plugins/) directly, as it otherwise also looks into vendor and can lead to memory issues.

You also need to load global functions manually now.
This needs to be added to your bootstrap:

/**
 * Load global functions.
 */
require CAKE . 'functions.php';

After this step, I was already able to navigate my app again, and execute almost all CLI commands. Same could be true for you at this point.

My upgrade tool as add-on

I wrote a small file based upgrade tool that can be used on top of the core one now.
Install that one and also here run

bin/cake upgrade files /path/to/your/app/

It will probably auto-fix some more things the original one didn’t so far.

It also contains a skeleton upgrader. For this you need to make sure you committed all files so far and you have a clean git state!
Then you can run it, too:

bin/cake upgrade /path/to/your/app/

It will overwrite all your skeleton files with the current 5.x version of that file.
You can diff it and revert whatever you added on purpose.
In the end you should have perfectly updated files in your application to now commit.

As next step run PHPStan to see all obvious issues, also execute tests and further click around on the app website to check what else might be needed now.

Manual changes needed

I documented a few manual changes that I needed to do.

A lot of models in the controllers needed adjustment:

-	protected $modelClass = 'Users';
+	protected ?string $defaultTable = 'Users';

If you didn’t want to go with protected string $modelClass.

Extension based serialization is now working differently and includes header negotiation.
So the following addition in controllers (or routes) would now be needed for views to also render as JSON:

public function viewClasses(): array {
    return [JsonView::class];
}

Note that this especially gets tricky once you want to render XML and e.g. one action both normally and with .xml extension.
For this I had to make sure to only return the respective views if there is an extension based routing in place:

public function viewClasses(): array {
    if (!$this->request->getParam('_ext')) {
        return [];
    }

    return [JsonView::class, XmlView::class];
}

Also don’t forget to change the way the view gets passed the serialization data:

-	$this->set('_serialize', ['countries']);
+	$this->viewBuilder()->setOptions(['serialize' => ['countries']]);

Templates can now throw exceptions as Entity::has() changed behavior:

$post->has('user') ? $this->Html->link($post->user->email, ['controller' => 'Users', 'action' => 'view', $post->user->id]) : ''

If you have any such lines in your (admin) bake templates, make sure to change them to:

$post->hasValue('user') ? $this->Html->link($post->user->email, ['controller' => 'Users', 'action' => 'view', $post->user->id]) : ''

Otherwise you get [TypeError] Cake\View\Helper\HtmlHelper::link(): Argument #1 ($title) must be of type array|string, null given exceptions.

Afterwards

I further checked what kind of changes needed to be done immediately, and which ones could be postponed.
Using Shim plugin for loadModel() call shimming for example I didnt have to touch hundreds of such calls for now.
With such shimming I can concentrate on getting all critical upgrades done first. Later, once it is all up and running, I can still come back and slowly replace all
"deprecated" things that are shimmed with the future proof way.

I now worked on also getting tests to run again.
Don’t forget to adjust your app skeleton with updated files from cakephp/app repository.
E.g.

    <extensions>
-        <extension class="Cake\TestSuite\Fixture\PHPUnitExtension"/>
+        <bootstrap class="Cake\TestSuite\Fixture\Extension\PHPUnitExtension"/>
    </extensions>

and other small changes can be spotted quickly by first commiting your code and then copying over the app skeleton files and making a diff comparison.
I usually use the IDE to then quickly "revert" any changes that are not needed or false positive, and the result should be the inclusion of all new needed changes.

Updating annotations

Use IdeHelper and bin/cake annotate all to make sure your annotations are all updated to 5.x.
If you are using PHPStorm, you can also regenerate your meta data for full autocomplete and IDE compatibility.

Showcases

See cakephp-sandbox:PR62 for commits based on my progress in this post.
It shows the changes I needed to make in order to get it working again for 5.x.
Well, and some more commits on master afterwards^^.

Tricky ones

I wont to outline a few tricky upgrade issues I faced, maybe it will be easier then for you to figure out

Dynamic component checking

With dynamic properties going away you also cannot just check on the components directly anymore:

if (isset($this->MyComponent)) {
    $this->MyComponent->doSomething();
}

With CakePHP 5 this would now silently always be false.
So here, you will need to use the registry for it instead:

if ($this->components()->has('MyComponent')) {
    $this->components()->get('MyComponent')->doSomething();
}

On top, you can of course also check on method existence if there is a chance that it could be not existing.
In my case I know inside this plugin that it exists once the component is available.

Further tips

Waiting for a necessary fix?

Especially in the early days of a new major the bugfix releases will contain quite a lot of crucial fixes.
If you are waiting on a specific one merged but not released, you can temporarily switch to the branch, e.g.

"cakephp/cakephp": "5.x-dev as 5.0.0",

Note that this shouldn’t get deployed as is. If you really need to, best to use commit hash on top, to make sure it doesn’t deploy more than you tested.

Good test coverage

A good test coverage and at least basic 200 OK checks on most important endpoints (URLs) can be super important to determine how successful the upgrade was.
So it can be worth investing a bit of time ahead of the upgrade to get a good coverage here so that afterwards you less likely deploy a broken piece somewhere.

For those that might not have had the time: Check your error logs afterwards very frequently and it will tell you right away where still issues appear or even 5xx errors.
Best to fix them up quickly as soon as they appear.

That’s it.

Enjoy you upgraded Cake app or plugin 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.