Symfony Request Object

This is proof-of-concept implementation of laravel's like form requests.

Rational

Most of Symfony developers uses forms to map request data to some Data Transfer Object. This object then passes to validator and system start to work with validated data converted to be compatible with application model.

Symfony/forms is great component which simplifies life alot when you are dealing with forms. But what if you don't have any forms? For example you are developing HTTP API and you already have pretty much structured request data, which should be validated.

FosRest bundle provides ParamFetcher, but it doesn't allow to validate all request and will fail on first failed constraint.

Laravel has FormRequest, which encapsulates all validation stuff inside of custom user-defined request. This allows to DRY request validation and validation of input data can be done in middleware at front-controller level.

Why not bring this idea to Symfony?

What should containe request as it's payload

Basically anything, but for our implementation we suggest to pass:

  • query params for GET and HEAD requests
  • union of request and files params for all other methods

How it works

This solutions is similar to Laravel's FormRequests but it responsible only for data validation If you need to restruct access to this particular request you should use symfony/security features.

For data validation this solution uses symfony/validation. All rules is just good old constraints provided by symfony. So if you need to validate simple request for new book, all that you need is to define CreateBookRequest extended from base class, and use in in your controller's actions:

namespace App\Http\Request;

use Symfony\Component\Validator\Constraints as Assert;

class CreateBookRequest extends Request
{
    public function rules()
    {
        return new Assert\Collection([
            'name' => new Assert\Required(['message' => 'Book name required']),
            'brief' => new Assert\Required(['message' => 'Book brief description required']),
            'year' => new Assert\Required(['message' => 'Year of publishing is required']),
            'cover' => new Assert\Image(['message' => 'Book cover image is required'])
        ]);
    }
}
public function createBookAction(CreateBookRequest $request)
{
    $coverImage = $this->coverMaker->make($request->get('cover');
    
    $book = new Book(
        $request->get('name'),
        $request->get('brief'),
        $request->get('year'),
        $coverImage
    );
    
    // ...
}

Request as DTO

This request objects can be used just as DTO, but you may may want to use more strict approaches. For example in php7.1 there will be typed properties, and you may want to make use of it:

class CreateBookRequest extends Request
{
    /** @Assert\Required(message="Book name required") */
    private string  $name;
    
    /** @Assert\Required(message="Book brief description required") */
    private string  $brief;
    
    /** @Assert\Required(message="Year of publishing is required") */
    private int     $year;
    
    /** @Assert\Image(message="Book cover image is required") */
    private UploadedFile $cover;
    
    // this could be moved into 
    // some trait for example    
    protected function payload()
    {
        return $this;
    }
    
    /**
     * This method allows you to manipulate with data.
     * It called right after data is validated
     * 
     * If you want to change data before validation
     * then override `__constructor`.
     */
    protected function resolve(array $payload)
    {
        // other php 7.1 features
        // array destructuring assignment 
        [
            'name' => $this->name,
            'brief' => $this->brief,
            'year' => $this->brief,
            'cover' => $this->cover
        ] = $payload;
    }
    
    // getters stuff...
}

Context-depending constraints using validation groups

If you for some reasone need to validate request data depending on context, you can use validation groups to have different constraints to be applied depending on payload:

namespace App\Http\Request;

use Symfony\Component\Validator\Constraints as Assert;

class CreateBookRequest extends Request
{
    protected function rules()
    {
        return new Assert\Collection([
            'provider' => new Assert\Choise(['basic', 'facebook', 'twitter']),
            'identity' => new Assert\Required(['message' => 'Your resource user id is required', 'group' => 'oauth']),
            'email' => new Assert\Email(['message' => 'Email is required', 'group' => 'basic']),
            'secret' => new Assert\Required(['message' => 'Your app token is required', 'group' => 'oauth']),
            'password' => new Assert\Required(['message' => 'Your password is required', 'group' => 'basic']),
        ]);
    }
    
    protected function validationGroups(array $payload)
    {
        return [
          'Default', 
          ['basic' => 'basic'][$payload['provider'] ?? 'basic'] ?? 'oauth'
        ];
    }
}