Virtual entity fields in CakePHP

I must say: I have not been using them in the years of their existence in CakePHP 3+.
But I now started and must say: They can sure be handy.

How do they work?

They basically calculate the results of this field at runtime for you based on the other fields in that entity:

protected function _getFullName() {
    return $this->first_name . ' ' . $this->last_name;
}

This will be available as $entity->full_name property based on the first and last name.

So the rule is _get prefix + CamelCase version of the field name. The field name itself will be exposed as $under_score property.

For details please refer to the docs.

Exposing them

So for now, the created properties are already accessible. They are not, however, auto-included in toArray() or JSON transformation yet.

This I found most helpful, though, and the main reason I didn’t just use methods, but virtual properties here.
Once you declare them in $_virtual array, any JSON representation of your entity can contain those virtual fields now.

In my case, there was a type as enum implementation. When exposing the entity via API, the data (as integer) was not readable to humans.
Providing the virtual field type_string as human-readable translation now is basically for free – while still providing the actual numeric value for easier comparison and data-processing.

    public function _getTypeString(): ?string
    {
        if ($this->type === null) {
            return null;
        }

        return static::types($this->type);
    }

Now comes the exposing part:

    $_virtual = [
        'type_string'
    ];

You might remember this from my recommendation of how to use enums properly in (Cake)PHP.
This is basically this auto-bakeable and very performant working implementation of enums as tinyint(2) and can be fully extended for the virtual property exposure.

The result of the index.json is then:

[
    {
        "name": "Foo",
        "type": 2,
        "type_string": "Core",
        ...
    }
]

For details on exposing see docs.

Edge Cases and Pitfalls

Especially when you use PHP7.1+ typehints, we need to be aware of the possible return types.
Be sure to always return nullable types, as for new entities or when fetching partial data from DB not always all fields are set.

The following version also accounts for partial existence. Depending on your model validation and DB constraints you might not need this part, though:

protected function _getFullName(): ?string {
    $pieces = [];
    if ($this->first_name) {
        $pieces[] = $this->first_name;
    }
    if ($this->last_name) {
        $pieces[] = $this->last_name;
    }
    if (!$pieces) {
        return null
    }
    return implode(' ', $pieces);
}

Refactoring for strictness

In some cases we expect the data to be present here and want to have meaning exceptions otherwise.
In that case, I just provide an entity method to convert as convenience wrapper. This way, the property can (and should) stay nullable:

protected function _getFullName(): ?string {
    if ($this->first_name === null && $this->last_name === null) {
        return null;
    }

    return static::fullName($this->first_name, $this->last_name);
}

public static function fullName(string $firstName, string $lastName): string {
    return $this->first_name . ' ' . $this->last_name;
}

Whereever I need to have the full name available for sure, I can use $entity::fullName($entity->first_name, $entity->last_name) now and know it to be present (as does PHPStan etc). It would otherwise throw an exception. In all other cases $entity->full_name (string|null) suffices then.

Why not Accessor or Mutator?

I so far completely stayed away from those two.
They would directly modify the entity fields on reading or writing. The problem I have with this is that this as "always overwriting" is not useful in all cases.
I rather have a frontend/view helper to modify the output here, or use pre-validation marshalling to clean the incoming data that will be passed into the entity.
That way I can control it better and have a clearer picture of what is actually stored in the DB or the entity at each time.

IDE Support

What you sure want is your IDE to understand, autocomplete and typehint those virtual fields for you.
And for that, the IdeHelper plugin now supports Entity annotations for these virtual field properties.

bin/cake annotations models -v

This will add your freshly added virtual field(s) into the entity’s docblock for you to get autocomplete/typehinting, remove IDE warnings and to remove errors in static analyzers like PHPStan.

 * ...
 * @property string|null $type_string
 */
class Module extends Entity {
}

readonly?

You can also change the tag to @property-read if you want to.
Those virtual fields only have a getter usually and cannot be written back anyway:

 * ...
 * @property-read string|null $type_string
 */

The IdeHelper understands this and will keep this kind of annotation then. The nice thing: If you try to set this field, the IDE will tell you that this cannot really work.

Update 2020-02

Check out the new post about virtual query fields.

3.67 avg. rating (75% score) - 6 votes

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.