The acsiomatic/http-payload-bundle
handles HTTP payload within Routes that behave like an API Endpoint in Symfony applications.
MapRequestBody is a controller argument attribute which:
- transforms incoming HTTP Request Body into an Object
- validates the object using Symfony Validation
- injects the object into the Route argument
MapUploadedFile is a controller argument attribute which:
- extracts an UploadedFile object from incoming HTTP Request
- validates the object using File Constraints
- injects the object into the Route argument
ResponseBody is a route attribute which:
- looks for a suitable response format through Content Negotiation
- serializes the data returned by the dispatched route
- exceptions thrown after the kernel.controller event are also serialized
- injects the serialized data into the Response object
composer require acsiomatic/http-payload-bundle
# config/packages/acsiomatic_http_payload.yaml
acsiomatic_http_payload:
request_body:
default:
formats: ['json']
deserialization_context: []
validation_groups: ['Default']
file_upload:
default:
constraints: []
response_body:
default:
formats: ['json']
serialization_context: []
The MapRequestBody attribute injects the HTTP Request Body into a Route argument. Incoming data is deserialized and validated before being injected.
# src/Controller/LuckyController.php
namespace App\Controller;
use Acsiomatic\HttpPayloadBundle\RequestBody\Attribute\MapRequestBody;
use App\NumberRange;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Annotation\Route;
#[AsController]
final class LuckyController
{
#[Route('/lucky/number', methods: ['GET'])]
public function number(
#[MapRequestBody] NumberRange $range,
): Response {
return new Response(
(string) random_int($range->min, $range->max)
);
}
}
# src/NumberRange.php
namespace App;
use Symfony\Component\Validator\Constraints as Assert;
final class NumberRange
{
#[Assert\GreaterThanOrEqual(0)]
public int $min;
#[Assert\GreaterThanOrEqual(propertyPath: 'min')]
public int $max;
}
The MapUploadedFile attribute fetches the file from the Request and applies custom constraints before injecting it into the Route argument.
# src/Controller/UserController.php
namespace App\Controller;
use Acsiomatic\HttpPayloadBundle\FileUpload\Attribute\MapUploadedFile;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Constraints\File;
#[AsController]
final class UserController
{
#[Route('/user/picture', methods: ['PUT'])]
public function picture(
#[MapUploadedFile(
constraints: new File(mimeTypes: ['image/png', 'image/jpeg']),
)] UploadedFile $picture,
): Response {
return new Response('Your picture was updated');
}
}
The ResponseBody attribute serializes the object returned by the Route and fills the Response content with it.
# src/Controller/CelebritiesController.php
namespace App\Controller;
use Acsiomatic\HttpPayloadBundle\ResponseBody\Attribute\ResponseBody;
use App\Person;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Annotation\Route;
#[AsController]
final class CelebritiesController
{
#[Route('/celebrities/einstein', methods: ['GET'])]
#[ResponseBody]
public function einstein(): Person
{
$person = new Person();
$person->name = 'Albert Einstein';
$person->birthdate = new \DateTimeImmutable('1879-03-14');
return $person;
}
}
# src/Person.php
namespace App;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
final class Person
{
public string $name;
#[Serializer\Context([DateTimeNormalizer::FORMAT_KEY => \DateTimeInterface::ATOM])]
public \DateTimeImmutable $birthdate;
}