feat(12-01): add Laravel request validation to lagoon
- lagoon.ValidateRequest ports Laravel 9 request validation: wildcard expansion, implicit-rule stop, bail, sometimes/nullable/blank skipping, size messages split by type and character-counted string lengths - ParseRules, In, CustomRule, UploadedFile and ErrorKeys for rule tables - pl/en lagoon::validation catalogs ported verbatim from WinterCMS - lagoon.Validate answers a numeric range failure with the bound that failed (min, max or numeric between) instead of always max
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Casts and validation
|
||||
description: Store JSON and encrypted columns with lagoon.Jsonable and lagoon.Encrypted, and validate input with Laravel-style rule strings through lagoon.Validate.
|
||||
description: Store JSON and encrypted columns with lagoon.Jsonable and lagoon.Encrypted, and validate input with lagoon.Validate and lagoon.ValidateRequest.
|
||||
section: database
|
||||
order: 50
|
||||
---
|
||||
@@ -94,11 +94,28 @@ fmt.Println(string(out))
|
||||
// {"title":["The title field is required."],"views":["The views may not be greater than 1000."]}
|
||||
```
|
||||
|
||||
The supported rules are `required`, `nullable`, `integer`, `numeric`, `between`, `min`, `max`, `in`, `unique`, `boolean`, `email`, `confirmed`, `different` and `mimes`. Any other rule is an error, so a rule that SummerCMS does not implement cannot be skipped by accident. On a field that is `integer` or `numeric`, `min`, `max` and `between` compare the number; on other fields they compare the length.
|
||||
|
||||
> [!NOTE]
|
||||
> A failed numeric range check is currently always reported with the `max` message, even when the value is below `min`, and a `min`-only rule then shows an empty limit. Check range errors by field, not by message text.
|
||||
The supported rules are `required`, `nullable`, `integer`, `numeric`, `between`, `min`, `max`, `in`, `unique`, `boolean`, `email`, `confirmed`, `different` and `mimes`. Any other rule is an error, so a rule that SummerCMS does not implement cannot be skipped by accident. On a field that is `integer` or `numeric`, `min`, `max` and `between` compare the number; on other fields they compare the length. A number below the lower bound gets the `min` message, one above the upper bound the `max` message, and a bound that came from `between` gets the numeric `between` message (`The views must be between 0 and 10.`).
|
||||
|
||||
`unique:<table>` runs a query to check that no other row of the table has the value in the field's column, so it needs a database handle; pass the transaction you are writing in. Soft-deleted rows do not count, and when the model you pass has an ID, its own row does not count either, so the same rules work for create and update. Pass a `phrasebook.Translator` as the last argument to get the messages in the request locale from the `lagoon::validate` catalog; with `nil` they are in English. See [Localization](../services/localization.md).
|
||||
|
||||
A handler validates before it fills and saves the model, as the create example on [Models](models.md) shows. Answer a non-nil error map with status 422 and the body shape the endpoint's existing clients expect.
|
||||
|
||||
## Request validation
|
||||
|
||||
`lagoon.Validate` checks a model's rules. A ported API endpoint has a different job: its 422 body has to match the one the PHP endpoint returns, message for message, in the request's language. `lagoon.ValidateRequest` does that by following Laravel 9's request validator rather than the model rules.
|
||||
|
||||
It takes the decoded request input and a rule table: a slice of `lagoon.RequestRule`, one per attribute, in the order the PHP controller declares them. Build each attribute's rules from the PHP rule string with `lagoon.ParseRules`, add `lagoon.In` for an `in` list whose values hold commas or quotes, and `lagoon.CustomRule` for a PHP closure rule. A closure's failure message is used exactly as the closure returns it. Keep rule tables in package variables: `lagoon.ParseRules` panics on an unknown rule, a wrong number of parameters or a regular expression that Go cannot compile, so a mistake stops the application at start-up instead of failing a request.
|
||||
|
||||
The behaviour follows Laravel:
|
||||
|
||||
- An attribute name may contain `*`. It is expanded against the input, so `posts.*.title` checks `posts.0.title`, `posts.1.title` and so on, and the messages name those attributes. A wildcard with nothing to expand checks nothing.
|
||||
- The rules of an attribute run in order. After a failed `required` (or another implicit rule: `present`, `filled`, `accepted`) the attribute stops, so `{"posts":[]}` against `required|array|min:1` gives the single message `The posts field is required.`. With `bail`, any failure stops the attribute.
|
||||
- The other rules run only when there is a value: they skip an absent attribute, a blank string, `null` under `nullable` and an absent key under `sometimes`.
|
||||
- `min`, `max`, `size` and `between` measure what Laravel measures: the number on an `integer` or `numeric` attribute, compared exactly as a decimal; the number of elements of an array; the size of an uploaded file in kilobytes; otherwise the length in characters, not bytes. Each picks the matching message, such as `max.string` or `max.file`.
|
||||
- `email` is PHP's `FILTER_VALIDATE_EMAIL`, the check WinterCMS uses by default, so `user@localhost` is refused. `date` accepts ISO 8601 dates and date-times, `Y/m/d`, `m/d/Y`, `d.m.Y` and `d-m-Y`, and refuses dates that do not exist. `after_or_equal` and `before_or_equal` take a date, `today`, `tomorrow`, `yesterday` or `now` (in UTC), or the name of another field.
|
||||
- `exists:table,column` counts matching rows in the database, so it needs the transaction handle. Like Laravel it does not skip soft-deleted rows, and it does not run once the attribute already has a message.
|
||||
- File rules (`file`, `image`, `mimes` and the size rules) take a `lagoon.UploadedFile`; `lagoon.UploadedFileFromHeader` builds one from a parsed multipart part. The type is read from the file content, not from its name.
|
||||
|
||||
The messages come from the `lagoon::validation` catalog, a copy of WinterCMS's validator messages in English and Polish, in the request locale. A message missing in Polish, such as the one for `after_or_equal`, comes out in English, as it does in WinterCMS. Attribute names show with spaces instead of underscores (`market price`), and expanded attributes keep their dotted name.
|
||||
|
||||
The result is Laravel's errors object, a map from attribute to messages, or `nil` when the input passes. Go maps have no order, so when an endpoint's body must list the attributes in PHP's order, `lagoon.ErrorKeys` returns them in that order for the rule table.
|
||||
|
||||
Reference in New Issue
Block a user