Errors are exceptions now — what customization code has to change
- Version
- 4.0 preview
- Updated
CBX used to report a refusal by returning false and parking a sentence in setError().
That protocol is being retired in favour of exceptions, and the change reaches customization code:
a custom property type or model that still returns false keeps working, but a custom one that
throws nothing and reports nothing will silently accept bad data.
If you maintain anything under data/customization/, read the first section. The rest explains why.
1. What you have to change
A custom property type's check()
// Before
function check($data) {
if ($somethingWrong) {
$this->setError(KText::_('That value is not allowed.'));
return false;
}
return true;
}
// After
function check($data) {
if ($somethingWrong) {
throw KenedoValidationException::forProperty($this, KText::_('That value is not allowed.'));
}
return true;
}
forProperty() attaches your property's name and label to the message, which is what lets a form
highlight the field instead of printing a sentence and leaving the operator to hunt for it.
setError() no longer exists — on KenedoProperty or on KenedoModel — so the "before" above
is now a fatal error rather than a deprecation. Returning false from check() is still read as a
refusal, but it can no longer carry a reason: validateData() turns it into "Field X reported
failed validation but gives no error message", which is all anyone could have salvaged from it.
Code that calls validateData() or store()
// Before
if ($model->validateData($data) === false) {
$errors = $model->getErrors();
// …
}
// After
try {
$model->validateData($data);
}
catch (KenedoValidationException $e) {
foreach ($e->getIssues() as $issue) {
// $issue['field'], $issue['label'], $issue['message']
}
}
validateData() returns true on success and throws on failure. It never returns false.
This is the change most likely to bite you, and it does so quietly. The same is true of
store(), copy(), delete() and publish(): none of them returns false any more. So a caller
that still reads
if ($model->store($data) === false) { // never true - the branch is dead
// ...report the failure
}
no longer reports anything, and a failed save reads as a successful one. Grep your
customizations for === false against those five methods; each one is a try/catch now.
Catching
try {
// …
}
catch (KenedoValidationException $e) { // the caller can fix it — show it on the form
}
catch (KenedoSystemException $e) { // the caller cannot — log it, apologise
}
catch (KenedoException $e) { // either of the above
}
2. Why
false cannot say which kind of problem it is
A failed save means either "you left the title empty" or "the database is gone". Those need
opposite handling — one belongs on the form, the other in a log with an apology — and false says
neither. Every layer guessed, and the guess was usually "system error", because that is the safe
thing to assume when you do not know.
The information died on the way up
Copying recurses into child entities. Each level caught, flattened the reason to a string, and
returned false, so by the time a controller saw it the reason was gone. A real example from this
work: refusing to copy a currency because its code must stay unique produced
A system error occurred during copying. Please notify your service provider.
Nothing was broken, and the operator could have fixed it in seconds. An exception carries its reason through the recursion without any layer having to co-operate.
Ignoring a refusal was too easy
$property->check($data); on its own is valid PHP that silently discards the answer. A missed check
is invisible until bad data is already stored. A thrown exception cannot be ignored by accident.
3. The exception types
| Class | Means | Answered with |
|---|---|---|
KenedoValidationException | The data was refused; the caller can fix it | 422, with per-field issues |
KenedoSystemException | Something failed that the caller cannot fix | 500, detail to the log only |
KenedoException | Base of both | — |
KenedoValidationException carries issues, not just a message: each has field, label and
message. A save reports every bad field at once rather than stopping at the first, so an operator
does not fix one, retry, and discover the next.
Validation failures are deliberately not logged as errors. A shop owner mistyping a value is not an incident, and logging it as one buries the incidents that matter.
One more code, for a refusal that is not about a field
KenedoProperty::ERROR_COPY_NOT_POSSIBLE marks a copy that cannot be performed at all — a unique
value with no rule for making a copy distinct. Controllers answer it with 409 and the reason,
rather than 500 and an apology.
Where it happened, as well as what
A refusal raised deep in a recursion needs to say WHERE. Copying a product copies its pages, and each page its questions - so "Field Title cannot be empty" can be about any one of forty child records the operator never named.
Every KenedoException therefore collects a context trail as it travels: each level that catches
and rethrows adds itself, and the controller puts the result in meta.failedAt, outermost first:
"meta": {
"logIdentifier": "wkagqslp3q",
"failedAt": ["copy of adminproducts ID 21301", "copy of adminpages ID 20640", "copy of adminquestions ID 24595"]
}
The message and the per-field issues are untouched - the trail is context about the journey, not a replacement for the reason. Flattening one into the other is the mistake the old protocol made every time.
Use $e->addContext('what this level was doing') when you catch and rethrow, and
$e->getContextualMessage() when you want the whole thing as one sentence.
If you wrap rather than rethrow, carry the trail across - KenedoSystemException::fromException()
does. A new exception object starts with an empty trail, so a wrapping level that forgets produces a
trail naming every level ABOVE the failure and not the failure itself.
4. Status
Complete. setError(), setErrors(), getError(), getErrors() and resetErrors() are gone
from KenedoModel and KenedoProperty, and every call site in the component has been converted:
the Kenedo core, all ten stock property types, all fourteen models that had them, and the
controllers and helpers that used to read them. The compatibility shim in validateData() is
removed. There is only one style in stock code now.
KenedoSystemException is genuinely thrown — previously it existed but nothing raised it, so every
real failure still travelled as false. Build one with KenedoSystemException::logged($detail), or
::fromException($e, $context) when wrapping something you caught: both write the detail to the
error log and hand back an exception carrying only the log identifier, which controllers put in the
response's meta.logIdentifier. Do not put the detail in the exception message — a 500 may be
read by a shop's customer.
KenedoModelLight is deprecated: it existed only to hand a model the four-method error bag, and
that bag is gone. It is now an empty subclass of KenedoModel so customization code naming it still
resolves — but if you extend it, note that KenedoModel::__construct() records the component and
refuses an empty one, where KenedoModelLight had no constructor at all. An override that does not
call parent::__construct($component) will now throw.
Not part of this and deliberately unchanged: ConfigboxJsonResponse::setErrors() (a response DTO),
the KenedoPlatform implementations' own error bag, and ConfigboxUserHelper.
@see docs/technical/com_configbox_kenedo_model.md