Symfony 7: API Platform ve En Iyi Uygulamalar
Symfony 7 ve API Platform 4 ile profesyonel REST API'leri gelistirmek icin kapsamli rehber. State Provider'lar, Processor'lar, dogrulama ve serializasyon orneklerle aciklanmistir.

API Platform 4, Symfony 7 ile birlikte REST ve GraphQL API olusturma yaklasimini kokeninden degistirmektedir. Bu yeni surum netlestirilmis bir felsefe uzerine insadir: endiselerin net ayrilmasi, basitlestirilmis State Provider ve Processor yapisi ve Symfony Object Mapper'in yerlesik entegrasyonu. Profesyonel bir API gelistirmek artik bu kadar sistematik bir hal almamisti.
Surum 4.2; saniyedeki istek sayisinda %32'ye varan performans artisi saglayan JSON Streamer'i, yeniden tasarlanmis filtre sistemini ve cekirdege dokunmadan operasyonlari ozellestirmeye yarayan Mutator'lari getirmektedir. Symfony 7 ve 8 destegi yerlesik olarak sunulmaktadir.
Kurulum ve Ilk Yapilandirma
API Platform, Symfony Flex araciligiyla birkaç komutla kurulmaktadir. Varsayilan yapilandirma cogu kullanim senaryosunu karsilarken tamamen ozerlestirilebilir olmaya devam etmektedir.
# terminal
# Create a new Symfony project with API Platform
composer create-project symfony/skeleton my-api
cd my-api
# Install API Platform with Doctrine ORM
composer require api
# Verify installation
php bin/console debug:router | grep apiSymfony Flex; rotalari, OpenAPI dokumantasyonunu ve /api adresinden erisilen Swagger UI arayuzunu otomatik olarak yapilandirmaktadir.
# config/packages/api_platform.yaml
api_platform:
title: 'My API'
version: '1.0.0'
# Supported response formats
formats:
jsonld: ['application/ld+json']
json: ['application/json']
# OpenAPI documentation
swagger:
versions: [3]
# Default pagination
defaults:
pagination_items_per_page: 30
pagination_maximum_items_per_page: 100Bu yapilandirma; serializasyon formatlarini, global sayfalama ayarlarini ve API dokumantasyon meta verilerini tanimlamaktadir.
Basit Bir API Kaynagi Olusturma
#[ApiResource] niteligi, bir Doctrine varligini REST kaynagi olarak disariya acmaktadir. API Platform; CRUD endpoint'leri, OpenAPI dokumantasyonu ve temel dogrulamalari otomatik olarak uretmektedir.
<?php
// src/Entity/Book.php
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\Patch;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
// Explicit definition of available operations
operations: [
new GetCollection(),
new Post(),
new Get(),
new Put(),
new Patch(),
new Delete(),
],
// Default ordering for collections
order: ['publishedAt' => 'DESC'],
// Pagination configuration for this resource
paginationItemsPerPage: 20
)]
class Book
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank(message: 'Title is required')]
#[Assert\Length(min: 2, max: 255)]
private ?string $title = null;
#[ORM\Column(type: 'text')]
#[Assert\NotBlank]
private ?string $description = null;
#[ORM\Column(length: 13, unique: true)]
#[Assert\Isbn]
private ?string $isbn = null;
#[ORM\Column]
private ?\DateTimeImmutable $publishedAt = null;
// Getters and setters...
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(string $description): static
{
$this->description = $description;
return $this;
}
public function getIsbn(): ?string
{
return $this->isbn;
}
public function setIsbn(string $isbn): static
{
$this->isbn = $isbn;
return $this;
}
public function getPublishedAt(): ?\DateTimeImmutable
{
return $this->publishedAt;
}
public function setPublishedAt(\DateTimeImmutable $publishedAt): static
{
$this->publishedAt = $publishedAt;
return $this;
}
}Bu varlik alti endpoint olusturmaktadir: GET /api/books, POST /api/books, GET /api/books/{id}, PUT /api/books/{id}, PATCH /api/books/{id} ve DELETE /api/books/{id}.
API Platform, tanimlayici olarak UUID v7'yi yerel duzeyde desteklemektedir. Bu yaklasim hem guvenlik (tahmin edilemez tanimlayicilar) hem de performans (olusturulma tarihine gore dogal siralama) acisindan avantaj saglamaktadir.
Aciga Cikarilan Verileri Kontrol Etmek icin Serializasyon Gruplari
Serializasyon gruplari; okuma (normalizasyon) ve yazma (denormalizasyon) sirasinda hangi ozelliklerin disariya acilacagini tam olarak tanimlamaktadir. Bu ayrim, API guvenlik ve esnekligi acisindan temel bir gereklilik olmaktadir.
<?php
// src/Entity/User.php
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\Patch;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use App\State\UserPasswordHasher;
#[ORM\Entity]
#[ORM\Table(name: '`user`')]
#[UniqueEntity('email')]
#[ApiResource(
operations: [
new GetCollection(),
// Custom processor for password hashing
new Post(processor: UserPasswordHasher::class,
validationContext: ['groups' => ['Default', 'user:create']]),
new Get(),
new Put(processor: UserPasswordHasher::class),
new Patch(processor: UserPasswordHasher::class),
new Delete(),
],
// Properties exposed when reading
normalizationContext: ['groups' => ['user:read']],
// Properties accepted when writing
denormalizationContext: ['groups' => ['user:create', 'user:update']],
)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['user:read'])]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
#[Assert\NotBlank]
#[Assert\Email]
#[Groups(['user:read', 'user:create', 'user:update'])]
private ?string $email = null;
#[ORM\Column]
private ?string $password = null;
// Never exposed when reading, only when writing
#[Assert\NotBlank(groups: ['user:create'])]
#[Groups(['user:create', 'user:update'])]
private ?string $plainPassword = null;
#[ORM\Column(length: 100)]
#[Groups(['user:read', 'user:create', 'user:update'])]
private ?string $fullName = null;
#[ORM\Column(type: 'json')]
#[Groups(['user:read'])]
private array $roles = [];
#[ORM\Column]
#[Groups(['user:read'])]
private ?\DateTimeImmutable $createdAt = null;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
}
// UserInterface implementation
public function getUserIdentifier(): string
{
return (string) $this->email;
}
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function getPassword(): string
{
return $this->password;
}
public function eraseCredentials(): void
{
$this->plainPassword = null;
}
// Getters and setters...
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): static
{
$this->email = $email;
return $this;
}
public function setPassword(string $password): static
{
$this->password = $password;
return $this;
}
public function getPlainPassword(): ?string
{
return $this->plainPassword;
}
public function setPlainPassword(?string $plainPassword): static
{
$this->plainPassword = $plainPassword;
return $this;
}
public function getFullName(): ?string
{
return $this->fullName;
}
public function setFullName(string $fullName): static
{
$this->fullName = $fullName;
return $this;
}
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
}Bu yapilandirmayla plainPassword, yanit govdesinde hicbir zaman yer almamakta; yalnizca olusturma veya guncelleme sirasinda gonderilebilmektedir.
Symfony mülakatlarında başarılı olmaya hazır mısın?
İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.
Is Mantigi icin State Processor'lar
State Processor'lar, is mantigi eklemek amacıyla kalicilik islemlerini yakalayin. API Platform 4, ozellik tabanli bagimlilik enjeksiyonu sayesinde bu sinifların olusturulmasini onemli olcude kolaylastirmaktadir.
<?php
// src/State/UserPasswordHasher.php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\User;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
/**
* Processor that hashes password before persistence
* @implements ProcessorInterface<User, User>
*/
final class UserPasswordHasher implements ProcessorInterface
{
public function __construct(
// Injection of standard Doctrine processor
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
private ProcessorInterface $persistProcessor,
private UserPasswordHasherInterface $passwordHasher,
) {
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User
{
// Hash password if provided
if ($data->getPlainPassword()) {
$hashedPassword = $this->passwordHasher->hashPassword(
$data,
$data->getPlainPassword()
);
$data->setPassword($hashedPassword);
$data->eraseCredentials();
}
// Delegate persistence to standard processor
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
}
}Bu kompozisyon deseni; standart kalicilik davranisini korurken herhangi bir is mantiginin (e-posta gonderme, olay yayinlama, gunluk tutma) eklenmesine olanak tanimaktadir.
Kosullu Mantikli Processor
Bir Processor, operasyon turune gore davranisini uyarlayabilmektedir.
<?php
// src/State/BookProcessor.php
namespace App\State;
use ApiPlatform\Metadata\DeleteOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Post;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Book;
use App\Service\NotificationService;
use App\Service\SearchIndexer;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* @implements ProcessorInterface<Book, Book|void>
*/
final class BookProcessor implements ProcessorInterface
{
public function __construct(
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
private ProcessorInterface $persistProcessor,
#[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')]
private ProcessorInterface $removeProcessor,
private NotificationService $notifications,
private SearchIndexer $searchIndexer,
) {
}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
// Deletion: use remove processor
if ($operation instanceof DeleteOperationInterface) {
$this->searchIndexer->remove($data);
return $this->removeProcessor->process($data, $operation, $uriVariables, $context);
}
// Creation: set publication date
if ($operation instanceof Post) {
$data->setPublishedAt(new \DateTimeImmutable());
}
// Standard persistence
$result = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
// Post-processing: indexing and notification
$this->searchIndexer->index($result);
if ($operation instanceof Post) {
$this->notifications->notifyNewBook($result);
}
return $result;
}
}Ozel Veri Kaynaklari icin State Provider'lar
State Provider'lar; harici API'ler, onbellek, dosyalar veya karmasik is mantigi gibi herhangi bir kaynaktan veri cekmektedir.
<?php
// src/State/PopularBooksProvider.php
namespace App\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Entity\Book;
use App\Repository\BookRepository;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
/**
* Provider that returns popular books with caching
* @implements ProviderInterface<Book>
*/
final class PopularBooksProvider implements ProviderInterface
{
public function __construct(
private BookRepository $bookRepository,
private CacheInterface $cache,
) {
}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
{
// 5-minute cache for popular books
return $this->cache->get('popular_books', function (ItemInterface $item) {
$item->expiresAfter(300);
return $this->bookRepository->findPopular(limit: 10);
});
}
}Bu Provider, ayri bir operasyonda kullanilmaktadir.
<?php
// src/Entity/Book.php (excerpt)
use App\State\PopularBooksProvider;
#[ApiResource(
operations: [
// ... other operations
new GetCollection(
uriTemplate: '/books/popular',
provider: PopularBooksProvider::class,
paginationEnabled: false,
),
],
)]
class Book
{
// ...
}Dinamik Gruplarla Gelismis Dogrulama
API Platform, Symfony Validator bilesenini yerlesik olarak entegre etmektedir. Dogrulama gruplari; operasyona veya baglamına gore degiskenlik gosterebilmektedir.
<?php
// src/Entity/Article.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity]
#[ApiResource(
operations: [
new GetCollection(),
new Get(),
// Strict validation on creation
new Post(validationContext: ['groups' => ['Default', 'article:create']]),
// More lenient validation for updates
new Put(validationContext: ['groups' => ['Default', 'article:update']]),
],
)]
class Article
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
#[Assert\Length(min: 10, max: 255)]
private ?string $title = null;
#[ORM\Column(type: 'text')]
#[Assert\NotBlank]
// Minimum 500 characters on creation
#[Assert\Length(min: 500, groups: ['article:create'])]
// Minimum 100 characters for updates
#[Assert\Length(min: 100, groups: ['article:update'])]
private ?string $content = null;
#[ORM\Column(length: 50)]
#[Assert\NotBlank(groups: ['article:create'])]
#[Assert\Choice(choices: ['draft', 'published', 'archived'])]
private ?string $status = 'draft';
#[ORM\Column(nullable: true)]
// Required only if status is "published"
#[Assert\NotBlank(groups: ['article:publish'])]
private ?\DateTimeImmutable $publishedAt = null;
// Getters and setters...
}Servis Yardimiyla Dinamik Dogrulama
Karmasik dogrulama kurallari icin ozel bir grup ureteci tam esneklik sunmaktadir.
<?php
// src/Validator/ArticleGroupsGenerator.php
namespace App\Validator;
use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface;
use App\Entity\Article;
use Symfony\Bundle\SecurityBundle\Security;
final class ArticleGroupsGenerator implements ValidationGroupsGeneratorInterface
{
public function __construct(
private Security $security,
) {
}
public function __invoke(object $object): array
{
assert($object instanceof Article);
$groups = ['Default'];
// Admins have fewer restrictions
if ($this->security->isGranted('ROLE_ADMIN')) {
$groups[] = 'admin';
return $groups;
}
// Additional validation if publishing
if ($object->getStatus() === 'published') {
$groups[] = 'article:publish';
}
return $groups;
}
}Karmasik dogrulamalar performansi olumsuz etkileyebilmektedir. Toplu iceri aktarmalarda bazi dogrulamalarin gecici olarak devre disi birakilmasi veya asenkron kisitlamalarin kullanilmasi degerlendirilmelidir.
Esnek Sorgular icin Filtreler
API Platform 4.2, endiseler arasinda net bir ayrim yaparak filtre sistemini bastan tasarlamaktadir. Filtreler, API istemcilerinin verileri aramasini ve siralamasini mumkun kilmaktadir.
<?php
// src/Entity/Product.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ApiResource]
// Text search on name and description
#[ApiFilter(SearchFilter::class, properties: [
'name' => 'partial', // LIKE %value%
'description' => 'partial',
'category.name' => 'exact', // Search on relation
'sku' => 'exact', // Exact match
])]
// Range filtering
#[ApiFilter(RangeFilter::class, properties: ['price', 'stock'])]
// Date filtering
#[ApiFilter(DateFilter::class, properties: ['createdAt', 'updatedAt'])]
// Boolean filtering
#[ApiFilter(BooleanFilter::class, properties: ['isActive', 'isFeatured'])]
// Customizable sorting
#[ApiFilter(OrderFilter::class, properties: [
'name',
'price',
'createdAt',
], arguments: ['orderParameterName' => 'sort'])]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $description = null;
#[ORM\Column(length: 50, unique: true)]
private ?string $sku = null;
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
private ?string $price = null;
#[ORM\Column]
private ?int $stock = null;
#[ORM\Column]
private ?bool $isActive = true;
#[ORM\Column]
private ?bool $isFeatured = false;
#[ORM\ManyToOne(targetEntity: Category::class)]
private ?Category $category = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
// Getters and setters...
}Bu filtreler OpenAPI dokumantasyonunu otomatik olarak olusturmakta ve su turde sorgulara olanak tanimaktadir:
GET /api/products?name=phone&price[gte]=100&price[lte]=500&isActive=true&sort[price]=ascIliskiler ve Alt Kaynaklar
API Platform; varlik iliskilerini serializasyon secenekleri ve alt kaynaklar araciligiyla zarif bir sekilde yonetmektedir.
<?php
// src/Entity/Author.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity]
#[ApiResource(
normalizationContext: ['groups' => ['author:read']],
)]
class Author
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['author:read', 'book:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['author:read', 'book:read'])]
private ?string $name = null;
#[ORM\Column(type: 'text', nullable: true)]
#[Groups(['author:read'])]
private ?string $biography = null;
#[ORM\OneToMany(mappedBy: 'author', targetEntity: Book::class)]
#[Groups(['author:read'])]
private Collection $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
// Getters and setters...
}<?php
// src/Entity/Book.php (with relation)
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity]
#[ApiResource(
normalizationContext: ['groups' => ['book:read']],
)]
// Subresource: GET /api/authors/{authorId}/books
#[ApiResource(
uriTemplate: '/authors/{authorId}/books',
operations: [new GetCollection()],
uriVariables: [
'authorId' => new Link(
fromProperty: 'books',
fromClass: Author::class
),
],
normalizationContext: ['groups' => ['book:read']],
)]
class Book
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['book:read', 'author:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['book:read', 'author:read'])]
private ?string $title = null;
#[ORM\ManyToOne(targetEntity: Author::class, inversedBy: 'books')]
#[ORM\JoinColumn(nullable: false)]
#[Groups(['book:read'])]
private ?Author $author = null;
// Getters and setters...
}Guvenlik ve Erisim Kontrolu
API Platform, Symfony guvenlik sistemiyle sorunsuz entegre olmaktadir. Voter'lar ve guvenlik ifadeleri kaynak erisimini denetlemektedir.
<?php
// src/Entity/Order.php
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ApiResource(
operations: [
// List: authenticated users see their orders
new GetCollection(
security: "is_granted('ROLE_USER')",
// Automatic filter by connected user
provider: UserOrdersProvider::class,
),
// Creation: any authenticated user
new Post(
security: "is_granted('ROLE_USER')",
processor: CreateOrderProcessor::class,
),
// Read: owner or admin
new Get(
security: "is_granted('ROLE_ADMIN') or object.getCustomer() == user",
securityMessage: "Access denied to this order.",
),
// Modification: admin only
new Patch(
security: "is_granted('ROLE_ADMIN')",
),
// Deletion: admin only
new Delete(
security: "is_granted('ROLE_ADMIN')",
),
],
)]
class Order
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false)]
private ?User $customer = null;
#[ORM\Column(length: 50)]
private ?string $status = 'pending';
#[ORM\Column(type: 'decimal', precision: 10, scale: 2)]
private ?string $total = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
// Getters...
public function getCustomer(): ?User
{
return $this->customer;
}
}object.getCustomer() == user ifadesi; ayrintili erisim denetimi icin mevcut varliga ve oturum acmis kullaniciya erisim saglamaktadir.
Otomatik API Testleri
API Platform, endpoint'leri kolayca test etmek icin PHPUnit trait'leri sunmaktadir.
<?php
// tests/Api/BookTest.php
namespace App\Tests\Api;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Book;
use App\Factory\BookFactory;
use App\Factory\UserFactory;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;
class BookTest extends ApiTestCase
{
use ResetDatabase;
use Factories;
public function testGetCollection(): void
{
// Create test data
BookFactory::createMany(30);
// GET request on collection
$response = static::createClient()->request('GET', '/api/books');
// Assertions
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains([
'@context' => '/api/contexts/Book',
'@type' => 'Collection',
'totalItems' => 30,
]);
// Verify pagination (20 items per page)
$this->assertCount(20, $response->toArray()['member']);
}
public function testCreateBook(): void
{
$user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]);
static::createClient()->request('POST', '/api/books', [
'auth_bearer' => $this->getToken($user),
'json' => [
'title' => 'Clean Code',
'description' => 'A Handbook of Agile Software Craftsmanship',
'isbn' => '9780132350884',
],
]);
$this->assertResponseStatusCodeSame(201);
$this->assertJsonContains([
'@type' => 'Book',
'title' => 'Clean Code',
]);
}
public function testCreateBookValidationFails(): void
{
$user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]);
static::createClient()->request('POST', '/api/books', [
'auth_bearer' => $this->getToken($user),
'json' => [
'title' => '', // Empty title = error
'isbn' => 'invalid-isbn',
],
]);
$this->assertResponseStatusCodeSame(422);
$this->assertJsonContains([
'@type' => 'ConstraintViolationList',
'violations' => [
['propertyPath' => 'title', 'message' => 'Title is required'],
],
]);
}
private function getToken(object $user): string
{
// Implementation depends on your authentication system
return 'test_token';
}
}Sonuc
Symfony 7 ile API Platform 4, PHP ekosisteminde profesyonel REST API olusturmanin doru dorugunda yer almaktadir. State Provider'lar (okuma) ile State Processor'lar (yazma) arasindaki net ayrim; serializasyon gruplari ve dogrulama sistemiyle birleste saglam ve surdurulebilir API'lerin gelistirilmesini mumkun kilmaktadir.
Kaliteli API'ler icin Kontrol Listesi
- Okuma ve yazma icin birbirinden farkli serializasyon gruplari kullanilmalidir
- Is mantigi icin State Processor uygulanmalidir (sifre sifrelemesi, bildirimler)
- Arama ve siralama icin filtreler yapilandirilmalidir
- Gruplarla operasyon bazli dogrulamalar uygulanmalidir
- Endpoint'ler guvenlik ifadeleriyle korunmalidir
- Her endpoint icin islevsel testler yazilmalidir
- API, OpenAPI meta verileri araciligiyla belgelenmis olmalidir
Pratik yapmaya başla!
Mülakat simülatörleri ve teknik testlerle bilgini test et.
API Platform 4'un felsefesi; kalintiliktan once kompozisyonu, gelenekten once yapilandirmayi one cikarmaktadir. Sonuc: REST/JSON-LD standartlarina uyan, olceklenebilir ve test edilebilir API'ler; ilk gununden itibaren uretim ortamina hazir olmaktadir.
Etiketler
Paylaş
