agentsclimarketplace

State provider

Skill api-platform/skillset/skills/state-provider

Creates state providers for read operations in API Platform — the read/Query side of its CQRS-style provider/processor split. Use whenever GET data needs custom retrieval or shaping — computed or enriched fields, transforming entities to different DTOs, decorating built-in Doctrine providers, sortable computed fields via repositoryMethod — or any 'the response should also include X' request, or to understand when a provider runs vs a processor (the read vs write phase), even if the user doesn't say 'provider'.From its SKILL.md

Install
npx -y skills add api-platform/skillset --skill state-provider

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.

SKILL.md

9.5 KB, ~2.1k tokens by cl100k_base, as published. Nobody here has run it

Creating 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".

Operationprovider runs (read)processor runs (write)
Get (item) / GetCollectionyesno
Post (collection)no (no URI vars, unsafe)yes
Put, Patch, Delete (item)yesyes

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

PersistenceItem ProviderCollection Provider
ORMapi_platform.doctrine.orm.state.item_providerapi_platform.doctrine.orm.state.collection_provider
MongoDB ODMapi_platform.doctrine_mongodb.odm.state.item_providerapi_platform.doctrine_mongodb.odm.state.collection_provider

Laravel (Eloquent): ProviderInterface is identical and resources reference a provider by class-string (provider: YourProvider::class). There are no api_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 uses stateOptions: new Options(modelClass: …) on the Eloquent Options and an OrderFilter parameter rather than repositoryMethod.

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 Request
    • resource_class: The API resource class
    • filters: 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). repositoryMethod scopes 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

  1. Return null for missing items — API Platform handles the 404
  2. Use CollectionOperationInterface to distinguish collection vs item
  3. When decorating, always delegate to the built-in provider first
  4. For computed fields on collections, iterate over the paginator (it's lazy)
  5. For sortable computed fields, use stateOptions(repositoryMethod:) — not a provider

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.