Claude Code Plugins

Community-maintained marketplace

Feedback

symfony:api-platform-resources

@MakFly/superpowers-symfony
15
0

Configure API Platform resources with operations, pagination, and output DTOs for clean, versioned REST/GraphQL APIs

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name symfony:api-platform-resources
description Configure API Platform resources with operations, pagination, and output DTOs for clean, versioned REST/GraphQL APIs

API Platform Resources

Installation

composer require api-platform/core

Basic Resource

<?php
// src/Entity/Product.php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity]
#[ApiResource(
    operations: [
        new GetCollection(),
        new Post(),
        new Get(),
        new Put(),
        new Patch(),
        new Delete(),
    ],
    paginationItemsPerPage: 20,
)]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Assert\NotBlank]
    #[Assert\Length(min: 3, max: 255)]
    private string $name;

    #[ORM\Column(type: 'text', nullable: true)]
    private ?string $description = null;

    #[ORM\Column]
    #[Assert\Positive]
    private int $price; // In cents

    #[ORM\Column]
    private \DateTimeImmutable $createdAt;

    public function __construct()
    {
        $this->createdAt = new \DateTimeImmutable();
    }

    // Getters and setters...
}

Operations Configuration

Customize Operations

#[ApiResource(
    operations: [
        new GetCollection(
            uriTemplate: '/products',
            paginationEnabled: true,
            paginationItemsPerPage: 30,
        ),
        new Post(
            uriTemplate: '/products',
            security: "is_granted('ROLE_ADMIN')",
            validationContext: ['groups' => ['create']],
        ),
        new Get(
            uriTemplate: '/products/{id}',
        ),
        new Put(
            uriTemplate: '/products/{id}',
            security: "is_granted('ROLE_ADMIN')",
        ),
        new Patch(
            uriTemplate: '/products/{id}',
            security: "is_granted('ROLE_ADMIN')",
        ),
        new Delete(
            uriTemplate: '/products/{id}',
            security: "is_granted('ROLE_ADMIN')",
        ),
    ],
)]
class Product { /* ... */ }

Custom Operations

use ApiPlatform\Metadata\Post;
use App\Controller\PublishProductController;

#[ApiResource(
    operations: [
        // Standard CRUD...
        new Post(
            uriTemplate: '/products/{id}/publish',
            controller: PublishProductController::class,
            name: 'publish_product',
            openapiContext: [
                'summary' => 'Publish a product',
                'description' => 'Makes the product visible to customers',
            ],
        ),
    ],
)]
class Product { /* ... */ }
<?php
// src/Controller/PublishProductController.php

namespace App\Controller;

use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\AsController;

#[AsController]
class PublishProductController extends AbstractController
{
    public function __construct(
        private EntityManagerInterface $em,
    ) {}

    public function __invoke(Product $product): Product
    {
        $product->setPublished(true);
        $product->setPublishedAt(new \DateTimeImmutable());

        $this->em->flush();

        return $product;
    }
}

Pagination

Default Pagination

#[ApiResource(
    paginationEnabled: true,
    paginationItemsPerPage: 20,
    paginationMaximumItemsPerPage: 100,
    paginationClientItemsPerPage: true, // Allow client to set itemsPerPage
)]
class Product { /* ... */ }

Client Pagination

GET /api/products?page=2
GET /api/products?itemsPerPage=50

Response includes pagination metadata:

{
    "@context": "/api/contexts/Product",
    "@id": "/api/products",
    "@type": "hydra:Collection",
    "hydra:totalItems": 150,
    "hydra:member": [...],
    "hydra:view": {
        "@id": "/api/products?page=2",
        "hydra:first": "/api/products?page=1",
        "hydra:last": "/api/products?page=8",
        "hydra:previous": "/api/products?page=1",
        "hydra:next": "/api/products?page=3"
    }
}

Cursor Pagination

#[ApiResource(
    paginationEnabled: true,
    paginationPartial: true,
    paginationViaCursor: [
        ['field' => 'createdAt', 'direction' => 'DESC'],
        ['field' => 'id', 'direction' => 'DESC'],
    ],
)]
class Product { /* ... */ }

Output DTOs

Separate your API representation from your entities:

<?php
// src/Dto/ProductOutput.php

namespace App\Dto;

final class ProductOutput
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly ?string $description,
        public readonly string $formattedPrice, // "€19.99"
        public readonly string $createdAt,
    ) {}
}
<?php
// src/State/ProductOutputProvider.php

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Dto\ProductOutput;
use App\Entity\Product;

class ProductOutputProvider implements ProviderInterface
{
    public function __construct(
        private ProviderInterface $decorated,
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        $product = $this->decorated->provide($operation, $uriVariables, $context);

        if ($product instanceof Product) {
            return $this->transform($product);
        }

        // Collection
        return array_map(fn($p) => $this->transform($p), $product);
    }

    private function transform(Product $product): ProductOutput
    {
        return new ProductOutput(
            id: $product->getId(),
            name: $product->getName(),
            description: $product->getDescription(),
            formattedPrice: sprintf('€%.2f', $product->getPrice() / 100),
            createdAt: $product->getCreatedAt()->format('c'),
        );
    }
}
#[ApiResource(
    operations: [
        new Get(
            output: ProductOutput::class,
            provider: ProductOutputProvider::class,
        ),
        new GetCollection(
            output: ProductOutput::class,
            provider: ProductOutputProvider::class,
        ),
    ],
)]
class Product { /* ... */ }

Input DTOs

<?php
// src/Dto/CreateProductInput.php

namespace App\Dto;

use Symfony\Component\Validator\Constraints as Assert;

final class CreateProductInput
{
    #[Assert\NotBlank]
    #[Assert\Length(min: 3, max: 255)]
    public string $name;

    public ?string $description = null;

    #[Assert\Positive]
    public int $priceInCents;
}
<?php
// src/State/CreateProductProcessor.php

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Dto\CreateProductInput;
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;

class CreateProductProcessor implements ProcessorInterface
{
    public function __construct(
        private EntityManagerInterface $em,
    ) {}

    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Product
    {
        /** @var CreateProductInput $data */
        $product = new Product();
        $product->setName($data->name);
        $product->setDescription($data->description);
        $product->setPrice($data->priceInCents);

        $this->em->persist($product);
        $this->em->flush();

        return $product;
    }
}
#[ApiResource(
    operations: [
        new Post(
            input: CreateProductInput::class,
            processor: CreateProductProcessor::class,
        ),
    ],
)]
class Product { /* ... */ }

Best Practices

  1. Use DTOs for complex transformations
  2. Validation on entity or input DTOs
  3. Security at operation level
  4. Pagination always enabled for collections
  5. Meaningful URIs using uriTemplate