API Platform Symfony REST:完全チュートリアルと面接対策 2026年版

API Platform 4.3を使用してSymfonyでREST APIを構築する方法を解説します。State Provider/Processorパターン、カスタムフィルター、セキュリティ設定、および技術面接で頻出する質問と回答を網羅した実践ガイドです。

API Platform Symfony REST チュートリアル

API Platform 4.3は、OpenAPIドキュメントの自動生成、コンテンツネゴシエーション、そして読み取り操作と書き込み操作を分離するクリーンなアーキテクチャを備え、SymfonyをREST APIの強力なフレームワークへと変貌させます。本チュートリアルでは、セットアップから高度なパターン、そしてシニア開発者とジュニア開発者を分ける面接質問まで解説します。

API Platform 4のアーキテクチャ

API Platform 4では、GET操作にState Provider、POST/PUT/PATCH/DELETE操作にState Processorを使用します。この分離はCQRS原則に沿っており、テストを容易にします。

Symfony 7へのAPI Platformのインストール

API Platformのインストールには、Symfony 7.2以上が必要です。バンドルはデフォルトでDoctrine ORMと統合されますが、Provider/Processorパターンを通じてカスタムデータソースもサポートしています。

bash
# Symfony FlexでAPI Platformをインストール
composer require api

# インストールを確認
php bin/console debug:router | grep api

apiレシピは、api-platform/symfonyとともにserializer、validator、property-accessコンポーネントをインストールします。Symfony Flexは自動的に/api配下にルートを設定します。

yaml
# config/packages/api_platform.yaml
api_platform:
    title: 'My API'
    version: '1.0.0'
    formats:
        jsonld: ['application/ld+json']
        json: ['application/json']
    docs_formats:
        jsonld: ['application/ld+json']
        jsonopenapi: ['application/vnd.openapi+json']
        html: ['text/html']
    defaults:
        stateless: true
        cache_headers:
            vary: ['Content-Type', 'Authorization', 'Accept-Language']

stateless: true設定により、すべてのAPIエンドポイントでPHPセッションが無効になり、メモリオーバーヘッドが削減され、水平スケーリングが可能になります。

Attributeを使用した最初のAPIリソースの作成

API Platform 4では、PHP 8のAttributeを使用してAPIリソースを宣言します。各エンティティは#[ApiResource]属性を通じてAPIエンドポイントになります。

src/Entity/Product.phpphp
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\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity]
#[ApiResource(
    operations: [
        new GetCollection(),
        new Get(),
        new Post(security: "is_granted('ROLE_ADMIN')"),
        new Put(security: "is_granted('ROLE_ADMIN')"),
        new Delete(security: "is_granted('ROLE_ADMIN')")
    ],
    paginationItemsPerPage: 30
)]
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: 'decimal', precision: 10, scale: 2)]
    #[Assert\Positive]
    private string $price;

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

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

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;
        return $this;
    }

    public function getPrice(): string
    {
        return $this->price;
    }

    public function setPrice(string $price): self
    {
        $this->price = $price;
        return $this;
    }

    public function getCreatedAt(): \DateTimeImmutable
    {
        return $this->createdAt;
    }
}

securityパラメータはSymfony Expression Languageを使用してアクセスを制御します。これにより、通常のセキュリティvoterと完全に統合されます。

カスタムState Providerの実装

State Providerは読み取り操作のデータ取得を処理します。カスタムProviderにより、Doctrine以外のデータソースとの統合やビジネスロジックの追加が可能になります。

src/State/ProductStateProvider.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Repository\ProductRepository;
use Psr\Cache\CacheItemPoolInterface;

final class ProductStateProvider implements ProviderInterface
{
    public function __construct(
        private ProductRepository $repository,
        private CacheItemPoolInterface $cache
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
    {
        if (isset($uriVariables['id'])) {
            $cacheKey = 'product_' . $uriVariables['id'];
            $item = $this->cache->getItem($cacheKey);
            
            if ($item->isHit()) {
                return $item->get();
            }
            
            $product = $this->repository->find($uriVariables['id']);
            
            if ($product) {
                $item->set($product)->expiresAfter(3600);
                $this->cache->save($item);
            }
            
            return $product;
        }

        return $this->repository->findActiveProducts();
    }
}

Providerをエンティティに登録するには、operationに指定します。

php
#[ApiResource(
    operations: [
        new Get(provider: ProductStateProvider::class),
        new GetCollection(provider: ProductStateProvider::class)
    ]
)]

カスタムState Processorによる書き込み操作

State Processorは作成、更新、削除の操作を処理します。イベントのディスパッチやデータ変換のカスタムロジックに最適です。

src/State/ProductStateProcessor.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use App\Message\ProductCreated;

final class ProductStateProcessor implements ProcessorInterface
{
    public function __construct(
        private EntityManagerInterface $em,
        private MessageBusInterface $messageBus
    ) {}

    public function process(
        mixed $data,
        Operation $operation,
        array $uriVariables = [],
        array $context = []
    ): Product {
        if ($operation instanceof \ApiPlatform\Metadata\Post) {
            $this->em->persist($data);
            $this->em->flush();
            
            $this->messageBus->dispatch(new ProductCreated($data->getId()));
            
            return $data;
        }

        if ($operation instanceof \ApiPlatform\Metadata\Delete) {
            $this->em->remove($data);
            $this->em->flush();
            
            return $data;
        }

        $this->em->flush();
        return $data;
    }
}

Processorをoperationに適用します。

php
#[ApiResource(
    operations: [
        new Post(processor: ProductStateProcessor::class),
        new Put(processor: ProductStateProcessor::class),
        new Delete(processor: ProductStateProcessor::class)
    ]
)]

カスタムフィルターとソート

API Platformは組み込みフィルターを提供しますが、カスタムフィルターによりフィルタリングロジックの完全な制御が可能です。

src/Filter/PriceRangeFilter.phpphp
namespace App\Filter;

use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\QueryBuilder;

final class PriceRangeFilter extends AbstractFilter
{
    protected function filterProperty(
        string $property,
        mixed $value,
        QueryBuilder $queryBuilder,
        QueryNameGeneratorInterface $queryNameGenerator,
        string $resourceClass,
        ?Operation $operation = null,
        array $context = []
    ): void {
        if ($property !== 'priceRange') {
            return;
        }

        $alias = $queryBuilder->getRootAliases()[0];
        $minParam = $queryNameGenerator->generateParameterName('minPrice');
        $maxParam = $queryNameGenerator->generateParameterName('maxPrice');

        if (isset($value['min'])) {
            $queryBuilder
                ->andWhere("$alias.price >= :$minParam")
                ->setParameter($minParam, $value['min']);
        }

        if (isset($value['max'])) {
            $queryBuilder
                ->andWhere("$alias.price <= :$maxParam")
                ->setParameter($maxParam, $value['max']);
        }
    }

    public function getDescription(string $resourceClass): array
    {
        return [
            'priceRange[min]' => [
                'property' => 'price',
                'type' => 'float',
                'required' => false,
                'description' => '最低価格',
            ],
            'priceRange[max]' => [
                'property' => 'price',
                'type' => 'float',
                'required' => false,
                'description' => '最高価格',
            ],
        ];
    }
}

エンティティにフィルターを適用します。

php
use ApiPlatform\Metadata\ApiFilter;
use App\Filter\PriceRangeFilter;

#[ApiResource]
#[ApiFilter(PriceRangeFilter::class)]
class Product
{
    // ...
}

シリアライゼーショングループとDTO

シリアライゼーショングループは、異なるコンテキストで公開されるフィールドを制御します。DTOはAPIレスポンスと内部エンティティを分離します。

src/Entity/User.phpphp
use Symfony\Component\Serializer\Annotation\Groups;

#[ApiResource(
    normalizationContext: ['groups' => ['user:read']],
    denormalizationContext: ['groups' => ['user:write']]
)]
class User
{
    #[Groups(['user:read'])]
    private ?int $id = null;

    #[Groups(['user:read', 'user:write'])]
    private string $email;

    #[Groups(['user:write'])]
    private string $password;

    #[Groups(['user:read'])]
    private \DateTimeImmutable $createdAt;
}

DTO変換には、State ProviderとProcessorを使用します。

src/Dto/ProductOutput.phpphp
namespace App\Dto;

final class ProductOutput
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly string $formattedPrice,
        public readonly string $createdAt
    ) {}
}
src/State/ProductOutputProvider.phpphp
namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Dto\ProductOutput;
use App\Repository\ProductRepository;

final class ProductOutputProvider implements ProviderInterface
{
    public function __construct(private ProductRepository $repository) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?ProductOutput
    {
        $product = $this->repository->find($uriVariables['id']);
        
        if (!$product) {
            return null;
        }

        return new ProductOutput(
            $product->getId(),
            $product->getName(),
            '¥' . number_format((float) $product->getPrice()),
            $product->getCreatedAt()->format('Y年m月d日')
        );
    }
}

セキュリティとAPI認証

API Platformは、Symfony SecurityおよびJWT認証と統合されます。API Platform 4では、operationレベルとresourceレベルの両方でセキュリティを設定できます。

php
#[ApiResource(
    security: "is_granted('ROLE_USER')",
    operations: [
        new GetCollection(),
        new Get(security: "is_granted('ROLE_USER') and object.owner == user"),
        new Post(security: "is_granted('ROLE_ADMIN')"),
        new Put(
            security: "is_granted('ROLE_ADMIN') or object.owner == user",
            securityMessage: 'このリソースを編集する権限がありません。'
        ),
        new Delete(security: "is_granted('ROLE_ADMIN')")
    ]
)]
class Order
{
    // ...
}

JWT認証の設定には、lexik/jwt-authentication-bundleを使用します。

yaml
# config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
    token_ttl: 3600
yaml
# config/packages/security.yaml
security:
    firewalls:
        api:
            pattern: ^/api
            stateless: true
            jwt: ~
    access_control:
        - { path: ^/api/login, roles: PUBLIC_ACCESS }
        - { path: ^/api, roles: ROLE_USER }

Symfonyの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

面接で頻出するAPI Platform質問

技術面接では、API Platformの深い理解が求められます。以下の質問と回答は、シニア開発者レベルの知識をカバーしています。

State ProviderとState Processorの違いは何ですか?

State Providerは読み取り操作(GET、GetCollection)でデータを取得する役割を担います。データベースクエリ、キャッシュ読み取り、外部API呼び出しなどを処理します。一方、State Processorは書き込み操作(POST、PUT、PATCH、DELETE)を処理し、データの永続化、イベントのディスパッチ、関連サービスの呼び出しを行います。この分離はCQRSパターンに従い、読み取りと書き込みのロジックを独立してスケーリングおよびテストできます。

API Platformでカスタムoperationを作成する方法は?

カスタムoperationは、標準のCRUD操作では対応できないビジネスロジックに使用します。独自のState Processorを実装し、operationで参照します。

php
#[ApiResource(
    operations: [
        new Post(
            uriTemplate: '/products/{id}/publish',
            controller: PublishProductController::class,
            name: 'publish_product'
        )
    ]
)]

N+1問題をAPI Platformで解決するには?

Doctrine extensionを使用してeager loadingを設定するか、カスタムState Providerでクエリを最適化します。#[ApiResource(fetchPartial: true)]オプションも部分的なfetchを有効にし、パフォーマンスを向上させます。

php
// Repository method with optimized query
public function findWithRelations(): array
{
    return $this->createQueryBuilder('p')
        ->leftJoin('p.category', 'c')
        ->addSelect('c')
        ->getQuery()
        ->getResult();
}

API Platformでのバリデーションはどのように機能しますか?

API PlatformはSymfony Validatorコンポーネントを使用します。エンティティのプロパティにバリデーション制約を追加すると、API Platformが自動的にリクエストデータを検証します。検証エラーは標準化されたJSON形式で返されます。グループを使用して、operation別に異なるバリデーションルールを適用することも可能です。

php
#[ApiResource(
    operations: [
        new Post(validationContext: ['groups' => ['create']]),
        new Put(validationContext: ['groups' => ['update']])
    ]
)]

API Platformのページネーションをカスタマイズするには?

デフォルトのページネーションはresourceレベルで設定できます。クライアント側の制御を許可することも、カスタムPaginatorを実装することも可能です。

php
#[ApiResource(
    paginationItemsPerPage: 30,
    paginationMaximumItemsPerPage: 100,
    paginationClientItemsPerPage: true
)]

テストとCI/CD統合

API Platformは、API Testの機能テストをサポートしています。

php
namespace App\Tests\Api;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;

class ProductTest extends ApiTestCase
{
    public function testGetProducts(): void
    {
        $response = static::createClient()->request('GET', '/api/products');

        $this->assertResponseIsSuccessful();
        $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
        $this->assertJsonContains(['@context' => '/api/contexts/Product']);
    }

    public function testCreateProduct(): void
    {
        $response = static::createClient()->request('POST', '/api/products', [
            'json' => [
                'name' => 'テスト商品',
                'price' => '1500.00'
            ],
            'headers' => ['Authorization' => 'Bearer ' . $this->getToken()]
        ]);

        $this->assertResponseStatusCodeSame(201);
        $this->assertJsonContains(['name' => 'テスト商品']);
    }
}

結論

API Platform 4.3は、SymfonyでREST APIを構築するための最も効率的なソリューションです。State Provider/Processorアーキテクチャにより、関心の分離が実現され、テストと保守が容易になります。本チュートリアルで解説したパターンは、プロダクション環境で実証済みであり、面接でも頻繁に問われる内容です。カスタムフィルター、DTOによるレスポンス変換、JWTセキュリティの統合をマスターすることで、スケーラブルなAPIアーキテクチャを設計できるようになります。

今日のチャレンジ

Symfony のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年8月25日 更新

タグ

#symfony
#api-platform
#rest-api
#php
#backend

共有

関連記事