From api-platform
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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/api-platform:operationsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Operation-level configuration that sits between the resource shape (see
Operation-level configuration that sits between the resource shape (see api-resource) and the read/write logic (see state-provider / state-processor).
Every operation runs two phases, each toggled by a boolean flag:
| Phase | Flag | Runs | Maps to (CQRS) |
|---|---|---|---|
| Read | read | a provider fetches the data | Query |
| Write | write | a processor persists / acts on the data | Command |
A full request is read → deserialize → validate → write → serialize. The provider
supplies the object the rest of the chain operates on; the processor performs the
side effect (persist, delete, send mail, build a file). This is API Platform's CQRS
split: GET-style reads go through a provider, state changes go through a
processor.
You leave read/write unset and API Platform resolves them at runtime from the
HTTP request (see MainController / WriteListener in core):
write defaults to "the method is not safe" — false for GET/HEAD,
true for POST, PUT, PATCH, DELETE.read defaults to "the operation has URI variables, or the method is safe" —
so item operations (which carry {id}) and all GETs read, but a collection
POST (no URI variables, unsafe) does not.| Operation | read | write |
|---|---|---|
Get (item) | true (has {id}) | false |
GetCollection | true (safe) | false |
Post (collection) | false (no URI vars, unsafe) | true |
Put, Patch (item) | true (has {id}) | true |
Delete (item) | true (has {id}) | true |
So a Get skips the processor by default, and a collection Post runs a processor
without a provider. Override either flag to decouple the phase from this default:
// Run a processor on a GET (file download, report generation, counter bump):
new Get(
uriTemplate: '/orders/{id}/download',
write: true, // turn the write phase ON — processor now fires
processor: OrderDownloadProcessor::class,
)
// Skip the built-in fetch on an item write (upsert: processor handles a missing row):
new Put(
read: false, // no provider runs; $data comes from deserialization only
processor: UpsertProcessor::class,
)
write: true is the common case: a Get whose write defaults to false would
never invoke its processor — flipping the flag is what enables it. Conversely
read: false stops the built-in provider on an item operation from a needless fetch.
The security attribute takes a Symfony ExpressionLanguage string evaluated
before the operation runs. Available variables: user, object (item operations),
request parameters when explicitly exposed.
#[ApiResource(
operations: [
new GetCollection(),
new Post(security: "is_granted('ROLE_ADMIN')"),
]
)]
class Invoice {}
object is the fetched resource — use it for ownership checks on Get, Put,
Patch, Delete:
new Get(security: "object.getOwner() == user")
new Patch(security: "object.getOwner() == user or is_granted('ROLE_ADMIN')")
securityPostDenormalize runs after the request body is applied — use it when the
decision depends on incoming values (e.g. preventing privilege escalation on PATCH).
Each parameter carries its own security expression, where the parameter name
becomes a variable bound to the submitted value. Declare them in parameters as
QueryParameter or HeaderParameter:
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\HeaderParameter;
use ApiPlatform\Metadata\QueryParameter;
use Symfony\Component\TypeInfo\Type\BuiltinType;
use Symfony\Component\TypeInfo\TypeIdentifier;
#[GetCollection(
parameters: [
'name' => new QueryParameter(security: 'is_granted("ROLE_ADMIN")'),
'auth' => new HeaderParameter(security: '"secured" == auth', nativeType: new BuiltinType(TypeIdentifier::STRING)),
'secret' => new QueryParameter(security: '"secured" == secret', nativeType: new BuiltinType(TypeIdentifier::STRING)),
],
)]
?name=foo evaluates is_granted("ROLE_ADMIN") (403 otherwise); the auth header
or ?secret=… must equal secured or the request is rejected. The parameter is
only checked when present — an absent or value-less parameter passes.
For query-level data isolation (multi-tenant, soft-delete) that must apply to every query regardless of operation, use a Doctrine extension or link handler instead — see securing-collections.
securityonly guards an already-fetched object; it does not scope collections.
API Platform runs Symfony's Validator on the deserialized object before the processor. Constraints live on the resource/input DTO (see custom-validator).
#[ApiResource(
operations: [
new Post(validationContext: ['groups' => ['Default', 'user:create']]),
new Patch(validationContext: ['groups' => ['Default', 'user:update']]),
]
)]
#[Assert\NotBlank(groups: ['user:create'])] // required on create only
#[Assert\Email(groups: ['user:create', 'user:update'])] // checked on both
public ?string $email = null;
By default a type mismatch in the body throws on the first bad field. Enable
collect_denormalization_errors to report every malformed field at once in the 422
violations array:
#[Post(validationContext: ['collect_denormalization_errors' => true])]
Each malformed field becomes a violations entry with a propertyPath, a
This value should be of type … message, and a hint explaining the failure.
Attach constraints to a QueryParameter (or HeaderParameter); invalid values
yield 422 before the provider runs:
use ApiPlatform\Metadata\QueryParameter;
use Symfony\Component\Validator\Constraints as Assert;
#[GetCollection(
parameters: [
'page' => new QueryParameter(constraints: [new Assert\Positive()]),
],
)]
deprecationReason adds a Deprecation header; sunset adds a Sunset header
with the removal date. Apply at resource or operation level.
#[ApiResource(
deprecationReason: 'Use /v2/invoices instead.',
sunset: '2026-01-01T00:00:00+00:00',
)]
class Invoice {}
deprecationReason emits a Deprecation header and sunset a Sunset header. To
also advertise a migration doc, add an explicit operation links: entry —
new Link('deprecation', 'https://…') renders Link: <…>; rel="deprecation".
With application/merge-patch+json, to update an existing nested resource you
must include its identifier. Omitting it makes the serializer treat the nested
object as new (and attempt to create it):
{ "shippingAddress": { "id": 12, "city": "Lyon" } }
Without "id", API Platform tries to create a new Address rather than patch #12.
Per-operation metadata (security, validationContext groups, collect_denormalization_errors,
deprecationReason/sunset, parameter validation/security) is mostly shared, but
auth and validation wiring differ:
viewAny, GET → view, POST → create, PATCH/PUT → update (PUT → create if
absent), DELETE → delete. Override the mapping with a policy: property on the
operation: new Patch(policy: 'myCustomPolicy'). The security ExpressionLanguage
string also works.middleware: property
per operation (new Patch(middleware: 'auth:sanctum')) or globally under
defaults.middleware in config/api-platform.php.rules (array / closure / FormRequest) per resource
or operation rather than Symfony validationContext groups — see the
custom-validator Laravel section. AuthenticationException → 401 and
AuthorizationException → 403 are mapped by default in the config's
exception_to_status.Parameter constraints are Laravel validation rule strings (e.g.
'min:2'), not Symfony constraints.security / securityPostDenormalizesecurity (see securing-collections)validationContext groups split create vs update constraintscollect_denormalization_errors enabled where clients need full error listsdeprecationReason + sunsetnpx claudepluginhub api-platform/skillset --plugin api-platformSecure API Platform resources with Symfony security expressions, voters, securityPostValidation, and operation-level access control.
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.
Provides API security guidance on authentication methods, rate limiting, input validation, CORS, security headers, and OWASP API Top 10 mitigations. Use for designing auth, implementing limits, or reviewing APIs.