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.

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.
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.
# Installare API Platform con Symfony Flex
composer require api
# Verificare l'installazione
php bin/console debug:router | grep apiLa recipe api installa api-platform/symfony insieme ai componenti serializer, validator e property-access. Symfony Flex configura automaticamente le route sotto /api.
# 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].
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.
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.
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
);
}
}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.
Pronto a superare i tuoi colloqui su Symfony?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
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.
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:
#[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.
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.
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:
{
"@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 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.
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 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 di Doctrine risolvono questo problema.
#[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
# 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: 400Disabilitare 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.
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
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 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 copre casi limite non affrontati qui. La documentazione sull'integrazione Doctrine ORM spiega in dettaglio la gestione delle relazioni tra entity.
Sapresti trovare il bug in Symfony?
Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Scritto da
Anthony Fillion-MailletFondatore di SharpSkill
Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.
Aggiornato il 25 agosto 2026
Tag
Condividi
Articoli correlati

Doctrine ORM: Padroneggiare le relazioni in Symfony
Guida completa alle relazioni Doctrine ORM in Symfony. OneToMany, ManyToMany, strategie di caricamento e ottimizzazione delle prestazioni con esempi pratici.

Domande di colloquio Symfony: Top 25 nel 2026
Le 25 domande di colloquio Symfony più frequenti. Architettura, Doctrine ORM, servizi, sicurezza, form e test con risposte dettagliate ed esempi di codice.

Symfony 7: API Platform e Best Practices
Guida completa ad API Platform 4 con Symfony 7: State Processors, State Providers, gruppi di serializzazione, filtri, sicurezza e test automatizzati per API REST professionali.