# Symfony 7: API Platform i Najlepsze Praktyki > Kompletny przewodnik po budowaniu profesjonalnych REST API z Symfony 7 i API Platform 4. State Providers, Processors, walidacja i serializacja z praktycznymi przykładami. - Published: 2026-01-12 - Updated: 2026-04-07 - Author: SharpSkill - Tags: symfony, api platform, php, rest api, api development - Reading time: 14 min --- API Platform 4 radykalnie przekształca sposób tworzenia API REST i GraphQL z Symfony 7. Nowa wersja przynosi przemyślaną filozofię: wyraźne rozdzielenie odpowiedzialności, uproszczone State Providers i Processors oraz natywną integrację Symfony Object Mapper. Budowanie profesjonalnego API nigdy nie było tak intuicyjne. > **Nowości w API Platform 4.2** > > Wersja 4.2 wprowadza JSON Streamer z przyrostem wydajności do +32% RPS, przeprojektowany system filtrów oraz Mutators do dostosowywania operacji bez ingerencji w rdzeń. Wsparcie dla Symfony 7 i 8 jest natywne. ## Instalacja i Początkowa Konfiguracja API Platform instaluje się za pomocą kilku poleceń dzięki Symfony Flex. Domyślna konfiguracja pokrywa większość przypadków użycia, pozostając w pełni konfigurowalna. ```bash # terminal # Create a new Symfony project with API Platform composer create-project symfony/skeleton my-api cd my-api # Install API Platform with Doctrine ORM composer require api # Verify installation php bin/console debug:router | grep api ``` Symfony Flex automatycznie konfiguruje trasy, dokumentację OpenAPI oraz interfejs Swagger UI dostępny pod adresem `/api`. ```yaml # config/packages/api_platform.yaml api_platform: title: 'My API' version: '1.0.0' # Supported response formats formats: jsonld: ['application/ld+json'] json: ['application/json'] # OpenAPI documentation swagger: versions: [3] # Default pagination defaults: pagination_items_per_page: 30 pagination_maximum_items_per_page: 100 ``` Ta konfiguracja definiuje formaty serializacji, globalną paginację oraz metadane dokumentacji API. ## Tworzenie Prostego Zasobu API Atrybut `#[ApiResource]` eksponuje encję Doctrine jako zasób REST. API Platform automatycznie generuje endpointy CRUD, dokumentację OpenAPI oraz podstawowe walidacje. ```php 'DESC'], // Pagination configuration for this resource paginationItemsPerPage: 20 )] class Book { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank(message: 'Title is required')] #[Assert\Length(min: 2, max: 255)] private ?string $title = null; #[ORM\Column(type: 'text')] #[Assert\NotBlank] private ?string $description = null; #[ORM\Column(length: 13, unique: true)] #[Assert\Isbn] private ?string $isbn = null; #[ORM\Column] private ?\DateTimeImmutable $publishedAt = null; // Getters and setters... public function getId(): ?int { return $this->id; } public function getTitle(): ?string { return $this->title; } public function setTitle(string $title): static { $this->title = $title; return $this; } public function getDescription(): ?string { return $this->description; } public function setDescription(string $description): static { $this->description = $description; return $this; } public function getIsbn(): ?string { return $this->isbn; } public function setIsbn(string $isbn): static { $this->isbn = $isbn; return $this; } public function getPublishedAt(): ?\DateTimeImmutable { return $this->publishedAt; } public function setPublishedAt(\DateTimeImmutable $publishedAt): static { $this->publishedAt = $publishedAt; return $this; } } ``` Ta encja generuje sześć endpointów: `GET /api/books`, `POST /api/books`, `GET /api/books/{id}`, `PUT /api/books/{id}`, `PATCH /api/books/{id}` oraz `DELETE /api/books/{id}`. > **Rekomendowany UUID v7** > > API Platform natywnie obsługuje UUID v7 jako identyfikatory. Takie podejście poprawia bezpieczeństwo (identyfikatory niemożliwe do przewidzenia) oraz wydajność (naturalne sortowanie według daty utworzenia). ## Grupy Serializacji do Kontroli Eksponowanych Danych Grupy serializacji precyzyjnie definiują, które właściwości są eksponowane podczas odczytu (normalizacja) i zapisu (denormalizacja). To rozdzielenie jest kluczowe dla bezpieczeństwa i elastyczności API. ```php ['Default', 'user:create']]), new Get(), new Put(processor: UserPasswordHasher::class), new Patch(processor: UserPasswordHasher::class), new Delete(), ], // Properties exposed when reading normalizationContext: ['groups' => ['user:read']], // Properties accepted when writing denormalizationContext: ['groups' => ['user:create', 'user:update']], )] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['user:read'])] private ?int $id = null; #[ORM\Column(length: 180, unique: true)] #[Assert\NotBlank] #[Assert\Email] #[Groups(['user:read', 'user:create', 'user:update'])] private ?string $email = null; #[ORM\Column] private ?string $password = null; // Never exposed when reading, only when writing #[Assert\NotBlank(groups: ['user:create'])] #[Groups(['user:create', 'user:update'])] private ?string $plainPassword = null; #[ORM\Column(length: 100)] #[Groups(['user:read', 'user:create', 'user:update'])] private ?string $fullName = null; #[ORM\Column(type: 'json')] #[Groups(['user:read'])] private array $roles = []; #[ORM\Column] #[Groups(['user:read'])] private ?\DateTimeImmutable $createdAt = null; public function __construct() { $this->createdAt = new \DateTimeImmutable(); } // UserInterface implementation public function getUserIdentifier(): string { return (string) $this->email; } public function getRoles(): array { $roles = $this->roles; $roles[] = 'ROLE_USER'; return array_unique($roles); } public function getPassword(): string { return $this->password; } public function eraseCredentials(): void { $this->plainPassword = null; } // Getters and setters... public function getId(): ?int { return $this->id; } public function getEmail(): ?string { return $this->email; } public function setEmail(string $email): static { $this->email = $email; return $this; } public function setPassword(string $password): static { $this->password = $password; return $this; } public function getPlainPassword(): ?string { return $this->plainPassword; } public function setPlainPassword(?string $plainPassword): static { $this->plainPassword = $plainPassword; return $this; } public function getFullName(): ?string { return $this->fullName; } public function setFullName(string $fullName): static { $this->fullName = $fullName; return $this; } public function setRoles(array $roles): static { $this->roles = $roles; return $this; } public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; } } ``` Dzięki tej konfiguracji `plainPassword` nigdy nie pojawia się w odpowiedziach, ale może być przesłany podczas tworzenia lub aktualizacji. ## State Processors dla Logiki Biznesowej State Processors przechwytują operacje utrwalania, aby dodać logikę biznesową. API Platform 4 upraszcza ich tworzenie poprzez wstrzykiwanie zależności oparte na atrybutach. ```php */ final class UserPasswordHasher implements ProcessorInterface { public function __construct( // Injection of standard Doctrine processor #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $persistProcessor, private UserPasswordHasherInterface $passwordHasher, ) { } public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User { // Hash password if provided if ($data->getPlainPassword()) { $hashedPassword = $this->passwordHasher->hashPassword( $data, $data->getPlainPassword() ); $data->setPassword($hashedPassword); $data->eraseCredentials(); } // Delegate persistence to standard processor return $this->persistProcessor->process($data, $operation, $uriVariables, $context); } } ``` Ten wzorzec kompozycji umożliwia dodanie dowolnej logiki (wysyłanie e-maili, zdarzenia, logowanie) przy zachowaniu standardowego zachowania utrwalania. ### Processor z Logiką Warunkową Processor może dostosowywać swoje zachowanie w zależności od typu operacji. ```php */ final class BookProcessor implements ProcessorInterface { public function __construct( #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $persistProcessor, #[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')] private ProcessorInterface $removeProcessor, private NotificationService $notifications, private SearchIndexer $searchIndexer, ) { } public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { // Deletion: use remove processor if ($operation instanceof DeleteOperationInterface) { $this->searchIndexer->remove($data); return $this->removeProcessor->process($data, $operation, $uriVariables, $context); } // Creation: set publication date if ($operation instanceof Post) { $data->setPublishedAt(new \DateTimeImmutable()); } // Standard persistence $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context); // Post-processing: indexing and notification $this->searchIndexer->index($result); if ($operation instanceof Post) { $this->notifications->notifyNewBook($result); } return $result; } } ``` ## State Providers dla Niestandardowych Źródeł Danych State Providers pobierają dane z dowolnego źródła: zewnętrznych API, pamięci podręcznej, plików lub złożonej logiki biznesowej. ```php */ final class PopularBooksProvider implements ProviderInterface { public function __construct( private BookRepository $bookRepository, private CacheInterface $cache, ) { } public function provide(Operation $operation, array $uriVariables = [], array $context = []): array { // 5-minute cache for popular books return $this->cache->get('popular_books', function (ItemInterface $item) { $item->expiresAfter(300); return $this->bookRepository->findPopular(limit: 10); }); } } ``` Ten provider jest używany w dedykowanej operacji. ```php ['Default', 'article:create']]), // More lenient validation for updates new Put(validationContext: ['groups' => ['Default', 'article:update']]), ], )] class Article { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank] #[Assert\Length(min: 10, max: 255)] private ?string $title = null; #[ORM\Column(type: 'text')] #[Assert\NotBlank] // Minimum 500 characters on creation #[Assert\Length(min: 500, groups: ['article:create'])] // Minimum 100 characters for updates #[Assert\Length(min: 100, groups: ['article:update'])] private ?string $content = null; #[ORM\Column(length: 50)] #[Assert\NotBlank(groups: ['article:create'])] #[Assert\Choice(choices: ['draft', 'published', 'archived'])] private ?string $status = 'draft'; #[ORM\Column(nullable: true)] // Required only if status is "published" #[Assert\NotBlank(groups: ['article:publish'])] private ?\DateTimeImmutable $publishedAt = null; // Getters and setters... } ``` ### Dynamiczna Walidacja z Serwisem Dla złożonych reguł walidacji niestandardowy generator grup oferuje pełną elastyczność. ```php security->isGranted('ROLE_ADMIN')) { $groups[] = 'admin'; return $groups; } // Additional validation if publishing if ($object->getStatus() === 'published') { $groups[] = 'article:publish'; } return $groups; } } ``` > **Wydajność Walidacji** > > Złożone walidacje mogą wpływać na wydajność. Przy imporcie zbiorczym należy rozważyć tymczasowe wyłączenie niektórych walidacji lub zastosowanie asynchronicznych ograniczeń. ## Filtry dla Elastycznych Zapytań API Platform 4.2 całkowicie przeprojektowuje system filtrów z wyraźnym rozdzieleniem odpowiedzialności. Filtry pozwalają klientom API na wyszukiwanie i sortowanie danych. ```php 'partial', // LIKE %value% 'description' => 'partial', 'category.name' => 'exact', // Search on relation 'sku' => 'exact', // Exact match ])] // Range filtering #[ApiFilter(RangeFilter::class, properties: ['price', 'stock'])] // Date filtering #[ApiFilter(DateFilter::class, properties: ['createdAt', 'updatedAt'])] // Boolean filtering #[ApiFilter(BooleanFilter::class, properties: ['isActive', 'isFeatured'])] // Customizable sorting #[ApiFilter(OrderFilter::class, properties: [ 'name', 'price', 'createdAt', ], arguments: ['orderParameterName' => 'sort'])] class Product { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] private ?string $name = null; #[ORM\Column(type: 'text', nullable: true)] private ?string $description = null; #[ORM\Column(length: 50, unique: true)] private ?string $sku = null; #[ORM\Column(type: 'decimal', precision: 10, scale: 2)] private ?string $price = null; #[ORM\Column] private ?int $stock = null; #[ORM\Column] private ?bool $isActive = true; #[ORM\Column] private ?bool $isFeatured = false; #[ORM\ManyToOne(targetEntity: Category::class)] private ?Category $category = null; #[ORM\Column] private ?\DateTimeImmutable $createdAt = null; #[ORM\Column(nullable: true)] private ?\DateTimeImmutable $updatedAt = null; // Getters and setters... } ``` Te filtry automatycznie generują dokumentację OpenAPI i umożliwiają zapytania takie jak: ``` GET /api/products?name=phone&price[gte]=100&price[lte]=500&isActive=true&sort[price]=asc ``` ## Relacje i Podzasoby API Platform elegancko obsługuje relacje między encjami dzięki opcjom serializacji i podzasobom. ```php ['author:read']], )] class Author { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['author:read', 'book:read'])] private ?int $id = null; #[ORM\Column(length: 255)] #[Groups(['author:read', 'book:read'])] private ?string $name = null; #[ORM\Column(type: 'text', nullable: true)] #[Groups(['author:read'])] private ?string $biography = null; #[ORM\OneToMany(mappedBy: 'author', targetEntity: Book::class)] #[Groups(['author:read'])] private Collection $books; public function __construct() { $this->books = new ArrayCollection(); } // Getters and setters... } ``` ```php ['book:read']], )] // Subresource: GET /api/authors/{authorId}/books #[ApiResource( uriTemplate: '/authors/{authorId}/books', operations: [new GetCollection()], uriVariables: [ 'authorId' => new Link( fromProperty: 'books', fromClass: Author::class ), ], normalizationContext: ['groups' => ['book:read']], )] class Book { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['book:read', 'author:read'])] private ?int $id = null; #[ORM\Column(length: 255)] #[Groups(['book:read', 'author:read'])] private ?string $title = null; #[ORM\ManyToOne(targetEntity: Author::class, inversedBy: 'books')] #[ORM\JoinColumn(nullable: false)] #[Groups(['book:read'])] private ?Author $author = null; // Getters and setters... } ``` ## Bezpieczeństwo i Kontrola Dostępu API Platform integruje się bezproblemowo z systemem bezpieczeństwa Symfony. Votery i wyrażenia bezpieczeństwa kontrolują dostęp do zasobów. ```php customer; } } ``` Wyrażenie `object.getCustomer() == user` zapewnia dostęp do bieżącej encji i zalogowanego użytkownika do precyzyjnych sprawdzeń uprawnień. ## Automatyczne Testy API API Platform udostępnia traity PHPUnit do łatwego testowania endpointów. ```php request('GET', '/api/books'); // Assertions $this->assertResponseIsSuccessful(); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains([ '@context' => '/api/contexts/Book', '@type' => 'Collection', 'totalItems' => 30, ]); // Verify pagination (20 items per page) $this->assertCount(20, $response->toArray()['member']); } public function testCreateBook(): void { $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); static::createClient()->request('POST', '/api/books', [ 'auth_bearer' => $this->getToken($user), 'json' => [ 'title' => 'Clean Code', 'description' => 'A Handbook of Agile Software Craftsmanship', 'isbn' => '9780132350884', ], ]); $this->assertResponseStatusCodeSame(201); $this->assertJsonContains([ '@type' => 'Book', 'title' => 'Clean Code', ]); } public function testCreateBookValidationFails(): void { $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); static::createClient()->request('POST', '/api/books', [ 'auth_bearer' => $this->getToken($user), 'json' => [ 'title' => '', // Empty title = error 'isbn' => 'invalid-isbn', ], ]); $this->assertResponseStatusCodeSame(422); $this->assertJsonContains([ '@type' => 'ConstraintViolationList', 'violations' => [ ['propertyPath' => 'title', 'message' => 'Title is required'], ], ]); } private function getToken(object $user): string { // Implementation depends on your authentication system return 'test_token'; } } ``` ## Podsumowanie API Platform 4 z Symfony 7 reprezentuje stan wiedzy w zakresie tworzenia profesjonalnych REST API w PHP. Wyraźne rozdzielenie między State Providers (odczyt) i State Processors (zapis), w połączeniu z grupami serializacji i systemem walidacji, umożliwia budowanie solidnych i łatwych w utrzymaniu API. ### Lista Kontrolna dla Jakościowych API - Stosowanie odrębnych grup serializacji dla odczytu i zapisu - Implementacja State Processors dla logiki biznesowej (haszowanie haseł, powiadomienia) - Konfiguracja filtrów do wyszukiwania i sortowania - Stosowanie walidacji per-operacja z grupami - Zabezpieczanie endpointów za pomocą wyrażeń bezpieczeństwa - Pisanie testów funkcjonalnych dla każdego endpointu - Dokumentowanie API za pomocą metadanych OpenAPI Filozofia API Platform 4 promuje kompozycję ponad dziedziczenie oraz konfigurację ponad konwencję. Rezultat: skalowalne, testowalne API zgodne ze standardami REST/JSON-LD, gotowe do produkcji od pierwszego dnia. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/symfony/symfony-7-api-platform-best-practices