Podsumowanie artykułu
- Używaj wersji LTS — zapewnia długoterminowe wsparcie i bezpieczeństwo; ignorowanie tego prowadzi do kosztownych i czasochłonnych aktualizacji.
- Testuj kod — testy chronią przed błędami podczas rozwoju i aktualizacji aplikacji; brak testów znacząco wydłuża czas debugowania i zwiększa ryzyko awarii.
- Stosuj narzędzia do analizy kodu i code style — poprawiają one jakość kodu, przyspieszają code review i zmniejszają ryzyko ludzkich błędów.
- Typuj dane i wymuszaj kontrolę typów — silne typowanie w PHP zwiększa stabilność i bezpieczeństwo aplikacji, zapobiegając trudnym do wykrycia błędom.
- Używaj gotowych bibliotek — gotowe rozwiązania są bardziej sprawdzone, bezpieczniejsze i łatwiejsze w utrzymaniu niż własne implementacje; ich brak to droga do chaosu.
Framework Symfony to świetny wybór przy tworzeniu aplikacji wymagających stabilności i skalowalności. Aby jednak w pełni wykorzystać potencjał tego frameworka PHP, warto stosować najlepsze praktyki, które zwiększają jakość i bezpieczeństwo napisanego kodu. W tym artykule pokazujemy, jak efektywnie budować aplikacje przy użyciu PHP Symfony framework, unikając typowych błędów i problemów.
#1 Zawsze używaj wersji LTS
Nie ma przy tym znaczenia, czy piszesz portfolio firmy, czy wielką aplikację e-commerce. Zawsze staraj się używać najnowszej wersji LTS.
LTS, to skrót od Long Term Support, co oznacza wersję frameworka o długim czasie wsparcia.
Jak to działa?
Symfony framework ma określony system wydań. Kolejno pojawiają się wersje X.0, X.1, X.2, X.3 oraz X.4.
To właśnie wersja X.4 jest wersją LTS.
Co konkretnie daje nam wersja LTS?
Wersje nie-LTS mają krótki czas wsparcia. Zwykle poprawki błędów są przewidziane na 6-8 miesięcy od daty wydania wersji. W przypadku wersji LTS developerzy frameworka deklarują się, że w ciągu 2 lat od wydania wersji będą pojawiać się poprawki błędów (bug fixes), a dodatkowo przez kolejny rok poprawki bezpieczeństwa (security fixes). Co więcej okres poprawek bezpieczeństwa dla wersji LTS może zostać wydłużony. Stało się tak chociażby dla wersji 5.4 wydanej w listopadzie 2021. Z powodu dużej popularności czas poprawek bezpieczeństwa został wydłużony do lutego 2029. To ponad 7 lat od daty premiery!
Klientowi zależy na funkcjonalnościach dostępnych w nowszych wersjach nie-LTS, których nie ma w aktualnym LTS-ie
W takim wypadku należy w pierwszej kolejności spojrzeć na kalendarz wydań Symfony, który jest dostępny w dokumentacji.
Co nam to da? Pracując nad projektem, zwykle mamy założone ramy czasowe na realizację zadań. Jeżeli nasz przewidywany czas budowania aplikacji zawiera datę wydania nowszej wersji LTS, możemy podjąć decyzję o wykorzystaniu wersji nie-LTS. Musimy jednak zwrócić uwagę klientowi na kilka ważnych kwestii:
- Wersje nie-LTS mogą być niestabilne. Należy poinformować klienta o użyciu takiej wersji.
- Wersje nie-LTS mogą mieć luki bezpieczeństwa, które nie zostały jeszcze załatane. Powinniśmy położyć większy nacisk na testy penetracyjne aplikacji.
- Wersje nie-LTS muszą być regularnie aktualizowane. Należy przewidzieć czas na aktualizacje (czasem wymagające korekty kodu) w czasie developmentu.
- Utrzymanie to słowo klucz. Aplikacja, którą budujemy świadczy o nas jako o firmie i developerach. Musimy poinformować klienta, że po wydaniu wersji LTS będziemy musieli zaktualizować aplikację, a następnie przez pewien czas monitorować jej działanie i dalsze aktualizacje wersji LTS. Z doświadczenia projektowego mogę powiedzieć, że przez pierwszy miesiąc po wydaniu LTS-a warto sprawdzać aktualizacje tzw. patch versions (odsyłam do opisu wersjonowania semantycznego) co około tydzień. W dalszym czasie utrzymania wystarczy zaktualizować pakiety raz na 6 miesięcy.
Co się stanie, jeśli nie użyjesz LTS?
W krótkiej perspektywie czasowej — nic. Jeśli chcesz pisać aplikacje i szybko o nich zapominać, to wersje nie-LTS są dla Ciebie. Tylko czy na prawdę chcesz być takim programistą?
Wyobraźmy sobie przypadek aktualizacji. Nie ma znaczenia, czy to aktualizacja „po latach działania” czy nagły hotfix bezpieczeństwa. Załóżmy na potrzeby porównania, że mamy dwie aplikacje. Każda z nich zawiera „front” dla użytkownika końcowego i „backoffice” dla administratorów.
Aplikacja 1 została napisana w Symfony 5.1.
Aplikacja 2 została napisana w Symfony 5.4.
Dlaczego musimy aktualizować? Załóżmy, że wyszła krytyczna podatność bezpieczeństwa. Użytkownik może uzyskać uprawnienia administracyjne, odpowiednio preparując zapytanie do aplikacji. Brzmi strasznie? Przejdźmy zatem do aktualizowania naszych aplikacji.
Aplikacja 1
Wsparcie dla Symfony 5.1 wygasło w styczniu 2021. Dla nas jako programistów oznacza to tyle, że od tego czasu nie pojawiła się żadna poprawka bezpieczeństwa! Co zatem musimy zrobić?
- Uruchamiamy testy jednostkowe, żeby phpunit wykrył deprecjacje w kodzie.
- Poprawiamy deprecjacje.
- Podnosimy w json wersję Symfony do 5.2 (kto choć raz aktualizował Symfony wie, że najbezpieczniej jest przechodzić po kolejnych minor-wersjach).
- Wywołujemy aktualizację pakietów z użyciem composera – composer update “symfony/*”
- Aktualizujemy inne zainstalowane pakiety, jeżeli są niekompatybilne z nowszą wersją frameworka.
- Przechodzimy kroki 1-5 aż dojdziemy do wersji 5.4 (LTS).
Dużo pracy, prawda? To, żeby nie było za wesoło — w punkcie 4 aktualizujemy inne biblioteki. Co w sytuacji, gdy będą tam wymagane spore zmiany w kodzie? Może się okazać, że „drobna” aktualizacja bezpieczeństwa sprowadzi się do przepisania dużej części kodu.
Aplikacja 2
Poprawki błędów dla Symfony 5.4 zakończyły się w listopadzie 2024, ale poprawki bezpieczeństwa są dostępne do lutego 2029. Co zatem musimy zrobić?
- Wywołujemy aktualizację pakietów z użyciem composera – composer update “symfony/*”
- Uruchamiamy testy jednostkowe, żeby phpunit wykrył deprecjacje w kodzie.
- Poprawiamy deprecjacje.
- Opcjonalnie możemy zaktualizować „przy okazji” inne biblioteki.
Wygląda to na zdecydowanie mniej pracy. A jeśli założymy, że dbamy o naszą aplikację i regularnie aktualizujemy biblioteki, to może się okazać, że nasz fix sprowadzi się wyłącznie do punktów 1 i 2.
#2 Testuj kod
Kod bez testów to błędny kod.
Jacob Kaplan-Moss, jeden z twórców frameworka Django, mówi: „Kod bez testów jest popsuty z założenia”.
Praktyka programistyczna pokazuje, że powyższe stwierdzenie jest jak najbardziej prawdziwe i ma zastosowanie w praktycznie każdym języku czy frameworku.
Dlaczego jest to tak ważne?
Testowanie jest bardzo istotne, ponieważ pozwala uniknąć błędów. I nie chodzi tu tylko o błędy programisty rozwijającego aplikację. Błędy mogą się pojawić na przykład w wyniku aktualizacji bibliotek użytych do budowy aplikacji. Co w sytuacji, gdy taka aktualizacja spowoduje zmianę działania biblioteki, a my tego nie dopilnujemy?
Jakie są rodzaje błędów?
Jak słyszymy „błąd”, to od razu myślimy o składni — ktoś zapomniał średnika albo nie domknął klamry. Jednak błędy składniowe występują stosunkowo rzadko. Zwłaszcza jeżeli kod pisze doświadczony programista.
Skupmy się więc na funkcjonalnościach aplikacji. Tutaj możemy zasadniczo wyróżnić dwa scenariusze, w których pojawiają się błędy.
Pierwszy z nich to błąd programistyczny wynikający z niejasnej dokumentacji lub jej braku.
Programista może źle zrozumieć dokumentację projektu. Czasem (o zgrozo!) może być też tak, że w ogóle jej nie ma i skazani jesteśmy na własne domysły. Dodatkowo mamy tendencję do upraszczania zadań. Załóżmy, że musimy wdrożyć skomplikowany przepływ danych. Rozpiszemy go sobie na kilku prostych przypadkach i okazuje się, że część etapów jest pomijana. W takim wypadku wielu programistów (zwłaszcza mniej doświadczonych) nie zaimplementuje wszystkich możliwości, bo przecież nasza rozpiska tego nie uwzględniła. I co z tego wyniknie? Nasz kod działa, ale nie do końca. Na produkcji przy skomplikowanych danych okaże się, że workflow jest błędny! I co? Reklamacja, poprawki, stres. Lepiej tego unikać.
Drugi scenariusz związany jest z bibliotekami, na które jednak nie mamy zbytnio wpływu.
Lubimy korzystać z gotowych rozwiązań. I szczerze mówiąc — dobrze. Nie wymyślajmy koła na nowo. Jak ktoś już coś napisał i udostępnia to za darmo, to korzystajmy z tego. Mniej naszego kodu do utrzymania. Tylko jest jeden problem — biblioteki trzeba aktualizować. Podnoszenie patcha czy minor wersji zwykle nie jest problematyczne, ale czasami musimy podnieść major wersję (tu ponownie odsyłam do wersjonowania semantycznego). W takim przypadku możemy mieć większy problem. Powiedzmy sobie szczerze: kto czyta changelogi paczek przy ich aktualizowaniu? Odpowiedź jest prosta: ten, kto po aktualizacji napotkał problem. A co jeśli problem nie jest widoczny na „pierwszy rzut oka”? Takim problemem może być na przykład inny sposób zaokrąglania liczb. Niby drobna rzecz, ale w naszym systemie fakturowym może być bardzo istotna. Tu też wyręczają nas testy!
Jak testować?
Ilu programistów tyle opinii. Ja z doświadczenia mogę powiedzieć, że nie ma jednego dobrego sposobu.
Są projekty, w których najpierw piszemy testy i dbamy o to, żeby nasz kod spełniał założenia testowe (Test-driven Development, TDD).
Zalety podejścia TDD są następujące:
- wiemy, że nasz kod spełnia wymagania funkcjonalne:
- pisząc najpierw test, jesteśmy pewni, że nie opublikujemy przypadkiem wersji bez wdrożonej funkcjonalności;
- dbamy o dobre praktyki wysokiego pokrycia testami;
- minimalizujemy ilość poprawek.
Oczywiście są też wady:
- podejście TDD jest czasochłonne;
- aby napisać dobre testy, dokumentacja musi zawierać dokładnie opisane przypadki testowe, co może wymagać zaangażowania testera na etapie przygotowania dokumentacji;
- podejście jest niewykonalne przy słabej dokumentacji lub jej braku, bo programiści lubią upraszczać i nie przewidują różnych przypadków.
W innych projektach testy piszemy już po wdrożeniu funkcjonalności.
W takim scenariuszu development jest szybszy. Wadą jest to, że programiści wolą dostosować test do kodu niż kod do testu (bo zwykle jest z tym mniej pracy), co może powodować błędy logiczne. Poza tym nie wszystkie przypadki będą uwzględnione w testach.
Zdarzają też projekty, w których, o zgrozo, nie ma testów. Przejmując takie rozwiązania, np. do utrzymania po innej firmie, podejmujemy ogromne ryzyko!
Kiedy uruchamiać testy?
Pierwsze uruchomienie testów powinno być nawykiem programisty. Po dodaniu lub zmianie funkcjonalności (niezależnie od tego czy test został napisany przed implementacją, czy po) uruchamiamy testy. Jeżeli przejdą, to znaczy, że działanie funkcjonalności jest najprawdopodobniej poprawne (najprawdopodobniej, bo cykl wytwarzania oprogramowania powinien zawierać czas na QA).
Kolejne uruchomienie testów powinno nastąpić przed wypchnięciem zmian na repozytorium. Najlepiej użyć do tego odpowiedniego git hooka.
Następne uruchomienie następuje w pipeline na repozytorium. To ostateczny test. W przypadku pipeline dla pull request-ów niepowodzenie testów nie powinno pozwolić na scalenie zmian. Niepowodzenie testów danej gałęzi (ang. branch) przeznaczonej do wdrożenia powinno uniemożliwić wdrożenie zmian.
O czym jeszcze warto pamiętać?
Powinniśmy pamiętać, że niedopuszczalny jest regres pokrycia kodu testami. Co to oznacza w praktyce?
Załóżmy, że mamy działającą aplikację. Ma ona pokrycie kodu testami na poziomie powiedzmy 92%.
Dokładamy do aplikacji nową funkcjonalność. Gałąź zawierająca funkcjonalność przeszła testy na pipeline, ale pokrycie kodu testami na tej gałęzi wynosi 88%.
Co to oznacza? Jedną z dwóch rzeczy:
- Nowa funkcjonalność nie ma napisanych testów.
- W ramach poprawki funkcjonalności zostały usunięte testy.
O ile przypadek pierwszy jest niedopuszczalny i powinien całkowicie dyskwalifikować pull requst, to w przypadku drugim dokonujący code review powinien dokładnie przeanalizować zadanie. Być może zmieniła się zasada działania albo funkcjonalność została usunięta, przez co testy stały się niepotrzebne. W takim wypadku zmiana nie powinna dyskwalifikować pull requesta.
Co się stanie, jeśli nie mamy testów?
Na to pytanie nie ma jednoznacznej odpowiedzi.
Jeśli mamy szczęście, a nasza aplikacja nie jest skomplikowana, może nie stanie się nic.
Zawsze jednak pojawia się pytanie: „Co, jeśli nie mamy szczęścia?”
Przeanalizujemy przypadek mikroserwisu do generowania faktur. Aplikacja korzysta z SDK bramki płatności oraz biblioteki ułatwiającej liczenie podatku VAT na podstawie stawki oraz ceny brutto.
Nastało okienko aktualizacyjne. Wywołujemy komendę composer outdated i widzimy, że w SDK bramki płatności oraz w bibliotece do wyliczania podatków zmieniły się wersje. Aktualizujemy biblioteki.
Jako że nie mamy testów automatycznych, sprawdzamy wszystko lokalnie „na oko”, a następnie wypuszczamy nasze zmiany na środowisko stage. Po tygodniu dział QA wraca z listą błędów. Okazuje się, że nasza aplikacja źle wylicza podatek.
Tak więc mamy listę błędów. Zaczynamy debugowanie aplikacji. Znajdujemy przypadek powodujący błędy. Okazuje się, że w przypadku faktury, która zawiera kilka pozycji suma VAT jest różna od kwoty VAT wyliczonej z kwoty brutto pobranej z bramki płatności.
Dlaczego? Bo biblioteka w nowej major-wersji zmieniła sposób zaokrąglania liczb. To spowodowało problemy. Dojście do źródła problemu zajęło nam kilka godzin.
To był przypadek optymistyczny, bo przecież nie raz już widzieliśmy wdrażanie i testowanie „na produkcji”.
A co gdybyśmy mieli testy?
Załóżmy, że mamy przypadek testowy, który pokrywa się z problemem spowodowanym przez zmiany w bibliotece, czyli mamy test, który porównuje wyliczony VAT z sumowanym VAT-em za pozycje faktury. Taki test już na etapie developmentu zwróci nam informacje o problemie, a co za tym idzie przyspieszy wdrożenie zmian.
#3 Używaj narzędzi do statycznej analizy kodu i code style
Im więcej osób pracuje nad projektem, tym trudniej o utrzymanie dobrego jakościowo kodu. Jednocześnie każdy programista ma swój styl pisania. Na pewnym etapie projektu możemy mieć w kodzie kompletny chaos, a refactoring stanie się bardzo czasochłonny.
W moim stacku projektowym bardzo dobrze sprawdza się zestaw narzędzi złożony z PHPStan oraz PHP Coding Standards Fixer (znany też jako PHP CS Fixer).
Czym jest analiza statyczna?
Analiza statyczna kodu to proces oceny kodu źródłowego bez jego uruchamiania. Jest to narzędzie dla programistów, które pomaga w identyfikacji błędów, problemów z bezpieczeństwem i zgodności ze standardami kodowania na wczesnym etapie. W odróżnieniu od analizy dynamicznej, która wymaga uruchomienia aplikacji, analiza statyczna skupia się na strukturze i składni kodu. Dzięki temu możliwe jest wykrycie potencjalnych problemów jeszcze przed fazą testów.
Jakie są zalety analizy statycznej kodu?
Pierwszą i chyba najważniejszą zaletą jest wczesne wykrywanie błędów. Narzędzia takie jak PHPStan sprawdzają typy i analizują strukturę kodu. Dzięki temu już na wczesnym etapie pisania kodu sygnalizują nam potencjalne problemy.
Nie można też zapomnieć o poprawie jakości i czytelności kodu. Kod dobry jakościowo jest również bardziej optymalny wydajnościowo. Dlaczego? Bo są operacje, które można uprościć. Jeśli wyeliminujemy zbędne operacje, czas odpowiedzi naszej aplikacji może znacząco wzrosnąć.
Dla menadżerów zdecydowaną zaletą będzie oszczędność czasu i kosztów oraz zwiększenie niezawodności produktu. Dlaczego? Ponieważ potencjalne błędy są wykrywane już na etapie developmentu, a co za tym idzie oszczędzamy czas na ewentualne poprawki. Brak poprawek prowadzi do zmniejszenia kosztów projektu, a co najważniejsze do zwiększenia zadowolenia Klienta. A każdy menadżer wie, że zadowolony Klient wraca i poleca usługi swoim znajomym.
Pokusiłbym się również o stwierdzenie, że narzędzia do statycznej analizy kodu ułatwiają pracę w zespołach i przyspieszają proces code review. Jak takie narzędzie może mieć taką moc? Odpowiedź jest bardzo prosta — pipeline. Narzędzia do analizy statycznej ZAWSZE powinniśmy uruchamiać w procesie CI/CD. Po co? Żeby kod słaby jakościowo nie został scalony. Co nam to da? Kiedy widzimy nieudany pipeline, poprawiamy błędy. Dzięki czemu oszczędzamy czas na code review, podczas którego inny członek zespołu mógłby wykryć problemy składniowe. Dodatkowo zmniejszamy udział błędu ludzkiego. Narzędzie automatyczne może wykryć problem, który byłby niewidoczny na pierwszy rzut oka, nawet jeżeli code review dokonywałby doświadczony programista.
Czym są narzędzia wymuszające code style?
Narzędzie do wymuszenia code style naprawia kod tak, aby był zgodny ze standardami – niezależnie od tego, czy chcesz przestrzegać standardów kodowania PHP zdefiniowanych w PSR-1, PSR-2 itp. lub innych standardów opracowanych przez społeczność. Pozwala też definiować swój styl pisania kodu.
Może zmodernizować kod (np. konwertując funkcję pow na operator ** w PHP 5.6) i zoptymalizować go.
Lintery (czyli narzędzia do identyfikowania problemów ze standardami kodowania) wyłącznie wykrywają błędy. Narzędzia takie jak PHP CS Fixer potrafią je naprawiać.
Jakie to daje korzyści?
Pierwszą i najważniejszą korzyścią jest to, że poprawiają kod za nas. Tego typu narzędzia podczas analizy znajdują miejsca wymagające optymalizacji i same wprowadzają usprawnienia. Dzięki temu nie musimy się martwić, że użyjemy funkcji czy konstrukcji językowej, która została zdeprecjonowana. Kod poprawi się sam.
Kolejną zaletą jest ułatwienie utrzymania standardów kodowania w ramach zespołu. Odpowiedzcie sobie na pytanie: ile razy Wasz pull request został cofnięty, ponieważ był niezgodny ze standardami narzucanymi przez bardziej doświadczonych członków zespołu? Ile razy po zmianie miejsca zatrudnienia na nowo uczyliście się standardów panujących w organizacji? A może w drugą stronę: ile razy odrzuciliście pull request ze względu na code style czy drobne usprawnienia? Użycie narzędzi takich jak PHP CS Fixer zdecydowanie ułatwia pracę zespołu, a nawet organizacji. Wystarczy tylko wypracować zasady i stosować je we wszystkich projektach. Dzięki temu wdrożenie nowych programistów staje się zdecydowanie łatwiejsze.
Co, jeśli nie chcę tego stosować?
Możesz się zastanawiać: po co stosować analizatory czy narzędzia code style, skoro projekt jest prowadzony jednoosobowo. Z jednej strony racja. Takie narzędzia trzeba wdrożyć, a to zajmuje czas. Jednak nawet kiedy pracujemy sami lepiej mieć wypracowane zasady kodowania. Dlaczego? Bo projekt może do nas wrócić — na rozwój lub na poprawki. Wyobraźcie sobie jak ciężko jest wrócić do kodu kiepskiej jakości, który pisaliśmy rok temu!
Patrząc szerzej, utrzymanie aplikacji często przechodzi z firmy na firmę. Jeżeli nie stosujemy zasad czy narzędzi do poprawy jakości, po kilku takich cyklach kod staje się strasznie chaotyczny. Pamiętajcie, że Wasz kod po utrzymaniu przez inne software house-y zawsze może do Was wrócić. To od Was jako twórców aplikacji zależy, czy wróci w dobrym stanie, czy też w stanie kompletnego chaosu.
#4 Używaj typowania i wymuszaj kontrolę typów
PHP w wersji 7.0 wprowadził prawdziwego game changera w stosunku do wersji 5.6. Do składni języka dodano obsługę typów, która wcześniej była możliwa wyłącznie przez anotacje (które tak na prawdę służyły tylko jako podpowiedzi). PHP 7.0 pozwalał na typowanie argumentów funkcji i typu zwracanego.
W dalszych wersjach wprowadzano następujące zmiany dotyczące typowania:
- 7.1 -> dodanie typów nullabe, typ void oraz iterable;
- 7.2 -> dodanie typu object;
- 7.4 -> dodanie typowania atrybutów (czyli pól w klasach);
- 8.0 -> wsparcie dla typu unii, dodanie typu mixed;
- 8.1 -> dodanie typu never;
- 8.2 -> zezwolenie na użycie null oraz false jako samodzielnych typów, dodanie literału true;
- 8.3 -> dodanie typowania stałych.
Wszystkie te zmiany zdecydowanie ułatwiają programowanie, a jednocześnie są całkowicie opcjonalne. Mimo tego nadal zachowano jedną z najważniejszych zasad PHP — zmienna nie ma typu; typ mają dane umieszone w zmiennej.
O co chodzi z wymuszeniem kontroli typów?
Język PHP standardowo nie potrzebuje typowania. Typowanie jednak ułatwia pracę. Przyjrzyjmy się prostemu przykładowi.
Widzimy tutaj funkcję, która z założenia powinna działać na liczbach całkowitych (ma do nich dodawać dwa). Przy wywołaniu funkcji przekazaliśmy parametr 3.14, który jest liczbą zmiennoprzecinkową! Mimo wszystko PHP przetworzył działanie i wypisał wynik: 5.14
Zmodyfikujmy trochę kod i dodajmy typowanie:
Teraz nasza funkcja informuje nas, że chce jako parametr otrzymać liczbę całkowitą i zwróci nam liczbę całkowitą. Tutaj zaczynają się problemy. PHP widząc typ zwrotny int, przeprowadzi niejawną konwersję typów. W związku z tym utracimy część danych. Dodatkowo PHP pokaże błąd składniowy. Wynik będzie następujący:
Rys. 1. Informacja o błędnym typie wyświetlona w PHPStorm
Dodatkowo skrypt nie zostanie poprawnie uruchomiony. W przeglądarce otrzymamy następujący wynik:
Fatal error: Uncaught TypeError: addTwo(): Argument #1 ($x) must be of type int, float given, called in /app/public/test.php on line 11 and defined in /app/public/test.php:5 Stack trace: #0 /app/public/test.php(11): addTwo(3.14) #1 {main} thrown in /app/public/test.php on line 5
W tym momencie przychodzi zastanowienie. Jak typy mają nam pomóc? Przecież przykład jasno pokazał, że bez typowania otrzymaliśmy poprawny wynik, a po jego dodaniu nagle program przestał działać tak, jak byśmy chcieli.
Odpowiedź jest prosta. Typowanie daje nam kontrolę, a ta kontrola jest bardzo potrzebna!
W Symfony framework bardzo często pracujemy z bazą danych. Jeżeli korzystamy z Doctrine ORM, to mamy klasy encji, które obrazują tabele w bazie danych. Jak dobrze wiemy encja jest odwzorowaniem tabeli w relacyjnej bazie danych. Tabele w bazie danych mają kolumny, które mają określone typy danych.
Wyobraźmy sobie teraz następujące sytuacje:
1. Nie mamy typowania danych
Nasza klasa encji będzie wyglądać następująco:
id;
}
public function getMyValue()
{
return $this->myValue;
}
public function setMyValue($myValue)
{
$this->myValue = $myValue;
}
}
Jak widać jedyne określenie typu danych jest dla bazy. W związku z tym z naszego kodu możemy podać dowolne dane do pola $myValue. Co to może spowodować? Błąd 500 przy próbie zapisu niepoprawnych danych. A żeby dotrzeć do błędu, musimy otworzyć połączenie do bazy danych. To zdecydowanie zbyt późno na wykrywanie błędów!
Zobaczmy zatem jakie inne możliwości mamy.
2. Mamy typowanie, bez wymuszenia ścisłej kontroli typów
W tym przypadku nasza klasa encji wygląda następująco:
id;
}
public function getMyValue(): int
{
return $this->myValue;
}
public function setMyValue(int $myValue): void
{
$this->myValue = $myValue;
}
}
Co stanie się w tym przypadku? Jeżeli automatyczna konwersja typów jest możliwa (na przykład przekażemy liczbę zmiennoprzecinkową), to dane zostaną zapisane do bazy danych. Niestety część danych może zostać utracona. Niestety nie zobaczymy tego do czasu pobrania danych. Nasz kod jest zatem podatny na błędy!
Przejdźmy zatem do kolejnej możliwości.
3. Mamy typowanie i wymuszoną ścisłą kontrolę typów.
id;
}
public function getMyValue(): int
{
return $this->myValue;
}
public function setMyValue(int $myValue): void
{
$this->myValue = $myValue;
}
}
Powyższy fragment kodu jest przypadkiem bardziej skomplikowanym. O niepoprawnym typie danych poinformuje nas już IDE, a jeśli mimo wszystko nie zauważymy ostrzeżenia, to aplikacja zwróci błąd 500 w przypadku niepoprawnego typu danych.
Co się stanie, jeżeli nie pilnujemy typów?
Przełóżmy nasze proste przykłady na bardziej realistyczny przypadek. Załóżmy, że pracujemy nad systemem e-commerce. Podstawą e-commerce są produkty. Produkt musi mieć swoją cenę (nie ważne czy zapisaną bezpośrednio w modelu produktu, czy też w innym modelu powiązanym).
Cenę produktu najbezpieczniej jest przechowywać jako liczbę całkowitą (czyli w postaci groszy). Dlaczego? Bo liczby całkowite lepiej zachowują się chociażby przy wyliczaniu rabatów. Ale to już temat na inny wpis.
Przy projekcie pracuje kilku programistów. W organizacji nie ma zasady stosowania ścisłej kontroli typów. Jeden z developerów przygotował klasę modelu (encję). Użył typowania, przygotował setter do ceny. Setter przyjmuje liczbę całkowitą. Inny programista był odpowiedzialny za przygotowanie panelu administracyjnego. Wiadomo, — człowiekowi wygodniej pracuje się na cenach złożonych z pełnych złotówek i groszy. Niestety developer nie sprawdził, jak w modelu zapisywana jest cena. Założył, że, tak jak w jego poprzedniej organizacji, jest przechowywana jako pole DECIMAL w bazie danych. W związku z czym nie wdrożył konwersji ze złotówek na grosze. Kod trafił na code review i nikt nie wykrył błędu. Po wgraniu na środowisko testowe okazało się, że występuje spory problem. W panelu administracyjnym zostaje wpisana cena 123,00 zł, a na frontendzie wyświetla się cena 1,23 zł. Jak do tego doszło? Setter „wyciął” część dziesiętną ceny i zapisał liczbę całkowitą do bazy danych. Frontend odebrał cenę w groszach, czyli 123 grosze. Po przeliczeniu dało to cenę 1,23 zł. A wystarczyło tylko dodać kontrolę typów. Interpreter od razu poinformowałby o problemie.
#5 Nie wymyślaj koła na nowo — używaj gotowych bibliotek
Wydaje się to oczywiste, a jednak w wielu projektach utrzymaniowych spotkałem się z customowym rozwiązywaniem dobrze znanych problemów.
Dlaczego tak się dzieje?
Wielu młodych programistów chce „pokazać, że umie”. Z tego powodu w swoich projektach tworzą customowe rozwiązania. I to nie rozwiązania dotyczące specyficznej logiki biznesowej rozwiązującej problemy klienta. Piszą setki linii kodu, które można by rozwiązać użyciem biblioteki i jednej linijki kodu.
Dlaczego jest to problemem?
Gotowe biblioteki mają rozbudowaną społeczność developerów, którzy z nich korzystają, a także tych, którzy je rozwijają. Jeżeli paczka jest popularna, a dodatkowo stale rozwijana, możemy mieć pewność, że zastosowane w niej mechanizmy są zdecydowanie bardziej wydajne od tych, które wypracujemy sami. W końcu żaden programista nie jest całą społecznością!
Kolejny problem to aktualizacje. Jeżeli komunikujemy się z jakimś interfejsem API, to możemy być pewni, że będzie on się zmieniał w przyszłości. Takie zmiany mogą zmusić programistów do wprowadzania ogromnych zmian w kodzie. W przypadku gotowych bibliotek najczęściej mamy zapewnioną kompatybilność wsteczną, więc może się okazać, że jedyna zmiana, którą musimy wykonać, to aktualizacja biblioteki. To bardzo wygodne rozwiązanie.
Jakie jeszcze mogą być zagrożenia wynikające z nieużywania bibliotek?
Tutaj chciałbym posłużyć się przykładem, który można spotkać w praktycznie każdej aplikacji. Tym przykładem będzie resetowanie hasła.
Jak działa przykładowy flow?
- Użytkownik klika przycisk „Nie pamiętam hasła”.
- Użytkownik podaje adres e-mail, na który ma założone konto.
- System generuje wiadomość e-mail z linkiem do resetu hasła.
- Użytkownik ustawia nowe hasło.
Przypadek bardzo prosty, a jednak trzeba pamiętać o kilku rzeczach „po drodze”:
- Musimy zadbać o to, żeby nie zdradzić czy konto o podanym adresie e-mail istnieje, czy nie (czyli zwracamy sukces za każdym razem).
- Musimy zadbać o unikalność linku do resetu hasła.
- Musimy zadbać o odpowiedni czas wygaśnięcia linku.
- Powinniśmy ukryć token resetu w adresie URL (czyli np. zapisać go w sesji i przekierować użytkownika).
- Powinniśmy ograniczyć ilość możliwych zapytań o reset hasła dla danego użytkownika w ciągu określonego czasu.
Zapewne znalazłoby się jeszcze kilka punktów, które mógłbym dopisać.
Prosta funkcjonalność, a im dłużej się o niej myśli, tym więcej znajduje się potencjalnych problemów związanych między innymi z zabezpieczeniami.
A wystarczy użyć biblioteki, na przykład SymfonyCasts/ResetPasswordBundle.
Czy przy użyciu bibliotek musimy na coś uważać?
Zdecydowanie tak!
Pierwszą rzeczą, na którą musimy zwrócić uwagę jest licencja. Zanim użyjemy biblioteki powinniśmy ją dokładnie przeczytać. Nie chcemy przecież doprowadzić do sytuacji, w której nasz komercyjny produkt stanie się darmowy, ponieważ załączyliśmy w nim bibliotekę opartą na licencji, która sprawia, że cały produkt staje się wolnym oprogramowaniem.
Kolejna rzecz to popularność biblioteki. Kiedy przeglądamy kartę biblioteki w repozytorium pakietów (jakim jest np. Packagist) możemy zobaczyć ilość instalacji paczki. Im więcej instalacji, tym paczka bardziej popularna, a co za tym idzie sprawdzona przez społeczność.
Warto również sprawdzić ilość problemów security czy otwarte issues w repozytorium biblioteki.
Co jeśli nie używamy bibliotek?
Tak na prawdę utrudniamy życie sobie lub innym developerom, którzy będą utrzymywać naszą aplikację.
Jak zawsze najlepiej pokazać to na przykładzie. Niech będzie nim wdrożenie systemu płatności w sklepie internetowym. Klient zdecydował się na wdrożenie bramki płatności XYZ. Bramka dostarcza pakiet SDK dla języka PHP oraz pełną dokumentację REST API. Jako programista decydujesz się wdrożyć własne rozwiązanie oparte na REST API. Aplikacja została wydana, klient zadowolony. Po pół roku dzwoni telefon — „Panie nie działa. Co mi pan sprzedał?”. Sprawdzacie aplikację — błąd 500. No ale jak to? Przecież działało. Sprawdzacie dokumentację REST API — nastąpiła wielka zmiana. Całkowicie zmieniła się struktura request i response. Co teraz? Siadacie i kodujecie rozwiązanie na nowo. Kilka godzin pracy jak nic.
A jak by to wyglądało, gdybyście użyli SDK? Pewnie skończyłoby się na aktualizacji biblioteki. To zdecydowanie szybsze rozwiązanie.
Oczywiście ten przykład jest wyolbrzymiony. Żadna bramka płatności nie zrobi breaking changes swojego API w ciągu pół roku. Ale podobne sytuacje można znaleźć również w innego typu aplikacjach.
Podsumowanie
Budowanie aplikacji przy użyciu Symfony PHP wymaga nie tylko znajomości frameworka, ale także stosowania dobrych praktyk, które zapewniają jakość, bezpieczeństwo i łatwość utrzymania kodu. Korzystanie z wersji LTS, pisanie testów, analiza kodu, silne typowanie oraz używanie gotowych bibliotek znacząco minimalizują ryzyko błędów i skracają czas rozwoju aplikacji. Ignorowanie tych zasad może prowadzić do problemów z aktualizacjami, spadku wydajności i wzrostu kosztów utrzymania. Warto pamiętać, że oprócz tych zasad, zawsze należy stosować się również do oficjalnych zaleceń zawartych w dokumentacji Symfony framework, w szczególności w sekcji Best Practices.