Kodėl Symfony išlieka populiarus 2024-aisiais
Symfony jau seniai nėra tik dar vienas PHP framework’as – tai brandus, gerai apgalvotas ekosistema, kuris suteikia kūrėjams stabilumą ir lankstumą. Kai pradedi dirbti su Symfony, iš pradžių gali atrodyti, kad tai per daug sudėtinga. Yra daug konfigūracijos, anotacijų, servisų konteineriaus subtilybių. Bet kai įsigilinsi, supranti, kad visa tai egzistuoja ne be priežasties.
Daugelis projektų, kurie prasidėjo su lengvesniais framework’ais, vėliau migruoja į Symfony, kai verslo logika tampa sudėtingesnė. Ir čia slypi pagrindinis dalykas – Symfony skirtas rimtiems projektams, kurie turi augti ir vystytis metų metus. Tai ne sprint’as, o maratonas.
Šiame straipsnyje pažvelgsime į realias geriausias praktikas, kurios padės jums išvengti skausmingų klaidų ir sukurti kodą, kurį norėsite palaikyti net po kelių metų.
Servisų konteinerio išmintis
Dependency Injection konteineris yra Symfony širdis. Bet daugelis kūrėjų jį naudoja tik paviršutiniškai, neišnaudodami viso potencialo. Pirmiausia – niekada neįtraukite konteinerio į savo servisus tiesiogiai. Tai anti-pattern’as, kuris vadinama „service locator” ir kuris nieko gero nežada.
Vietoj to, visada deklaruokite priklausomybes per konstruktorių. Taip jūsų kodas tampa testuojamas, skaitomas ir suprantamas. Symfony automatinis wiring’as čia tikrai padeda – framework’as pats sugeba suprasti, kokius servisus reikia įterpti.
// Blogai
class UserManager
{
public function __construct(private ContainerInterface $container)
{
}
public function doSomething()
{
$mailer = $this->container->get('mailer');
}
}
// Gerai
class UserManager
{
public function __construct(
private MailerInterface $mailer,
private LoggerInterface $logger
) {
}
}
Kitas svarbus momentas – naudokite interface’us vietoj konkrečių klasių. Tai leidžia lengvai keisti implementacijas, testuoti su mock’ais ir laikytis SOLID principų. Symfony konfigūracijoje galite nurodyti, kurią konkrečią klasę naudoti tam tikram interface’ui.
Dar viena dažna klaida – kurti per daug smulkių servisų. Taip, single responsibility principas yra svarbus, bet nereikia eiti į kraštutinumus. Jei jūsų servisas turi vieną metodą ir niekada nebus perpanaudotas kitur, galbūt jis turėtų būti tiesiog private metodas kitoje klasėje.
Kontrolerių higiena ir routing’o strategijos
Kontroleriai Symfony turėtų būti kuo plonesni. Jų vienintelė atsakomybė – priimti HTTP užklausą, perduoti ją atitinkamam servisui ir grąžinti atsakymą. Visa verslo logika turi gyventi servise, ne kontroleryje.
Matau projektus, kur kontroleriai tampa 500 eilučių monstrais su SQL užklausomis, verslo logika ir viskuo kitu. Tai katastrofa palaikomumui. Jei jūsų kontrolerio metodas ilgesnis nei 20-30 eilučių, greičiausiai darote kažką ne taip.
// Blogai
public function createUser(Request $request): Response
{
$data = json_decode($request->getContent());
// 50 eilučių validacijos
// 30 eilučių duomenų bazės operacijų
// 20 eilučių email siuntimo
// ir t.t.
}
// Gerai
public function createUser(
Request $request,
UserCreator $userCreator
): Response
{
$user = $userCreator->create(
$request->request->all()
);
return $this->json($user, Response::HTTP_CREATED);
}
Dėl routing’o – naudokite anotacijas arba atributus (PHP 8+), ne YAML failus. Taip jūsų route’ai yra šalia kontrolerio kodo, ir nereikia šokinėti tarp failų. Tai ypač patogu didesniuose projektuose.
Pavadinkite savo route’us prasmingai. Venkite tokių pavadinimų kaip „app_user_1” ar „homepage_2”. Naudokite aiškius, aprašomuosius pavadinimus: „user_profile_edit”, „api_product_list” ir panašiai. Tai labai palengvina debugging’ą ir URL generavimą.
Doctrine ORM – draugas ar priešas?
Doctrine yra galingas įrankis, bet jis gali tapti našumo košmaru, jei nežinote, ką darote. Pirmiausia – visada naudokite eager loading, kai žinote, kad jums reikės susijusių objektų. Lazy loading’as sukelia N+1 query problemą, kuri gali užmušti jūsų aplikacijos greitį.
// Blogai - sukurs daug atskirų užklausų
$users = $userRepository->findAll();
foreach ($users as $user) {
echo $user->getProfile()->getBio(); // Kiekvienam user atskira užklausa
}
// Gerai
$users = $userRepository->createQueryBuilder('u')
->leftJoin('u.profile', 'p')
->addSelect('p')
->getQuery()
->getResult();
Antra svarbi praktika – nenaudokite entity’ų kaip DTO API atsakymams. Entity’ai yra domain objektai, jie turi savo gyvenimo ciklą, ryšius, lazy loading’ą. Kai serializuojate juos tiesiogiai į JSON, gaunate visokių problemų – nuo circular references iki netyčia atskleistos informacijos.
Sukurkite atskirus DTO objektus arba naudokite Symfony Serializer su grupėmis. Taip turite pilną kontrolę, kas bus grąžinta API atsakyme.
Dar vienas dalykas – venkite repository metodų, kurie grąžina entity’us su daliniais duomenimis. Jei jums reikia tik kelių laukų, geriau naudokite DTO arba grąžinkite masyvą, ne entity objektą. Doctrine entity su daliniais duomenimis gali sukelti netikėtų problemų, kai bandysite jį persist’inti.
Formų validacija ir duomenų apdorojimas
Symfony Form komponentas yra vienas galingiausių framework’o dalių, bet jis taip pat vienas sudėtingiausių. Daugelis kūrėjų jo vengia ir rašo validaciją rankiniu būdu. Tai klaida, nes prarandate daug automatizacijos ir saugumo funkcijų.
Tačiau nenaudokite formų visur. API endpoint’ams, kur tiesiog priimate JSON, formos dažnai būna overkill. Čia geriau naudoti Symfony Validator komponentą tiesiogiai su DTO objektais. Formos puikiai tinka tradicinėms web aplikacijoms su HTML formomis.
class CreateUserDTO
{
#[Assert\NotBlank]
#[Assert\Email]
public string $email;
#[Assert\NotBlank]
#[Assert\Length(min: 8)]
public string $password;
}
// Kontroleryje
public function create(
Request $request,
ValidatorInterface $validator
): Response
{
$dto = $serializer->deserialize(
$request->getContent(),
CreateUserDTO::class,
'json'
);
$errors = $validator->validate($dto);
if (count($errors) > 0) {
// Handle errors
}
}
Svarbu – validacijos taisyklės turi būti entity’je arba DTO, ne kontroleryje ar servise. Taip jos yra perpanaudojamos ir centralizuotos. Naudokite custom constraint’us sudėtingesnei logikai, vietoj to, kad rašytumėte validaciją rankiniu būdu.
Dar viena dažna klaida – naudoti formas duomenų transformavimui. Formos skirtos validacijai ir duomenų surinkimui iš vartotojo, ne verslo logikai. Jei jums reikia transformuoti duomenis prieš išsaugant, darykite tai servise.
Event’ai ir Messenger komponentas
Symfony event sistema leidžia sukurti labai decoupled architektūrą. Bet su ja reikia būti atsargiems. Naudokite event’us tik tikrai svarbiems dalykams, ne kiekvienai smulkmenai. Per daug event’ų daro kodą sunkiai sekamu.
Gera praktika – naudoti event’us cross-cutting concerns dalykams: logavimui, auditui, notifikacijoms. Bet jei jūsų event listener’is keičia pagrindinę verslo logiką, greičiausiai turėtumėte persvarstyti architektūrą.
Messenger komponentas yra fantastiškas asynchroniniam darbui. Bet ne viskas turi būti async. Jei operacija greitai įvykdoma ir turi būti sinchroninė (pvz., vartotojas laukia rezultato), nereikia jos dėti į queue.
// Gerai tinka async:
// - Email siuntimas
// - Vaizdų apdorojimas
// - Duomenų eksportas
// - Trečių šalių API kvietimai
// Blogai tinka async:
// - Vartotojo autentifikacija
// - Duomenų validacija
// - Paprasti CRUD veiksmai
Kai naudojate Messenger, visada turėkite retry strategiją. Dalykai gali nepavykti – tinklo problemos, laikini trečių šalių servisų gedimai. Sukonfigūruokite protingą retry logiką su exponential backoff.
Ir labai svarbu – message handler’iai turi būti idempotent’iški. Tai reiškia, kad jei message apdorojamas kelis kartus, rezultatas turi būti tas pats. Nes distribuotose sistemose message’ai kartais gali būti pristatyti daugiau nei vieną kartą.
Saugumo aspektai ir autentifikacija
Symfony Security komponentas yra galingas, bet sudėtingas. Naujiems kūrėjams jis gali atrodyti kaip juodoji magija. Bet kai supranti pagrindinius principus, viskas tampa aiškiau.
Niekada nekurkite savo autentifikacijos sistemos nuo nulio. Naudokite Symfony Security komponentą arba bent jau patikrintus bundle’us. Saugumas yra per daug svarbus, kad eksperimentuotumėte.
Dėl slaptažodžių – visada naudokite Symfony password hasher’į. Jis automatiškai pasirenka geriausią algoritmą (šiuo metu bcrypt arba argon2i). Ir niekada nelyginkite slaptažodžių su == arba ===. Naudokite password hasher’io verify metodą.
// Blogai
if ($user->getPassword() === $hashedPassword) {
// ...
}
// Gerai
if ($passwordHasher->verify($user->getPassword(), $plainPassword)) {
// ...
}
API autentifikacijai naudokite JWT arba API tokens, ne sesijas. Sesijos netinka stateless API. Yra puikių bundle’ų kaip LexikJWTAuthenticationBundle, kurie visa tai supaprastina.
Dėl authorization – naudokite Voter’ius sudėtingesnei logikai. Jei jūsų access control taisyklės sudėtingesnės nei paprasti role’ai, Voter’iai yra teisingas kelias. Jie leidžia centralizuoti authorization logiką ir padaro ją testuojamą.
Testing’as ir code quality
Testing’as nėra optional, ypač didesniuose projektuose. Symfony turi puikią testing infrastruktūrą, bet daugelis projektų turi labai mažai testų arba visai jų neturi.
Pradėkite nuo functional testų, ne unit testų. Functional testai testuoja visą request/response ciklą ir suteikia daugiau pasitikėjimo, kad jūsų aplikacija veikia. Unit testai yra svarbūs sudėtingai verslo logikai, bet jie neturėtų būti vieninteliai testai.
class UserControllerTest extends WebTestCase
{
public function testCreateUser(): void
{
$client = static::createClient();
$client->request('POST', '/api/users', [], [],
['CONTENT_TYPE' => 'application/json'],
json_encode([
'email' => '[email protected]',
'password' => 'secure123'
])
);
$this->assertResponseStatusCodeSame(201);
$this->assertJsonContains(['email' => '[email protected]']);
}
}
Naudokite PHPStan arba Psalm static analysis. Šie įrankiai sugauna daug klaidų dar prieš paleidžiant kodą. Pradėkite nuo žemesnio level’io ir palaipsniui kelkite. Net level 5-6 jau suteikia didelę vertę.
Code style – naudokite PHP CS Fixer arba PHP_CodeSniffer. Nustatykit juos CI/CD pipeline’e, kad kodas būtų automatiškai patikrintas. Nesvarbu, kokį style pasirinksite, svarbu, kad jis būtų konsistentiškas visame projekte.
Ir dar viena svarbi praktika – code review. Net jei dirbi vienas, po kelių dienų pažiūrėk į savo kodą iš naujo. Dažnai pastebėsi dalykų, kuriuos galima pagerinti.
Kas lieka už kadro, bet yra ne mažiau svarbu
Symfony geriausios praktikos nėra tik apie kodą. Tai apie visą development procesą, komandos darbą ir ilgalaikį projekto palaikymą.
Dokumentacija yra kritiškai svarbi. Ne tik API dokumentacija, bet ir architektūros sprendimai, deployment instrukcijos, troubleshooting guide’ai. Naudokite ADR (Architecture Decision Records) – trumpus dokumentus, kurie paaiškina, kodėl priėmėte tam tikrus sprendimus. Po metų jūs patys sau padėksite.
Performance monitoring’as turėtų būti įjungtas nuo pirmos dienos. Naudokite Symfony Profiler development’e ir įrankius kaip Blackfire ar New Relic production’e. Nežinodami, kur jūsų aplikacija lėta, negalėsite jos optimizuoti efektyviai.
Dependency management – reguliariai atnaujinkite priklausomybes. Senos bibliotekos turi saugumo spragų ir bug’ų. Bet neatnaujinkite visko iš karto – darykite tai palaipsniui ir testuokite po kiekvieno atnaujinimo.
Ir galiausiai – mokykitės iš Symfony pačios kodo bazės. Tai vienas geriausiai parašytų open source projektų. Kai nežinote, kaip kažką padaryti, pažiūrėkite, kaip tai padaryta Symfony core’e. Ten rasite daug įkvėpimo ir gerų praktikų.
Symfony nėra lengvas framework’as, bet jis yra galingas ir patikimas. Laikydamiesi šių praktikų, sukursite projektus, kurie išgyvens metus ir bus malonūs palaikyti. O tai ir yra tikrasis sėkmės matas.
