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.