Hi,
I use CakePHP’s entities as domain models mostly, where I keep the business rules. For those rules to apply correctly, it is important that the object is in a valid state, ie. `status` must be a `InvoiceStatus` enum. The way it is, I must pass an array of values when I instantiate the object:
new Invoice([ 'status' => 'notWhatIexpected' ]);
Invoice is created with an invalid property.
Same applies to `LineItem`, which belongs to `Invoice`. An Invoice with 0 line items makes little sense. How can I catch this? I was thinking about a static creation method, but what is the CakePHP way?
Copilot’s suggestion is to check all values manually before instantiating the invoice, but this doesn’t seem right. Because the validity of the Invoice model is a kind of business rule in itself (“An invoice status can be draft or final”). Therefore I want to encapsulate this logic in the Invoice model.
Regards
What you are looking for is Application Rules
Those are applied when you try to save an entity (and maybe associated entities as well)
As the docs say
rules focus on comparing data against the existing state of your application and/or network.
So your entity state can/must be wrong as it needs to hold the wrong state, but the check logic is being done by the table instance.
If application rules don’t pass the saving process fails.
What you expect is, that validation happens the moment an entity gets state injected into it, which is not what Cake is built upon.
Entities are dumb objects which just repesent state (or desired state) in a row/in the db
Table objects do the logic and validation.
1 Like
Thanks for the link, I wasn’t aware of that part of the docs. Shouldn’t business rules / invariants be framework agnostic? There’s a lot of CakePHP specific code in those rule checks.
Interesting. I thought it was the other way round, table classes being just “dumb” persistence models (hence the Table suffix).
Btw. the doc also states:
entities represent individual rows or domain objects in your application
Well then its up to what you define as domain objects 
That said: If you really want it validated upon creating/patching, then use normal validation rules. Afterwards, getErrors() will give you the issues if any.
I’m trying to go with the standard definition here, which is the model that describes a key aspect of my business domain.
One way would be to create a seperate model in ie. /domain, and then using the data from the entity to run the actual business logic on it. That way, the CakePHP models are not abused for domain matters and the domain stays free of framework logic.
I thought about this a little bit more and I think a clean way would be to only use CakePHP’s entity objects inside a repository. CakePHP docs themselves suggest the use of the repository pattern.
class ClientRepository
{
public function __construct( ClientsTable $clientsTable ){}
public function save( ClientDomainObject $client ) {
// Map $client domain object to CakePHP client entity here
$this->clientsTable->save($clientEntity);
}
}
The repository would therefore receive and return pure PHP domain objects - that is, objects that are unaware of any implementation, even CakePHP. All they do is contain business logic. Inside the repository, they would be mapped to/from CakePHP’s entities, which are then used for the database operations.
As a result, I wouldn’t have to abuse CakePHP entities as domain objects. It would also be possible to enforce valid object state upon creation without relying on static creator methods.
For the InvoiceStatus invariant, keep the rule in InvoiceTable::buildRules() and let the table’s rules checker evaluate it, rather than making the entity constructor enforce persistence concerns. A small custom rule can reject an invoice with zero line items during the normal save path, while the enum type itself should still be enforced at the DTO or factory boundary.
Invoice status isn’t really only a persistence concern (draft, sent, cancelled etc.). This is a business or even legal requirement. Same with line items.
I know that CakePHP-style application rules are applied in the saving process, but the whole point of this post is to ask what ways there are to prevent passing around invalid domain objects. In fact, I want to prevent them from being “dumb” entity mapping objects and “real” OOP/DDD objects instead. This may sound nerdy, but I think the whole point of writing OOP code is to have meaningful objects at the core of your application.
That distinction is fair. A domain-level value object or factory can reject impossible InvoiceStatus transitions before the CakePHP entity reaches buildRules(), leaving buildRules() as the persistence boundary.
For the status transitions, a domain method can reject an invalid change before persistence; keep InvoiceTable rules as a second save-time check for zero line items. That separates object invariants from database validation.
For the InvoiceStatus and line-item invariants, a factory that refuses incomplete input is the narrowest boundary here. Keep buildRules() as a second save-time check, since callers can still mutate entities afterward.
They can mutate entities, but they shouldn’t be allowed to bypass invariants (no direct setters, hence why Cakephp entities don’t really work here). So I am not sure if the db level validation is necessary.
You can also look into cakephp-workflow as that is designed to ensure your entities’ valid state if we are talking about transitions and whats valid or not.
This takes away a lot of critical business logic from an island solution within this app code.
Often times you will end up with more than one workflow across the system anyway, and then it gets really messy.
Thanks @dereuromark for the link, that plugin actually seems to cover some of the state transition logic for invoices I had to implement. Since I have other models like Contracts that also have state transitions this might come in handy, especially when it also has an overview that stakeholders can have a look at. But if I’m not mistaken it is not designed to enforce for a new invoice instance to have a buyer, seller, line items, invoice number etc?
Correct, it doesn’t. Guards hang off transitions, not off construction - the plugin answers “may this invoice move from draft to issued”, not “may this object exist”.
That split is probably what you want anyway. “An invoice has a buyer, seller, line items and a number” isn’t true of a draft, otherwise you could never start one. What’s true is that an issued invoice has them. So it’s a transition guard, and then it is the plugin’s job:
#[Guard('issue')]
public function ensureComplete(): bool|string
{
$invoice = $this->getEntity();
if (!$invoice?->buyer_id || !$invoice->seller_id) {
return 'Buyer and seller are required before issuing';
}
if (!$invoice->invoice_lines) {
return 'An invoice must have at least one line item';
}
return true;
}
The invoice number is a different animal again: a gap-free sequence has to be handed out by the database under a lock, so it can’t be a domain invariant at all - no constructor validation gets you uniqueness or gaplessness.