API Platform GraphQL Symfony: スキーマ、ミューテーション、面接対策 2026

SymfonyでAPI Platform GraphQLを構築する方法を解説。スキーマ自動生成、カスタムリゾルバー、セキュリティ設定、リアルタイムサブスクリプション、技術面接の質問と回答を網羅。

API Platform GraphQL Symfony: スキーマ、ミューテーション、面接対策 2026

API Platform GraphQLは、SymfonyアプリケーションをRESTに内在する過剰取得・過少取得の問題を解決する、型安全で強力なAPIに変換します。PHP属性からの自動スキーマ生成とRelay仕様の完全サポートにより、API Platform 4.xは最小限の設定で本番環境に対応したGraphQL実装を提供します。

GraphQL vs REST in API Platform の違い

GraphQLはリクエストで必要なフィールドだけを単一のクエリで取得しますが、RESTは固定されたレスポンス構造を返します。API Platformは同じリソース定義から両方のエンドポイントを生成し、クライアントがユースケースに合ったプロトコルを選択できます。

SymfonyでのGraphQLサポートのインストールと有効化

API PlatformはGraphQL機能を専用パッケージに分離しています。このモジュラーアプローチにより、RESTのみを必要とするプロジェクトではコアを軽量に保てます。

bash
# Install GraphQL support
composer require api-platform/graphql

インストール後、/graphqlエンドポイントが自動的に利用可能になります。スキーマは追加設定なしで既存の#[ApiResource]属性から生成されます。

src/Entity/Book.phpphp
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\GraphQl\Mutation;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource(
    graphQlOperations: [
        new Query(),
        new QueryCollection(),
        new Mutation(name: 'create'),
        new Mutation(name: 'update'),
        new Mutation(name: 'delete'),
    ]
)]
class Book
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private string $title;

    #[ORM\Column(length: 13)]
    private string $isbn;

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

    // Getters and setters...
}

この単一のエンティティ定義により、IDまたはコレクションによる書籍取得のクエリと、作成・更新・削除操作のミューテーションが公開されます。GraphQLスキーマはPHP型を直接反映します:stringString!に、nullable型はオプションフィールドになります。

GraphQLクエリとミューテーションの記述

GraphQLクエリは返すフィールドを正確に指定します。この精度により、帯域幅の無駄を排除し、クライアント側のデータ変換を削減できます。

graphql
# Fetch a single book with specific fields
query GetBook {
  book(id: "/books/42") {
    title
    isbn
    publishedAt
  }
}

# Fetch a collection with pagination
query ListBooks {
  books(first: 10, after: "cursor123") {
    edges {
      node {
        id
        title
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

ミューテーションはRelay仕様に従い、inputオブジェクトとリクエスト追跡用のclientMutationIdを使用します。

graphql
# Create a new book
mutation CreateBook {
  createBook(input: {
    title: "Domain-Driven Design"
    isbn: "9780321125217"
    publishedAt: "2003-08-30"
    clientMutationId: "create-1"
  }) {
    book {
      id
      title
    }
    clientMutationId
  }
}

# Update an existing book
mutation UpdateBook {
  updateBook(input: {
    id: "/books/42"
    title: "Updated Title"
    clientMutationId: "update-1"
  }) {
    book {
      id
      title
    }
  }
}

clientMutationIdは、バッチシナリオでクライアントがリクエストとレスポンスを関連付けるのに役立ちます。API Platformはこれをレスポンスでそのまま返します。

複雑なビジネスロジック用カスタムリゾルバーの実装

デフォルトのCRUD操作は基本的なケースをカバーしますが、実際のアプリケーションにはカスタムビジネスロジックが必要です。API Platformはクエリとミューテーション用のリゾルバーインターフェースを提供しています。

src/Resolver/BookBestSellerResolver.phpphp
namespace App\Resolver;

use ApiPlatform\GraphQl\Resolver\QueryCollectionResolverInterface;
use App\Repository\BookRepository;

final class BookBestSellerResolver implements QueryCollectionResolverInterface
{
    public function __construct(
        private readonly BookRepository $bookRepository
    ) {}

    /**
     * @param iterable<Book> $collection
     * @return iterable<Book>
     */
    public function __invoke(iterable $collection, array $context): iterable
    {
        // Access GraphQL arguments from context
        $limit = $context['args']['limit'] ?? 10;
        $period = $context['args']['period'] ?? 'month';

        return $this->bookRepository->findBestSellers($limit, $period);
    }
}

カスタムリゾルバーをエンティティ設定に登録します:

src/Entity/Book.phpphp
#[ApiResource(
    graphQlOperations: [
        new QueryCollection(
            name: 'bestSellers',
            resolver: BookBestSellerResolver::class,
            args: [
                'limit' => ['type' => 'Int', 'default_value' => 10],
                'period' => ['type' => 'String', 'default_value' => 'month'],
            ]
        ),
    ]
)]
class Book
{
    // ...
}

これにより、limitperiod引数を受け取るbestSellersクエリが公開され、デフォルトのDoctrineクエリではなくカスタムリポジトリロジックが実行されます。

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

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

VoterとExpressionを使用したGraphQL操作のセキュリティ

GraphQLのセキュリティ設定はRESTとは独立して動作します。各操作はSymfonyのエクスプレッション言語を使用して独自のアクセスルールを定義できます。

src/Entity/Book.phpphp
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\Mutation;

#[ApiResource(
    graphQlOperations: [
        new Query(
            security: "is_granted('ROLE_USER')"
        ),
        new QueryCollection(
            security: "is_granted('ROLE_USER')"
        ),
        new Mutation(
            name: 'create',
            security: "is_granted('ROLE_EDITOR')"
        ),
        new Mutation(
            name: 'update',
            security: "is_granted('ROLE_EDITOR') and object.getAuthor() == user",
            securityMessage: "Only the author can update this book."
        ),
        new Mutation(
            name: 'delete',
            security: "is_granted('ROLE_ADMIN')"
        ),
    ]
)]
class Book
{
    // ...
}

セキュリティエクスプレッションのobject変数は、アクセスされているエンティティを参照します。これにより、きめ細かな所有権チェックが可能になります。複雑な認可ロジックには、インライン式よりもSymfony Security votersがクリーンなソリューションを提供します。

src/Security/Voter/BookVoter.phpphp
namespace App\Security\Voter;

use App\Entity\Book;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;

class BookVoter extends Voter
{
    public const EDIT = 'BOOK_EDIT';
    public const DELETE = 'BOOK_DELETE';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::EDIT, self::DELETE])
            && $subject instanceof Book;
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token
    ): bool {
        $user = $token->getUser();
        if (!$user instanceof UserInterface) {
            return false;
        }

        /** @var Book $book */
        $book = $subject;

        return match($attribute) {
            self::EDIT => $book->getAuthor() === $user,
            self::DELETE => in_array('ROLE_ADMIN', $user->getRoles()),
            default => false,
        };
    }
}

GraphQLサブスクリプションによるリアルタイム更新

API PlatformはMercureを通じてGraphQLサブスクリプションを実装しています。Mercureはサーバー送信イベント用のプロトコルです。サブスクリプションは、リソースが変更されたときにデータをクライアントにプッシュします。

src/Entity/Book.phpphp
use ApiPlatform\Metadata\GraphQl\Subscription;

#[ApiResource(
    mercure: true,
    graphQlOperations: [
        new Query(),
        new Mutation(name: 'update'),
        new Subscription(),
    ]
)]
class Book
{
    // ...
}

クライアントは標準のGraphQLサブスクリプション構文を使用して変更を購読します:

graphql
subscription BookUpdates {
  updateBookSubscribe(input: { id: "/books/42" }) {
    book {
      id
      title
      updatedAt
    }
  }
}

ミューテーションが書籍を更新すると、Mercureが購読しているすべてのクライアントに変更をブロードキャストします。このパターンは、コラボレーションアプリケーション、ライブダッシュボード、リアルタイム通知に適しています。高スループットシナリオでは、サブスクリプションをSymfony Messengerと組み合わせて、ミューテーション処理と通知ディスパッチを分離します。

面接対策:API Platform GraphQL

Symfonyポジションの技術面接では、GraphQL統合に関する質問が増えています。これらの質問は、仕様とAPI Platformの実装の両方の理解をテストします。

Q: API PlatformはGraphQLスキーマをどのように生成しますか?

API Platformは#[ApiResource]属性とPHP型宣言をイントロスペクトしてスキーマを構築します。エンティティプロパティがフィールドになり、PHP型がGraphQL型にマッピングされます。開発モードではリクエストごとにスキーマが再生成され、本番環境ではキャッシュされます。

Q: QueryQueryCollection操作の違いは何ですか?

Queryは識別子で単一のアイテムを取得し、id引数が必要です。QueryCollectionはオプションのフィルタリング、ページネーション、ソートとともに複数のアイテムを返します。両方ともカスタムリゾルバーを持つことができますが、インターフェースが異なります:QueryItemResolverInterfaceQueryCollectionResolverInterfaceです。

Q: API Platform GraphQLでN+1クエリ問題をどのように処理しますか?

DataLoaderパターンは複数のデータベースクエリを1つにバッチ処理します。API PlatformはクエリエクステンションのフェッチジョインによるDoctrineの即時読み込みと統合されています。複雑なケースでは、DoctrineのaddSelect()を使用して初期クエリで関連付けを取得するカスタムリゾルバーを実装します。

php
// Custom query extension for eager loading
public function applyToCollection(
    QueryBuilder $queryBuilder,
    QueryNameGeneratorInterface $queryNameGenerator,
    string $resourceClass,
    ?Operation $operation = null,
    array $context = []
): void {
    $queryBuilder
        ->addSelect('author')
        ->leftJoin('o.author', 'author');
}

Q: 同じリソースでRESTとGraphQLのセキュリティルールを異なるものにできますか?

はい。REST操作は#[Get]#[Post]などでsecurityを使用し、GraphQL操作は#[Query]#[Mutation]などでsecurityを使用します。この分離により、一方のプロトコルに対してより厳格なルールを設定できます。一般的なパターンは、パブリッククライアント向けに読み取り専用GraphQLを公開し、RESTミューテーションには認証を要求するものです。

Q: GraphQLスキーマにカスタムスカラー型を追加するにはどうすればよいですか?

config/packages/api_platform.yamlでカスタム型を登録し、シリアライゼーションロジックを実装します:

yaml
# config/packages/api_platform.yaml
api_platform:
    graphql:
        enabled: true
        graphiql:
            enabled: true
src/GraphQL/Type/DateTimeType.phpphp
namespace App\GraphQL\Type;

use GraphQL\Type\Definition\ScalarType;

final class DateTimeType extends ScalarType
{
    public string $name = 'DateTime';

    public function serialize($value): string
    {
        return $value->format(\DateTimeInterface::RFC3339);
    }

    public function parseValue($value): \DateTimeImmutable
    {
        return new \DateTimeImmutable($value);
    }

    public function parseLiteral($valueNode, ?array $variables = null): \DateTimeImmutable
    {
        return new \DateTimeImmutable($valueNode->value);
    }
}

ページネーション戦略:カーソル vs ページベース

API PlatformはRelay Connection仕様に従い、デフォルトでカーソルベースのページネーションを使用します。挿入が結果をシフトしないため、このアプローチはリアルタイムデータをオフセットページネーションよりもうまく処理します。

graphql
# Cursor-based (default)
query {
  books(first: 10, after: "YXJyYXljb25uZWN0aW9uOjk=") {
    edges {
      cursor
      node {
        title
      }
    }
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}

ページベースのページネーションは、クライアントが直接ページアクセスを必要とするシンプルなユースケースに適しています:

php
// Enable page-based pagination
#[ApiResource(
    paginationType: 'page',
    graphQlOperations: [
        new QueryCollection(paginationType: 'page'),
    ]
)]
class Book {}
graphql
# Page-based
query {
  books(page: 2, itemsPerPage: 20) {
    collection {
      title
    }
    paginationInfo {
      totalCount
      lastPage
    }
  }
}

カーソルページネーションはOFFSETクエリを回避するため、スケール時のパフォーマンスが向上します。トレードオフは、クライアントが任意のページにジャンプできないことです。

SymfonyでのGraphQLエンドポイントのテスト

機能テストはSymfonyのテストクライアントを使用してGraphQLの動作を検証します。API PlatformはGraphQL専用のテストトレイトを提供しています。

tests/GraphQL/BookTest.phpphp
namespace App\Tests\GraphQL;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Book;

class BookTest extends ApiTestCase
{
    public function testQueryBook(): void
    {
        $client = static::createClient();

        // Create test data
        $book = new Book();
        $book->setTitle('Test Book');
        $book->setIsbn('1234567890123');
        $book->setPublishedAt(new \DateTimeImmutable());

        $em = static::getContainer()->get('doctrine')->getManager();
        $em->persist($book);
        $em->flush();

        // Execute GraphQL query
        $response = $client->request('POST', '/graphql', [
            'json' => [
                'query' => '
                    query GetBook($id: ID!) {
                        book(id: $id) {
                            title
                            isbn
                        }
                    }
                ',
                'variables' => [
                    'id' => '/books/' . $book->getId(),
                ],
            ],
        ]);

        $this->assertResponseIsSuccessful();
        $data = $response->toArray();

        $this->assertEquals('Test Book', $data['data']['book']['title']);
        $this->assertEquals('1234567890123', $data['data']['book']['isbn']);
    }

    public function testMutationRequiresAuthentication(): void
    {
        $client = static::createClient();

        $response = $client->request('POST', '/graphql', [
            'json' => [
                'query' => '
                    mutation CreateBook {
                        createBook(input: {
                            title: "Unauthorized Book"
                            isbn: "0000000000000"
                            clientMutationId: "test"
                        }) {
                            book { id }
                        }
                    }
                ',
            ],
        ]);

        $data = $response->toArray();
        $this->assertArrayHasKey('errors', $data);
    }
}

これらのテストは、成功した操作とセキュリティの適用の両方を検証します。php bin/phpunit tests/GraphQL/で実行して、API動作のリグレッションを検出します。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

API Platform GraphQL in Symfony 重要ポイント

  • api-platform/graphqlをインストールすると、#[ApiResource]属性からの自動スキーマ生成で/graphqlエンドポイントが有効になります
  • QueryQueryCollectionMutation操作を使用して、各リソースが公開するGraphQL操作を制御します
  • CRUD以外のカスタムビジネスロジックにはQueryItemResolverInterfaceまたはMutationResolverInterfaceを実装します
  • GraphQL操作のセキュリティ式はRESTとは独立して機能し、プロトコルごとに異なるアクセスルールを許可します
  • リソースが更新されたときにクライアントに変更をプッシュするリアルタイムサブスクリプションにはMercureを有効にします
  • カーソルベースのページネーションはページベースよりもリアルタイムデータをうまく処理しますが、直接ページアクセスを犠牲にします
  • ApiTestCaseとJSONのPOSTリクエストを使用して/graphqlへのGraphQLエンドポイントをテストします
  • 面接の質問はスキーマ生成、N+1問題、セキュリティ分離、カスタムリゾルバーに焦点を当てます
今日のチャレンジ

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

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

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

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

2026年8月27日 更新

共有

関連記事