Entity
Static-analyzer friendly
"PHPStan level 7" approved. If you are looking into non-entity approaches, consider DTOs.
Entity read()
You want to read nested properties of your entity, but you do not want tons of non-empty checks?
if ($entity->tags && !empty($entity->tags[2]->name)) {} else {}With modern PHP, the ?-> operator can already overcome some of this, but in some cases the read() approach is still better.
Add the trait first:
use Shim\Model\Entity\ReadTrait;
class MyEntity extends Entity {
use ReadTrait;Then you can use it like this:
echo $entity->read('tags.2.name', $default);This means you are OK with part of the path being empty/null. If you want the opposite — making sure all required fields in the path are present — check the next part.
Entity require()
// In some service method at the beginning
public function buildPdf(Product $product): string {
$product->require('supplier.company.state.country');
// Render template
}This allows you to define your required contains (relations) on the entity and otherwise results in a speaking and clear message.
It avoids the usual kind of hidden and non-speaking errors:
warning: 2 :: Attempt to read property "sku" on null
deprecated: 8192 :: strlen(): Passing null to parameter #1 ($string) of type string is deprecated
warning: 2 :: foreach() argument must be of type array|object, null given
These otherwise show up inside the business logic or rendering when certain relations are expected but not present.
Add the trait and you are all set:
use Shim\Model\Entity\RequireTrait;
class MyEntity extends Entity {
use RequireTrait;Entity get...OrFail() / set...OrFail()
You want to use "asserted return/param values" or "safe chaining" in your entities? Then you want to ensure you are not getting null values returned where you expect actual values.
Add the trait first:
use Shim\Model\Entity\GetSetTrait;
class MyEntity extends Entity {
use GetSetTrait;You can also use the GetTrait/SetTrait separately if you don't need both get/set functionality.
Now in code you can use:
$entity->getOrFail('field'); // Cannot be null
$entity->getFieldOrFail(); // Cannot be null
$entity->setOrFail('field', $value); // Cannot be null
$entity->setFieldOrFail($value); // Cannot be nullPHPStan can now help you in more detail, for example:
Parameter #1 $value of method App\Model\Entity\User::setActiveOrFail() expects bool, null given.
Annotations
If you use the above and want to use the magic methods, make sure to let the IdeHelper add the annotations for them. Use the included annotator to get all method annotations into your entities:
'IdeHelper' => [
'annotators' => [
\IdeHelper\Annotator\EntityAnnotator::class => \Shim\Annotator\EntityAnnotator::class,
],
],This replaces the default one with the Shim version and adds support for these get/set methods on top.
Modified vs dirty
By default, patching as well as manual assignment on the entity often results in more dirty fields than actually modified ones. The value might still be the very same, but it is marked as dirty and most likely will be part of the database update call.
In some cases it can be useful to know what actually changed, for example for auditing and logging purposes. Here the ModifiedTrait comes into play.
Version note
Since CakePHP 5.2, isDirty() now handles this the same way, so this trait is only needed prior to that version.
// CakePHP 5.1 or 5.0
$data = ['foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz'];
$entity = new TestEntity($data, ['markClean' => true, 'markNew' => false]);
$entity->set('foo', 'foo');
$entity->set('bar', 'baaaaaar');
$entity->set('foo_bar', 'foo bar');
$result = $entity->getDirty();
$expected = ['foo', 'bar', 'foo_bar'];
$this->assertEquals($expected, $result);
$result = $entity->isDirty('foo');
$this->assertTrue($result);
$result = $entity->isModifiedValue('foo');
$this->assertFalse($result);
$result = $entity->getModifiedFields();
$expected = ['bar', 'foo_bar'];
$this->assertEquals($expected, $result);