From api-platform
Creates or modifies API Platform resources with DTOs and Object Mapper. Use when adding endpoints, exposing entities over HTTP, defining input/output DTOs, or configuring nested sub-resources.
How this skill is triggered — by the user, by Claude, or both
Slash command
/api-platform:api-resourceThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
An API Platform resource is built from three concerns (see
An API Platform resource is built from three concerns (see https://api-platform.com/docs/core/design/):
#[ApiResource] describing
the public shape. Single source of truth for Hydra, OpenAPI and GraphQL.Design the public shape first; the resource class doesn't have to be a Doctrine entity. How you wire it to persistence is a separate decision:
| Approach | When | Hookup |
|---|---|---|
| Entity as Resource | Prototyping, plain CRUD, internal model == public shape | #[ApiResource] on the entity; built-in Doctrine provider/processor (zero wiring) |
| DTO with Object Mapper | Decoupled public shape over a Doctrine entity/document — whether fields match 1:1 or need renames/transforms | stateOptions: new Options(entityClass:/documentClass:) and #[Map] on the DTO |
| Input DTOs per Operation | Write model differs from read model (stricter create/update payloads) | per-operation input: + processor |
| Custom Provider/Processor | Non-CRUD, external data, complex domain logic, CQRS | hand-written ProviderInterface/ProcessorInterface (see state-provider/state-processor) |
Rule of thumb: entity-as-resource is convenient but couples your public contract to your schema — fine for prototypes, probably not for large or non-CRUD systems. Decouple with a DTO as soon as the two shapes diverge.
Mark the entity itself — built-in Doctrine providers/processors handle everything. No provider, processor, or mapping to write:
use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ApiResource]
class Book
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
public ?int $id = null;
#[ORM\Column]
public string $title = '';
}
Use this for prototypes and straight CRUD. Migrate to a DTO (strategies below) once the API shape must differ from the schema.
One mechanism, not two: API Platform's ObjectMapperProvider /
ObjectMapperProcessor activate only when both are present — stateOptions
naming the entityClass: (or documentClass:) and a #[Map] attribute on the
DTO (and on the input entity for writes). On read it runs
map($entity, $resourceClass); on write map($inputDto, $entityClass) then persists
via the Doctrine processor. There is no Object-Mapper mode without stateOptions.
Fields that line up by name map automatically; add #[Map(source: …)] to rename or
transform: to convert. The same config covers both the trivial 1:1 case and
arbitrary renames/transforms:
<?php
namespace App\ApiResource;
use ApiPlatform\Doctrine\Odm\State\Options;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use App\Document\Order as DocumentOrder;
use App\Transformer\CustomerTransformer;
use Symfony\Component\ObjectMapper\Attribute\Map;
#[ApiResource(
operations: [
new Get(stateOptions: new Options(documentClass: DocumentOrder::class)),
new GetCollection(stateOptions: new Options(documentClass: DocumentOrder::class)),
]
)]
#[Map(target: DocumentOrder::class)]
final class Order
{
public string $id;
// Simple 1:1 field mapping (automatic when names match)
public string $status;
// Map from a different source field
#[Map(source: 'customerName')]
public string $buyer;
// Transform with a custom callable
#[Map(source: 'rawData', transform: new CustomerTransformer())]
public CustomerDto $customer;
}
Implement TransformCallableInterface for complex mappings:
<?php
namespace App\Transformer;
use Symfony\Component\DependencyInjection\Attribute\Exclude;
use Symfony\Component\ObjectMapper\TransformCallableInterface;
#[Exclude]
final class CustomerTransformer implements TransformCallableInterface
{
public function __construct(private readonly string $field = 'name') {}
public function __invoke(mixed $value, object $source, ?object $target): mixed
{
// $value = source field value, $source = full source object
$dto = new CustomerDto();
$dto->name = $value[$this->field] ?? '';
return $dto;
}
}
Use #[Exclude] so Symfony's container doesn't try to autowire transformer constructor args.
When the write model differs from the read model, give individual operations their
own input: DTO and processor (the resource class stays the read model):
#[ApiResource(
operations: [
new Post(input: CreateOrderInput::class, processor: OrderCreateProcessor::class),
new Patch(input: UpdateOrderInput::class, processor: OrderUpdateProcessor::class),
]
)]
class Order { /* read model */ }
For resources nested under parents (e.g., /accounts/{accountId}/mailboxes/{mailboxId}/messages):
#[ApiResource(
operations: [
new Get(
uriTemplate: '/accounts/{accountId}/mailboxes/{mailboxId}/messages/{id}',
uriVariables: ['accountId', 'mailboxId', 'id'],
stateOptions: new Options(
handleLinks: MessageLinkHandler::class,
documentClass: Message::class,
)
),
new GetCollection(
uriTemplate: '/accounts/{accountId}/mailboxes/{mailboxId}/messages',
uriVariables: ['accountId', 'mailboxId'],
stateOptions: new Options(
handleLinks: MessageLinkHandler::class,
documentClass: Message::class,
),
itemUriTemplate: '/accounts/{accountId}/mailboxes/{mailboxId}/messages/{id}',
),
]
)]
The handleLinks class validates parent ownership and applies security filters. See the securing-collections skill for implementation details.
Hidden IRI fields for URI generation:
#[ApiProperty(readable: false, writable: false)]
#[Map(source: 'account', transform: new DocumentIdTransformer())]
public string $accountId;
Add non-CRUD actions on the same resource:
new Put(
uriTemplate: '/orders/{id}/cancel',
input: CancelOrderInput::class,
processor: OrderCancelProcessor::class,
name: '_api_order_cancel',
),
new Get(
write: true, // triggers processor on GET
uriTemplate: '/orders/{id}/download',
processor: OrderDownloadProcessor::class,
name: '_api_order_download',
),
Use write: true on Get operations that need a processor (e.g., file downloads):
a Get's write phase is off by default, so the processor never fires until you flip
the flag. See operations → Read & write phases for the read/write lifecycle.
A backed enum property serializes to its ->value automatically:
#[ApiResource]
class Person
{
public GenderType $genderType; // backed enum → {"genderType": "female"}
}
Expose a backed enum as a read-only resource so clients can discover allowed values:
use ApiPlatform\Metadata\ApiResource;
#[ApiResource]
enum AvailabilityStatus: string
{
case InStock = 'InStock';
case OutOfStock = 'OutOfStock';
}
GET /availability_statuses lists all cases; GET /availability_statuses/InStock
returns one.
For monetary/scientific values that must avoid float drift, type a property as
\BcMath\Number (PHP 8.4 native, requires ext-bcmath). It serializes as a string
to preserve precision:
class Invoice
{
public ?\BcMath\Number $total; // → "300.55"
}
The design-first split, DTOs, Object Mapper, per-operation input:, nested
uriVariables/handleLinks, custom operations and backed-enum resources all work on
Laravel. Deltas:
#[ApiResource] on a class extending
Illuminate\Database\Eloquent\Model; the Eloquent provider/processor handle CRUD
with zero wiring.Options (ApiPlatform\Laravel\Eloquent\State\Options) uses
modelClass: (not entityClass:/documentClass:); it carries modelClass +
handleLinks only — no repositoryMethod.stateOptions: new Options(modelClass: ProductModel::class)
with #[Map(source: ProductModel::class)] on the DTO; per-property #[Map] and
TransformCallableInterface are identical.#[ApiProperty]
at the class level with property: (#[ApiProperty(property: 'title', identifier: true)]).
DTO/ApiResource classes use property-level attributes. \BcMath\Number and backed
enums behave the same.When creating a new resource:
src/ApiResource/#[ApiResource] with operationsstateOptions)uriVariables and handleLinks#[Map] attributes for entity/document mapping#[ApiProperty] for OpenAPI documentationnpx claudepluginhub api-platform/skillset --plugin api-platformMaps Symfony entities to API Platform v4 DTOs using the Symfony Object Mapper (#[Map], stateOptions) for decoupled input/output contracts.
Provides patterns for Laravel API Resources covering transformation, conditional attributes, nested relationships, collections, and pagination links. Use for standardizing JSON API responses.
Configures API Platform operations with security expressions, validation groups, parameter validation, deprecation headers, and nested PATCH. Use for endpoint access control, validation, and debugging merge-patch.