This Symfony Bundle provides request objects support for Symfony controller actions.
Require the bundle with composer:
composer require nelexa/request-dto-bundle| Bundle version | Symfony version | PHP version(s) | 
|---|---|---|
| 1.0.* 1.1.* 1.2.0 | ^5.0 | ^7.4 | 
| ~1.2.1 | ^5.0 | ^7.4 | ^8.0 | 
| 1.3.0 - 1.3.1 | ^5.1 | ^7.4 | ^8.0 | ^8.1 | 
| ~1.3.2 | ^5.1 | ^6.0 | ^7.4 | ^8.0 | ^8.1 | 
| ~1.3.3 | ^4.4 |^5.1 | ^6.0 | ^7.4 | ^8.0 | ^8.1 | 
To specify an object as an argument of a controller action, an object must implement one of 4 interfaces:
- \Nelexa\RequestDtoBundle\Dto\QueryObjectInterfacequery parameters for GET or HEAD request methods.
- \Nelexa\RequestDtoBundle\Dto\RequestObjectInterfacerequest parameters for POST, PUT or DELETE request methods (ex. Content-Type: application/x-www-form-urlencoded) or query parameters for GET and HEAD request methods.
- \Nelexa\RequestDtoBundle\Dto\RequestBodyObjectInterfacefor POST, PUT, DELETE request body contents (ex. Content-Type: application/json).
- \Nelexa\RequestDtoBundle\Dto\ConstructRequestObjectInterfacefor mapping a request for a data transfer object in the class constructor.
Create request DTO:
use Nelexa\RequestDtoBundle\Dto\RequestObjectInterface;
use Symfony\Component\Validator\Constraints as Assert;
class UserRegistrationRequest implements RequestObjectInterface
{
    /** @Assert\NotBlank() */
    public ?string $login = null;
    /**
     * @Assert\NotBlank()
     * @Assert\Length(min="6")
     */
    public ?string $password = null;
    /**
     * @Assert\NotBlank()
     * @Assert\Email()
     */
    public ?string $email = null;
}Use in the controller:
<?php
declare(strict_types=1);
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\ConstraintViolationListInterface;
class AppController extends AbstractController
{
    /**
     * @Route("/sign-up", methods={"POST"})
     */
    public function registration(
        UserRegistrationRequest $userRegistrationRequest,
        ConstraintViolationListInterface $errors
    ): Response {
        $data = ['success' => $errors->count() === 0];
        
        if ($errors->count() > 0){
            $data['errors'] = $errors;
        }
        else{
            $data['data'] = $userRegistrationRequest;
        }
        
        return $this->json($data);
    }
}If you declare an argument with type \Symfony\Component\Validator\ConstraintViolationListInterface as nullable, then if there are no errors, it will be null.
...
    /**
     * @Route("/sign-up", methods={"POST"})
     */
    public function registration(
        UserRegistrationRequest $userRegistrationRequest,
        ?ConstraintViolationListInterface $errors
    ): Response {
        return $this->json(
            [
                'success' => $errors === null,
                'errors' => $errors,
            ]
        );
    }
...If the argument \Symfony\Component\Validator\ConstraintViolationListInterface is not declare, then the exception \Nelexa\RequestDtoBundle\Exception\RequestDtoValidationException will be thrown, which will be converted to the json or xml format.
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class AppController extends AbstractController{
    /**
     * @Route("/sign-up", methods={"POST"})
     */
    public function registration(UserRegistrationRequest $userRegistrationRequest): Response {
        return $this->json(['success' => true]);
    }
}Send POST request:
curl 'https://127.0.0.1/registration' -H 'Accept: application/json' -H 'Content-Type: application/x-www-form-urlencoded' --data-raw 'login=johndoe'
Response:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
Content response:
{
    "type": "https://tools.ietf.org/html/rfc7807",
    "title": "Validation Failed",
    "detail": "password: This value should not be blank.\nemail: This value should not be blank.",
    "violations": [
        {
            "propertyPath": "password",
            "title": "This value should not be blank.",
            "parameters": {
                "{{ value }}": "null"
            },
            "type": "urn:uuid:c1051bb4-d103-4f74-8988-acbcafc7fdc3"
        },
        {
            "propertyPath": "email",
            "title": "This value should not be blank.",
            "parameters": {
                "{{ value }}": "null"
            },
            "type": "urn:uuid:c1051bb4-d103-4f74-8988-acbcafc7fdc3"
        }
    ]
}use Nelexa\RequestDtoBundle\Dto\ConstructRequestObjectInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\ConstraintViolationListInterface;
class ExampleDTO implements ConstructRequestObjectInterface
{
    /** @Assert\Range(min=1) */
    private int $page;
    /**
     * @Assert\NotBlank
     * @Assert\Regex("~^\d{10,13}$~", message="Invalid phone number")
     */
    private string $phone;
    public function __construct(Request $request)
    {
        $this->page = $request->request->getInt('p', 1);
        // sanitize phone number
        $phone = (string) $request->request->get('phone');
        $phone = preg_replace('~\D~', '', $phone);
        $this->phone = (string) $phone;
    }
    public function getPage(): int
    {
        return $this->page;
    }
    public function getPhone(): string
    {
        return $this->phone;
    }
}
class AppController extends AbstractController
{
    public function exampleAction(
        ExampleDTO $dto,
        ConstraintViolationListInterface $errors
    ): Response {
        $data = [
            'page' => $dto->getPage(),
            'phone' => $dto->getPhone(),
            'errors' => $errors,
        ];
        return $this->json($data, $errors->count() === 0 ? 200 : 400);
    }
}Changes are documented in the releases page.
The MIT License (MIT). Please see LICENSE for more information.

