Guide

Use Messenger with an Input Object

Set the messenger option to input, and API Platform will automatically dispatch the given Input as a message instead of the Resource. Indeed, it'll add a default DataTransformer (see input/output) that handles the given input. In this example, we'll handle a ResetPasswordRequest on a custom operation on our User resource:

// src/App/Entity.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use App\Dto\ImportBookRequest;
use Doctrine\ORM\Mapping as ORM;
#[ApiResource(
    operations: [
        new Post(
            uriTemplate: '/books/import',
            /*
            * 202 Accepted HTTP Status indicates that the request has been received and will be treated later,
            * without giving an immediate return to the client
            *
            * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202
            */
            status: 202,
            /*
             * Use a custom Input object for this route.
             */
            input: ImportBookRequest::class,
            /*
             * The HTTP response that will be generated by API Platform will be empty,
             * and the serialization process will be skipped.
             */
            output: false,
            /*
             * Use `messenger=input` to use the Input object instead of the current one.
             */
            messenger: 'input',
        )
    ]
)]
#[ORM\Entity]
final class Book
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    public string $id;
    #[ORM\Column]
    public string $isbn;
    #[ORM\Column]
    public string $title;
    #[ORM\Column]
    public string $author;
}
 
// src/DoctrineMigrations.php
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Migration extends AbstractMigration
{
    public function up(Schema $schema): void
    {
        $this->addSql('CREATE TABLE book (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, isbn VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, author VARCHAR(255) NOT NULL)');
    }
    public function down(Schema $schema): void
    {
        $this->addSql('DROP TABLE book');
    }
}
 
// src/App/Dto.php
namespace App\Dto;
use Symfony\Component\Validator\Constraints as Assert;
/*
 * This Input object only aims to import a Book from its ISBN.
 */
final class ImportBookRequest
{
    #[Assert\NotBlank]
    public string $isbn;
}
 
// src/App/Handler.php
namespace App\Handler;
use App\Dto\ImportBookRequest;
use App\Entity\Book;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class ImportBookRequestHandler
{
    public function __construct(private readonly EntityManagerInterface $entityManager)
    {
    }
    /*
     * When a `POST` request is issued on `/books/import`, this Message Handler will receive an
     * `App\Dto\ImportBookRequest` object instead of a `Book` one because we specified it as `input`
     * and set `messenger=input`.
     */
    public function __invoke(ImportBookRequest $request)
    {
        /*
         * Create the real Book object from the Input one.
         * (you should probably want to import the Book data from a public API)
         */
        $book = new Book();
        $book->isbn = $request->isbn;
        $book->title = 'Le problème à trois corps';
        $book->author = 'Cixin Liu';
        /*
         * Save the real Book object in the database.
         */
        $this->entityManager->persist($book);
        $this->entityManager->flush();
    }
}
 
// src/App/Playground.php
namespace App\Playground;
use Symfony\Component\HttpFoundation\Request;
function request(): Request
{
    return Request::create('/books/import', 'POST', [
        'headers' => [
            'Content-Type' => 'application/json',
        ],
    ], [], [], [], [
        'json' => <<<JSON
sbn": "9782330113551"
JSON
        ]);
    }
}
 
// src/App/Tests.php
namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Book;
use Symfony\Component\HttpFoundation\Response;
use PhpDocumentGenerator\Playground\TestGuideTrait;
final class BookTest extends ApiTestCase
{
    use TestGuideTrait;
    public function testAsAnonymousICanAccessTheDocumentation(): void
    {
        /*
         * Import Book using its ISBN.
         */
        static::createClient()->request('POST', '/books/import', [
            'json' => [
                'isbn' => '9782330113551',
            ],
        ]);
        $this->assertResponseStatusCodeSame(Response::HTTP_ACCEPTED);
        /*
         * Check the Message Handler has been successfully called.
         * The Book should have been imported and created in the database.
         */
        static::getContainer()->get('doctrine')->getRepository(Book::class)->findOneBy([
            'isbn' => '9782330113551',
        ]);
    }
}
// src/App/Entity.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use App\Dto\ImportBookRequest;
use Doctrine\ORM\Mapping as ORM;
#[ApiResource(
    operations: [
        new Post(
            uriTemplate: '/books/import',
            /*
            * 202 Accepted HTTP Status indicates that the request has been received and will be treated later,
            * without giving an immediate return to the client
            *
            * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202
            */
            status: 202,
            /*
             * Use a custom Input object for this route.
             */
            input: ImportBookRequest::class,
            /*
             * The HTTP response that will be generated by API Platform will be empty,
             * and the serialization process will be skipped.
             */
            output: false,
            /*
             * Use `messenger=input` to use the Input object instead of the current one.
             */
            messenger: 'input',
        )
    ]
)]
#[ORM\Entity]
final class Book
{
    #[ORM\Id, ORM\Column, ORM\GeneratedValue]
    public string $id;
    #[ORM\Column]
    public string $isbn;
    #[ORM\Column]
    public string $title;
    #[ORM\Column]
    public string $author;
}
 
// src/DoctrineMigrations.php
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Migration extends AbstractMigration
{
    public function up(Schema $schema): void
    {
        $this->addSql('CREATE TABLE book (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, isbn VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL, author VARCHAR(255) NOT NULL)');
    }
    public function down(Schema $schema): void
    {
        $this->addSql('DROP TABLE book');
    }
}
 
// src/App/Dto.php
namespace App\Dto;
use Symfony\Component\Validator\Constraints as Assert;
/*
 * This Input object only aims to import a Book from its ISBN.
 */
final class ImportBookRequest
{
    #[Assert\NotBlank]
    public string $isbn;
}
 
// src/App/Handler.php
namespace App\Handler;
use App\Dto\ImportBookRequest;
use App\Entity\Book;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final class ImportBookRequestHandler
{
    public function __construct(private readonly EntityManagerInterface $entityManager)
    {
    }
    /*
     * When a `POST` request is issued on `/books/import`, this Message Handler will receive an
     * `App\Dto\ImportBookRequest` object instead of a `Book` one because we specified it as `input`
     * and set `messenger=input`.
     */
    public function __invoke(ImportBookRequest $request)
    {
        /*
         * Create the real Book object from the Input one.
         * (you should probably want to import the Book data from a public API)
         */
        $book = new Book();
        $book->isbn = $request->isbn;
        $book->title = 'Le problème à trois corps';
        $book->author = 'Cixin Liu';
        /*
         * Save the real Book object in the database.
         */
        $this->entityManager->persist($book);
        $this->entityManager->flush();
    }
}
 
// src/App/Playground.php
namespace App\Playground;
use Symfony\Component\HttpFoundation\Request;
function request(): Request
{
    return Request::create('/books/import', 'POST', [
        'headers' => [
            'Content-Type' => 'application/json',
        ],
    ], [], [], [], [
        'json' => <<<JSON
sbn": "9782330113551"
JSON
        ]);
    }
}
 
// src/App/Tests.php
namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Book;
use Symfony\Component\HttpFoundation\Response;
use PhpDocumentGenerator\Playground\TestGuideTrait;
final class BookTest extends ApiTestCase
{
    use TestGuideTrait;
    public function testAsAnonymousICanAccessTheDocumentation(): void
    {
        /*
         * Import Book using its ISBN.
         */
        static::createClient()->request('POST', '/books/import', [
            'json' => [
                'isbn' => '9782330113551',
            ],
        ]);
        $this->assertResponseStatusCodeSame(Response::HTTP_ACCEPTED);
        /*
         * Check the Message Handler has been successfully called.
         * The Book should have been imported and created in the database.
         */
        static::getContainer()->get('doctrine')->getRepository(Book::class)->findOneBy([
            'isbn' => '9782330113551',
        ]);
    }
}

Copyright © 2023 Kévin Dunglas

Sponsored by Les-Tilleuls.coop