API Platform GraphQL w Symfony: Schematy, Mutacje i Pytania Rekrutacyjne 2026

Kompletny przewodnik po integracji GraphQL z API Platform w Symfony. Schematy, zapytania, mutacje, resolwery, zabezpieczenia i pytania na rozmowę kwalifikacyjną.

API Platform GraphQL Symfony schematy mutacje

API Platform GraphQL przekształca aplikacje Symfony w wydajne, typowane interfejsy API, które rozwiązują problemy nadmiernego i niedostatecznego pobierania danych charakterystyczne dla REST. Dzięki automatycznemu generowaniu schematów z atrybutów PHP i pełnej obsłudze specyfikacji Relay, API Platform 4.x dostarcza produkcyjną implementację GraphQL wymagającą minimalnej konfiguracji.

Kluczowa różnica: GraphQL vs REST w API Platform

Zapytania GraphQL pobierają dokładnie potrzebne pola w pojedynczym żądaniu, podczas gdy REST zwraca ustalone struktury odpowiedzi. API Platform generuje oba endpointy z tej samej definicji zasobu, pozwalając klientom wybrać protokół odpowiedni dla danego przypadku użycia.

Instalacja i włączenie obsługi GraphQL w Symfony

API Platform wydziela funkcjonalność GraphQL do osobnego pakietu. Takie modułowe podejście utrzymuje rdzeń lekkim dla projektów potrzebujących wyłącznie REST.

bash
# Instalacja obsługi GraphQL
composer require api-platform/graphql

Po zainstalowaniu endpoint /graphql staje się automatycznie dostępny. Schemat generuje się z istniejących atrybutów #[ApiResource] bez dodatkowej konfiguracji.

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;

    // Gettery i settery...
}

Ta pojedyncza definicja encji udostępnia zapytania do pobierania książek po ID lub jako kolekcji, plus mutacje dla operacji tworzenia, aktualizacji i usuwania. Schemat GraphQL odzwierciedla typy PHP bezpośrednio: string staje się String!, typy nullable stają się polami opcjonalnymi.

Pisanie zapytań i mutacji GraphQL

Zapytania GraphQL określają dokładnie, które pola mają być zwrócone. Ta precyzja eliminuje marnowanie przepustowości i redukuje transformacje danych po stronie klienta.

graphql
# Pobranie pojedynczej książki z określonymi polami
query GetBook {
  book(id: "/books/42") {
    title
    isbn
    publishedAt
  }
}

# Pobranie kolekcji z paginacją
query ListBooks {
  books(first: 10, after: "cursor123") {
    edges {
      node {
        id
        title
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Mutacje stosują specyfikację Relay z obiektami input i clientMutationId do śledzenia żądań.

graphql
# Utworzenie nowej książki
mutation CreateBook {
  createBook(input: {
    title: "Domain-Driven Design"
    isbn: "9780321125217"
    publishedAt: "2003-08-30"
    clientMutationId: "create-1"
  }) {
    book {
      id
      title
    }
    clientMutationId
  }
}

# Aktualizacja istniejącej książki
mutation UpdateBook {
  updateBook(input: {
    id: "/books/42"
    title: "Zaktualizowany tytuł"
    clientMutationId: "update-1"
  }) {
    book {
      id
      title
    }
  }
}

clientMutationId pomaga klientom korelować odpowiedzi z żądaniami w scenariuszach wsadowych. API Platform zwraca go niezmienionego w odpowiedzi.

Implementacja własnych resolwerów dla złożonej logiki biznesowej

Domyślne operacje CRUD pokrywają podstawowe przypadki, ale prawdziwe aplikacje potrzebują własnej logiki biznesowej. API Platform dostarcza interfejsy resolwerów dla zapytań i mutacji.

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
    {
        // Dostęp do argumentów GraphQL z kontekstu
        $limit = $context['args']['limit'] ?? 10;
        $period = $context['args']['period'] ?? 'month';

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

Rejestracja własnego resolwera w konfiguracji encji:

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
{
    // ...
}

To udostępnia zapytanie bestSellers, które przyjmuje argumenty limit i period, wykonując własną logikę repozytorium zamiast domyślnych zapytań Doctrine.

Gotowy na rozmowy o Symfony?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Zabezpieczanie operacji GraphQL z Voterami i wyrażeniami

Konfiguracja bezpieczeństwa dla GraphQL działa niezależnie od REST. Każda operacja może definiować własne reguły dostępu używając języka wyrażeń 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: "Tylko autor może aktualizować tę książkę."
        ),
        new Mutation(
            name: 'delete',
            security: "is_granted('ROLE_ADMIN')"
        ),
    ]
)]
class Book
{
    // ...
}

Zmienna object w wyrażeniach bezpieczeństwa odnosi się do encji, do której uzyskiwany jest dostęp. Umożliwia to szczegółowe sprawdzanie własności. Dla złożonej logiki autoryzacji votery Symfony Security dostarczają czystsze rozwiązanie niż wyrażenia inline.

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,
        };
    }
}

Aktualizacje w czasie rzeczywistym z subskrypcjami GraphQL

API Platform implementuje subskrypcje GraphQL przez Mercure, protokół dla zdarzeń wysyłanych przez serwer. Subskrypcje przekazują dane do klientów, gdy zasoby się zmieniają.

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

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

Klienci subskrybują zmiany używając standardowej składni subskrypcji GraphQL:

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

Kiedy mutacja aktualizuje książkę, Mercure rozgłasza zmianę do wszystkich zasubskrybowanych klientów. Ten wzorzec pasuje do aplikacji współpracy, paneli na żywo i powiadomień w czasie rzeczywistym.

Pytania rekrutacyjne: API Platform GraphQL

Rozmowy techniczne na stanowiska Symfony coraz częściej obejmują integrację GraphQL. Te pytania testują zrozumienie zarówno specyfikacji, jak i implementacji API Platform.

P: Jak API Platform generuje schemat GraphQL?

API Platform introspektuje atrybuty #[ApiResource] i deklaracje typów PHP, aby zbudować schemat. Właściwości encji stają się polami, z typami PHP mapowanymi na typy GraphQL. Schemat regeneruje się przy każdym żądaniu w trybie deweloperskim i cache'uje w produkcji.

P: Jaka jest różnica między operacjami Query i QueryCollection?

Query pobiera pojedynczy element po identyfikatorze i wymaga argumentu id. QueryCollection zwraca wiele elementów z opcjonalnym filtrowaniem, paginacją i sortowaniem. Oba mogą mieć własne resolwery, ale ich interfejsy się różnią: QueryItemResolverInterface vs QueryCollectionResolverInterface.

P: Jak obsłużyć problemy N+1 zapytań w API Platform GraphQL?

Wzorzec DataLoader grupuje wiele zapytań do bazy danych w jedno. API Platform integruje się z eager loading Doctrine przez fetch joins w rozszerzeniu zapytania. Dla złożonych przypadków należy zaimplementować własny resolwer używający addSelect() Doctrine do pobierania asocjacji w początkowym zapytaniu.

php
// Własne rozszerzenie zapytania dla eager loading
public function applyToCollection(
    QueryBuilder $queryBuilder,
    QueryNameGeneratorInterface $queryNameGenerator,
    string $resourceClass,
    ?Operation $operation = null,
    array $context = []
): void {
    $queryBuilder
        ->addSelect('author')
        ->leftJoin('o.author', 'author');
}

P: Czy reguły bezpieczeństwa REST i GraphQL mogą się różnić dla tego samego zasobu?

Tak. Operacje REST używają security na #[Get], #[Post], itd., podczas gdy operacje GraphQL używają security na #[Query], #[Mutation], itd. Ta separacja pozwala na bardziej rygorystyczne reguły dla jednego protokołu. Częsty wzorzec udostępnia GraphQL tylko do odczytu dla publicznych klientów, podczas gdy mutacje REST wymagają uwierzytelnienia.

P: Jak dodać własne typy skalarne do schematu GraphQL?

Należy zarejestrować własny typ w config/packages/api_platform.yaml i zaimplementować logikę serializacji:

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);
    }
}

Strategie paginacji: kursorowa vs stronicowa

API Platform domyślnie stosuje paginację kursorową zgodną ze specyfikacją Relay Connection. To podejście lepiej obsługuje dane w czasie rzeczywistym niż paginacja offsetowa, ponieważ wstawienia nie przesuwają wyników.

graphql
# Kursorowa (domyślna)
query {
  books(first: 10, after: "YXJyYXljb25uZWN0aW9uOjk=") {
    edges {
      cursor
      node {
        title
      }
    }
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}

Paginacja stronicowa pasuje do prostszych przypadków użycia, gdzie klienci potrzebują bezpośredniego dostępu do stron:

php
// Włączenie paginacji stronicowej
#[ApiResource(
    paginationType: 'page',
    graphQlOperations: [
        new QueryCollection(paginationType: 'page'),
    ]
)]
class Book {}
graphql
# Stronicowa
query {
  books(page: 2, itemsPerPage: 20) {
    collection {
      title
    }
    paginationInfo {
      totalCount
      lastPage
    }
  }
}

Paginacja kursorowa działa lepiej na dużą skalę, ponieważ unika zapytań OFFSET. Kompromisem jest to, że klienci nie mogą przeskakiwać do dowolnych stron.

Testowanie endpointów GraphQL w Symfony

Testy funkcjonalne weryfikują zachowanie GraphQL używając klienta testowego Symfony. API Platform dostarcza dedykowany trait testowy dla 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();

        // Utworzenie danych testowych
        $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();

        // Wykonanie zapytania GraphQL
        $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);
    }
}

Te testy walidują zarówno pomyślne operacje, jak i egzekwowanie bezpieczeństwa. Uruchomienie ich poleceniem php bin/phpunit tests/GraphQL/ pozwala wychwycić regresje w zachowaniu API.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Kluczowe wnioski dla API Platform GraphQL w Symfony

  • Zainstalowanie api-platform/graphql włącza endpoint /graphql z automatycznym generowaniem schematu z atrybutów #[ApiResource]
  • Użycie operacji Query, QueryCollection i Mutation kontroluje, które operacje GraphQL każdy zasób udostępnia
  • Implementacja QueryItemResolverInterface lub MutationResolverInterface obsługuje własną logikę biznesową wykraczającą poza CRUD
  • Wyrażenia bezpieczeństwa na operacjach GraphQL działają niezależnie od REST, pozwalając na różne reguły dostępu na protokół
  • Włączenie Mercure umożliwia subskrypcje w czasie rzeczywistym, które przekazują zmiany do klientów, gdy zasoby się aktualizują
  • Paginacja kursorowa obsługuje dane w czasie rzeczywistym lepiej niż stronicowa, ale rezygnuje z bezpośredniego dostępu do stron
  • Testowanie endpointów GraphQL z ApiTestCase i żądaniami JSON POST do /graphql
  • Pytania rekrutacyjne koncentrują się na generowaniu schematu, problemach N+1, separacji bezpieczeństwa i własnych resolwerach
Wyzwanie dnia

Znajdziesz błąd w Symfony?

Prawdziwy fragment kodu, ukryty błąd, jedna próba dziennie. Bez konta, żeby spróbować.

Anthony Fillion-Maillet

Autor:

Anthony Fillion-Maillet

Założyciel SharpSkill

Programista fullstack od ponad 10 lat. Prowadzi SharpSkill i odpowiada za wszystko, co się tu ukazuje.

Zaktualizowano 27 sierpnia 2026

Tagi

#symfony
#graphql
#api-platform
#php
#backend

Udostępnij

Powiązane artykuły