ResponseColumnGuardTrait
Guards advert responses against values that do not fit their columns.
The form guard (…\Front\{Bazaar,Comcat}\Components\ResponseFormComponent\Traits\FormTrait)
is only one input, not the write path. Every response save (front form through ApiBridge,
admin edit through the Admin to Api bridge, external :Api:*:Response:save) ends in
ResponseManager::save(), that is in SaveTrait, which calls beforeSave(). The
external path never meets the form: the payload goes through the hydrator straight into the
entity and DecimalType::convertToDatabaseValue() is a passthrough.
The database does not guard the columns usefully either. With a strict sql_mode an
overflow fails the save with a generic database error; with a non-strict one it throws
nothing and silently stores nonsense in advert_responses:
"abc"becomes0.000(an offer for zero),"1e1000"becomes9999999.999(the column maximum),"-5"is stored as a negative offer,"12,5"becomes12.000(the comma truncates),- a phone longer than 20 characters is truncated (an unreachable contact),
- an e-mail longer than 255 characters is truncated (the reply goes to a wrong address),
- a subject longer than 255 characters is truncated (Comcat collects it, Bazaar leaves it
null). The stored value is then shown to the advertiser and sent in the e-mail (ResponseManager::afterSave()).
The guard therefore sits in beforeSave() of both ResponseManagers, the only place every
response write passes through (insert and update). A rejection is VALIDATION_FAILED (like
DecimalQuantityGuardTrait), so
SaveTrait rolls back the whole operation and no half-saved response remains.
NOTE: limits are never copied here; they are read by reflection from #[ORM\Column] of the
entity that is really saved (ColumnLimits), so the guard follows schema changes.
IMPORTANT: after the check the price is overwritten with the value the guard measured (dot
form rounded to the column scale). Otherwise there is a gap between guard and write:
"12,5" passes the guard as 12.5, but the raw string would reach the database and become
12.000.
NOTE: an empty value passes, because price and phone are optional (price is a nullable
column). Requiredness belongs to the form / MISSING_REQUIRED_FIELD, not to this guard.
Table of Contents
Methods
- guardResponseColumnLimits() : Result|null
- Rejects a response whose value does not fit its typed column.
- responseColumnFailure() : Result
- Builds the uniform rejection; `errorCode` is a machine-readable reason.
Methods
guardResponseColumnLimits()
Rejects a response whose value does not fit its typed column.
private
guardResponseColumnLimits(Response|Response $response) : Result|null
Returns null when everything is valid (the price is then normalized to the form that
gets written).
The entity is typed by the shared ancestors of both modules: the same beforeSave() also
handles \App\UI\Admin\{Bazaar,Comcat}\Models\Entities\Response (the Admin to Api bridge
does not rewrite \Admin\ to \Api\), so a narrower type would make an admin edit fail
with a TypeError.
Parameters
Return values
Result|nullresponseColumnFailure()
Builds the uniform rejection; `errorCode` is a machine-readable reason.
private
responseColumnFailure(string $message, string $errorCode) : Result
Parameters
- $message : string
- $errorCode : string