API Platform GraphQL Symfony: Схеми, Мутації та Питання на Співбесіді 2026

Повний посібник з інтеграції GraphQL з API Platform у Symfony. Схеми, запити, мутації, резолвери, безпека та питання на технічну співбесіду.

API Platform GraphQL Symfony схеми мутації

API Platform GraphQL перетворює додатки Symfony на потужні, типобезпечні API, що вирішують проблеми надмірного та недостатнього отримання даних, притаманні REST. Завдяки автоматичній генерації схем з PHP-атрибутів та повній підтримці специфікації Relay, API Platform 4.x надає готову до продакшену реалізацію GraphQL, що вимагає мінімальної конфігурації.

Ключова відмінність: GraphQL vs REST в API Platform

Запити GraphQL отримують саме ті поля, які потрібні, в одному запиті, тоді як REST повертає фіксовані структури відповідей. API Platform генерує обидва ендпоінти з однієї дефініції ресурсу, дозволяючи клієнтам обирати протокол, що підходить для їхнього випадку використання.

Встановлення та увімкнення підтримки GraphQL у Symfony

API Platform виділяє функціональність GraphQL в окремий пакет. Такий модульний підхід зберігає ядро легким для проєктів, яким потрібен лише REST.

bash
# Встановлення підтримки GraphQL
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;

    // Гетери та сетери...
}

Ця єдина дефініція сутності надає запити для отримання книг за ID або як колекції, плюс мутації для операцій створення, оновлення та видалення. Схема GraphQL безпосередньо відображає типи PHP: string стає String!, nullable-типи стають опціональними полями.

Написання запитів та мутацій GraphQL

Запити GraphQL точно вказують, які поля повертати. Ця точність усуває марнування пропускної здатності та зменшує трансформації даних на стороні клієнта.

graphql
# Отримання однієї книги з конкретними полями
query GetBook {
  book(id: "/books/42") {
    title
    isbn
    publishedAt
  }
}

# Отримання колекції з пагінацією
query ListBooks {
  books(first: 10, after: "cursor123") {
    edges {
      node {
        id
        title
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Мутації дотримуються специфікації Relay з об'єктами input та clientMutationId для відстеження запитів.

graphql
# Створення нової книги
mutation CreateBook {
  createBook(input: {
    title: "Domain-Driven Design"
    isbn: "9780321125217"
    publishedAt: "2003-08-30"
    clientMutationId: "create-1"
  }) {
    book {
      id
      title
    }
    clientMutationId
  }
}

# Оновлення існуючої книги
mutation UpdateBook {
  updateBook(input: {
    id: "/books/42"
    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
    {
        // Доступ до аргументів GraphQL з контексту
        $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
{
    // ...
}

Це надає запит bestSellers, що приймає аргументи limit та period, виконуючи власну логіку репозиторію замість стандартних запитів Doctrine.

Готовий до співбесід з Symfony?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Захист операцій GraphQL за допомогою Voter'ів та виразів

Конфігурація безпеки для 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: "Лише автор може оновлювати цю книгу."
        ),
        new Mutation(
            name: 'delete',
            security: "is_granted('ROLE_ADMIN')"
        ),
    ]
)]
class Book
{
    // ...
}

Змінна object у виразах безпеки посилається на сутність, до якої здійснюється доступ. Це дозволяє детальні перевірки власності. Для складної логіки авторизації Symfony Security voter'и забезпечують чистіше рішення, ніж 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,
        };
    }
}

Оновлення в реальному часі з GraphQL Subscriptions

API Platform реалізує GraphQL subscriptions через Mercure — протокол для подій, що надсилаються сервером. Subscriptions передають дані клієнтам, коли ресурси змінюються.

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

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

Клієнти підписуються на зміни, використовуючи стандартний синтаксис GraphQL subscription:

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

Коли мутація оновлює книгу, Mercure транслює зміну всім підписаним клієнтам. Цей патерн підходить для додатків співпраці, живих дашбордів та сповіщень у реальному часі.

Питання на співбесіді: API Platform GraphQL

Технічні співбесіди на позиції Symfony все частіше охоплюють інтеграцію GraphQL. Ці питання перевіряють розуміння як специфікації, так і реалізації API Platform.

П: Як API Platform генерує схему GraphQL?

API Platform досліджує атрибути #[ApiResource] та оголошення типів PHP для побудови схеми. Властивості сутності стають полями, з типами PHP, що відображаються на типи GraphQL. Схема регенерується при кожному запиті в режимі розробки та кешується в продакшені.

П: Яка різниця між операціями Query та QueryCollection?

Query отримує один елемент за ідентифікатором і вимагає аргументу id. QueryCollection повертає декілька елементів з опціональною фільтрацією, пагінацією та сортуванням. Обидві можуть мати власні резолвери, але їхні інтерфейси відрізняються: QueryItemResolverInterface та QueryCollectionResolverInterface.

П: Як обробляти проблеми N+1 запитів в API Platform GraphQL?

Патерн DataLoader групує декілька запитів до бази даних в один. API Platform інтегрується з eager loading Doctrine через fetch joins у розширенні запиту. Для складних випадків слід реалізувати власний резолвер, що використовує addSelect() Doctrine для отримання асоціацій у початковому запиті.

php
// Власне розширення запиту для eager loading
public function applyToCollection(
    QueryBuilder $queryBuilder,
    QueryNameGeneratorInterface $queryNameGenerator,
    string $resourceClass,
    ?Operation $operation = null,
    array $context = []
): void {
    $queryBuilder
        ->addSelect('author')
        ->leftJoin('o.author', 'author');
}

П: Чи можуть правила безпеки REST та GraphQL відрізнятися для одного ресурсу?

Так. Операції REST використовують security на #[Get], #[Post] тощо, тоді як операції GraphQL використовують security на #[Query], #[Mutation] тощо. Це розділення дозволяє встановлювати суворіші правила для одного протоколу. Поширений патерн надає GraphQL лише для читання публічним клієнтам, тоді як мутації REST вимагають автентифікації.

П: Як додати власні скалярні типи до схеми 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. Цей підхід краще обробляє дані в реальному часі, ніж offset-пагінація, оскільки вставки не зміщують результати.

graphql
# Курсорна (за замовчуванням)
query {
  books(first: 10, after: "YXJyYXljb25uZWN0aW9uOjk=") {
    edges {
      cursor
      node {
        title
      }
    }
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}

Сторінкова пагінація підходить для простіших випадків використання, де клієнтам потрібен прямий доступ до сторінок:

php
// Увімкнення сторінкової пагінації
#[ApiResource(
    paginationType: 'page',
    graphQlOperations: [
        new QueryCollection(paginationType: 'page'),
    ]
)]
class Book {}
graphql
# Сторінкова
query {
  books(page: 2, itemsPerPage: 20) {
    collection {
      title
    }
    paginationInfo {
      totalCount
      lastPage
    }
  }
}

Курсорна пагінація працює краще на масштабі, оскільки уникає запитів OFFSET. Компроміс полягає в тому, що клієнти не можуть переходити до довільних сторінок.

Тестування ендпоінтів GraphQL у Symfony

Функціональні тести верифікують поведінку GraphQL, використовуючи тестовий клієнт Symfony. API Platform надає спеціалізований trait для тестування 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();

        // Створення тестових даних
        $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();

        // Виконання запиту 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);
    }
}

Ці тести валідують як успішні операції, так і застосування безпеки. Запуск їх командою php bin/phpunit tests/GraphQL/ дозволяє виявити регресії в поведінці API.

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Ключові висновки для API Platform GraphQL у Symfony

  • Встановлення api-platform/graphql вмикає ендпоінт /graphql з автоматичною генерацією схеми з атрибутів #[ApiResource]
  • Використання операцій Query, QueryCollection та Mutation контролює, які операції GraphQL надає кожен ресурс
  • Реалізація QueryItemResolverInterface або MutationResolverInterface обробляє власну бізнес-логіку за межами CRUD
  • Вирази безпеки на операціях GraphQL працюють незалежно від REST, дозволяючи різні правила доступу для кожного протоколу
  • Увімкнення Mercure забезпечує subscriptions у реальному часі, що передають зміни клієнтам при оновленні ресурсів
  • Курсорна пагінація обробляє дані в реальному часі краще, ніж сторінкова, але втрачає прямий доступ до сторінок
  • Тестування ендпоінтів GraphQL з ApiTestCase та JSON POST-запитами до /graphql
  • Питання на співбесіді фокусуються на генерації схеми, проблемах N+1, розділенні безпеки та власних резолверах
Щоденний виклик

Чи знайдеш ти помилку в Symfony?

Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Anthony Fillion-Maillet

Автор:

Anthony Fillion-Maillet

Засновник SharpSkill

Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.

Оновлено 27 серпня 2026 р.

Теги

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

Поділитися

Пов'язані статті