DerEuroMark View RSS

A blog about Frameworks (CakePHP), MVC, Snippets, Tips and more
Hide details



Simple local dev env for (Cake)PHP apps 21 Aug 2024 10:45 AM (8 months ago)

The last years I was using devilbox a lot.
It has quite a simple setup for CakePHP apps, that are not using too many different external services.

Over the years that got less maintained. I started to also look into other options.
I saw several docker based systems to become more popular. Maybe I will have the chance to dive more into those, too, one day.
Traefik seems to be a powerful tool if you need more customized systems.
I want to present a less dev-op and more dev-friendly tool that uses it internally.

ddev

ddev is the new easy to setup local dev.
One of the really cool things about this modern tool is that can recognize different framework types and support them out of the box.

Setup

Once you installed ddev, just create your fresh cakephp/app clone via composer create-project or manually and run the ddev config command.
CakePHP apps should be autodetected, and webroot should be the public folder detected.

Just create your app_local.php from the example file and you are all set to go.

ddev start

Use this command to log into the container:

ddev ssh

Here you can also execute composer or migrations:

composer install
bin/cake migrations status|migrate|...

Usage

ddev launch

This should open the browser and you should see the default home screen of your (fresh) app.
All should be working right away.

It will probably open your browser and jump to your homepage. In case the app name is app, the URL would be

http://app.ddev.site/

It doesn’t need to modify your /etc/hosts file even. It just works out of the box.

If you want to see your local configs, use

ddev describe

Useful shortcuts

If you just want to run some bin/cake command, you can also use the shortcut method from the outside without the need to ssh into the container:

ddev cake [command]

Without [command] you just the same overview as always.

Addons

Some people prefer Mysql workbench or alike to connect to the DB. I personally like
phpmyadmin as it is super simple to also inline-edit.

After the installation, calling

ddev phpmyadmin

directly opens the browser with in my case

https://app.ddev.site:8037/

showing the PHPMyAdmin dashboard and your initial DB.

There are quite a bunch of other addons available, too.

Xdebug

The powerful tool to easily debug your code at runtime is also enabled and used relatively easy, I noticed.

ddev xdebug

enables Xdebug.
Then activate the browser IDE key with e.g. PHPSTORM for PHPStorm IDE. Add the server config on the IDE site to map the external path to the internal (/var/www/html) and then
start listening.

Once you set a breakpoint and reload the page, it should jump right into the IDE here.
Stepping through is vital to find out why in certain cases it jumps into a different path than expected. Using dd() here can be cumbersome as you might need several tries to find the path it took, and
to see the values it has on each decision point.

See docs for details.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

CakePHP and static analyzers 19 Mar 2024 3:25 AM (last year)

In this post, I discuss the usefulness of clean coding and static analyzers used to introspect it.
I will also outline a few neat tools and tricks to get there faster.

Static Analyzers

Statically analyzing your code can be super helpful to find obvious bugs early on, even without a single test case.
Type errors, a few basic mistakes below the API can still quite easily be found by such tools introspecting the code from the outside.

In the PHP world, we mainly refer to PHPStan or Psalm here.
They both serve their use cases, aiming at a slightly different focus. Psalm, these days, seems to have left the other one behind, in terms of functionality and depth of introspection. PHPStan seems a bit easier to get started, however.

If in doubt which to chose/try, go with PHPStan. Better cost-income ratio: Most relevant issues solved with minimum effort.
Psalm can still be checked on top afterwards to see if any issues remain that have been missed. For most apps there is only a very slight benefit of doing this, you will also
have to look through quite a bit of false positives and noise to find relevant things.

PHPStan

In terms of PHPStan, your code should usually always reach at least level 5. Ideally, you are even compatible with the stricter checks and other sniffer enhancements of level 7, though. Level 8 is "nullable" topic and often not realistic for larger apps. If you can, even better!

It is usually hard to become cleaner then the core and plugin code you depend on. Cake 5 is on level 8 and probably leading in PHP frameworks on that metric. Most plugins should also be. So from that side you are safe.

Make sure to use the CakePHP extension for PHPStan for better out of the box results for this framework:

composer require --dev phpstan/extension-installer cakedc/cakephp-phpstan

Note that adding the extension-installer makes it also work right away, no further configuration necessary.

Psalm

If you prefer psalm, make sure to set up at least a psalm.xml with some configs.
Your base level should be lower (e.g. errorLevel="4"), and you can work yourself higher piece by piece until you reach level 1.
Psalm is very delayed, sometimes to a fault. Ignore certain "irrelevant" and more noisy checks.

Make sure to use the cakephp-psalm extension here to get better out of the box results for this framework.

Annotations

This is one of the easiest and most important topics: Always keep your (class) annotations up to date.
The IdeHelper plugin takes care of 99% fully automatically.
You only got to do some tweaking and manual adjustments for edge cases.

They provide valuable information not only for the IDE and you, but also for the static analyzers.
As @property/@method tags it sets important meta data for them to know what methods are available, what the possible input and output types are.

Clean Code

Here I mainly mean to code defensive, taking edge cases into consideration. Also to code as strict as possible to avoid issues along the way.
Strict interface contracting, SOLID principles and good type-hinting can help, as well. Interfaces and a certain architecture, however, are often more important for framework or plugin code. For project code you do not have to "over-engineer" certain topics. Here no one will further extend your code, or depend on it.
Also casting can be an important tool prior to passing certain "user inputted" values further into methods.

Be precise in method arguments and return values

This is also quite important here: If you expect "mixed" arguments everywhere, you basically destroy the use of the tools outlined above.
Try to always accept only one type of input, maybe "nullable" (allowing null as 2nd argument type) on top if needed. In PHP 7.1+ world, it would be ?type for type-hinting.
The same goes for return values.

With PHP 8 you can also use union types of sorts. But try to keep the amount of types always to the absolute minimum.

Setters/getters

These are especially hard to get right in CakePHP, as the entities by design are quite dirty to allow simple usage and customization.
All properties can always be null, even if the DB field is not nullable. You could not have queried the field, or hydration here was not complete.
As such, you will always either have the tools complaining here if you annotate it truthfully (type|null) or you will trip over the code in production despite proper validation and tests due to the hidden "lie".

Try to use the getOrFail pattern as much as possible where you expect a not-null return value and otherwise explicitly check on !== null and throw respective exceptions here.

Especially around entities and their fields it is often impossible to have the annotations be "correct" for all cases.
Even "required" fields can be empty (null) if not pulled (selected fields) from the DB. So for cases like these it can be useful to leverage the Shim plugin enhancements:

use Shim\Model\Entity\GetSetTrait;

class MyEntity extends Entity {

    use GetSetTrait;

Now you can use these methods on values that are expected to be present:

$entity->getOrFail('field'); // Cannot be null
$entity->getFieldOrFail(); // Cannot be null

DTOs and typed objects

Try to go away from larger associative arrays in favor of clear objects.
Ideally, those are either value objects or DTOs.

$this->Pr->displayInfo($pullRequest['head']['ref'], $pullRequest['head']['sha']);

becomes

$this->Pr->displayInfo($pullRequestDto->getHead()->getRef(), $pullRequestDto->getHead()->getSha());

Using an API with methods and a clear param/return type helps to validate the expected type.
If e.g. the type on the displayInfo() method here does not match the type of the getter, you can now get warned or the code error early.
With assoc arrays array<string, mixed> you have no such type-safety.

Generics

Whenever you return a collection of something, make sure the element type is set/known, e.g. on the returning method. If this is not possible, use an inline annotation:

/** iterable<\App\Model\Entity\Article> $articles */
$articles = $somewhere->getArticleCollection();

When you now foreach over them, the entity fields are visible and autocompleted, and static analyzers can help here, too.

Issues and workarounds

There are known issues with this rather new feature of generics in most modern IDEs, including PHPStorm (1, 2).

To workaround those, and have it work for both IDE (human devs) as well as analyzers (PHPStan/Psalm), you can also just call it an array for now:

/** array<\App\Model\Entity\Article> $articles */
$articles = $somewhere->getArticleCollection();
foreach (...)

Making mixed/unknown vars visible

Now this is a pro-tip to really deep dive into potentially "hidden" issues.

Your app might appear to be fine in PHPStan level 7/8. Everything green, all good.
But in fact, often times, it is just green because PHPStan ignored certain variables due to it being mixed or unknown.
To make those visible and clarify where an inline annotation is needed for actual protection against regressions or issues, add the following library to your stack:

composer require --dev symplify/phpstan-rules

In your PHPStan file:

rules:
    - Symplify\PHPStanRules\Rules\Explicit\NoMixedPropertyFetcherRule
    - Symplify\PHPStanRules\Rules\Explicit\NoMixedMethodCallerRule

You will now get the feedback on where PHPStan silently ignored certain lines and checks. Once you added the necessary inline annotations you should see the issues being visible again and you can fix up your code some more.

If you cannot fix all issues, you can also comment out those two lines again and only run them occasionally.

Asserts

With assert() there is another alternative to throwing exceptions available.
I mainly use it for theoretical (practically unlikely to impossible) cases, where PHPStan would think a value could be null or some other type that by code flow is not possible.
Instead of checking and throwing exceptions this can be a shortcut here.

Example:

$foreignKey = $relation->getForeignKey(); // Return type says array|string
assert(is_string($foreignKey), 'Not a string');

We know that for our code and relation it can always only return a scalar value.
So the shortcut is fine here.

It also helps developers to catch certain issues early without imposing those same checks in production (sure, often somewhat nano-optimization).

That said: Since those can (and often are) disabled for production, if those are realistic issues there, it will lead to fatal errors with sometimes not clear exception messages.
So for those scenarios it is better to handle these bail-early cases with classic Exceptions and throw meaningful errors messages to be logged. In many cases
those can also be 404s then, instead of 5xx. In my case, those go to a different log handling and thus do not necessarily alert me via message right away.

Composer command

It can be useful to have a composer command ("scripts") for the commands to execute.
For PHPStan, for example, add this to your composer.json file scripts section:

    "scripts": {
        ...
        "stan": "phpstan analyze",

Note: I would not recommend using both for an application, only for a vendor lib/plugin maybe.
The reason is simple: They are reporting similar things and you will have to work twice for some of the workaround or silencing – with little benefit for you.

As noted above, psalm is more fine-grained and detailed, which for most apps can be almost too much. So personally prefer to have it work with PHPStan and am happy with it on level 7 or 8.
It finds almost 99% of the important issues without hours of extra work spent on the topic.

Continuous Integration

Make sure to set up your static analyzer checks as CI based hook.
So not only run it locally before pushing and pull requesting, but also on that PR.
The skeleton app contains an example here you can copy and paste or adjust to your needs.

I for example use Gitlab and the included free Gitlab CI.
It uses a .gitlab-ci.yml in the repository root and basically lists:

    - composer test
    - composer stan
    - composer cs-check

I recommend this order, as the tests are the most important result, the CS issues are the least important. In most pipelines it usually

Limitations

Maybe just to complete the topic, a few things are worth mentioning that it cannot (yet) cover.

The static analyzers usually can not look into the templates and PHP code there.
So here, you need to run classic tests, usually controller based integration tests.

As they only check the type, not the content, don’t rely on them for value based contracts and asserts.
Here, use classic unit tests.

Summing it up

Using most or all of the above will make your code much more robust and bug-free. It will also make any refactoring way easier as side effects will be found out to a much higher percentage and the developer alerted immediately in the PR.
Instead of finding a lot of issues after the fact, those can be taken into consideration and addressed in the same step.

You still need to your tests and test coverage, though 🙂
This just compliments it where they don’t reach. And since coverage is usually between 20-40% (very rarely 60%) for most larger projects, this is usually an important 2nd layer, that comes pretty much for free.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

CakePHP background processing 12 Mar 2024 8:13 AM (last year)

The easy way 🙂

Some might also already be in the #CakePHP coding world for some time.
They might remember an article from like 11 years ago: queue-deferred-execution-in-cakephp.
If not, maybe catch up on that one first, as that is the intro to this new post of 2024 now.
It explains the main reasons why a queue is a must have for every CakePHP app, even if not that big just yet.

11 years later

It started more as a demo tool for background processing and wasn’t quite meant as stable plugin for larger/enterprise CakePHP apps.
With more and more users that loved the easiness of the plugin, the dependency free usage that integrates perfectly into a CakePHP app, and the default features it ships with out of the box it seemed to slowly grow into the status quo, though.

So what has changed in 10+ years of building the Queue plugin, half a million downloads and thousands of users and apps later?

It is now officially out of any demo state, and seems to reliable do its job even on larger databases and codebases.
The Queue plugin now integrates even more seamlessly into any app.
Not just a requirement for sending emails properly, but for any kind of work offloading, like PDF generation or media processing.

It also moved away from the legacy "PHP serialization" of objects to a more safe and sane approach using JSON for encoding/decoding.
With the following article I will show case some of the new benefits of the now freshly released v8 version.

Main benefits

Just to quickly summarize:

QueueScheduler

With this optional plugin on top you can easily make scheduled runs of Commands or shell scripts.
They will be put into the queue and run when it is time.
It can be fully controlled from the backend, so no server access is necessary, only admin access to the backend.

Check it out: QueueScheduler plugin

IDE autocomplete

For PHPStorm and IDEs that allow meta information to be used to make the framework experience smoother, the plugin provides an easy way to integrate with the
must-have IdeHelper plugin.

It scans all existing tasks and provides them as autocomplete on your createJob() call:

Config as typed object

Associative arrays with unknown or unclear keys became a code smell over time.
So naturally, with powerful IDEs and PHPStan/Psalm, this also became more visible.

The Queue plugin allows now configuration to be passed with a clear fluent interface for the object that builds the config:

$config = $queuedJobsTable->newConfig()
    ->setPriority(2)
    ->setReference('foo')
    ->setNotBefore('+1 hour');
$queuedJobsTable->createJob('OrderUpdateNotification', $data, $config);

All methods on that JobConfig object are fully autocompleted and typehinted.

DTO usage

Similar topic for the $data param, both as input and as output on the concrete task.
It now supports any object passed that can serialize itself into an associative array and therefore a valid payload to pass into the task.

You can, for example, use DTOs from the CakeDto plugin to handle the transfer process.

Set up a DTO per task in your dto.xml, e.g.

<dto name="OrderUpdateNotificationQueueData" immutable="true">
    <field name="orderId" type="int" required="true"/>
    <field name="type" type="string" required="true"/>
    ...
</dto>

Instead of a plain array you can now rely on a clean API for input:

$dataDto = OrderUpdateNotificationQueueDataDto::createFromArray([
    'orderId' => $order->id,
    'type' => 'orderConfirmationToCustomer',
]);
$this->fetchTable('Queue.QueuedJobs')->createJob('OrderUpdateNotification', $dataDto);

Any of the required fields not provided or fields not defined will throw a clear exception.

Same then for the counterpart within the task:

public function run(array $data, int $jobId): void {
    $dataDto = OrderUpdateNotificationQueueDataDto::createFromArray($data);

    $orderId = $dataDto->getOrderId();
    $order = $this->fetchTable('Orders')->get($orderId, contain: ['OrderItems']);
    $this->getMailer('OrderConfirmation')->send($dataDto->getType(), [$order]);
}

PHPStan together with tests can now fully monitor and assert necessary data.
No more chaos with associative arrays – instead you got maximum IDE/autocomplete and testing capability.

DIC support

If you use the Dependency Injection Container provided by CakePHP you can also use
it inside your tasks.

use Queue\Queue\ServicesTrait;

class MyCustomTask extends Task {

    use ServicesTrait;

    public function run(array $data, int $jobId): void {
        $myService = $this->getService(MyService::class);
        ...
    }
}

As you see here you have to add the ServicesTrait to your task which then allows you to use the $this->getService() method.

Live demo

Check out the sandbox examples. You play around with it live.

You need help?

As a freelancer I am available to some extend. You can hire me for your (CakePHP) project to

Feel free to reach out.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

Upgrading MySQL 5 to 8 7 Mar 2024 3:17 PM (last year)

Are you planning to upgrade an existing app and DB from Mysql 5 to 8 any time soon?
Well, if you do, maybe you want to read the next paragraphs.

I had to do it, and let me tell you: There are some pitfalls awaiting.
The following is a note of a few things I encountered and how I dealt with it.

Preparation

In order to prepare and verify I first created not only a backup, but also a textual commit
of the current schema (.sql file of schema).

After the update, doing the same again, I am able to (git) diff the SQL schemas and see
if there are any noteworthy differences.

Legacy or not

Even if you don’t have a super legacy system from like 2010, odds are your database schema
has still a few incompatible relics.

Detecting them, creating migrations early on and moving away from those is the best strategy
before actually upgrading. This prevents data loss, both actual and semantic.

One example:
integer is dropping the long deprecated "length", so integer(5) will just be integer moving forward.
But one piece of the code was kind of coupled to it, deciding on the length how many digits the number can have in validation.
So having validation now not output anything anymore (even for numbers larger 99999), can be also
problematic for data integrity.

What I did here: Moving the length to the comment as [schema] length: 5 and making sure validation is using that instead.
One could also hardcode this directly in the app. But based on the framework, this is a bit more agnostic approach
that should work also across different DBs/apps if needed.

Booleans

Now this is the most serious one I encountered:

Once upon a time I moved all int/tinyint columns from signed to unsigned where negativ numbers would never be stored and make no sense.
Part might have been (nano)optimization, but overall the most useful aspect of this is that in the application we don’t have to worry about handling
non-positive numbers coming from DB.

Now, tinyint(1) always was and is special: Since MySQL doesn’t have a native bool type, they are using this more or less officially as their boolean representation.
MariaDB has a more native approach on this and provides even constants as aliases.
And this means: tinyint(1) is literally the only int based type that is valid with a (1) length. It will be kept, whereas tinyint(2) already loses it and internally becomes tinyint(4), displayed only as tinyint.

But either way I now had tons of tinyint(1) unsigned NOT NULL default '0' columns for all the booleans in my tables.

Turns out that Mysql 8 did a rather stupid or inconsistent thing here:
A tinyint(1) MUST now be signed to be valid as boolean representation.
If is it unsigned, instead of switching the sign or allowing signed/unsigned to work (which would both make sense) it will lose the length completely, becoming a non-boolean suddenly.
So this is a huge issue if you forget to change this prior to the migration as afterwards it is not distinguishable from any normal tinyint column.

I did write a small script that essentially migrates this ad-hoc where needed:

if (preg_match('/^tinyint\(1\) unsigned/', $type)) {
    $type = 'tinyint(1) NOT NULL DEFAULT \'0\'';
    $todo[] = 'ALTER TABLE' . ' ' . $table['table_name'] . ' CHANGE `' . $name . '` `' . $name . '` ' . $type . ';';
}

The full script can also be found here. One can also just copy and paste the needed parts as PHP agnostic version.

In closing I can also say that I wish MySQL 8 would have finally added the same native bool type other DBs already have here.
And/or make the upgrade less breaking in the "unsigned" part.

Date(time)

Some of the fixture or test data was set to 0000-00-00 or 0000-00-00 00:00 for not null date/datetime columns.
This will now also break.
So best to also run update statements to set them to some random (valid) datetime.

Other than that I didn’t run into any larger issues.

Hope it helps for you to avoid some of these traps.

Further resources

Apparently, there is also an upgrade-checker-utility available that can point out some breaking changes.
See also this more detailed resource.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

Enums in CakePHP – reloaded 30 Jan 2024 9:32 AM (last year)

If you have been in the Cake ecosystem for a while, you might remember my 13+ year old blog post about how to work with enums in CakePHP the easy way.
I have been using them heavily for all my apps so far.
Tinyint(2) mapping to an alias as well as translatable label text for the template output.

After (Cake)PHP and DBs progressed quite a bit, let’s have a new look on the subject.

PHP 8 enums

With PHP 8 we now have enums and more specifically also backed enums.
The latter are what is mainly useful to us now, as they provide a key value mapping to the DB column values.

CakePHP 5.0.5+ comes with pretty much full support of such enums for your baked code and template forms.

String vs Int

I personally like to use int here, as they provide the same benefits as outlined in the previous post:

They also would seamlessly upgrade from the former "static enums" of mine.

So how would they look like? Let’s assume we move the field status to this new approach:

namespace App\Model\Enum;

use Cake\Database\Type\EnumLabelInterface;
use Cake\Utility\Inflector;

enum UserStatus: int implements EnumLabelInterface {

    case Inactive = 0;
    case Active = 1;
    ...

    /**
     * @return string
     */
    public function label(): string {
        return Inflector::humanize(Inflector::underscore($this->name));
    }

}

You want to implement the EnumLabelInterface in order to have a nice label text to display, e.g. in dropdown forms for add/edit or as text on index/view.

Setup

You define your table column as tinyint(2) or string and then map it to the EnumType in your Table’s initialize() method.

use App\Model\Enum\UserStatus;
use Cake\Database\Type\EnumType;

$this->getSchema()->setColumnType('status', EnumType::from(UserStatus::class));

Bake

With Bake 3.1.0 you can bake your enums easily.
The convention is to use {EntityName}{FieldName}, e.g.

bin/cake bake enum UserStatus

In case you use tinyint(2) as your column type instead of string, use

bin/cake bake enum UserStatus -i

I would recommend quickly adding the cases you to include, using a simple list or case:value pairs:

bin/cake bake enum UserGender male,female,diverse 

The values will be the same as cases if not specified further.

For int it will use the numeric position as value:

bin/cake bake enum UserStatus inactive,active -i
// same as
bin/cake bake enum UserStatus inactive:0,active:1 -i

Also note that you can fully bin cake bake all now when putting these config into the comment of a DB column, prefixed with [enum]:

    ->addColumn('status', 'tinyinteger', [
        'default' => 0,
        'limit' => 2,
        'null' => false,
        'comment' => '[enum] inactive:0,active:1'
    ])
    ->addColumn('gender', 'string', [
        'default' => null,
        'limit' => 10,
        'null' => true,
        'comment' => '[enum] male,female,diverse'
    ])

It will generate both enum classes for you and auto-map them into the UsersTable class.
On top, it will also prepare the views for it and forms will also work out of the box with it.

Usage

With the enum being a value object of sorts it is now not necessary anymore to provide the options manually.
So the forms for mapped columns can now be changed to

// echo $this->Form->create($user); before somewhere
-echo $this->Form->input('status', ['options' => $user->statuses()]);
+echo $this->Form->input('status');

In cases where your form does not pass in the entity, you still want to define them as options key.
Here you need to either manually iterate over the key and value pairs, or just use the Tools plugin (3.2.0+) EnumOptions trait.

use Tools\Model\Enum\EnumOptionsTrait;

enum UserStatus: int implements EnumLabelInterface {

    use EnumOptionsTrait;

    ...

}

and

echo $this->Form->control('status', ['options' => \App\Model\Enum\UserStatus::options()]);

The same applies if you ever need to narrow down the options (e.g. not display some values as dropdown option), or if you want to resort.
These features were also present in the former way.

Comparing, e.g. in controllers, works with the ->value:

if ($this->request->getData('status') == UserStatus::Active->value)) {...}

For string values you can use === comparison right away always. For int ones you would need to cast the values before doing so, as those integers usually get posted as strings.

Wherever you need to create an enum manually, e.g. in the controller action, you can use

// Now UserStatus enum of that value
$user->status = UserStatus::from((int)$this->request->getData('status'));

Authentication

With TinyAuth 4.0.1 there is now also full support for enums in (User) roles.

A warning, however: Any non scalar values in the session data will make all sessions break each time there is a change to it.
It can be the Enum class here, but also other value objects or alike.
I usually try to keep them out of there, so I don’t have to nuke all sessions after such updates all the time.

Translation

As in the previous post, this is an often needed feature that should be available here if needed.
For our enums here you can wrap the label() return value in __() and should be done.

Make sure to manually add those values to your PO files, as the parser cannot find them when using such variable cases.

Bitmasks

The Tools.Bitmasked behavior is also upgraded to support Enums now:

use App\Model\Enum\CommentStatus;

$this->Comments->addBehavior('Tools.Bitmasked', [
    'bits' => CommentStatus::class, 
    'mappedField' => 'statuses'],
);

By using an Enum for bits it will automatically switch the incoming and outcoming bit values to Enum instances.

You can also manually set the bits using an array, but then you would have to also set enum to the Enum class:

$this->Comments->addBehavior('Tools.Bitmasked', [
    'bits' => CommentStatus::tryFrom(CommentStatus::None->value)::options(), 
    'enum' =>  CommentStatus::class, 
    'mappedField' => 'statuses'],
);

DTOs

With 2.2.0 there is also support for such enums in CakePHP DTOs.

Example:

<dto name="FooBar" immutable="true">
    ...
    <field name="someUnit" type="\App\Model\Enum\MyUnit"/>
    <field name="someStringBacked" type="\App\Model\Enum\MyStringBacked"/>
    <field name="someIntBacked" type="\App\Model\Enum\MyIntBacked"/>
</dto>

Also JSON serialize/unserialize will work fine, for e.g. cross system API calls.

Outlook

With CakePHP 5.1 there are plans to also further provide validation for enums.
See also

There is also an enum library out there that adds quite some syntactic sugar on top.
Check it out, maybe there is something useful to take away for your own enum collections.

Postgres native enum

Postgres has recently added its own native Enum type.
I would advice against using that one for now, as it does not have the outlined benefits and out of the box support from the framework yet.
Instead you can use the name database agnostic approach from above.

Summing it up

PHPStan supports this new enum approach quite well and also likes it more that the methods are now more narrow in return types.

Given that the native enums and their more strict validation on the value(s) can provide pretty much all the features so far mentioned in the static enum approach as well as some neat new things on top (like auto built-in support for forms), it is recommended now to use this approach where you can in your apps.

Also upgrading from my previous approach to this should be quite straight forward.
Enjoy!

Update 2024-03

With CakePHP 5.0.7 also int backed enum validation now works as expected.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

Templating and Icons in CakePHP 11 Dec 2023 4:32 PM (last year)

There is a new plugin on the block: Templating
Check it out.

PS: This is a direct successor of www.dereuromark.de/2022/12/19/font-icons-in-cakephp-apps.
So all of the functionality around icons is directly ported, as well. Please read that article for some history on the motivation and benefits of the Icon helper.

I want to quickly showcase some of the new shiny things it ships with here.

HtmlStringable interface

This interface can be used for any HTML (stringish) template your helpers and template elements generate.

use Templating\View\HtmlStringable;

class SvgGraph implements HtmlStringable { ... }

// in your templates
$icon = new SvgIcon($name);
$this->Html->link($icon, '/my/url');

Use can also use the Html value object directly:

use Templating\View\Html;

$html = Html::create('<i>text</i>');
$this->Html->link($html, '/my/url');

The neat part about this when using it in combination with such value objects:
No more 'escapeTitle' overhead needed.

You can use the helpers shipped with this plugin, you can add the traits yourself to your helpers or just write your own
3-liner for it.

// in your AppView::initialize()
$this->addHelper('Templating.Html');
$this->addHelper('Templating.Form');

Before:

$icon = $this->Icon->render('delete');

$this->Form->postLink($icon, ['action' => 'delete', $id], ['escapeTitle' => false]);

After:

$icon = $this->Icon->render('delete');

$this->Form->postLink($icon, ['action' => 'delete', $id]);

Note that when using declare(strict_types=1); you need to manually cast when passing this to methods that only accept string:

$icon = new SvgIcon($name);
// CustomHelper::display(string $html) does not accept Stringable
$this->Custom->display((string)$icon);

When not using strict_types this is optional.

It is recommended to adjust this helper and method on project level then, adding the interface into the signature
as done for Html and Form helpers.

public function display(string|HtmlStringable $icon, array $options = []): string {
    if ($icon instanceof HtmlStringable) {
        $options['escapeTitle'] = false;
        $icon = (string)$icon;
    }

    return parent::display($icon, $options);
}

When not using PHP templating, but e.g. Twig or alike, you can also easily write your own helper methods there that would internally do this and accept those stringable icons.

(Font) icons

The plugin ships with a helper to handle most common font icons and contains useful convenience wrappers.
On top of the former way using plain strings (see linked article and Tools plugin) it now provides the icons as value objects (using HtmlStringable interface).

The main advantages have been outlined above. This aims to provide a very convenient way of using (font) icons now in your templates.

Setup

// in your AppView::initialize()
$this->addHelper('Templating.Icon');

Make sure to set up at least one icon set:

Or add your custom Icon class.

The icon set config you want to use can be passed to the helper – or stored as default config in Configure key 'Icon' (recommended).
You can add as many icon sets as you want.

E.g.

'Icon' => [
    'sets' => [
        'bs' => \Templating\View\Icon\BootstrapIcon::class,
        ...
    ],
],

For some Icon classes, there is additional configuration available:

In this case make sure to use an array instead of just the class string:

'Icon' => [
    'sets' => [
        'material' => [
            'class' => \Templating\View\Icon\MaterialIcon::class,
            'namespace' => 'material-symbols-round',
        ],
        ...
    ],
],

Don’t forget to also set up the necessary stylesheets (CSS files), fonts and alike for each active set.

Usage

render()

Display font icons using the default namespace or an already prefixed one.

echo $this->Html->link(
    $this->Icon->render('view', $options, $attributes),
    $url,
);

Especially if you have multiple icon sets defined, any icon set after the first one would require prefixing:

echo $this->Html->link(
    $this->Icon->render('bs:view', $options, $attributes),
    $url,
);

You can alias them via Configure for more usability:

// In app.php
'Icon' => [
    'map' => [
        'view' => 'bs:eye',
        'translate' => 'fas:language',
        ...
    ],
],

// in the template
echo $this->Icon->render('translate', [], ['title' => 'Translate this']);

This way you can also rename icons (and map them in any custom way).

I personally like to rename them to a more speaking way to the action they are supposed to show.
So I use e.g.

    'details' => 'fas:chevron-right',
    'yes' => 'fas:check',
    'no' => 'fas:times',
    'prev' => 'fas:arrow-left',
    'next' => 'fas:arrow-right',
    'translate' => 'fas:language',
    ...

They are then way easier to remember than the actual (cryptic) icon image/name.

names()

You can get a nested list of all configured and available icons.

For this make sure to set up the path config to the icon meta files as per each collector.
E.g.:

'Icon' => [
    // For being able to parse the available icons
    'sets' => [
        'fa' => [
            ...
            'path' => '/path/to/font-awesome/less/variables.less',
        ],
        'bs' => [
            ...
            'path' => '/path/to/bootstrap-icons/font/bootstrap-icons.json',
        ],
        'feather' => [
            ...
            'path' => '/path/to/feather-icons/dist/icons.json',
        ],
        'material' => [
            ...
            'path' => '/path/to/material-symbols/index.d.ts',
        ],
        ...
    ],
],

You can then use this to iterate over all of them for display:

$icons = $this->Icon->names();
foreach ($icons as $iconSet => $list) {
    foreach ($list as $icon) {
        ...
    }
}

Configuration

You can enable checkExistence to ensure each icon exists or otherwise throws a warning in logs:

'Icon' => [
    'checkExistence' => true,
    ...
],

Auto-complete

Now for the most powerful feature and probably most helpful one:
Let your IDE (e.g. PHPStorm) provide you the available icons when you type $this->Icon->render( and quick-select from the dropdown list.

In order for this to work you just need to add the IconRenderTask shipped with this plugin and you are all set.

    'IdeHelper' => [
        ...
        'generatorTasks' => [
            \Templating\Generator\Task\IconRenderTask::class,
        ],

Demo

See the examples in the sandbox. The source code can be opened via GitHub.
Real life examples you can also run locally by spinning up the sandbox in a local docker container.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

Improved Paginator for CakePHP 7 Dec 2023 12:28 AM (last year)

The default paginator throws a 404 on "OutOfBounds", so when you happen to access the pagination with a too high page number.

What can be the issue with this?

So first: This is not very user friendly.
Often times this comes from an old indexed (e.g. Google) page, and therefore the user would rather expect to land on the last page here instead of having to try to guess how to fix the URL here instead.

Another issue is that in a delete of exactly the first element of the last page on that pagination list, the redirect "back" would also end up in a 404, since now the page count decreased in the meantime, either by that user or someone else or even the system (deactivation/cleanup through background tasks).

In all those cases the last actual page to automatically opening up would totally suffice and be much better in terms of usability.

Finally, the error logs are filling up on totally unnecessary info here.
I already separate my logs on actual issues vs 404s, and 404s that are internally triggered (referer is own site) end up on the actual error list. So having those false positives filtered out helps to alert me about the real things going boom or dead links that require fixing.

The solution: Redirect

The Shim plugin now ships with a trait to do that for you.

Add it to your AppController and enjoy the out-of-the-box magic.

...
use Shim\Controller\RedirectOutOfBoundsTrait;

class AppController extends Controller {

    use RedirectOutOfBoundsTrait;

    ...

}

Some notes:

This trait is currently for CakePHP 5, but should be easily backportable to v4.

Other improvements the Shim plugin offers

Usually a custom Paginator has to be set in each controller separately.
The Shim.Controller, if extended, allows a more global configuration.

use Shim\Controller\Controller;

class AppController extends Controller {
}

In your config set Paginator.className for what Paginator should be used across all controllers:

'Paginator' => [
    'className' => \My\Special\CustomPaginator::class,
],

Your custom Paginator class should still extend Cake\Datasource\Paging\PaginatorInterface, of course.

Note: The same improvement also directly comes with the RedirectOutOfBoundsTrait above, so if you are using that one, you are already covered.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

Database migration tips for CakePHP 4 Dec 2023 6:55 AM (last year)

CakePHP uses Phinx by default.
The following tips will help with larger and legacy projects to maybe clean things up.

Migration file count

Problems

If your migrations piled up over the years, and you have like hundreds of migrations, maybe even renaming tables and fields multiple times,
it can become hard to understand what field and what attributes on it have been introduced.

I also encountered some legacy migrations that used raw SQL to insert table structures.
This is also something that usually should be avoided in favor of using the consistent API.

Snapshots

The solution for the problems mentioned above is usually to make a fresh snapshot.
It will just dump the whole table and field structure into a fresh file.

Let’s imagine the project is already live for years.
Here you don’t want to create any downtime, or at least switch to the snapshot as smooth as possible.
I will outline a few steps how I did that once.

Process

Make sure all your servers are up to date with latest migrations before you start.
Also the team should be aware and not introduce new migrations while doing this snapshot reset.

Let’s get started then:

bin/cake bake migration_snapshot InitDb

I first bake my new snapshot.

Then I go inside and remove all generated tables that come from a plugin that I use to keep as plugin based tables.
In my case it is Tools and Queue for example:

"bin/cake migrations migrate -p Tools --no-lock",
"bin/cake migrations migrate -p Queue --no-lock",
"bin/cake migrations migrate --no-lock"

If you don’t have any or always copy the schema over to your app, then this step can be skipped.

Then I start to compare the freshly generated migration with the ones already present.
Once I think I have a good understanding I remove all other migration files from config/Migrations/, leaving only the new snapshot.

I then add the following logic to the top of it in up():

// Add the version string here of your freshly generated `InitDb` migration
$version = '20231203033518';
$result = $this->query('SELECT * FROM phinxlog WHERE version < ' . $version . ' LIMIT 1')
    ->fetch();
if ($result) {
    $this->execute('DELETE FROM phinxlog WHERE version < ' . $version);

    return;
}

// Actual migrations start here

This will do the following:
If this is an empty DB, it will just normally migrate.
But if it an existing and filled one (e.g. local, staging or production), it will skip them all and also remove all "missing" files we removed earlier before
marking this migration as fully migrated.

So either way you end up with the same DB structure and auto-cleaned on top.

The same process would also work for any of your plugins. Just maybe don’t do that on public ones, as other consumers might run into this without being aware.

You can also delete directly, and instead focus on the support of all DB types, including Mssql.
This is an actual example from a new major release (v8) of a plugin:

public function up(): void {
    // We expect all v7 migrations to be run before this migration (including 20231112807150_MigrationAddIndex)
    $version = '20240307154751';
    if (ConnectionManager::getConfig('default')['driver'] === 'Cake\Database\Driver\Sqlserver') {
        $this->execute('DELETE FROM queue_phinxlog WHERE [version] < \'' . $version . '\'');
    } else {
        $this->execute('DELETE FROM queue_phinxlog WHERE `version` < \'' . $version . '\'');
    }

    if ($this->hasTable('queued_jobs')) {
        return;
    }

    // Actual migrations are all here, generated via bake migration_snapshot

Finalization

Careful: Before actually moving forward, best to create a file based export of your actual DB and then run the migrations here on an empty DB and do the same.
Then compare (diff based) if those are actually the same or if some important fields or constraints might be missing, that somehow didn’t get regenerated.

Also run a full test in your new DB to see if the tests still pass. Finally also click through the GUI/frontend and confirm that it is working as it should.

Deployment

Once you are quite confident over it, you can deploy this and the deployment part of migrations should auto-change as mentioned above flawlessly.

Bad or conflicting migration names

I had some plugin migrations called

20150425180802_Init.php
20150511062806_AddIndex.php
20150621110142_Rename.php
...

and application migrations

20141129110205_Init.php
...

The problem here is that each of those names like "Init" will be a class name and loaded at the same time.
So having too generic class names across plugins and your application, it will fail the migration process (incl the test harness).

The good news:
Changing those names can be done in a BC way. Even if already run on production.
The reason is that it uses only the version number (timestamp) to identify, so renaming just the name after the _ is OK at any time.

I just made the plugin ones more unique by prefixing "Migration{PluginName}" in front of them:

20150425180802_MigrationQueueInit.php
20150511062806_MigrationQueueAddIndex.php
20150621110142_MigrationQueueRename.php
...

With this I had no more collisions between app and plugins and my migrations especially also for test harness now run fine:

(new \Migrations\TestSuite\Migrator())->runMany([
    ['connection' => 'test'],
    ['plugin' => 'Tools', 'connection' => 'test'],
    ['plugin' => 'Queue', 'connection' => 'test'],
    ['plugin' => 'QueueScheduler', 'connection' => 'test'],
]);

Hope these tips can help you in your everyday migration work.

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

Working with decimals in PHP apps 26 Nov 2023 3:39 PM (last year)

DB Basics

Let’s talk about some of the basics first.

float vs decimal

In general, floating-point values are usually stored as float in the database.
In earlier versions of databases they also often had precision/scale (e.g. float(5,2) for values like 123.56).
As per standard this is not something that should be used anymore for Mysql and probably other DBs.
So we can say they represent float without the exact decimal places being relevant.

In case the scale (number of decimal places) is relevant, it is best to use a special type of float called decimal.
Here we can set precision and scale accordingly, e.g. decimal(5,2).

Why not int?

Some people might ask.
Well, in some rare cases this might work, but for most other cases it usually never stays int.
It also makes it harder to add scale afterwards, lets say from 1.00 represented as 100 to 1.000 represented as 1000.
Same value for floating-point, but x10 in integer representation. With this change a huge risk of adding human error when
modifying your code and calculations etc.
Some more thoughts can be found on this old reddit page.

PHP

Now when we read those in PHP apps through ORMs or PDO, they are often transformed into their "float" type form.
For calculating and keeping the scale as well as other math and rounding issues it is however often not practical to do the same for decimal.
There are also issues when passing the float around or comparing them directly (see this).
With float you are also bound to the max value of that system (32 or 64 bit), strings don’t have that limit. This is, of course, only relevant for cases with very larger numbers.

So while some ORMs still use float by default others by default cast to string. CakePHP ORM, for example.

A string can be passed around more easily, including serialized and keeping the scale.
However, using it in normal calculations requires a cast to float and back to string afterwards.
Especially since with strict_types set it can easily break your app hard.
Also PHPStan would otherwise not be happy with the code – and we want to use here the highest level of protection for the code. So it sure makes sense for it to point out those missing casts where they appear. With every cast the scale can go boom or be set to a different way too long value.
A possible workaround is to handle it using bcmath functionality, those are not always so easy to work with, however.

A small example on even basic additions being problematic in float:

$floatAmount1 = 0.1;
$floatAmount2 = 0.2;

$sumFloat = $floatAmount1 + $floatAmount2;

echo "Sum using float: $sumFloat\n"; // Output: Sum using float: 0.30000000000000004

$stringAmount1 = '0.1';
$stringAmount2 = '0.2';

$sumString = bcadd($stringAmount1, $stringAmount2, 1);

echo "Sum using string: $sumString\n"; // Output: Sum using string: 0.3

As shown, it is not too user friendly to use bcmath API.
So in light of this it can make sense to use value objects here.

Value Object

What is a value object? See this explanation.

For monetary fields itself there can be reasons to use the moneyphp/money library.
It comes with quite a powerful harness and might be bit too much for some. For me at least – dealing with a simpler decimal type setup – I was looking for something else.
At least for non-monetary values it sure doesn’t seem like a good fit anyway, so lets assume those are height/weight values or alike we want to cover using decimals.

For this reason I would like to introduce php-collective/decimal-object which provides a simple API to work with decimals in a typehinted and autocomplete way.

$itemWeightString = '1.78';
$packageWeightString = '0.10';
$itemWeight = Decimal::create($itemWeightString);

$totalWeight = $itemWeight->multiply(4)->add($packageWeightString);
echo $totalWeight; // Output: 7.22

It will transform itself into the string representation once needed (echo, json_encode, …). Until then, internally, one can operate with it easily.

ORMs and Frameworks

Now, when working with ORMs, exchanging the existing strings with the value objects should be rather straightforward.
Usually, the casting is configured and can be switched out for this specific field type.

I am curious how "easy" this is across different ORMs and frameworks actually.
So if people want to comment or mail me their solution, I will add them below for the sake of completeness but also to compare notes.
Please chime in, would be awesome to have a concrete comparison on this.

CakePHP

Since I know most about this framework and ORM, I will present the way in the CakePHP (ORM) in more detail.

Let’s imagine we have a schema for migrations as follow:

$table = $this->table('articles');
$table->addColumn('weight', 'decimal', [
    'default' => null,
    'null' => false,
    'precision' => 7,
    'scale' => 3,
]);
$table->addColumn('height', 'decimal', [
    'default' => null,
    'null' => false,
    'precision' => 7,
    'scale' => 3,
]);
...
$table->update();

Here we should only need to switch out the type in the bootstrap config:

TypeFactory::map('decimal', \CakeDecimal\Database\Type\DecimalObjectType::class);

It uses dereuromark/cakephp-decimal plugin which provides the type class for the ORM.
The type class would now transform all "decimal" typed DB fields to a value object on read from the DB, and the floating-point string for storing as decimal value.

This can also be done on a per-field basis in the table classes. But switching them out type based seems more DRY at this point.

Now any form input sending a decimal value will be marshalled into the value object.

// Those could come from the POST form data
$data = [
    'weight' => '12.345',
    'height' => '1.678',
];
$article = $this->Articles->patchEntity($article, $data);

// Now both Decimal objects
// $article->weight;
// $article->height;

Same when reading the article from DB:

$article = $this->Articles->get($id);

// Now both Decimal objects
// $article->weight;
// $article->height;

The linked plugin also contains basic support for localization, which can be important for other languages/countries (, vs .).
But let’s keep things concise for now.

Symfony

In Doctrine one needs to switch out the entity mapping using the following config:

# app/config/packages/doctrine.yaml

doctrine:
    dbal:
        types:
            decimal: App\Doctrine\Type\DecimalType

The type class could maybe look like this (untested, though):

// src/Doctrine/Type/DecimalType.php

namespace App\Doctrine\Type;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use PhpCollective\DecimalObject\Decimal;

class DecimalType extends Type
{
    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        return $platform->getDecimalTypeDeclarationSQL($fieldDeclaration);
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        // Convert the database value to your application value
        return $value !== null ? Decimal::create($value) : null;
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        // Convert your application value to the database value
        return $value !== null ? (string)$value : null;
    }

    public function getName()
    {
        return 'decimal';
    }
}

Laravel

Should be along these lines (untested, though):

// app/Casts/DecimalCast.php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class DecimalCast implements CastsAttributes
{
    public function get($model, string $key, $value, array $attributes)
    {
        // Convert the database value to your application value
        return $value !== null ? Decimal::create($value) : null;
    }

    public function set($model, string $key, $value, array $attributes)
    {
        // Convert your application value to the database value
        return $value !== null ? (string)$value : null;
    }
}

Now, you can use this cast in your Eloquent model:

// app/Models/YourModel.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class YourModel extends Model
{
    protected $casts = [
        'your_decimal_column' => \App\Casts\DecimalCast::class,
    ];

However, this seems to be for specific fields, not all fields across all tables. Not sure if there is also the generic switch.

Others?

Waiting for some input here from the PHP community 🙂

Considerations

The concrete implementation is not quite as important as long as it does the job.

Immutability

When using value objects here it is important that they are immutable. Any time you modify the value the assignment makes sure that you didn’t accidentally modify a previous one that gets further passed around modified now.

$weight = $articleWeight->add($packageWeight);

If the decimal object were not immutable, $articleWeight would now have a different value (same as $weight), and any further usage would be off.
So $weight as result should be the only modified.

Localization/I18n

I personally don’t think those should be bundled into the value object itself, but rather be part of the presentation layer and the helpers there responsible for further displaying it in a specific way.
As such, frameworks like CakePHP offer helper classes for the templates to be passed to.
The linked one provides examples for currency(), format() and alike using PHP’s intl library.

For me the main task of the value object is a sane way to work with it internally as well as being able to transform back and forth from serialized form (APIs, DB, …).

Performance

I haven’t made any larger performance testing. So far normal usage of the object didn’t seem to indicate a slower page load compared to strings.
Would be interesting if someone has any insights on this.
Maybe also for different ORMs/frameworks, even though I doubt there will be much of a difference here between them.

Memory

Extra memory usage should be rather small with modern PHP versions. They often use value objects already, e.g. for datetime, enum, …
Prove my assumption wrong.

Conclusion

Any such value object (VO) approach seems to be quite more useful in most apps then just dealing with basic strings.
Especially the speaking API seems to improve readability.

Demo

If you want to play around with a live PHP demo, check out the examples in the sandbox.

Further Links

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?

CakePHP 5 upgrade guide 28 Sep 2023 4:07 AM (last year)

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:

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 🙂

Add post to Blinklist Add post to Blogmarks Add post to del.icio.us Digg this! Add post to My Web 2.0 Add post to Newsvine Add post to Reddit Add post to Simpy Who's linking to this post?