CakePHP 3.8 (PHP 5.3) to CakePHP5.x (PHP 8.3.9)

Marc C
10 min readDec 13, 2024

--

Edit: This post became somewhat of a spaghetti mess, and the project actually got relaunched using cakePHP 4.5
I’m leaving it “as is” because I may refer back to it later for notes, but it is absolutely a terrible “how-to” guide.

composer require --update-with-dependencies "cakephp/cakephp:3.10.x"

Install PHP 7.4.x or use mamp and set your PATH.

In composer.json, add the following:

"require": {
"php": ">=7.4",
...

In Application.php around 行49 change (\DebugKit\Plugin::class) to (‘DebugKit’).

composer.json should look like this (change it if necessary.)

 {
"name": "cakephp/app",
"description": "CakePHP skeleton app",
"homepage": "https://cakephp.org",
"type": "project",
"license": "MIT",
"require": {
"php": ">=7.4",
"cakephp/cakephp": "3.10.*",
"cakephp/migrations": "^2.0.0",
"cakephp/plugin-installer": "^1.0",
"friendsofcake/cakephp-csvview": "~3.0",
"mobiledetect/mobiledetectlib": "2.*"
},
"require-dev": {
"cakephp/bake": "^1.0",
"cakephp/cakephp-codesniffer": "^3.0",
"cakephp/debug_kit": "~3.0",
"josegonzalez/dotenv": "3.*",
"phpunit/phpunit": "^5|^6",
"psy/psysh": "@stable"
},
"suggest": {
"markstory/asset_compress": "An asset compression plugin which provides file concatenation and a flexible filter system for preprocessing and minification.",
"dereuromark/cakephp-ide-helper": "After baking your code, this keeps your annotations in sync with the code evolving from there on for maximum IDE and PHPStan compatibility."
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Test\\": "tests/",
"Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
}
},
"scripts": {
"post-install-cmd": "App\\Console\\Installer::postInstall",
"post-create-project-cmd": "App\\Console\\Installer::postInstall",
"check": [
"@test",
"@cs-check"
],
"cs-check": "phpcs --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
"cs-fix": "phpcbf --colors --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
"test": "phpunit --colors=always"
},
"prefer-stable": true,
"config": {
"sort-packages": true,
"allow-plugins": {
"cakephp/plugin-installer": true,
"composer/installers": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

Set up your DB and run mamp, check your app. It should be flawlessly running 3.10.5 with PHP 7.4.

Install PHP8.3.9 and set PATH.

git clone https://github.com/cakephp/upgrade
cd upgrade
git checkout 4.x
composer install --no-dev

# Rename locale files
bin/cake upgrade file_rename locales <path/to/app>

# Rename template files
bin/cake upgrade file_rename templates <path/to/app>

app/config/app.php

'paths' => [
'plugins' => [ROOT . DS . 'plugins' . DS],
'templates' => [ROOT . DS . 'templates' . DS],
'locales' => [RESOURCES . 'locales' . DS],
],

Then run:

bin/cake upgrade rector --rules phpunit80 /Applications/MAMP/htdocs/movie/tests
bin/cake upgrade rector --rules cakephp40 /Applications/MAMP/htdocs/movie/src

# use this to monitor progress
top -o cpu

Change your PATH back to PHP 7.*

#do each one-by-one
composer require --dev --update-with-dependencies "phpunit/phpunit:^8.0"
composer remove friendsofcake/cakephp-csvview
composer remove cakephp/migrations
composer remove --dev cakephp/debug_kit
composer require --update-with-all-dependencies "cakephp/cakephp:4.0.*"
composer require "friendsofcake/cakephp-csvview:^4.0"
composer require "cakephp/migrations:^3.0"
composer require --dev "cakephp/debug_kit:^4.0"
bin/cake --version #fix any errors you see here

Cake 4.0 Structure:

/Applications/MAMP/htdocs/movie/
├── src/
│ ├── Controller/
│ │ ├── AppController.php
│ │ └── OtherController.php
│ ├── Model/
│ │ ├── Entity/
│ │ └── Table/
│ ├── Shell/
│ │ └── ConsoleShell.php
│ ├── templates/ #put the /templates folder INSIDE the /src folder
│ │ ├── Error/
│ │ │ ├── error400.php
│ │ │ ├── error500.php
│ │ │ └── other_error.php
│ │ ├── Layout/
│ │ │ ├── default.php
│ │ │ └── error.php
│ │ └── Pages/
│ │ └── home.php
│ ├── View/
│ │ └── AppView.php
│ └── other_folders/
├── config/
│ └── app.php
├── logs/
├── plugins/
├── tests/
└── vendor/

Cake 4.0 doesn’t use *.ctp so replace any remaining references and files with *.php.

Let’s switch to PHP 8.3

    "require": {
"php": "^8.1 || ^8.2 || ^8.3",
"cakephp/cakephp": "4.5.*", #and let's upgrade to cake that supports PHP 8.3

Change you PATH and MAMP to the correct PHP 8 version.

composer require --update-with-all-dependencies "cakephp/cakephp:4.5.*"

Run the Upgrade Tool for src and tests

bin/cake upgrade rector --rules cakephp40 /path/to/src
bin/cake upgrade rector --rules cakephp41 /path/to/src
bin/cake upgrade rector --rules cakephp45 /path/to/src

bin/cake upgrade rector --rules cakephp40 /path/to/tests
bin/cake upgrade rector --rules cakephp41 /path/to/tests
bin/cake upgrade rector --rules cakephp45 /path/to/tests

src/Controller/AppController.php needs some changes

// for setting a cookie
use Cake\Http\Cookie\Cookie;
use Cake\Http\Response;

// $this->loadComponent('Cookie'); *Remove this line
$this->loadComponent('FormProtection'); //add this line just above 'Paginator'

// adjust the beforeFilter() method
public function beforeFilter(\Cake\Event\EventInterface $event) {

parent::beforeFilter($event);

// Set a cookie if it doesn't already exist
if (!$this->request->getCookie('my_cookie')) {
$cookie = new Cookie('my_cookie', 'value', new \DateTime('+1 year'));
$this->response = $this->response->withCookie($cookie);
}

// Read a cookie
$cookieValue = $this->request->getCookie('my_cookie');
$this->set('cookieValue', $cookieValue);

// more...

src/templates/User/Users/login.php

<!-- replace this: -->
<input type="email" name="email" class="form-control <?= !empty($error['mail']) ? 'error' : '' ?>"
id="email" value="<?= !empty($data['mail']) ? $data['mail'] : '' ?>"
placeholder="<?= __('LABEL_LOGIN_EMAIL') ?>" required="required">
<!-- with this: -->
<?= $this->Form->control('email', [
'type' => 'email',
'class' => 'form-control ' . (!empty($error['email']) ? 'error' : ''),
'id' => 'email',
'value' => !empty($data['email']) ? $data['email'] : '',
'placeholder' => __('LABEL_LOGIN_EMAIL'),
'required' => true
]); ?>
<!-- and replace this: -->
<input type="password" name="password"
class="form-control <?= !empty($error['password']) ? 'error' : '' ?>"
value="<?= !empty($data['password']) ? $data['password'] : '' ?>" id="password"
placeholder="<?= __('LABEL_LOGIN_COMPANY_ID') ?>" required="required" minlength="5" maxlength="12">
<!-- with this -->
<?= $this->Form->control('password', [
'type' => 'password',
'class' => 'form-control ' . (!empty($error['password']) ? 'error' : ''),
'value' => !empty($data['password']) ? $data['password'] : '',
'id' => 'password',
'placeholder' => __('LABEL_LOGIN_COMPANY_ID'),
'required' => true,
'minlength' => 5,
'maxlength' => 12
]); ?>

bootstrap.php

// use Cake\Error\ErrorHandler;
use Cake\Error\ErrorTrap; // new
use Cake\Error\ExceptionTrap; // new

$isCli = PHP_SAPI === 'cli';
if ($isCli) {
(new ConsoleErrorHandler(Configure::read('Error')))->register();
} else {
//(new ErrorHandler(Configure::read('Error')))->register();
(new ErrorTrap(Configure::read('Error')))->register(); // new
(new ExceptionTrap(Configure::read('Error')))->register(); // new
}

New dependency:

composer require vlucas/phpdotenv

# and update debug_kit
composer require --dev cakephp/debug_kit:^4.5

config/bootstrap.php

//old
if (!env('APP_NAME') && file_exists(CONFIG . '.env')) {
$dotenv = new \josegonzalez\Dotenv\Loader([CONFIG . '.env']);
$dotenv->parse()
->putenv()
->toEnv()
->toServer();
}

//new
if (!env('APP_NAME') && file_exists(CONFIG . '.env')) {
// Use Dotenv to load environment variables from .env file
$dotenv = \Dotenv\Dotenv::createImmutable(CONFIG); // Updated to use Dotenv
$dotenv->safeLoad(); // Parses and loads .env variables safely
}

app.php

//old
'locales' => [APP . 'locales' . DS],
//new
'locales' => [ROOT . DS . 'resources' . DS . 'locales' . DS],

src/Controller/User/UsersController.php

// old
$log = $this->LogLogin->newEntity();

// new
$log = $this->LogLogin->newEntity($data);

PHP 8+ does not support unparenthesized ternary operators, so in templates/element/admin/breadcum.php

// old
<a <?= $i == ($length - 1) ? '' : !empty($value) ?
"href='" . $value . "'" : '' ?>><?= $key ?></a>

// new
<a <?= $i == ($length - 1) ? '' : (!empty($value) ?
"href='" . $value . "'" : '') ?>><?= $key ?></a>
composer require --dev cakephp/bake
composer require cakephp/authentication
composer update
bin/cake version
// 4.5.8

Application.php:

use Authentication\Middleware\AuthenticationMiddleware;

public function bootstrap(): void
{
// Call parent to load bootstrap from files.
parent::bootstrap();

if (PHP_SAPI === 'cli') {
$this->bootstrapCli();
$this->addPlugin('Bake');
$this->addPlugin('Authentication');
}

/*
* Only try to load DebugKit in development mode
* Debug Kit should not be installed on a production system
*/
if (Configure::read('debug')) {
$this->addPlugin('DebugKit');
}

// Load more plugins here
}

composer.json:

{
"name": "cakephp/app",
"description": "CakePHP skeleton app",
"homepage": "https://cakephp.org",
"type": "project",
"license": "MIT",
"require": {
"php": "^8.1 || ^8.2 || ^8.3",
"cakephp/cakephp": "4.5.*",
"cakephp/authentication": "^2.11",
"cakephp/migrations": "^3.0",
"cakephp/plugin-installer": "^1.0",
"friendsofcake/cakephp-csvview": "^4.0",
"mobiledetect/mobiledetectlib": "2.*",
"vlucas/phpdotenv": "^5.6"
},
"require-dev": {
"cakephp/bake": "^2.4",
"cakephp/cakephp-codesniffer": "^3.0",
"cakephp/debug_kit": "^4.5",
"josegonzalez/dotenv": "3.*",
"phpunit/phpunit": "^8.3",
"psy/psysh": "@stable"
},
"suggest": {
"markstory/asset_compress": "An asset compression plugin which provides file concatenation and a flexible filter system for preprocessing and minification.",
"dereuromark/cakephp-ide-helper": "After baking your code, this keeps your annotations in sync with the code evolving from there on for maximum IDE and PHPStan compatibility."
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Test\\": "tests/",
"Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
}
},
"scripts": {
"post-install-cmd": "App\\Console\\Installer::postInstall",
"post-create-project-cmd": "App\\Console\\Installer::postInstall",
"check": [
"@test",
"@cs-check"
],
"cs-check": "phpcs --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
"cs-fix": "phpcbf --colors --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
"test": "phpunit --colors=always"
},
"prefer-stable": true,
"config": {
"sort-packages": true,
"allow-plugins": {
"cakephp/plugin-installer": true,
"composer/installers": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}
composer test
# fix any remaining issues

Other new things in CakePHP 4 versus 3:

// old
$AppUI
// new
$AppUI['role_id']

FormProtectionComponent

AuthenticationService (🔍Auth->user and make changes)

// old example
$company_id = $this->Auth->user('company_id');
// new example
$company_id = $this->request->getAttribute('identity')['company_id'] ?? null;

• redirectUrl is not a valid method for redirection in CakePHP.

• Use the redirect() method with the desired URL (e.g., /sp/dashboard or /pc/dashboard).

Congrats. You now have CakePHP 4.5.8 and PHP 8.3.9

Now let’s Upgrade to Cake 5.0

Guide: https://book.cakephp.org/5/en/appendices/5-0-upgrade-guide.html

# Install the upgrade tool
git clone https://github.com/cakephp/upgrade
cd upgrade
git checkout 5.x
composer install --no-dev

Plugin routes are automatically loaded in CakePHP 5.x, so in Application.php

public function routes(RouteBuilder $routes): void
{
require CONFIG . 'routes.php';
// Load the routes configuration file.
//$routes->loadPluginRoutes();
}

Now run:

bin/cake routes

Let’s have a look at our composer.json

{
"name": "cakephp/app",
"description": "CakePHP skeleton app",
"homepage": "https://cakephp.org",
"type": "project",
"license": "MIT",
"require": {
"php": "^8.3",
"cakephp/cakephp": "5.1.*",
"cakephp/migrations": "^4.0",
"cakephp/authentication": "^3.2",
"cakephp/plugin-installer": "^2.0",
"friendsofcake/cakephp-csvview": ">=5.0",
"mobiledetect/mobiledetectlib": "4.8.03",
"vlucas/phpdotenv": "^5.6"
},
"require-dev": {
"cakephp/bake": "^3.0.0",
"cakephp/cakephp-codesniffer": "^5.0",
"cakephp/debug_kit": "^5.0.0",
"josegonzalez/dotenv": "4.*",
"phpunit/phpunit": "10.5.5 || ^11.1.3",
"psy/psysh": "@stable"
},
"suggest": {
"markstory/asset_compress": "An asset compression plugin which provides file concatenation and a flexible filter system for preprocessing and minification.",
"dereuromark/cakephp-ide-helper": "After baking your code, this keeps your annotations in sync with the code evolving from there on for maximum IDE and PHPStan compatibility."
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Test\\": "tests/",
"Cake\\Test\\": "vendor/cakephp/cakephp/tests/"
}
},
"scripts": {
"post-install-cmd": "App\\Console\\Installer::postInstall",
"post-create-project-cmd": "App\\Console\\Installer::postInstall",
"check": [
"@test",
"@cs-check"
],
"cs-check": "phpcs --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
"cs-fix": "phpcbf --colors --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
"test": "phpunit --colors=always",
"stan": "phpstan analyze"
},
"prefer-stable": true,
"config": {
"sort-packages": true,
"allow-plugins": {
"cakephp/plugin-installer": true,
"composer/installers": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

CakePHP5.x specific Error configuration in config/app.ph

// old
'Error' => [
'errorLevel' => E_ALL & ~E_USER_DEPRECATED, // Suppress deprecation warnings
'exceptionRenderer' => \Cake\Error\Renderer\HtmlErrorRenderer::class,
'ignoredDeprecationPaths' => [
'config/bootstrap.php', // Ignore deprecations in bootstrap.php
],
'skipLog' => [],
'log' => true,
'trace' => true,
],

// new
'Error' => [
'errorLevel' => E_ALL & ~E_USER_DEPRECATED, // Suppress deprecation warnings
'exceptionRenderer' => \Cake\Error\Renderer\HtmlErrorRenderer::class, // Use the correct renderer class
'ignoredDeprecationPaths' => [
'config/bootstrap.php', // Ignore deprecations in bootstrap.php
],
'skipLog' => [],
'log' => true,
'trace' => true,
],

And clear your cache:

bin/cake cache clear_all

I found this warning:

Deprecated: Since 5.0.0: Using `false` to disable logging scopes is deprecated. Use `null` instead.

So, for example, in config/app.php change any “false” to “null” under ‘Log’

'Log' => [
'debug' => [
'className' => FileLog::class,
'path' => LOGS,
'file' => 'debug',
'url' => env('LOG_DEBUG_URL', null),
'scopes' => null, // 'false' depracated since 5.0
'levels' => ['notice', 'info', 'debug'],
],
'error' => [
'className' => FileLog::class,
'path' => LOGS,
'file' => 'error',
'url' => env('LOG_ERROR_URL', null),
'scopes' => null, // 'false' depracated since 5.0
'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'],
],

Application.php no longer needs a “routes() functino. Because routes.php handles all routing with this:

use Cake\Routing\Route\DashedRoute;
use Cake\Routing\RouteBuilder;

/*
* This file is loaded in the context of the `Application` class.
* So you can use `$this` to reference the application class instance
* if required.
*/
return function (RouteBuilder $routes): void {
/*
* The default class to use for all routes
*
* The following route classes are supplied with CakePHP and are appropriate
* to set as the default:
*
* - Route
* - InflectedRoute
* - DashedRoute
*
* If no call is made to `Router::defaultRouteClass()`, the class used is
* `Route` (`Cake\Routing\Route\Route`)
*
* Note that `Route` does not do any inflections on URLs which will result in
* inconsistently cased URLs when used with `{plugin}`, `{controller}` and
* `{action}` markers.
*/
$routes->setRouteClass(DashedRoute::class);

$routes->scope('/', function (RouteBuilder $builder): void {
/*
* Here, we are connecting '/' (base path) to a controller called 'Pages',
* its action called 'display', and we pass a param to select the view file
* to use (in this case, templates/Pages/home.php)...
*/
$builder->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);

/*
* ...and connect the rest of 'Pages' controller's URLs.
*/
$builder->connect('/pages/*', 'Pages::display');

/*
* Connect catchall routes for all controllers.
*
* The `fallbacks` method is a shortcut for
*
* ```
* $builder->connect('/{controller}', ['action' => 'index']);
* $builder->connect('/{controller}/{action}/*', []);
* ```
*
* It is NOT recommended to use fallback routes after your initial prototyping phase!
* See https://book.cakephp.org/5/en/development/routing.html#fallbacks-method for more information
*/
$builder->fallbacks();
});

In CakePHP 5.x, the RequestHandlerComponent is no longer included by default as it has been deprecated in favor of middleware and modern request handling patterns.

In CakePHP 5.x, the PaginatorComponent has been removed in favor of using the PaginatorHelper directly in views and manual pagination logic in controllers.

I18n for translation changed a bit in 5.x, but we can ensure all the global functions are registered in composer.json

"autoload": {
"psr-4": {
"App\\": "src/"
},
"files": [
"vendor/cakephp/cakephp/src/functions.php"
]
}

Now let’s take care of

PHPstan

composer require -- dev phpstan/phpstan
composer show -- dev phpstan/phpstan
vendor/bin/phpstan analyse src --level 1--memory-limit 512M

Make a new file in root directory phpstan.neon (not necessary)

includes:
- vendor/phpstan/phpstan/cakephp/extension.neon

parameters:
level: 2 # Start with level 2, increase gradually to 5 or more
paths:
- src
- tests
scanFiles:
- config/bootstrap.php
checkMissingIterableValueType: false
checkUninitializedProperties: false
checkGenericClassInNonGenericObjectType: false
tmpDir: tmp

--

--

Marc C
Marc C

Written by Marc C

Both a creative and critical thinker, I am a programmer, bicycle rider, and woodworker, based in Japan.

No responses yet