From api-platform
Creates state providers for read operations in API Platform's CQRS-style provider/processor split. Use for custom retrieval, computed fields, DTO transformation, or decorating Doctrine providers.
How this skill is triggered — by the user, by Claude, or both
Slash command
/api-platform:state-providerThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
State Providers retrieve data for read operations (Get, GetCollection) — the
State Providers retrieve data for read operations (Get, GetCollection) — the read phase of an operation, the Query side of API Platform's CQRS-style split (a state-processor handles the Command/write side).
A provider runs when the operation's read flag is true. You normally leave it
unset and API Platform resolves it from the request: read defaults to "the
operation has URI variables, or the HTTP method is safe", and write (the
processor) to "the method is not safe".
| Operation | provider runs (read) | processor runs (write) |
|---|---|---|
Get (item) / GetCollection | yes | no |
Post (collection) | no (no URI vars, unsafe) | yes |
Put, Patch, Delete (item) | yes | yes |
Override the flag to decouple the phase from the verb: read: false skips the
built-in fetch on an item write (upsert), and write: true runs a processor on a
Get (see state-processor). See operations → Read & write phases for the
full read → deserialize → validate → write → serialize lifecycle.
<?php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\State\ProviderInterface;
/**
* @implements ProviderInterface<YourResource|null>
*/
final class YourResourceProvider implements ProviderInterface
{
public function provide(
Operation $operation,
array $uriVariables = [],
array $context = []
): object|array|null {
if ($operation instanceof CollectionOperationInterface) {
return $this->repository->findAll();
}
return $this->repository->find($uriVariables['id']);
}
}
Wrap the default provider to add custom logic (computed fields, enrichment):
<?php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class EnrichedItemProvider implements ProviderInterface
{
public function __construct(
#[Autowire(service: 'api_platform.doctrine.orm.state.item_provider')]
private ProviderInterface $itemProvider,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object
{
$data = $this->itemProvider->provide($operation, $uriVariables, $context);
if (null === $data) {
return null; // API Platform returns 404 automatically
}
// Enrich with computed fields
$data->computedScore = $this->computeScore($data);
return $data;
}
}
| Persistence | Item Provider | Collection Provider |
|---|---|---|
| ORM | api_platform.doctrine.orm.state.item_provider | api_platform.doctrine.orm.state.collection_provider |
| MongoDB ODM | api_platform.doctrine_mongodb.odm.state.item_provider | api_platform.doctrine_mongodb.odm.state.collection_provider |
Laravel (Eloquent):
ProviderInterfaceis identical and resources reference a provider by class-string (provider: YourProvider::class). There are noapi_platform.doctrine.*service ids — the built-in Eloquent providers are bound in the container by their class name (ApiPlatform\Laravel\Eloquent\State\ItemProvider/…\State\CollectionProvider). To decorate one, type-hint that concrete class in your constructor (the container resolves it). Stable scalar/computed-field sorting usesstateOptions: new Options(modelClass: …)on the EloquentOptionsand anOrderFilterparameter rather thanrepositoryMethod.
Iterate over paginated results to add computed fields:
use ApiPlatform\State\Pagination\PaginatorInterface;
final class MailboxListProvider implements ProviderInterface
{
public function __construct(
#[Autowire(service: 'api_platform.doctrine_mongodb.odm.state.collection_provider')]
private ProviderInterface $collectionProvider,
private MailboxHelper $helper,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): PaginatorInterface
{
$mailboxes = $this->collectionProvider->provide($operation, $uriVariables, $context);
foreach ($mailboxes as $item) {
$item->totalMessages = $this->helper->countMessages($item->getId());
}
return $mailboxes;
}
}
A provider can fetch one resource type and return another:
final class RawMessageProvider implements ProviderInterface
{
public function __construct(
#[Autowire(service: 'api_platform.doctrine_mongodb.odm.state.item_provider')]
private ProviderInterface $itemProvider,
private RawMessageBuilderInterface $builder,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?RawMessage
{
// Fetch the Document\Message using Doctrine
$message = $this->itemProvider->provide($operation, $uriVariables, $context);
if (!$message) {
return null;
}
// Transform to a completely different API resource
$raw = new RawMessage();
$raw->id = $message->getId();
$raw->content = $this->builder->build($message);
return $raw;
}
}
['id' => '123', 'accountId' => '456'])request: The Symfony HttpFoundation Requestresource_class: The API resource classfilters: Applied query parameter filters#[Get(provider: YourProvider::class)]
#[GetCollection(provider: YourCollectionProvider::class)]
class YourResource {}
When a field is a SQL aggregate that must also be sortable in the database, a
plain provider can't help — sorting happens in the query. The cleanest way is to
point the built-in Doctrine provider at a custom repository method that returns a
QueryBuilder, via stateOptions (API Platform 4.4+). The provider runs your
query builder, then layers pagination, filters and link handlers on top of it.
// Repository: return a QueryBuilder, not results
class CartRepository extends EntityRepository
{
public function getCartsWithTotalQuantity(): QueryBuilder
{
return $this->createQueryBuilder('o')
->leftJoin('o.items', 'items')
->addSelect('COALESCE(SUM(items.quantity), 0) AS totalQuantity')
->addGroupBy('o.id');
}
}
use ApiPlatform\Doctrine\Orm\State\Options;
#[ORM\Entity(repositoryClass: CartRepository::class)]
#[GetCollection(
stateOptions: new Options(repositoryMethod: 'getCartsWithTotalQuantity'),
processor: [self::class, 'process'],
write: true,
parameters: [
'sort[:property]' => new QueryParameter(filter: new SortFilter(), properties: ['totalQuantity']),
],
)]
class Cart
{
public int|string|null $totalQuantity;
// A non-HIDDEN scalar addSelect makes rows arrive as [entity, 'totalQuantity' => N].
// Flatten them back onto the entity with a processor:
public static function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
foreach ($data as &$value) {
$cart = $value[0];
$cart->totalQuantity = $value['totalQuantity'] ?? 0;
$value = $cart;
}
return $data;
}
}
Use a Doctrine collection extension (see securing-collections) instead when the query change must apply to every operation/query for the resource (e.g. multi-tenant isolation).
repositoryMethodscopes one operation's base query; an extension mutates them all.
Embed the same related resource at different depths by switching the
normalization group on one property with #[Context]:
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(normalizationContext: ['groups' => ['initial']])]
class Order
{
#[Groups(['initial'])]
public ?Customer $customer = null;
#[Groups(['initial'])]
#[Context(['normalization' => ['groups' => ['summary']]])]
public ?Customer $billedTo = null; // serialized with the 'summary' group only
}
null for missing items — API Platform handles the 404CollectionOperationInterface to distinguish collection vs itemstateOptions(repositoryMethod:) — not a providernpx claudepluginhub api-platform/skillset --plugin api-platformImplements API Platform v4 State Providers and Processors using ProviderInterface/ProcessorInterface to decouple data retrieval and persistence from entities, with contract enforcement and validation.
Creates state processors for API Platform write operations (POST/PUT/PATCH/DELETE) with custom persistence, side effects, and CQRS-style provider/processor split.
Generates CQRS-compliant queries, handlers, DTOs, read model interfaces, and unit tests for PHP 8.4. Creates immutable read-only operations for single, list, search, and count queries with pagination.