API Platform GraphQL Symfony: Schema, Mutation และคำถามสัมภาษณ์ 2026
คู่มือฉบับสมบูรณ์สำหรับการผสาน API Platform GraphQL กับ Symfony เรียนรู้ schema อัตโนมัติ mutation, custom resolver, ความปลอดภัย และคำถามสัมภาษณ์ทางเทคนิคสำหรับนักพัฒนา 2026

API Platform GraphQL เปลี่ยนแปลงแอปพลิเคชัน Symfony ให้กลายเป็น API ที่ทรงพลังและ type-safe ซึ่งแก้ปัญหา over-fetching และ under-fetching ที่มีอยู่ใน REST ด้วยการสร้าง schema อัตโนมัติจาก PHP attribute และรองรับข้อกำหนด Relay อย่างสมบูรณ์ API Platform 4.x มอบการใช้งาน GraphQL ที่พร้อมใช้งานจริงโดยต้องการการกำหนดค่าขั้นต่ำ
GraphQL ร้องขอเฉพาะ field ที่ต้องการใน query เดียว ในขณะที่ REST ส่งคืนโครงสร้าง response ที่ตายตัว API Platform สร้าง endpoint ทั้งสองจากนิยาม resource เดียวกัน ทำให้ client เลือกโปรโตคอลที่เหมาะกับกรณีใช้งานของตนได้
การติดตั้งและเปิดใช้งาน GraphQL Support ใน Symfony
API Platform แยกฟังก์ชัน GraphQL ออกเป็น package เฉพาะ แนวทางแบบ modular นี้ช่วยให้ core มีขนาดเบาสำหรับโปรเจกต์ที่ต้องการเฉพาะ REST
# Install GraphQL support
composer require api-platform/graphqlเมื่อติดตั้งแล้ว endpoint /graphql จะพร้อมใช้งานโดยอัตโนมัติ Schema จะถูกสร้างจาก attribute #[ApiResource] ที่มีอยู่โดยไม่ต้องกำหนดค่าเพิ่มเติม
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...
}นิยาม entity เดียวนี้เปิดเผย query สำหรับการดึงหนังสือตาม ID หรือเป็น collection รวมถึง mutation สำหรับการดำเนินการ create, update และ delete GraphQL schema สะท้อนประเภท PHP โดยตรง: string กลายเป็น String! และประเภท nullable กลายเป็น field ที่เป็นตัวเลือก
การเขียน Query และ Mutation GraphQL
Query GraphQL ระบุอย่างแม่นยำว่า field ใดที่จะถูกส่งคืน ความแม่นยำนี้ขจัดการสูญเสีย bandwidth และลดการแปลงข้อมูลฝั่ง client
# 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
}
}
}Mutation ปฏิบัติตามข้อกำหนด Relay ด้วย object input และ clientMutationId สำหรับการติดตาม request
# 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 ช่วยให้ client เชื่อมโยง response กับ request ในสถานการณ์ batch API Platform ส่งคืนค่านี้โดยไม่เปลี่ยนแปลงใน response
การใช้งาน Custom Resolver สำหรับ Business Logic ที่ซับซ้อน
การดำเนินการ CRUD เริ่มต้นครอบคลุมกรณีพื้นฐาน แต่แอปพลิเคชันจริงต้องการ business logic ที่กำหนดเอง API Platform มี resolver interface สำหรับ query และ mutation
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);
}
}ลงทะเบียน custom resolver ในการกำหนดค่า entity:
#[ApiResource(
graphQlOperations: [
new QueryCollection(
name: 'bestSellers',
resolver: BookBestSellerResolver::class,
args: [
'limit' => ['type' => 'Int', 'default_value' => 10],
'period' => ['type' => 'String', 'default_value' => 'month'],
]
),
]
)]
class Book
{
// ...
}การกำหนดค่านี้เปิดเผย query bestSellers ที่รับ argument limit และ period โดยรัน repository logic ที่กำหนดเองแทน query Doctrine เริ่มต้น
พร้อมที่จะพิชิตการสัมภาษณ์ Symfony แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
การรักษาความปลอดภัยของ GraphQL Operation ด้วย Voter และ Expression
การกำหนดค่าความปลอดภัยสำหรับ GraphQL ทำงานแยกจาก REST แต่ละ operation สามารถกำหนดกฎการเข้าถึงของตัวเองโดยใช้ภาษา expression ของ Symfony
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 ใน security expression อ้างอิงถึง entity ที่กำลังถูกเข้าถึง สิ่งนี้ช่วยให้ตรวจสอบความเป็นเจ้าของอย่างละเอียดได้ สำหรับ authorization logic ที่ซับซ้อน Symfony Security voters มอบโซลูชันที่สะอาดกว่า expression แบบ inline
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,
};
}
}การอัปเดตแบบ Real-Time ด้วย GraphQL Subscription
API Platform ใช้งาน GraphQL subscription ผ่าน Mercure ซึ่งเป็นโปรโตคอลสำหรับ server-sent events Subscription จะส่งข้อมูลไปยัง client เมื่อ resource เปลี่ยนแปลง
use ApiPlatform\Metadata\GraphQl\Subscription;
#[ApiResource(
mercure: true,
graphQlOperations: [
new Query(),
new Mutation(name: 'update'),
new Subscription(),
]
)]
class Book
{
// ...
}Client สมัครรับการเปลี่ยนแปลงโดยใช้ไวยากรณ์ subscription GraphQL มาตรฐาน:
subscription BookUpdates {
updateBookSubscribe(input: { id: "/books/42" }) {
book {
id
title
updatedAt
}
}
}เมื่อ mutation อัปเดตหนังสือ Mercure จะกระจายการเปลี่ยนแปลงไปยัง client ที่สมัครรับทั้งหมด รูปแบบนี้เหมาะกับแอปพลิเคชันแบบ collaborative, dashboard แบบ real-time และการแจ้งเตือนทันที สำหรับสถานการณ์ที่มี throughput สูง ให้รวม subscription เข้ากับ Symfony Messenger เพื่อแยกการประมวลผล mutation ออกจากการส่งการแจ้งเตือน
คำถามสัมภาษณ์: API Platform GraphQL
การสัมภาษณ์ทางเทคนิคสำหรับตำแหน่ง Symfony มักครอบคลุมการผสาน GraphQL มากขึ้นเรื่อยๆ คำถามเหล่านี้ทดสอบความเข้าใจทั้งข้อกำหนดและการใช้งานของ API Platform
ถ: API Platform สร้าง GraphQL schema อย่างไร?
API Platform ตรวจสอบ attribute #[ApiResource] และการประกาศประเภท PHP เพื่อสร้าง schema คุณสมบัติของ entity จะกลายเป็น field โดยประเภท PHP ถูก map ไปยังประเภท GraphQL Schema จะถูกสร้างใหม่ในทุก request ในโหมด development และถูก cache ใน production
ถ: ความแตกต่างระหว่าง operation Query และ QueryCollection คืออะไร?
Query ดึงรายการเดียวตาม identifier และต้องการ argument id QueryCollection ส่งคืนหลายรายการพร้อมการ filtering, pagination และ sorting ที่เป็นตัวเลือก ทั้งสองสามารถมี custom resolver ได้ แต่ interface ต่างกัน: QueryItemResolverInterface vs QueryCollectionResolverInterface
ถ: จะจัดการปัญหา N+1 query ใน API Platform GraphQL ได้อย่างไร?
รูปแบบ DataLoader รวมหลาย database query เป็นหนึ่งเดียว API Platform ผสานกับ eager loading ของ Doctrine ผ่าน fetch join ใน query extension สำหรับกรณีที่ซับซ้อน ให้ใช้ custom resolver ที่ใช้ addSelect() ของ Doctrine เพื่อดึง association ใน query เริ่มต้น
// 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');
}ถ: กฎความปลอดภัยของ REST และ GraphQL สามารถแตกต่างกันสำหรับ resource เดียวกันได้หรือไม่?
ได้ REST operation ใช้ security บน #[Get], #[Post] ฯลฯ ในขณะที่ GraphQL operation ใช้ security บน #[Query], #[Mutation] ฯลฯ การแยกนี้อนุญาตให้มีกฎที่เข้มงวดกว่าสำหรับโปรโตคอลหนึ่ง รูปแบบทั่วไปคือการเปิดเผย GraphQL แบบ read-only สำหรับ client สาธารณะในขณะที่ REST mutation ต้องการการยืนยันตัวตน
ถ: จะเพิ่มประเภท scalar ที่กำหนดเองไปยัง GraphQL schema ได้อย่างไร?
ลงทะเบียนประเภทที่กำหนดเองใน config/packages/api_platform.yaml และใช้งาน serialization logic:
# config/packages/api_platform.yaml
api_platform:
graphql:
enabled: true
graphiql:
enabled: truenamespace 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);
}
}กลยุทธ์การแบ่งหน้า: Cursor vs Page-Based
API Platform ใช้ค่าเริ่มต้นเป็นการแบ่งหน้าแบบ cursor ตามข้อกำหนด Relay Connection วิธีนี้จัดการข้อมูลแบบ real-time ได้ดีกว่าการแบ่งหน้าแบบ offset เพราะการแทรกข้อมูลไม่ทำให้ผลลัพธ์เลื่อน
# Cursor-based (default)
query {
books(first: 10, after: "YXJyYXljb25uZWN0aW9uOjk=") {
edges {
cursor
node {
title
}
}
pageInfo {
endCursor
hasNextPage
}
}
}การแบ่งหน้าแบบ page-based เหมาะกับกรณีใช้งานที่ง่ายกว่าเมื่อ client ต้องการเข้าถึงหน้าโดยตรง:
// Enable page-based pagination
#[ApiResource(
paginationType: 'page',
graphQlOperations: [
new QueryCollection(paginationType: 'page'),
]
)]
class Book {}# Page-based
query {
books(page: 2, itemsPerPage: 20) {
collection {
title
}
paginationInfo {
totalCount
lastPage
}
}
}การแบ่งหน้าแบบ cursor ทำงานได้ดีกว่าในระดับขนาดใหญ่เพราะหลีกเลี่ยง query OFFSET ข้อแลกเปลี่ยนคือ client ไม่สามารถข้ามไปหน้าใดก็ได้
การทดสอบ GraphQL Endpoint ใน Symfony
การทดสอบเชิงฟังก์ชันยืนยันพฤติกรรม GraphQL โดยใช้ test client ของ Symfony API Platform มี trait การทดสอบเฉพาะสำหรับ GraphQL
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 ใน Symfony
- ติดตั้ง
api-platform/graphqlเพื่อเปิดใช้งาน endpoint/graphqlพร้อมการสร้าง schema อัตโนมัติจาก attribute#[ApiResource] - ใช้ operation
Query,QueryCollectionและMutationเพื่อควบคุมว่า GraphQL operation ใดที่แต่ละ resource เปิดเผย - ใช้งาน
QueryItemResolverInterfaceหรือMutationResolverInterfaceสำหรับ business logic ที่กำหนดเองนอกเหนือจาก CRUD - Security expression บน GraphQL operation ทำงานแยกจาก REST ทำให้มีกฎการเข้าถึงที่แตกต่างกันต่อโปรโตคอล
- เปิดใช้งาน Mercure สำหรับ subscription แบบ real-time ที่ส่งการเปลี่ยนแปลงไปยัง client เมื่อ resource ถูกอัปเดต
- การแบ่งหน้าแบบ cursor จัดการข้อมูลแบบ real-time ได้ดีกว่าแบบ page-based แต่เสียสละการเข้าถึงหน้าโดยตรง
- ทดสอบ GraphQL endpoint ด้วย
ApiTestCaseและ JSON POST request ไปยัง/graphql - คำถามสัมภาษณ์มุ่งเน้นที่การสร้าง schema ปัญหา N+1 การแยกความปลอดภัย และ custom resolver
คุณหาบั๊กใน Symfony เจอไหม
โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

เขียนโดย
Anthony Fillion-Mailletผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 27 สิงหาคม 2569
แชร์
บทความที่เกี่ยวข้อง

ความปลอดภัย REST API Symfony ปี 2026: OAuth2, Rate Limiting และคำถามสัมภาษณ์
เรียนรู้วิธีรักษาความปลอดภัย REST API Symfony ด้วย OAuth2 token introspection, rate limiting และการตรวจสอบ JWT ครอบคลุมฟีเจอร์ความปลอดภัย Symfony 7.3 ช่องโหว่ทั่วไป และคำถามสัมภาษณ์ทางเทคนิค

API Platform กับ Symfony 2026: สถาปัตยกรรมสมัยใหม่และคำถามสัมภาษณ์งาน
คู่มือครบถ้วนเกี่ยวกับ API Platform กับ Symfony ในปี 2026 ครอบคลุมสถาปัตยกรรม SmartPlatform, State Providers, custom filters และคำถามสัมภาษณ์ทางเทคนิคสำหรับนักพัฒนา

ความปลอดภัย REST API ของ Symfony: การยืนยันตัวตน, JWT และคำถามสัมภาษณ์ 2026
คู่มือฉบับสมบูรณ์เกี่ยวกับความปลอดภัย REST API ของ Symfony ด้วย LexikJWTAuthenticationBundle 3.2 เรียนรู้การตั้งค่า JWT, refresh token, voter, rate limiting และคำถามสัมภาษณ์ทั่วไปสำหรับ Symfony 7.2