CTO ( CH Studio), Trainer, Contributor.
IN: Request, Preparation, Consolidation
CORE: Execution
OUT: Filtering, Adaptation, Response
PSR-15, or specific implementations…
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);
}
}
Validate everything before reaching application code …
… Combine and reuse objects easily …
… Centralize logic for easier maintenance.
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"
}
}
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"
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.
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.
{
"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"
/**
* @param DateTimeInterface $startsAt
* @param DateTimeInterface $expiresAt
* @param string $authorName
* @param string $authorEmail
*/
function createContract(
$startsAt,
$expiresAt,
$authorName,
$authorEmail
) {
/** code logic */
}
createContract(
'a',
'a',
-18,
0
);
// Error in the function body because parameters have the wrong type.
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
Nice but don't cover all the cases.
function createContract(
Contract $contract
) {
/**
* $contract contains all properties
* All values are valid and respect the rules.
*/
}
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;
}
}
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
$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);
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.
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.
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…
}
{
"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."
}
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;
}
}
$contract->author()->email() // Email object
A bit more complex……huge possibilities
Can be extended and adapted to your specific needs
Try Valinor inside a Middleware !
A GitHub project with an exhaustive README,
Under active development.
Have you invalid object in your code ?
Are your current tools the best for you ?
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