A Custom Operation: /projects/{id}/archive
A Custom Operation: /projects/{id}/archive
~16 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
The SIX default operations (chapter 11) cover CRUD – an action like "archive project" is NEITHER create NOR replace NOR delete, but a CUSTOM, named operation of its own.
Adding the archived field
#[ORM\Column]
#[Groups(['project:read'])]
private bool $archived = false;
public function isArchived(): bool
{
return $this->archived;
}
public function setArchived(bool $archived): static
{
$this->archived = $archived;
return $this;
}DELIBERATELY WITHOUT project:write – archived should NOT be settable via a normal PATCH, ONLY via the dedicated endpoint below.
Defining the custom operation
use ApiPlatform\Metadata\Post;
use App\State\ArchiveProjectProcessor;
new Post(
uriTemplate: '/projects/{id}/archive',
security: "is_granted('" . ProjectVoter::EDIT . "', object)",
processor: ArchiveProjectProcessor::class,
read: true,
),read: true tells API Platform to LOAD the project FIRST, as with a GET (via the {id} in the URL), BEFORE the processor gets called – the voter check from chapter 53 thereby works JUST like with the default operations.
Implementing the processor
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Project;
use Doctrine\ORM\EntityManagerInterface;
final class ArchiveProjectProcessor implements ProcessorInterface
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
) {
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Project
{
/** @var Project $data */
$data->setArchived(true);
$this->entityManager->flush();
return $data;
}
}THANKS TO read: true, $data is ALREADY the LOADED Project object – setArchived(true) followed by flush() is enough, NO custom persist() call is needed, since the entity is ALREADY MANAGED by Doctrine.
Testing the custom operation
curl -k -X POST https://localhost/api/projects/1/archive \
-H "Authorization: Bearer $TOKEN"{
"id": 1,
"name": "Website Relaunch",
"archived": true
}Achtung: POST instead of PATCH is DELIBERATELY chosen HERE: the operation SEMANTICALLY has its OWN name ("archive", not a generic "update") – a COMMON REST convention for actions that do NOT naturally express themselves as a plain field change.
Tipp: EXACTLY this pattern (a custom field WITHOUT a write group PLUS a dedicated custom operation) fits ANY "action" rather than "state change" – e.g. also for /projects/{id}/restore as the COUNTERPART to archive.