API Platform z Symfony w 2026: Architektura, State Providers i pytania rekrutacyjne

Opanuj API Platform 4.2 z Symfony: State Providers, Processors, Object Mapper, JSON Streamer i optymalizacje wydajności. Najczęstsze pytania rekrutacyjne dla doświadczonych programistów.

Diagram architektury API Platform Symfony z przepływem REST API

API Platform 4.2 zmienia sposób, w jaki aplikacje Symfony udostępniają interfejsy REST i GraphQL. Ta wersja wprowadza Symfony Object Mapper do czystego rozdzielenia zasobów, JSON Streamer dla znaczącego wzrostu wydajności oraz przeprojektowany system filtrów. Dla programistów przygotowujących się do rozmów kwalifikacyjnych, zrozumienie tych wzorców architektonicznych wyróżnia seniorów od juniorów.

Wymagania API Platform 4.2

API Platform 4.2 wymaga Symfony 7.4 lub 8.0. Wsparcie dla Symfony 6.4 i 7.0-7.3 zostało wycofane. JSON Streamer zapewnia do 32% więcej żądań na sekundę na endpointach kolekcji.

Konfiguracja API Platform 4.2 z Symfony

API Platform instaluje się przez Symfony Flex z automatyczną konfiguracją. Domyślna konfiguracja obsługuje większość przypadków użycia, pozostając jednocześnie w pełni konfigurowalną dla złożonych wymagań domenowych.

bash
# Instalacja API Platform
composer require api-platform/symfony

# Dokumentacja API dostępna pod /api/
# Otwórz http://localhost:8000/api/ po uruchomieniu serwera
symfony serve

Recipe Flex konfiguruje grupy serializacji, integrację z Doctrine oraz generowanie dokumentacji OpenAPI. Zasoby API udostępniają operacje CRUD poprzez dodanie pojedynczego atrybutu do klas encji.

src/Entity/Book.phpphp
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity]
#[ApiResource(
    operations: [
        new GetCollection(),
        new Get(),
        new Post(security: "is_granted('ROLE_ADMIN')"),
        new Put(security: "is_granted('ROLE_ADMIN')"),
        new Delete(security: "is_granted('ROLE_ADMIN')")
    ],
    normalizationContext: ['groups' => ['book:read']],
    denormalizationContext: ['groups' => ['book:write']]
)]
class Book
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Assert\NotBlank]
    #[Groups(['book:read', 'book:write'])]
    private string $title;

    #[ORM\Column(type: 'text')]
    #[Groups(['book:read', 'book:write'])]
    private string $description;

    #[ORM\Column]
    #[Groups(['book:read'])]
    private \DateTimeImmutable $createdAt;

    // Gettery i settery...
}

Ta konfiguracja generuje pięć endpointów z automatyczną walidacją, serializacją i dokumentacją OpenAPI.

State Providers: Pobieranie danych z dowolnego źródła

State Providers kontrolują sposób, w jaki API Platform pobiera dane dla operacji GET. Domyślny provider Doctrine obsługuje pobieranie encji, ale własne providery umożliwiają integrację z zewnętrznymi API, Elasticsearch lub danymi z pamięci podręcznej.

src/State/BookStateProvider.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Repository\BookRepository;
use Psr\Cache\CacheItemPoolInterface;

final class BookStateProvider implements ProviderInterface
{
    public function __construct(
        private BookRepository $repository,
        private CacheItemPoolInterface $cache
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        // Pobieranie pojedynczego elementu
        if (isset($uriVariables['id'])) {
            $cacheKey = sprintf('book_%d', $uriVariables['id']);
            $item = $this->cache->getItem($cacheKey);
            
            if ($item->isHit()) {
                return $item->get();
            }
            
            $book = $this->repository->find($uriVariables['id']);
            $item->set($book)->expiresAfter(3600);
            $this->cache->save($item);
            
            return $book;
        }

        // Pobieranie kolekcji z własnym filtrowaniem
        return $this->repository->findActiveBooks();
    }
}

Rejestracja providera dla konkretnych operacji:

php
#[ApiResource(
    operations: [
        new GetCollection(provider: BookStateProvider::class),
        new Get(provider: BookStateProvider::class),
        // Pozostałe operacje używają domyślnego providera Doctrine
        new Post(),
        new Put(),
    ]
)]
class Book { /* ... */ }

State Processors: Obsługa mutacji z logiką biznesową

State Processors obsługują operacje POST, PUT, PATCH i DELETE. Otrzymują zdeserializowane dane i aplikują logikę biznesową przed zapisem.

src/State/BookStateProcessor.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Book;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

final class BookStateProcessor implements ProcessorInterface
{
    public function __construct(
        private EntityManagerInterface $em,
        private MailerInterface $mailer,
        private ProcessorInterface $persistProcessor
    ) {}

    public function process(
        mixed $data,
        Operation $operation,
        array $uriVariables = [],
        array $context = []
    ): mixed {
        // Logika biznesowa przed zapisem
        if ($data instanceof Book && $operation instanceof Post) {
            $data->setCreatedAt(new \DateTimeImmutable());
            $data->setSlug($this->generateSlug($data->getTitle()));
        }

        // Delegowanie do procesora Doctrine
        $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);

        // Powiadomienie po zapisie
        if ($operation instanceof Post) {
            $this->notifyNewBook($data);
        }

        return $result;
    }

    private function generateSlug(string $title): string
    {
        return strtolower(preg_replace('/[^a-zA-Z0-9]+/', '-', $title));
    }

    private function notifyNewBook(Book $book): void
    {
        $email = (new Email())
            ->to('catalog@example.com')
            ->subject('Nowa książka dodana: ' . $book->getTitle())
            ->text('Nowa książka została dodana do katalogu.');
        $this->mailer->send($email);
    }
}
Dekorowanie procesora

Dekorowanie domyślnego procesora Doctrine za pomocą #[AsDecorator] zachowuje zachowanie persystencji, dodając jednocześnie własną logikę. Ten wzorzec eliminuje konieczność duplikowania operacji ORM.

Object Mapper: Rozdzielenie zasobów API od encji

API Platform 4.2 integruje komponent Symfony Object Mapper w celu oddzielenia reprezentacji API od encji domenowych. Ta separacja umożliwia różne modele odczytu/zapisu i chroni wewnętrzne struktury encji.

src/ApiResource/BookResource.phpphp
namespace App\ApiResource;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use App\Entity\Book;
use Symfony\Component\ObjectMapper\Attribute\Map;

#[ApiResource(
    shortName: 'Book',
    operations: [
        new GetCollection(),
        new Get()
    ]
)]
#[Map(target: Book::class)]
class BookResource
{
    public ?int $id = null;
    
    public string $title;
    
    public string $description;
    
    // Pole obliczane, nie w encji
    public int $wordCount;
    
    // Sformatowana data dla konsumentów API
    public string $publishedDate;
}

Provider mappera automatycznie transformuje encje na zasoby:

src/State/BookResourceProvider.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\BookResource;
use App\Repository\BookRepository;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;

final class BookResourceProvider implements ProviderInterface
{
    public function __construct(
        private BookRepository $repository,
        private ObjectMapperInterface $mapper
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        if (isset($uriVariables['id'])) {
            $book = $this->repository->find($uriVariables['id']);
            return $book ? $this->toResource($book) : null;
        }

        return array_map(
            fn(Book $book) => $this->toResource($book),
            $this->repository->findAll()
        );
    }

    private function toResource(Book $book): BookResource
    {
        $resource = $this->mapper->map($book, BookResource::class);
        $resource->wordCount = str_word_count($book->getDescription());
        $resource->publishedDate = $book->getCreatedAt()->format('j F Y');
        return $resource;
    }
}

Gotowy na rozmowy o Symfony?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

JSON Streamer: 32% wzrost wydajności

Komponent JSON Streamer serializuje duże kolekcje bez ładowania całych zbiorów danych do pamięci. Benchmarki na API Sylius wykazały 32,4% wzrost liczby żądań na sekundę.

Włączenie streamingu na poziomie zasobu lub operacji:

php
#[ApiResource(
    operations: [
        new GetCollection(
            jsonStream: true,  // Włączenie JSON streaming
            paginationItemsPerPage: 100
        ),
        new Get()
    ]
)]
class Book { /* ... */ }

Streaming jest szczególnie korzystny dla:

  • Endpointów kolekcji z ponad 50 elementami
  • Zasobów z zagnieżdżonymi relacjami
  • API obsługujących klientów mobilnych z ograniczoną przepustowością

Specyfikacja OpenAPI również została zoptymalizowana. Mutualizacja JSON Schema zmniejsza rozmiar pliku o 30%, poprawiając czasy ładowania dokumentacji.

Pytania rekrutacyjne: Architektura API Platform

Rozmowy techniczne na stanowiska Symfony często obejmują wzorce API Platform. Te pytania oceniają zrozumienie architektury frameworka wykraczające poza podstawowe operacje CRUD.

"Wyjaśnij różnicę między State Providers a Processors"

Oczekiwana odpowiedź: State Providers pobierają dane dla operacji odczytu (GET). Zwracają encje, DTO lub tablice. State Processors obsługują operacje zapisu (POST, PUT, PATCH, DELETE). Otrzymują zdeserializowane dane wejściowe i wykonują logikę biznesową przed zapisem. Rozdzielenie to podąża za zasadami CQRS: zapytania przez Providers, komendy przez Processors.

"Kiedy użyć własnego API Resource zamiast bezpośredniego udostępniania encji?"

Oczekiwana odpowiedź: Własne zasoby stosuje się gdy:

  • Reprezentacja API różni się od schematu bazy danych
  • Pola obliczane wymagają agregacji z wielu encji
  • Modele zapisu i odczytu wymagają różnych struktur
  • Wewnętrzne pola encji muszą pozostać ukryte przed konsumentami API
  • Kompatybilność wersji wymaga stabilnych kontraktów podczas ewolucji encji

"Jak API Platform obsługuje walidację?"

Oczekiwana odpowiedź: API Platform używa constraintów Symfony Validator na właściwościach encji. Walidacja uruchamia się automatycznie podczas deserializacji przed wykonaniem State Processor. Grupy walidacyjne kontrolują, które constrainty są stosowane dla danej operacji. Własne walidatory integrują się poprzez standardowe mechanizmy Symfony.

php
#[ApiResource(
    operations: [
        new Post(validationContext: ['groups' => ['create']]),
        new Put(validationContext: ['groups' => ['update']])
    ]
)]
class Book
{
    #[Assert\NotBlank(groups: ['create', 'update'])]
    private string $title;

    #[Assert\Isbn(groups: ['create'])]
    private string $isbn;  // Wymagane tylko przy tworzeniu
}
Częsty błąd na rozmowach

Kandydaci często opisują walidację jako "automatyczną" bez wspominania o grupach walidacyjnych lub własnych constraintach. Rekruterzy szukają zrozumienia sposobu dostosowywania walidacji per operacja.

"Jakie mechanizmy bezpieczeństwa zapewnia API Platform?"

Oczekiwana odpowiedź: API Platform integruje się z Symfony Security poprzez:

  • Atrybut security na operacjach dla dostępu opartego na rolach
  • securityPostDenormalize dla sprawdzeń na poziomie obiektu po bindowaniu danych
  • Votery dla złożonej logiki autoryzacji
  • Rate limiting przez integrację z Symfony Rate Limiter
php
#[ApiResource(
    operations: [
        new Get(
            security: "is_granted('ROLE_USER')"
        ),
        new Put(
            security: "is_granted('ROLE_ADMIN') or object.getOwner() == user",
            securityPostDenormalize: "is_granted('BOOK_EDIT', object)"
        )
    ]
)]

Filtry i paginacja: Zaawansowane wzorce zapytań

Filtry API Platform umożliwiają klientom wykonywanie zapytań do kolekcji za pomocą parametrów URL. System filtrów w wersji 4.2 został przeprojektowany dla lepszej rozszerzalności.

php
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Metadata\ApiFilter;

#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: [
    'title' => 'partial',      // LIKE %value%
    'author.name' => 'exact'   // Zagnieżdżona właściwość
])]
#[ApiFilter(DateFilter::class, properties: ['createdAt'])]
#[ApiFilter(OrderFilter::class, properties: ['title', 'createdAt'])]
class Book { /* ... */ }

Wygenerowane endpointy:

text
GET /api/books?title=symfony           # Szukanie po tytule
GET /api/books?createdAt[after]=2026-01-01  # Zakres dat
GET /api/books?order[createdAt]=desc   # Sortowanie

Testowanie zasobów API Platform

API Platform dostarcza klienta testowego upraszczającego testy funkcjonalne. Klasa ApiTestCase oferuje asercje specyficzne dla odpowiedzi API.

tests/Api/BookTest.phpphp
namespace App\Tests\Api;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Book;

class BookTest extends ApiTestCase
{
    public function testGetCollection(): void
    {
        $response = static::createClient()->request('GET', '/api/books');

        $this->assertResponseIsSuccessful();
        $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
        $this->assertJsonContains([
            '@context' => '/api/contexts/Book',
            '@type' => 'Collection'
        ]);
    }

    public function testCreateBook(): void
    {
        $response = static::createClient()->request('POST', '/api/books', [
            'json' => [
                'title' => 'Najlepsze praktyki Symfony',
                'description' => 'Przewodnik po nowoczesnym rozwoju Symfony'
            ],
            'headers' => ['Authorization' => 'Bearer ' . $this->getToken()]
        ]);

        $this->assertResponseStatusCodeSame(201);
        $this->assertJsonContains(['title' => 'Najlepsze praktyki Symfony']);
    }

    public function testCreateBookValidationFails(): void
    {
        $response = static::createClient()->request('POST', '/api/books', [
            'json' => ['description' => 'Brak tytułu'],
            'headers' => ['Authorization' => 'Bearer ' . $this->getToken()]
        ]);

        $this->assertResponseStatusCodeSame(422);
        $this->assertJsonContains([
            'violations' => [
                ['propertyPath' => 'title', 'message' => 'This value should not be blank.']
            ]
        ]);
    }
}

Kluczowe wnioski dla API Platform z Symfony

  • State Providers obsługują operacje GET, State Processors obsługują mutacje. Ta separacja umożliwia czystą architekturę z własnymi źródłami danych i logiką biznesową
  • Komponent Object Mapper oddziela zasoby API od encji Doctrine, umożliwiając różne modele odczytu/zapisu i chroniąc wewnętrzne struktury
  • JSON Streamer zapewnia 32% wzrost wydajności na endpointach kolekcji poprzez serializację bez pełnej alokacji pamięci
  • Bezpieczeństwo integruje się poprzez standardowe mechanizmy Symfony: wyrażenia security, Votery i komponent Rate Limiter
  • Grupy walidacyjne dostosowują egzekwowanie constraintów per operacja
  • Filtry automatycznie udostępniają parametry zapytań, z filtrami search, date i order pokrywającymi większość przypadków użycia
  • Pytania rekrutacyjne koncentrują się na decyzjach architektonicznych: kiedy używać własnych providerów, jak rozdzielać modele odczytu/zapisu oraz wzorce implementacji bezpieczeństwa

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Wyzwanie dnia

Znajdziesz błąd w Symfony?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 8 września 2026

Tagi

#api-platform
#symfony
#rest-api
#state-providers
#interview

Udostępnij

Powiązane artykuły