Laravel Data & Validation
by Ruben Van Assche
Validation is complex, and we don't want to write it, yet having it within an application is super important. Laravel data tries to solve this problem by automatically writing validation rules and making it fun to add additional validation rules.
But to achieve this, we must make clear rules for validation in Laravel data. We've implemented many features in the previous years without thinking about the bigger picture, which created a lot of weird scenarios within the package.
That's why this spec was written. We've declared how validation works and will keep this spec as the guideline to implement the validation logic in Laravel data. Changes to the validation code should always be checked with the spec, and if required, the spec should be updated after discussion if it would be a worthwhile change.
This spec is opinionated, and that's with a reason. You may agree or disagree with it, but the plan is to stick to it. Certain decisions were made only to make the validation of this package work at a bare minimum. Some of these decisions might be strange, and you might think it would be valuable that feature A or B would be implemented. Rest assured, we thought this thing through! In the end, the Laravel validation system is not made to be generated semi-automatically. Using this spec, we try to make it run as smoothly as possible.
A small note about this document: This is a beta version of the spec. It has a lot of information about things laravel-data had in the past and some sections on why we've decided on a specific approach. When this spec is implemented, we will update it so it describes the whole process without extra information and gives a clear understanding of laravel-data's validation logic. Sections explaining design decisions would be stored in a validation design considerations document.
Some common terms
- Property mapping: the name of a property can be mapped, so a property could have the name
$firstName
within its PHP class, while we accept it within the payload of a request asfirst_name
. - Attribute rules: rules that can be added using PHP attributes on the property definitions within the PHP class. These attributes will add additional validation rules to the automatically generated validation rules.
- Data Collectable: an array, Collection, or Paginator that contains data objects. It is a type for a property in a data class.
- RequiringRule: an interface that describes a Laravel rule that requires a property to be provided when validating. Sometimes, conditions are tied to this.
- ValidationPath: an object describing the current path in a nested data objects tree where the validation currently generates rules. For example, if we have a
BlogPostData
object with a nestedBlogPostDatesData
object in a$dates
property. Then, the validation path would bedates
. All rules would be generated with the prefixdates.
- The nested data structure: often, data objects are built from other data objects, which are sometimes stored within a root data object as a property or as a data collecatble. When validating, we want to validate this whole nested data structure.
At which point is validation ran
Validation ALWAYS runs before a data object exists. If you want to validate data objects when they are created as PHP objects, then you should write the validation logic by yourself. There are a few reasons for this:
- A data object cannot be created from any input. For example, setting an int as an array will crash with a problematic exception. There's no way to provide feedback to the user on what happened with validation messages. A two-step validation pipeline was considered, but it overcomplicates a lot of stuff, and it is just weird to perform validation twice.
- A Laravel validator works on an array, not an object, which means we create a data object from an array, then convert it back into an array and validate it. That's just useless.
- Laravel validation rules work on simple types: strings, arrays, booleans, ... In a complex data object, we'll have more complicated types to which some rules probably won't work
- Exclude rules can be implemented, but it works counter-intuitive. We would fill up the whole data object and then start emptying it when we have an exclude rule
When do we run validation
Validation runs on a few different occasions:
- When you're injecting a data object into a controller, the data is coming from the request, so to have the same functionality as Laravel validation requests, the input will be validated
- When you create a data object using
validateAndCreate
- When you create a data collection using
validateAndCollect
- When you validate a data object using
validate
- When you validate a data collection using
validate
- When using from
from
,optional
orcollect
and thevalidateAllPayloads
method on a data object returnstrue
- When using from
from
,optional
orcollect
and thevalidateAllPayloads
config value indata.php
istrue
On all other occasions, validation won't run. Which includes:
- Creating a new data object by calling
new DataClass(...)
- When using from
from
,optional
orcollect
and thevalidateAllPayloads
method is not manually overwritten to returntrue
- When using from
from
,optional
orcollect
and thevalidateAllPayloads
config value indata.php
is not manually overwritten to returntrue
Mapping and naming
The names of properties in a data object can be mapped in two ways:
class DataObject extends Data
{
#[MapName(input: 'first_name')]
public string $firstName;
#[MapInputName('last_name')]
public string $lastName;
}
Additionally, a mapper can be used:
class DataObject extends Data
{
#[MapName(SnakeCaseMapper::class)]
public string $firstName;
#[MapInputName(SnakeCaseMapper::class)]
public string $lastName;
}
This is a quicker version than manually typing out the names, especially since it can be set for a complete class.
It is impossible to map from multiple names since these will be used in the validation process.
Properties will be mapped before validation, the mapped property will be removed from the payload and replaced with its original name. This change will also be stored in a MappingMap
. This map is required when validation fails since Laravel's validator will output messages for the original property names that failed the validation. Since these names will be the original ones and not the mapped ones, we need the MappingMap
to map them back so that these validation messages can appear beneath the correct fields in the UI.
Since the validation errors need to be remapped, we'll also need to create a data-specific extended version of the MessageBag
, Validator
, and ValidationException
since it is not possible to update the names of the properties in the MessageBag
.