# API Platform Symfony REST: Tutorial Completo e Domande da Colloquio 2026 > Sviluppare API REST con API Platform 4.3 e Symfony 7. State Providers, Processors, gruppi di serializzazione e domande da colloquio per sviluppatori Symfony. - Published: 2026-08-25 - Updated: 2026-08-25 - Author: Anthony Fillion-Maillet - Tags: api-platform, symfony, rest-api, php, tutorial - Reading time: 12 min --- API Platform 4.3 trasforma Symfony in un framework REST API potente con documentazione OpenAPI automatica, content negotiation e un'architettura pulita che separa le operazioni di lettura e scrittura. Questo tutorial copre l'installazione, i pattern avanzati e le domande da colloquio che distinguono gli sviluppatori senior dai junior. > **Architettura API Platform 4** > > API Platform 4 utilizza State Providers per le operazioni GET e State Processors per POST/PUT/PATCH/DELETE. Questa separazione si allinea ai principi CQRS e semplifica notevolmente il testing. ## Installare API Platform su Symfony 7 L'installazione di API Platform richiede Symfony 7.2 o superiore. Il bundle si integra di default con Doctrine ORM, ma supporta sorgenti dati personalizzate attraverso il pattern Provider/Processor. ```bash # Installare API Platform con Symfony Flex composer require api # Verificare l'installazione php bin/console debug:router | grep api ``` La recipe `api` installa `api-platform/symfony` insieme ai componenti serializer, validator e property-access. Symfony Flex configura automaticamente le route sotto `/api`. ```yaml # config/packages/api_platform.yaml api_platform: title: 'My API' version: '1.0.0' formats: jsonld: ['application/ld+json'] json: ['application/json'] docs_formats: jsonld: ['application/ld+json'] jsonopenapi: ['application/vnd.openapi+json'] html: ['text/html'] defaults: stateless: true cache_headers: vary: ['Content-Type', 'Authorization', 'Accept-Language'] ``` L'impostazione `stateless: true` disabilita le sessioni PHP per tutti gli endpoint API, riducendo l'overhead di memoria e abilitando lo scaling orizzontale. ## Creare la Prima Risorsa API con Attributi API Platform 4 utilizza attributi PHP 8 per dichiarare le risorse API. Ogni entity diventa un endpoint API attraverso l'attributo `#[ApiResource]`. ```php // src/Entity/Product.php 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')") ], paginationItemsPerPage: 30 )] class Product { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank] #[Assert\Length(min: 3, max: 255)] private string $name; #[ORM\Column(type: 'decimal', precision: 10, scale: 2)] #[Assert\Positive] private string $price; #[ORM\Column] private \DateTimeImmutable $createdAt; public function __construct() { $this->createdAt = new \DateTimeImmutable(); } public function getId(): ?int { return $this->id; } public function getName(): string { return $this->name; } public function setName(string $name): static { $this->name = $name; return $this; } public function getPrice(): string { return $this->price; } public function setPrice(string $price): static { $this->price = $price; return $this; } public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; } } ``` Il parametro `security` su ogni operazione sfrutta l'Expression Language di Symfony. La funzione `is_granted()` verifica le decisioni dei voter, abilitando il controllo degli accessi basato sui ruoli senza controller personalizzati. ## Gruppi di Serializzazione per Modellare le Response I gruppi di serializzazione controllano quali proprietà appaiono nelle response API. Operazioni diverse possono esporre campi diversi dalla stessa entity. ```php // src/Entity/User.php namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; #[ORM\Entity] #[ApiResource( operations: [ new GetCollection(normalizationContext: ['groups' => ['user:list']]), new Get(normalizationContext: ['groups' => ['user:read']]), new Post( normalizationContext: ['groups' => ['user:read']], denormalizationContext: ['groups' => ['user:write']] ) ] )] class User { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['user:list', 'user:read'])] private ?int $id = null; #[ORM\Column(length: 180)] #[Groups(['user:list', 'user:read', 'user:write'])] private string $email; #[ORM\Column] #[Groups(['user:write'])] private string $password; #[ORM\Column] #[Groups(['user:read'])] private \DateTimeImmutable $registeredAt; #[ORM\Column(type: 'json')] #[Groups(['user:read'])] private array $roles = []; } ``` Il `normalizationContext` controlla l'output (da PHP a JSON). Il `denormalizationContext` controlla l'input (da JSON a PHP). Il campo `password` usa solo `user:write`, impedendo che appaia mai nelle response. ## State Providers per Sorgenti Dati Personalizzate I State Providers recuperano dati per le operazioni GET. I provider di default `ItemProvider` e `CollectionProvider` usano Doctrine, ma i provider personalizzati abilitano qualsiasi sorgente dati: Elasticsearch, API esterne o valori calcolati. ```php // src/State/ProductStatsProvider.php namespace App\State; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\Repository\ProductRepository; use App\Dto\ProductStats; final readonly class ProductStatsProvider implements ProviderInterface { public function __construct( private ProductRepository $productRepository ) {} public function provide(Operation $operation, array $uriVariables = [], array $context = []): ProductStats { $totalProducts = $this->productRepository->count([]); $averagePrice = $this->productRepository->getAveragePrice(); $lowStockCount = $this->productRepository->countLowStock(threshold: 10); return new ProductStats( totalProducts: $totalProducts, averagePrice: $averagePrice, lowStockCount: $lowStockCount ); } } ``` ```php // src/Dto/ProductStats.php namespace App\Dto; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use App\State\ProductStatsProvider; #[ApiResource( operations: [ new Get( uriTemplate: '/products/stats', provider: ProductStatsProvider::class ) ] )] final readonly class ProductStats { public function __construct( public int $totalProducts, public float $averagePrice, public int $lowStockCount ) {} } ``` Il DTO `ProductStats` (Data Transfer Object) non è un'entity Doctrine. Rappresenta dati calcolati esposti su `/api/products/stats`. Il provider calcola i valori ad ogni richiesta. ## State Processors per Operazioni di Scrittura I State Processors gestiscono le operazioni POST, PUT, PATCH e DELETE. I processor personalizzati permettono l'esecuzione di logica di business prima o dopo la persistenza. ```php // src/State/UserRegistrationProcessor.php namespace App\State; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; final readonly class UserRegistrationProcessor implements ProcessorInterface { public function __construct( private EntityManagerInterface $entityManager, private UserPasswordHasherInterface $passwordHasher ) {} public function process( mixed $data, Operation $operation, array $uriVariables = [], array $context = [] ): User { // $data è l'entity User deserializzata dal body della richiesta $hashedPassword = $this->passwordHasher->hashPassword( $data, $data->getPlainPassword() ); $data->setPassword($hashedPassword); $data->eraseCredentials(); $this->entityManager->persist($data); $this->entityManager->flush(); return $data; } } ``` Il processor viene collegato all'operazione POST sull'entity User: ```php #[ApiResource( operations: [ new Post( processor: UserRegistrationProcessor::class, denormalizationContext: ['groups' => ['user:create']] ) ] )] ``` Il processor esegue l'hash della password prima che Doctrine persista l'entity. La proprietà `plainPassword` usa un gruppo di serializzazione di sola scrittura e non viene mai memorizzata nel database. ## Filtri per Query Parameter API Platform fornisce filtri integrati per cercare, ordinare e filtrare le collection. I filtri aggiungono query parameter agli endpoint GET collection. ```php // src/Entity/Article.php namespace App\Entity; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Doctrine\Orm\Filter\DateFilter; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; #[ORM\Entity] #[ApiResource] #[ApiFilter(SearchFilter::class, properties: [ 'title' => 'partial', 'author.name' => 'exact', 'category' => 'exact' ])] #[ApiFilter(DateFilter::class, properties: ['publishedAt'])] #[ApiFilter(OrderFilter::class, properties: ['publishedAt', 'title'])] class Article { // Proprietà dell'entity... } ``` La configurazione dei filtri abilita questi query parameter: - `GET /api/articles?title=symfony` (corrispondenza parziale sul titolo) - `GET /api/articles?author.name=John` (corrispondenza esatta su entity correlata) - `GET /api/articles?publishedAt[after]=2026-01-01` (intervallo di date) - `GET /api/articles?order[publishedAt]=desc` (ordinamento) I filtri personalizzati estendono `AbstractFilter` per logiche di query complesse che i filtri integrati non possono esprimere. ## Gestione degli Errori e Validazione API Platform si integra con Symfony Validator. Gli errori di validazione restituiscono 422 Unprocessable Entity con dettagli del problema RFC 7807. ```php // src/Entity/Order.php use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] #[ApiResource] class Order { #[ORM\Column] #[Assert\NotBlank(message: 'La quantità dell\'ordine è obbligatoria')] #[Assert\Positive(message: 'La quantità deve essere maggiore di zero')] #[Assert\LessThanOrEqual(value: 100, message: 'Massimo 100 articoli per ordine')] private int $quantity; } ``` Un errore di validazione restituisce: ```json { "@type": "ConstraintViolationList", "status": 422, "violations": [ { "propertyPath": "quantity", "message": "La quantità deve essere maggiore di zero" } ] } ``` I validatori personalizzati con constraint a livello di classe gestiscono la validazione cross-field, come assicurarsi che la data di fine di un ordine sia successiva alla data di inizio. ## Domande da Colloquio: Conoscenza Approfondita di API Platform I colloqui tecnici verificano la comprensione oltre l'uso base. Queste domande appaiono frequentemente per posizioni backend Symfony. **D: Come si differenzia API Platform dalla scrittura manuale di controller?** API Platform genera operazioni CRUD dai metadati delle entity. Un singolo attributo `#[ApiResource]` produce endpoint GET, POST, PUT, PATCH e DELETE con documentazione OpenAPI, content negotiation, paginazione e validazione. I controller manuali richiedono l'implementazione separata di ogni funzionalità. API Platform riduce il codice boilerplate del 70-80% per le operazioni REST standard, rimanendo estensibile per logica personalizzata attraverso State Providers e Processors. **D: Spiega la differenza tra State Providers e State Processors.** I State Providers gestiscono il recupero dei dati (operazioni GET). Implementano `ProviderInterface::provide()` e restituiscono entity, DTO o collection. I State Processors gestiscono la mutazione dei dati (POST, PUT, PATCH, DELETE). Implementano `ProcessorInterface::process()` e ricevono l'oggetto deserializzato dal body della richiesta. Questa separazione segue i principi CQRS: letture e scritture hanno percorsi di codice distinti. **D: Come si previene l'esposizione di campi sensibili nelle response API?** I gruppi di serializzazione controllano la visibilità dei campi. I campi sensibili (password, ID interni, dati di audit) vengono assegnati a gruppi di sola scrittura o esclusi completamente. Usare `#[Groups(['admin:read'])]` per campi che solo gli amministratori dovrebbero vedere, poi configurare il `normalizationContext` a livello di operazione per includere quel gruppo solo per gli endpoint admin. **D: Come funziona la paginazione in API Platform?** API Platform pagina le collection di default con 30 elementi per pagina. Il query parameter `page` controlla l'offset. I metadati Hydra nelle response JSON-LD includono `hydra:view` con link first/last/next/previous. Configurazione tramite `paginationItemsPerPage`, `paginationMaximumItemsPerPage` e `paginationClientItemsPerPage` (permette ai client di richiedere dimensioni di pagina diverse). **D: Quando si userebbe un DTO invece di esporre direttamente l'entity?** I DTO disaccoppiano il contratto API dallo schema del database. Usare i DTO quando: (1) la rappresentazione API differisce significativamente dalla struttura dell'entity, (2) più entity si combinano in una response, (3) campi calcolati appaiono nelle response, (4) la validazione dell'input differisce dai constraint dell'entity, o (5) l'entity usa [Doctrine Inheritance](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/inheritance-mapping.html) che complica la serializzazione. I DTO prevengono anche l'esposizione accidentale di nuovi campi entity quando lo schema cambia. ## Testare gli Endpoint API Platform API Platform funziona con il framework di test di Symfony. `ApiTestCase` fornisce metodi per effettuare richieste autenticate e assertion sulle response JSON. ```php // tests/Api/ProductTest.php namespace App\Tests\Api; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; use App\Entity\Product; class ProductTest extends ApiTestCase { public function testGetCollection(): void { $response = static::createClient()->request('GET', '/api/products'); $this->assertResponseIsSuccessful(); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains(['@context' => '/api/contexts/Product']); $this->assertMatchesResourceCollectionJsonSchema(Product::class); } public function testCreateProduct(): void { $response = static::createClient()->request('POST', '/api/products', [ 'json' => [ 'name' => 'Test Product', 'price' => '29.99' ], 'headers' => [ 'Authorization' => 'Bearer ' . $this->getAdminToken() ] ]); $this->assertResponseStatusCodeSame(201); $this->assertJsonContains([ 'name' => 'Test Product', 'price' => '29.99' ]); } public function testCreateProductValidationFails(): void { static::createClient()->request('POST', '/api/products', [ 'json' => ['name' => 'AB'], // Troppo corto 'headers' => ['Authorization' => 'Bearer ' . $this->getAdminToken()] ]); $this->assertResponseStatusCodeSame(422); $this->assertJsonContains([ 'violations' => [ ['propertyPath' => 'name'] ] ]); } } ``` Il metodo `assertMatchesResourceCollectionJsonSchema()` valida le response contro il [JSON Schema](https://json-schema.org/) generato automaticamente, rilevando regressioni quando la struttura dell'entity cambia. ## Ottimizzazione delle Performance con Eager Loading Le query N+1 degradano le performance degli endpoint collection. L'opzione `fetchEager` di API Platform e i [Query Hints](https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/dql-doctrine-query-language.html#query-hints) di Doctrine risolvono questo problema. ```php #[ApiResource( operations: [ new GetCollection( extraProperties: ['doctrine_orm_fetch_join' => true] ) ] )] #[ORM\Entity] class Order { #[ORM\ManyToOne(fetch: 'EAGER')] #[ORM\JoinColumn(nullable: false)] private Customer $customer; #[ORM\OneToMany(mappedBy: 'order', fetch: 'EAGER')] private Collection $items; } ``` Per query complesse, i State Providers personalizzati con DQL ottimizzato superano l'eager loading automatico. Misurare il conteggio delle query con il Symfony Profiler prima e dopo l'ottimizzazione. ## Configurazione API Platform per la Produzione ```yaml # config/packages/api_platform.yaml api_platform: title: '%env(API_TITLE)%' version: '%env(API_VERSION)%' show_webby: false defaults: stateless: true cache_headers: max_age: 3600 shared_max_age: 3600 vary: ['Content-Type', 'Authorization'] extra_properties: standard_put: true exception_to_status: Symfony\Component\Security\Core\Exception\AccessDeniedException: 403 App\Exception\BusinessException: 400 ``` Disabilitare `show_webby` (la mascotte) in produzione. La proprietà `standard_put` assicura che PUT sostituisca completamente le risorse invece di fare merge, seguendo la semantica [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-4.3.4). ## Punti Chiave su API Platform nel 2026 - I State Providers recuperano dati per le richieste GET. I State Processors gestiscono POST/PUT/PATCH/DELETE. Questa separazione permette testing pulito e sorgenti dati personalizzate. - I gruppi di serializzazione controllano quali campi appaiono nelle response. Usare gruppi diversi per viste lista vs dettaglio, e gruppi di sola scrittura per le password. - I filtri aggiungono query parameter per ricerca, ordinamento e intervalli di date. I filtri integrati coprono la maggior parte dei casi, i filtri personalizzati gestiscono query complesse. - I DTO disaccoppiano i contratti API dagli schemi del database. Usarli quando la struttura della response differisce dall'entity o quando si combinano dati da più sorgenti. - Il [Symfony Serializer](/technologies/symfony/interview-questions/serializer) gestisce la conversione JSON-entity. Comprendere i suoi normalizer e le opzioni di contesto è essenziale per un uso avanzato di API Platform. - API Platform 4.3 è la release stabile corrente. La versione 5.0 (in alpha) richiede Symfony 7.4 o 8.0 e abbandona il supporto per le versioni Symfony più vecchie. - La documentazione su [api-platform.com](https://api-platform.com/docs/) copre casi limite non affrontati qui. La documentazione sull'[integrazione Doctrine ORM](/blog/symfony/doctrine-orm-mastering-relationships) spiega in dettaglio la gestione delle relazioni tra entity. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/symfony/api-platform-symfony-rest-tutorial