Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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:writearchived 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

api/src/State/ArchiveProjectProcessor.php
<?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.