Symfony 7 ir API Platform 3

Kas naujo Symfony 7 pasaulyje

Symfony 7 pasirodė kaip tikras gaivališkis PHP bendruomenėje, ir nors iš pirmo žvilgsnio gali pasirodyti, kad tai tik eilinis versijos atnaujinimas, realybė yra kur kas įdomesnė. Šis framework’as jau seniai nėra vien tik įrankis kurti svetaines – tai pilnavertė ekosistema, kuri puikiai dera su moderniais API kūrimo standartais.

Pirmiausia verta paminėti, kad Symfony 7 išlaikė savo tradiciją būti atgaline suderinamumu orientuotu projektu. Tai reiškia, kad jei jūsų projektas veikė ant Symfony 6.4, migracija neturėtų sukelti galvos skausmo. Tačiau svarbiausia čia ne tai, ko nesulaužė, o tai, ką pridėjo ir patobulino.

Vienas iš ryškiausių pokyčių – tai PHP 8.2 kaip minimali reikalaujama versija. Gali pasirodyti, kad tai tik techninis reikalavimas, bet iš tiesų tai atidaro duris naudoti visas naujausias PHP galimybes. Readonly klasės, disjunctive normal form tipai ir kiti modernūs dalykai dabar yra ne tik galimi, bet ir aktyviai naudojami pačiame framework’e.

API Platform 3 – ne tik CRUD generatorius

Kai kalbame apie API Platform 3, daugelis žmonių vis dar mano, kad tai tiesiog įrankis greitai sugeneruoti CRUD operacijas. Taip, jis tai daro puikiai, bet tai tik ledkalnio viršūnė. Trečioji versija atnešė tokių architektūrinių pokyčių, kad dabar galima kurti tikrai sudėtingas API sistemas, neprarandant kodo skaidrumo.

Vienas iš svarbiausių pakeitimų – tai nauja būdo, kaip aprašomi API resursai, filosofija. Anksčiau viskas sukosi aplink Doctrine entitus, o dabar galite naudoti paprastus PHP objektus (DTO) kaip pirminį šaltinį. Tai gali skambėti kaip smulkmena, bet praktikoje tai reiškia, kad jūsų domenų logika gali būti visiškai atskirta nuo API sluoksnio.


#[ApiResource(
operations: [
new Get(),
new GetCollection(),
new Post(processor: CreateUserProcessor::class),
],
provider: UserProvider::class
)]
class User
{
public function __construct(
public readonly string $id,
public string $email,
public string $name,
) {}
}

Šis pavyzdys rodo, kaip galite aprašyti resursą nenaudodami Doctrine anotacijų. Viskas paprasta, skaitoma ir lengvai testuojama.

State Providers ir Processors – tikroji galia

Jei norite suprasti, kodėl API Platform 3 yra toks lankstus, turite įsisavinti State Providers ir State Processors koncepciją. Tai ne tiesiog dar vienas abstraction layer – tai architektūrinis sprendimas, leidžiantis atskirti duomenų gavimą nuo jų apdorojimo.

State Provider atsakingas už tai, kaip jūsų duomenys yra gaunami. Galbūt jie ateina iš Doctrine, galbūt iš Elasticsearch, o gal net iš išorinio API. Jums nereikia keisti resurso aprašymo – tiesiog pakeičiate provider’į:


final class UserProvider implements ProviderInterface
{
public function __construct(
private UserRepository $repository,
private CacheInterface $cache,
) {}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$cacheKey = ‘user_’ . $uriVariables[‘id’];

return $this->cache->get($cacheKey, function() use ($uriVariables) {
return $this->repository->find($uriVariables[‘id’]);
});
}
}

Čia matome, kaip lengvai galima įterpti cache’inimą neliečiant pagrindinės logikos. State Processor veikia analogiškai, tik apdorojant duomenų keitimą (POST, PUT, PATCH, DELETE).

Validacija ir transformacijos – daugiau nei Assert anotacijos

Symfony validacijos komponentas visada buvo galingas, bet kartu su API Platform 3 jis įgauna naują prasmę. Dabar galite kurti sudėtingas validacijos taisykles, kurios priklauso nuo konteksto, vartotojo rolės ar net išorinių sistemų būsenos.

Praktikoje tai reiškia, kad galite turėti skirtingas validacijos taisykles skirtingoms operacijoms. Pavyzdžiui, kuriant naują vartotoją, email laukas yra privalomas, bet atnaujinant – neprivalomas:


#[ApiResource(
operations: [
new Post(validationContext: ['groups' => ['create']]),
new Patch(validationContext: ['groups' => ['update']]),
]
)]
class User
{
#[Assert\NotBlank(groups: ['create'])]
#[Assert\Email(groups: ['create', 'update'])]
public string $email;
}

Bet tikroji magija prasideda, kai pradedi naudoti custom validators. Galite sukurti validator’ių, kuris patikrina, ar email domenas yra leidžiamų sąraše, ar vartotojo amžius atitinka tam tikrus reikalavimus, ar net ar išorinė sistema patvirtina tam tikrus duomenis.

Serialization groups ir duomenų transformavimas

Viena iš dažniausių problemų kuriant API – tai kaip kontroliuoti, kokie duomenys yra grąžinami skirtingoms vartotojų grupėms. Administratorius turėtų matyti daugiau informacijos nei paprastas vartotojas, bet kaip tai elegantiškai implementuoti?

API Platform 3 kartu su Symfony Serializer komponentu siūlo labai elegantišką sprendimą per serialization groups. Idėja paprasta – kiekvienam laukui priskiri grupes, o tada API resurso lygmenyje nurodi, kurias grupes naudoti:


class User
{
#[Groups(['user:read', 'user:write', 'admin:read'])]
public string $email;

#[Groups([‘admin:read’])]
public string $ipAddress;

#[Groups([‘user:read’, ‘admin:read’])]
public \DateTimeInterface $createdAt;
}

Tada API resurse:


#[ApiResource(
operations: [
new Get(normalizationContext: ['groups' => ['user:read']]),
new GetCollection(
normalizationContext: ['groups' => ['user:read']],
security: "is_granted('ROLE_ADMIN')",
securityPostDenormalize: "is_granted('ROLE_ADMIN')",
),
]
)]

Tokiu būdu administratorius GET užklausoje gaus visus laukus, įskaitant IP adresą, o paprastas vartotojas – tik jam skirtą informaciją.

OpenAPI dokumentacija ir Swagger UI integracija

Vienas iš didžiausių API Platform privalumų – automatinė OpenAPI (buvusi Swagger) dokumentacijos generacija. Bet trečiojoje versijoje tai buvo iš esmės perrašyta, ir dabar dokumentacija yra ne tik automatinė, bet ir labai lengvai pritaikoma.

Galite pridėti custom aprašymus, pavyzdžius, net keisti endpoint’ų grupavimą dokumentacijoje. Ir visa tai daroma per PHP atributus, be jokių YAML ar JSON failų:


#[ApiResource(
operations: [
new Post(
openapi: new Model\Operation(
summary: 'Sukurti naują vartotoją',
description: 'Šis endpoint'as leidžia sukurti naują vartotoją sistemoje. Email turi būti unikalus.',
requestBody: new Model\RequestBody(
content: new \ArrayObject([
'application/json' => [
'schema' => [
'type' => 'object',
'properties' => [
'email' => ['type' => 'string', 'example' => '[email protected]'],
'name' => ['type' => 'string', 'example' => 'Jonas Jonaitis'],
],
],
],
])
)
)
),
]
)]

Taip, tai atrodo gana verbose, bet realybėje tokių detalių aprašymų reikia tik sudėtingesniems endpoint’ams. Dauguma atvejų automatinė generacija veikia puikiai.

Saugumo aspektai ir autentifikacija

Saugumas API kūrime yra ne mažiau svarbus nei funkcionalumas. Symfony 7 kartu su API Platform 3 siūlo keletą būdų, kaip apsaugoti jūsų endpoint’us, ir svarbiausia – viskas integruota į bendrą Symfony saugumo sistemą.

Paprasčiausias būdas – naudoti security atributą tiesiog API resurso aprašyme:


#[ApiResource(
operations: [
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Delete(
security: "is_granted('ROLE_ADMIN') or object.owner == user",
securityMessage: 'Tik administratoriai arba resursų savininkai gali juos ištrinti.'
),
]
)]

Bet kas su JWT, OAuth2 ar kitais moderniais autentifikacijos metodais? Čia į pagalbą ateina LexikJWTAuthenticationBundle, kuris puikiai integruojasi su visa ekosistema. Nustatymas paprastas, o rezultatas – saugus ir standartizuotas API.

Praktinis patarimas: nenaudokite session-based autentifikacijos API endpoint’uose. JWT ar API raktai yra daug tinkamesni stateless API architektūrai. Taip jūsų API bus lengviau horizontaliai skalė ir neturėsite problemų su CORS.

Realaus pasaulio pavyzdys ir geriausia praktika

Teorija teorija, bet kaip visa tai atrodo realiame projekte? Paimkime tipinį e-commerce API atvejį – produktų valdymas su kategorijomis, kainomis ir inventoriumi.

Pirma, sukuriame DTO, kuris reprezentuoja produktą:


#[ApiResource(
operations: [
new GetCollection(provider: ProductCollectionProvider::class),
new Get(provider: ProductProvider::class),
new Post(
processor: CreateProductProcessor::class,
security: "is_granted('ROLE_ADMIN')"
),
new Patch(
processor: UpdateProductProcessor::class,
security: "is_granted('ROLE_ADMIN')"
),
],
normalizationContext: ['groups' => ['product:read']],
denormalizationContext: ['groups' => ['product:write']],
)]
class Product
{
#[Groups(['product:read'])]
public string $id;

#[Groups([‘product:read’, ‘product:write’])]
#[Assert\NotBlank]
#[Assert\Length(min: 3, max: 255)]
public string $name;

#[Groups([‘product:read’, ‘product:write’])]
#[Assert\NotBlank]
#[Assert\Positive]
public float $price;

#[Groups([‘product:read’, ‘product:write’])]
#[Assert\PositiveOrZero]
public int $stock;

#[Groups([‘product:read’])]
public \DateTimeInterface $createdAt;
}

Tada sukuriame Provider’į, kuris gauna duomenis iš Doctrine:


final class ProductProvider implements ProviderInterface
{
public function __construct(
private ProductRepository $repository,
private PriceCalculator $priceCalculator,
) {}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$product = $this->repository->find($uriVariables[‘id’]);

if (!$product) {
return null;
}

// Galime pridėti papildomos logikos, pvz., dinamiškai skaičiuoti kainą
$dto = new Product();
$dto->id = $product->getId();
$dto->name = $product->getName();
$dto->price = $this->priceCalculator->calculateFinalPrice($product);
$dto->stock = $product->getStock();
$dto->createdAt = $product->getCreatedAt();

return $dto;
}
}

Matote šabloną? Provider’is atsakingas už duomenų gavimą ir transformavimą į DTO. Jis gali pridėti papildomos logikos, kaip kainų skaičiavimas su nuolaidomis, bet pats DTO lieka paprastas ir testuojamas.

Processor’ius apdoroja duomenų keitimą:


final class CreateProductProcessor implements ProcessorInterface
{
public function __construct(
private EntityManagerInterface $entityManager,
private EventDispatcherInterface $eventDispatcher,
) {}

public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Product
{
$entity = new ProductEntity();
$entity->setName($data->name);
$entity->setPrice($data->price);
$entity->setStock($data->stock);
$entity->setCreatedAt(new \DateTimeImmutable());

$this->entityManager->persist($entity);
$this->entityManager->flush();

// Galime išsiųsti event’ą kitiems sistemos komponentams
$this->eventDispatcher->dispatch(new ProductCreatedEvent($entity));

// Grąžiname DTO
$dto = new Product();
$dto->id = $entity->getId();
$dto->name = $entity->getName();
$dto->price = $entity->getPrice();
$dto->stock = $entity->getStock();
$dto->createdAt = $entity->getCreatedAt();

return $dto;
}
}

Kai viskas susideda į vieną visumą

Symfony 7 ir API Platform 3 kartu sudaro galingą duetą modernių API sistemų kūrimui. Svarbiausia čia ne tai, kad galite greitai sugeneruoti CRUD operacijas (nors tai irgi naudinga), o tai, kad turite visą ekosistemą, kuri leidžia kurti sudėtingas, saugias ir lengvai prižiūrimas sistemas.

Jei tik pradedate su šiomis technologijomis, rekomenduoju pradėti nuo paprastų CRUD operacijų su Doctrine entitais. Kai suprasite pagrindus, pereikite prie DTO ir custom Provider’ių/Processor’ių. Taip pamažu įsigilinsite į architektūrą ir suprasite, kodėl tam tikri sprendimai buvo priimti.

Svarbu nepamiršti, kad dokumentacija yra jūsų geriausia draugė. Tiek Symfony, tiek API Platform turi puikią dokumentaciją su daugybe pavyzdžių. Taip pat aktyvios bendruomenės – jei užstrigsite, visada galite paklausti Slack kanale ar Stack Overflow.

Ir paskutinis patarimas – nenaudokite visų feature’ų iš karto. Pradėkite paprastai, o sudėtingumą pridėkite tik tada, kai to tikrai reikia. Geriausias kodas yra tas, kuris išsprendžia problemą paprasčiausiu būdu, o ne tas, kuris naudoja visas įmanomas framework’o galimybes.

Daugiau

Grafana Loki: log agregacija Kubernetes