feat(bookstack) : add BookStackLink API resource with CRUD operations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 18:10:24 +01:00
parent 1e311242a9
commit bc331982d5
3 changed files with 191 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use App\State\BookStackLinkProcessor;
use App\State\BookStackLinkProvider;
use Symfony\Component\Serializer\Attribute\Groups;
#[ApiResource(
operations: [
new GetCollection(
uriTemplate: '/tasks/{taskId}/bookstack/links',
normalizationContext: ['groups' => ['bookstack_link:read']],
provider: BookStackLinkProvider::class,
security: "is_granted('IS_AUTHENTICATED_FULLY')",
),
new Post(
uriTemplate: '/tasks/{taskId}/bookstack/links',
denormalizationContext: ['groups' => ['bookstack_link:write']],
normalizationContext: ['groups' => ['bookstack_link:read']],
provider: BookStackLinkProvider::class,
processor: BookStackLinkProcessor::class,
security: "is_granted('IS_AUTHENTICATED_FULLY')",
),
new Delete(
uriTemplate: '/tasks/{taskId}/bookstack/links/{id}',
provider: BookStackLinkProvider::class,
processor: BookStackLinkProcessor::class,
security: "is_granted('IS_AUTHENTICATED_FULLY')",
),
],
)]
final class BookStackLink
{
#[Groups(['bookstack_link:read'])]
public ?int $id = null;
#[Groups(['bookstack_link:read', 'bookstack_link:write'])]
public int $bookstackId = 0;
#[Groups(['bookstack_link:read', 'bookstack_link:write'])]
public string $bookstackType = '';
#[Groups(['bookstack_link:read', 'bookstack_link:write'])]
public string $title = '';
#[Groups(['bookstack_link:read', 'bookstack_link:write'])]
public string $url = '';
#[Groups(['bookstack_link:read'])]
public ?string $createdAt = null;
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\ApiResource\BookStackLink;
use App\Entity\Task;
use App\Entity\TaskBookStackLink;
use App\Repository\TaskBookStackLinkRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
final readonly class BookStackLinkProcessor implements ProcessorInterface
{
public function __construct(
private EntityManagerInterface $em,
private TaskBookStackLinkRepository $linkRepository,
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): ?BookStackLink
{
if ($operation instanceof Delete) {
return $this->handleDelete($uriVariables);
}
return $this->handleCreate($data, $uriVariables);
}
private function handleCreate(mixed $data, array $uriVariables): BookStackLink
{
assert($data instanceof BookStackLink);
$taskId = $uriVariables['taskId'] ?? 0;
$task = $this->em->getRepository(Task::class)->find($taskId);
if (null === $task) {
throw new NotFoundHttpException('Task not found.');
}
$link = new TaskBookStackLink();
$link->setTask($task);
$link->setBookstackId($data->bookstackId);
$link->setBookstackType($data->bookstackType);
$link->setTitle($data->title);
$link->setUrl($data->url);
$this->em->persist($link);
$this->em->flush();
$result = new BookStackLink();
$result->id = $link->getId();
$result->bookstackId = $link->getBookstackId();
$result->bookstackType = $link->getBookstackType();
$result->title = $link->getTitle();
$result->url = $link->getUrl();
$result->createdAt = $link->getCreatedAt()->format('c');
return $result;
}
private function handleDelete(array $uriVariables): null
{
$linkId = $uriVariables['id'] ?? 0;
$link = $this->linkRepository->find($linkId);
if (null === $link) {
throw new NotFoundHttpException('Link not found.');
}
$this->em->remove($link);
$this->em->flush();
return null;
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\State;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Post;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\BookStackLink;
use App\Entity\TaskBookStackLink;
use App\Repository\TaskBookStackLinkRepository;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
final readonly class BookStackLinkProvider implements ProviderInterface
{
public function __construct(
private TaskBookStackLinkRepository $linkRepository,
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array|BookStackLink
{
if ($operation instanceof Post) {
return new BookStackLink();
}
if ($operation instanceof Delete) {
$link = $this->linkRepository->find($uriVariables['id'] ?? 0);
if (null === $link) {
throw new NotFoundHttpException('Link not found.');
}
$dto = new BookStackLink();
$dto->id = $link->getId();
return $dto;
}
$taskId = $uriVariables['taskId'] ?? 0;
$links = $this->linkRepository->findByTaskId($taskId);
return array_map(static function (TaskBookStackLink $link): BookStackLink {
$dto = new BookStackLink();
$dto->id = $link->getId();
$dto->bookstackId = $link->getBookstackId();
$dto->bookstackType = $link->getBookstackType();
$dto->title = $link->getTitle();
$dto->url = $link->getUrl();
$dto->createdAt = $link->getCreatedAt()->format('c');
return $dto;
}, $links);
}
}