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 Symfony REST Tutorial

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].

src/Entity/Product.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')")
    ],
    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.

src/Entity/User.phpphp
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.

src/State/ProductStatsProvider.phpphp
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
        );
    }
}
src/Dto/ProductStats.phpphp
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.

src/State/UserRegistrationProcessor.phpphp
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.

src/Entity/Article.phpphp
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.

src/Entity/Order.phpphp
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 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.

tests/Api/ProductTest.phpphp
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.

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.

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.
Sfida del giorno

Sapresti trovare il bug in Symfony?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore 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

#api-platform
#symfony
#rest-api
#php
#tutorial

Condividi

Articoli correlati