agentsclimarketplace

Operations

Skill api-platform/skillset/skills/operations

Configures API Platform operations — security expressions, validation groups, denormalization error collection, parameter validation and parameter-level security, deprecation headers, and nested PATCH. Use whenever the user wants to restrict who can call an endpoint, vary validation between create and update, validate query/header parameters, deprecate an endpoint, or debug merge-patch on nested resources — including plain 'protect this endpoint' or 'only admins can X' requests.From its SKILL.md

Install
npx -y skills add api-platform/skillset --skill operations

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.4 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it

Operations: Security, Validation & Lifecycle

Operation-level configuration that sits between the resource shape (see api-resource) and the read/write logic (see state-provider / state-processor).

Read & write phases (the provider/processor lifecycle)

Every operation runs two phases, each toggled by a boolean flag:

PhaseFlagRunsMaps to (CQRS)
Readreada provider fetches the dataQuery
Writewritea processor persists / acts on the dataCommand

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.

The flags default from the request, not the verb

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.
Operationreadwrite
Get (item)true (has {id})false
GetCollectiontrue (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.

Securing operations

The security attribute takes a Symfony ExpressionLanguage string evaluated before the operation runs. Available variables: user, object (item operations), request parameters when explicitly exposed.

Role-based

#[ApiResource(
    operations: [
        new GetCollection(),
        new Post(security: "is_granted('ROLE_ADMIN')"),
    ]
)]
class Invoice {}

Object-based (item operations)

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).

Parameter-based

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. security only guards an already-fetched object; it does not scope collections.

Validation

API Platform runs Symfony's Validator on the deserialized object before the processor. Constraints live on the resource/input DTO (see custom-validator).

Validation groups per operation

#[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;

Collect denormalization errors

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.

Validating query parameters

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()]),
    ],
)]

Deprecating endpoints

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

Nested PATCH gotcha

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.

Laravel

Per-operation metadata (security, validationContext groups, collect_denormalization_errors, deprecationReason/sunset, parameter validation/security) is mostly shared, but auth and validation wiring differ:

  • Authorization integrates with Laravel policies, not Symfony voters. Once a policy exists, API Platform auto-maps operations to methods: GET collection → 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.
  • Authentication / middleware is attached with the Laravel middleware: property per operation (new Patch(middleware: 'auth:sanctum')) or globally under defaults.middleware in config/api-platform.php.
  • Validation uses Laravel 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.
  • Query/header Parameter constraints are Laravel validation rule strings (e.g. 'min:2'), not Symfony constraints.

Checklist

  • Write operations restricted with security / securityPostDenormalize
  • Collection isolation handled by an extension, not security (see securing-collections)
  • validationContext groups split create vs update constraints
  • collect_denormalization_errors enabled where clients need full error lists
  • Query parameters carry validation constraints
  • Deprecated endpoints set deprecationReason + sunset

What ships with it

Read from the repository

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

Gives 0 of the 12 instructions most operations skills give in ~2.2k tokens

Counted across 483 of the 484 authors here whose files we hold, read 2026-08-07

  • Collect monitoring data throughout the simulationin 14 of 483, across 6 files
  • Set the random seed for reproducibilityin 14 of 483, across 6 files
  • Validate simulations against analytical solutionsin 12 of 483, across 4 files
  • Clarify goals, constraints, and inputsin 11 of 483, across 2 files
  • Implement contract tests for integration pointsin 11 of 483, across 2 files
  • Implement strangler fig infrastructure with API gatewayin 11 of 483, across 2 files
  • Audit modernized components for security vulnerabilitiesin 11 of 483, across 2 files
  • Avoid Python blocking calls in processesin 10 of 483, across 3 files
  • Use resource context managers for automatic cleanupin 9 of 483, across 2 files
  • Maintain consistent time unitsin 9 of 483, across 2 files
  • Validate outcomes against success criteriain 8 of 483, across 1 file
  • Analyze the legacy codebase for technical debtin 8 of 483, across 1 file

Said here and by no other author read

  • restrict write operations using security expressions
  • use extensions for collection data isolation
  • split create and update validation contexts
  • enable collecting denormalization errors
  • add constraints to query and header parameters
  • set deprecation reason and sunset date

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

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.