The missing validation middleware

Stéphane Hulard

CTO ( CH Studio), Trainer, Contributor.

Middleware ?

Like an onion 🧅

Different layers…

IN: Request, Preparation, Consolidation

CORE: Execution

OUT: Filtering, Adaptation, Response

…to reuse logic easily !

PSR-15, or specific implementations…

How ?

                            namespace App\Infrastructure\Http\Middlewares;

class Authenticate implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        try {
            $payload = $request->getContent();
            /** Execute here validation logic and extract object */
        } catch (ValidationError $error) {
            return Response::fromValidationError(422, $error);
        }

        return $handler->handle($request);
    }
}
                        
                            namespace App\Infrastructure\Http\Middlewares;

class Authenticate implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        try {
            $payload = $request->getContent();
            /** Execute here validation logic and extract object */
        } catch (ValidationError $error) {
            return Response::fromValidationError(422, $error);
        }

        return $handler->handle($request);
    }
}
                        
                            namespace App\Infrastructure\Http\Middlewares;

class Authenticate implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        try {
            $payload = $request->getContent();
            /** Execute here validation logic and extract object */
        } catch (ValidationError $error) {
            return Response::fromValidationError(422, $error);
        }

        return $handler->handle($request);
    }
}
                        
                            namespace App\Infrastructure\Http\Middlewares;

class Authenticate implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        try {
            $payload = $request->getContent();
            /** Execute here validation logic and extract object */
        } catch (ValidationError $error) {
            return Response::fromValidationError(422, $error);
        }

        return $handler->handle($request);
    }
}
                        
                            namespace App\Infrastructure\Http\Middlewares;

class Authenticate implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface {
        try {
            $payload = $request->getContent();
            /** Execute here validation logic and extract object */
        } catch (ValidationError $error) {
            return Response::fromValidationError(422, $error);
        }

        return $handler->handle($request);
    }
}
                        

Advantages ?

Validate everything before reaching application code …

… Combine and reuse objects easily …

… Centralize logic for easier maintenance.

Validation ?Why and How ?

Start with a problem…

A Contract management project:

▶ As a User I want to create a Contract.
▶ A Contract is composed of dates and an Author.
▶ An Author is identified by its Email address.

                            POST /contracts

{
    "starts_at": "2022-01-01 00:00:00",
    "expires_at": "2022-12-31 00:00:00",
    "author": {
        "name": "Neslon Mandela",
        "email": "nelson.mandela@mail.com"
    }
}
                            
                        

Define objects: Contract

                            class Contract
{
    public function __construct(
        private DateTimeInterface $startsAt,
        private DateTimeInterface $expiresAt,
        private Author $author
    ) {
        if ($expiresAt < $startsAt) {
            throw new InvalidArgumentException(
                '"expiresAt" date must be after "startsAt".'
            );
        }
    }
}
                            
                        

✅️: "expiresAt" must be after "startsAt"

Define objects: Author

                            class Author
{
    public function __construct(
        private string $name,
        private Email $email
    ) {
        if (empty($name)) {
            throw new InvalidArgumentException(
                "Author name can't be empty."
            );
        }
    }
}
                            
                        

✅️: "name" must not be empty.

Define objects: Email

                            class Email
{
    public function __construct(
        private string $email
    ) {
        if (false === filter_var($email, FILTER_VALIDATE_EMAIL)) {
            throw new \InvalidArgumentException(
                "Email $email is not valid."
            );
        }
    }
}
                            
                        

✅️: "email" must be compliant to RFC.

Object hierarchy

                            {
    "starts_at": "2022-01-01 00:00:00",
    "expires_at": "2022-12-31 00:00:00",
    "author": {
        "name": "Neslon Mandela",
        "email": "nelson.mandela@mail.com"
    }
}
                            
                        
                            {
    "starts_at": "2022-01-01 00:00:00",
    "expires_at": "2022-12-31 00:00:00",
    "author": {
        "name": "Neslon Mandela",
        "email": "nelson.mandela@mail.com"
    }
}
                            
                        
                            {
    "starts_at": "2022-01-01 00:00:00",
    "expires_at": "2022-12-31 00:00:00",
    "author": {
        "name": "Neslon Mandela",
        "email": "nelson.mandela@mail.com"
    }
}
                            
                        
                            {
    "starts_at": "2022-01-01 00:00:00",
    "expires_at": "2022-12-31 00:00:00",
    "author": {
        "name": "Neslon Mandela",
        "email": "nelson.mandela@mail.com"
    }
}
                            
                        

3 objects to express our input: "Contract", "Author", "Email"

Back to basics

                            /**
 * @param DateTimeInterface $startsAt
 * @param DateTimeInterface $expiresAt
 * @param string $authorName
 * @param string $authorEmail
 */
function createContract(
    $startsAt,
    $expiresAt,
    $authorName,
    $authorEmail
) {
    /** code logic */
}
                            
                        

Break it easily…

                            createContract(
    'a',
    'a',
    -18,
    0
);

// Error in the function body because parameters have the wrong type.
                            
                        

With static type hinting !

                            function createContract(
    DateTimeInterface $startsAt,
    DateTimeInterface $expiresAt,
    string $authorName,
    string $authorEmail
) {
    /** code logic */
}
                            
                        
                            createContract(
    'a',
    'a',
    -18,
    0
);

// Type error: Argument 1 passed to createContract() must be DateTimeInterface, string given,
// called in xxx.php on line xx
                            
                        

Only basic PHP typing…

Nice but don't cover all the cases.

Use the objects !

                            function createContract(
    Contract $contract
) {
    /**
     * $contract contains all properties
     * All values are valid and respect the rules.
     */
}
                            
                        

Prepare input data

                            use Symfony\Component\Validator\Constraints as Assert;

class Author
{
    /** @Assert\NotEmpty */
    private string $name;

    public function __construct(
        string $name,
        private Email $email
    ) {
        $this->name = $name;
    }
}
                            
                        

Prepare input data

                            use Symfony\Component\Validator\Validator\ValidatorInterface;

$validator = /** Some code to retrieve the ValidatorInterface instance */
$email = new Email(…);

$author = new Author('', $email);

$errors = $validator->validate($author);
if (count($errors) > 0) {
    //Build error response
}

//Build valid response
                            
                        
                            use Symfony\Component\Validator\Validator\ValidatorInterface;

$validator = /** Some code to retrieve the ValidatorInterface instance */
$email = new Email(…);

$author = new Author('', $email);

$errors = $validator->validate($author);
if (count($errors) > 0) {
    //Build error response
}

//Build valid response
                            
                        

Validates before…

                            $validator = Validator::make($request->all(), [
    'name' => 'required|max:255'
]);

if ($validator->fails()) {
    /** Build error response */
}

$author = new Author($request->get('name'), $email);
                            
                        
                            $validator = Validator::make($request->all(), [
    'name' => 'required|max:255'
]);

if ($validator->fails()) {
    /** Build error response */
}

$author = new Author($request->get('name'), $email);
                            
                        
                            $validator = Validator::make($request->all(), [
    'name' => 'required|max:255'
]);

if ($validator->fails()) {
    /** Build error response */
}

$author = new Author($request->get('name'), $email);
                            
                        

Limitations ?

An input can be very complex…

Modeling as object clearly define expectations…

Validators doesn't work efficiently with those objects.

💡 cuyz/valinor

Map any input into a strongly-typed value object structure.

How it works ?

Read functions and objects signatures,

Build validation rules by itself,

Validates the data before creating objects,

Ensure that you get ready to use objects at the end.

Show me the code !

                            try {
    $contract = (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(
            Contract::class,
            new \CuyZ\Valinor\Mapper\Source\JsonSource($json)
        );
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
    // Do something…
}
                            
                        
                            try {
    $contract = (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(
            Contract::class,
            new \CuyZ\Valinor\Mapper\Source\JsonSource($json)
        );
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
    // Do something…
}
                            
                        
                            try {
    $contract = (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(
            Contract::class,
            new \CuyZ\Valinor\Mapper\Source\JsonSource($json)
        );
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
    // Do something…
}
                            
                        
                            try {
    $contract = (new \CuyZ\Valinor\MapperBuilder())
        ->mapper()
        ->map(
            Contract::class,
            new \CuyZ\Valinor\Mapper\Source\JsonSource($json)
        );
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
    // Do something…
}
                            
                        

Validation errors ?

{
    "starts_at": "2022-01-01 00:00:00",
    "author": {
        "name": "",
        "email": "nelson.mandelamail.com"
    }
}
{
    "expiresAt.value": "Cannot be empty and must be filled with a value matching type.",
    "author.email": "Email nelson.mandelamail.com is not valid.",
    "author": "Author name can't be empty."
}

Supports type annotations

                            class Author
{
    /** @var non-empty-string */
    private string $name;

    public function __construct(
        string $name,
        private Email $email
    ) {
        $this->name = $name;
        if (empty($name)) {
            throw new InvalidArgumentException(
                "Author name can't be empty."
            );
        }
    }
}
                            
                        
                            class Author
{
    /** @var non-empty-string */
    private string $name;

    public function __construct(
        string $name,
        private Email $email
    ) {
        if (empty($name)) {
            throw new InvalidArgumentException(
                "Author name can't be empty."
            );
        }
        $this->name = $name;
    }
}
                            
                        

Ready to use Objects

                            $contract->author()->email() // Email object
                        

🤯️ ultime validation !

A bit more complex……huge possibilities

Can be extended and adapted to your specific needs

Try Valinor inside a Middleware !

What to do now ?

Give it a try !

A GitHub project with an exhaustive README,

Under active development.

Think about validation

Have you invalid object in your code ?

Are your current tools the best for you ?

Stay open minded 😉️

All the solutions exists for correct reasons,

Stay informed of new tools and practices,

Try new things, you might be surprised !

@s_hulard

https://github.com/CuyZ/Valinor
https://github.com/shulard/ipc-valinor-sample