state-provider skill
Creates state providers for read operations in API Platform to handle custom data retrieval, enrichment, and DTO transformations.
- GitHub stars
- 29
- Files
- 1
- Last commit
- Oct 22, 2026
- License
- —
- Allowed tools
- —
What does the state-provider skill do?
state-provider creates state providers for read operations in API Platform to handle custom data retrieval, enrichment, and DTO transformations.
Summary generated from the skill's SKILL.md.
About the state-provider skill
Developers building API Platform applications can use this skill to implement custom data retrieval logic for read operations. It focuses on the Query side of the CQRS-style split, allowing for the creation of state providers that handle Get and GetCollection operations. This is essential when an API needs to return computed fields, enriched data, or transformed Data Transfer Objects (DTOs) instead of raw database entities.
Users can implement these providers by decorating built-in services like the Doctrine ORM item provider or the MongoDB ODM collection provider. The skill supports complex scenarios such as transforming one resource type into another or adding extra information to paginated results. For database-level requirements like sorting by aggregate SQL values, it facilitates using the repositoryMethod option within stateOptions to point to custom QueryBuilder methods. This ensures that data shaping happens at the correct phase of the API lifecycle.
What an agent can do with it
- 1Enriching entities with computed fieldsAn AI agent can help write a provider that wraps a default Doctrine provider to add calculated scores to an object.
- 2Transforming entities into DTOsA developer can use this to fetch a database document and return a completely different API resource type.
- 3Adding data to paginated listsYou can iterate over paginated results to inject extra properties like message counts into each item.
- 4Implementing sortable database aggregatesUse this to connect a specific repository method to an operation so that computed SQL fields remain sortable.
Written from the skill's SKILL.md. Every use case is checked against a quote from it.
How to install the state-provider skill
Each command copies the skill folder into the client's skills directory. Restart the client or start a new session to load it.Install the state-provider skill in Plugin
/plugin marketplace add api-platform/skillset /plugin install api-platform@api-platform-skillset
Install the state-provider skill in Claude Code
~/.claude/skills/state-provider/SKILL.md
$ npx degit api-platform/skillset/skills/state-provider ~/.claude/skills/state-providerInstall the state-provider skill in Claude Code (project)
.claude/skills/state-provider/SKILL.md
$ npx degit api-platform/skillset/skills/state-provider .claude/skills/state-providerInstall the state-provider skill in Codex
~/.agents/skills/state-provider/SKILL.md
$ npx degit api-platform/skillset/skills/state-provider ~/.agents/skills/state-providerInstall the state-provider skill in Gemini CLI
~/.gemini/skills/state-provider/SKILL.md
$ npx degit api-platform/skillset/skills/state-provider ~/.gemini/skills/state-providerInstall the state-provider skill in Claude.ai
npx degit api-platform/skillset/skills/state-provider state-provider && zip -r state-provider.zip state-provider # claude.ai → Customize → Skills → + → Create skill → Upload a skill → state-provider.zip
SKILL.md
View on GitHubCreating State Providers
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.
Basic Structure
<?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']);
}
}
Decorating Built-in Providers
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;
}
}
Doctrine service names
| 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.
Enriching Paginated Collections
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;
}
}
Cross-Resource Providers (Returning a Different Type)
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;
}
}
Provider Parameters
- $operation: Metadata about the operation (Get, GetCollection, etc.)
- $uriVariables: URI path variables (e.g.,
['id' => '123', 'accountId' => '456']) - $context: Additional context:
request: The Symfony HttpFoundation Requestresource_class: The API resource classfilters: Applied query parameter filters
Assigning to Resource
#[Get(provider: YourProvider::class)]
#[GetCollection(provider: YourCollectionProvider::class)]
class YourResource {}
Computed / Sortable Fields via a Repository Method (Doctrine)
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.
Per-Property Serialization Context
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
}
Best Practices
- Return
nullfor missing items — API Platform handles the 404 - Use
CollectionOperationInterfaceto distinguish collection vs item - When decorating, always delegate to the built-in provider first
- For computed fields on collections, iterate over the paginator (it's lazy)
- For sortable computed fields, use
stateOptions(repositoryMethod:)— not a provider
Files in this skill
1state-provider skill questions
How do I install the state-provider skill in Claude Code?
Run npx degit api-platform/skillset/skills/state-provider ~/.claude/skills/state-provider. Claude Code loads skills from ~/.claude/skills (all projects) or .claude/skills (one project) and uses a skill when its description matches the task.
Which clients can use the state-provider skill?
Any client that reads the Agent Skills format: Claude Code, Codex, Gemini CLI and claude.ai (upload as a ZIP). Scripts in the skill run only where the client allows code execution.
When does a state provider run?
A provider runs during the read phase of an operation when the read flag is true, typically for GET requests.
What should a provider return if an item is not found?
You should return null, and API Platform will automatically handle it by returning a 404 error.
How do I handle sortable computed fields in Doctrine?
Instead of a plain provider, point the built-in Doctrine provider to a custom repository method using stateOptions.