PHPStan w praktyce

„W C++ o błędach mówi nam kompilator, w PHP klient”. Pomijając fakt, że o niektórych błędach np. składniowych powie nam interpreter, to według mnie teza ta może po części wynikać ze specyfiki pracy w tym języku – mowa tutaj o „szybkich i tanich” projektach. PHP zrobiło ogromny krok do przodu od wersji 7, a każda kolejna czyni ten język bardziej spójnym. A to bardzo ważne biorąc pod uwagę, że to właśnie on jest najpopularniejszym językiem używanym w internecie.

Języki korzystające z kompilatora muszą znać typ każdej zmiennej, zwracany typ każdej metody, parametru itp. zanim program zostanie uruchomiony. Kompilator sprawdzi czy program jest poprawnie napisany oraz wyświetli nam błędy w kodzie. Można powiedzieć że zachowuje się jak pierwsza linia obrony przed możliwością wdrożenia aplikacji na produkcję.

A jak ma się do tego PHP? No właściwie nijak. Jeżeli zrobimy błąd, program wysypie się dopiero wykonując daną linię kodu. A co w takim przypadku?

<?php
if (rand(0,1)) {
    echo 'OK';
} else { 
    thereIsNoFunctionLikeThat();
}

Niestety, program zadziała… lub nie – w zależności czy spróbuje wykonać linię 3 lub 5. Nieciekawie. O ile ten przypadek jest dosyć prosty to wyobraźcie sobie, kiedy trafiacie do kilkunastoletniego projektu bez testów. Co wtedy? Oczywiście nowoczesne IDE takie jak PHPStorm w znacznym stopniu wam pomoże, jednak możecie wyciągnąć rękę do…

PHPStan – PHP Static Analysis Too

Można powiedzieć że PHPStan w PHP pełni rolę kompilatora. Skupia się on na znajdowaniu błędów w programie bez uruchamiania go. Po sprawdzeniu całego kodu źródłowego, wyświetla szczegółowy raport, w zależności od wybranego przez nas poziomu. W dniu pisania tego artykułu istnieją poziomy od 0 do 7, gdzie im większy, tym bardziej szczegółowa będzie analiza. Szczegółowy opis poszczególnych poziomów znajduje się tutaj. Z własnego doświadczenia mogę powiedzieć, że rozwiązanie to idealnie sprawdza się w pracy z legacy code – po pierwszym uruchomieniu na poziomie 0 mamy obraz „zanieczyszczenia” projektu. Poprawiając wszystkie błędy możemy zwiększać poziom analizy aż do satysfakcjonującego nas poziomu.

PHPStan vs PHPStorm

Jak już wcześniej wspomniałem, nowoczesne IDE na bieżąco analizuje pisany przez nas kod, wskazując nam błędy o których interpreter nam nie powie. W takim razie po co używać dodatkowej analizy, skoro IDE już to robi?

Pracując w zespole ważnym jest, aby trzymać się pewnych ustalonych standardów. W tym przypadku wystarczy, że skonfigurujemy PHP Stan’a jako pre commit hook’a, dzięki czemu nie będziemy w stanie wysłać własnych zmian. Dodatkowo nic nie stoi na przeszkodzie aby podłączyć analizę pod narzędzie CI takie jak Jenkins czy GitLab.

Warto również wspomnieć, że PHPStan w odróżnieniu od IDE PHPStorm jest narzędziem open-source – całkowicie darmowym! Mimo wszystko polecam korzystanie z obu naraz, ponieważ PHPStorm dla studentów jest całkowicie darmowy, a dla reszty posiada 30 dniowy okres ewaluacji, który z pewnością przekona Was do zakupu.

Instalacja

Instalacja PHPStan jest banalnie prosta. Potrzebujemy do niej tylko Composera – reszta potrzebnych zależności zostanie pobrana automatycznie.

composer require --dev phpstan/phpstan

Uruchomienie

Na potrzeby tego artykułu utworzyłem pusty projekt Symfony 4. Po instalacji PHPStan możemy wykonać poniższą komendę:

php vendor/bin/phpstan analyse src/ --level 0

Poniżej możemy zobaczyć raport dotyczący wykonanej analizy:

php vendor/bin/phpstan analyse src --level 0
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
 [OK] No errors                                                     

Jak widać na raporcie, w pustym projekcie nie znaleziono żadnych błędów. Dla kolejnych poziomów efekt będzie identyczny, zresztą co by było gdyby coś się znalazło? Źle to by świadczyło o narzędziu lub o samym Symfony.

PHPStan w praktyce

Skoro sprawdzenie czystego projektu nie pokazało nam żadnych błędów, utwórzmy prosty kontroler zawierający kilka niespodzianek:

<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;

class UserController
{
    public function index(): Response
    {
        switch (rand(0, 4)) {
            case 0:
                $text = $this->getCaseZero();
                break;
            case 1:
                $text = $this->getCaseOne();
                break;
            case 2:
                $text = $this->getCaseTwo();
                break;
            case 3:
                $text = $this->getCaseThree("expected int");
                break;
            case 4:
                return new Response('ok');
        }
    }

    private function getCaseOne(): int
    {
        return "text1";
    }

    /**
     * @return int
     */
    private function getCaseTwo(): string
    {
        return "text2";
    }

    private function getCaseThree(int $text): string
    {
        return (string)$text;
    }
}

Na pierwszy rzut oka widać że coś tutaj jest nie tak. Czy ten program ma prawo zadziałać? Niestety tak – w przypadku gdy funckja rand() wylosuje nam numer 4. Co na to PHPStan? Zacznijmy od level 0:

php vendor/bin/phpstan analyse src --level 0
 2/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
 ------ ---------------------------------------------------------------------------
  Line   Controller/UserController.php                                              
 ------ ---------------------------------------------------------------------------
  15     Call to an undefined method App\Controller\UserController::getCaseZero(). 
 ------ ---------------------------------------------------------------------------
 [ERROR] Found 1 error                                                            

Faktycznie, w linii 11 staramy się się skorzystać z metody getCaseZero() która nie istnieje. Jednak z pewnością to nie jedyna niespodzianka Ustawiając level 1 efekt będzie ten sam. Ustawmy więc level 2:

php vendor/bin/phpstan analyse src --level 2
 2/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
 ------ --------------------------------------------------------------------------- 
  Line   Controller/UserController.php                                              
 ------ --------------------------------------------------------------------------- 
  15     Call to an undefined method App\Controller\UserController::getCaseZero().  
  39     PHPDoc tag @return with type int is incompatible with native type string.  
 ------ ---------------------------------------------------------------------------                                                                            
 [ERROR] Found 2 errors

PHPStan zwraca nam uwagę, że w linii 39 w PHPDoc’u określamy zwracany typ integer, który nie jest zgodny ze zwracanym typem i faktycznie tak jest. Dodam że PHPStan braki PHPDoc’ów nie określa jako błąd, ale jeżeli już takowe w sprawdzanym kodzie się znajdują, to muszą być poprawne. Efekt dla level 3:

php vendor/bin/phpstan analyse src --level 3
 2/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ ------------------------------------------------------------------------------------------ 
  Line   Controller/UserController.php                                                             
 ------ ------------------------------------------------------------------------------------------ 
  15     Call to an undefined method App\Controller\UserController::getCaseZero().                 
  33     Method App\Controller\UserController::getCaseOne() should return int but returns string.  
  39     PHPDoc tag @return with type int is incompatible with native type string.                 
 ------ ------------------------------------------------------------------------------------------                                                                                    
 [ERROR] Found 3 errors

Sytuacja podobna do poprzedniej, jednak tutaj już mamy poważny błąd – zamiast zwracanego typu integer zwracamy string. Sprawdzając level 4 nie otrzymamy nowych informacji. Jednak po ustawieniu level 5 otrzymamy poniższy wynik:

php vendor/bin/phpstan analyse src --level 5
 2/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

 ------ ------------------------------------------------------------------------------------------------ 
  Line   Controller/UserController.php                                                                   
 ------ ------------------------------------------------------------------------------------------------ 
  15     Call to an undefined method App\Controller\UserController::getCaseZero().                       
  24     Parameter #1 $text of method App\Controller\UserController::getCaseThree() expects int, string  
         given.                                                                                          
  33     Method App\Controller\UserController::getCaseOne() should return int but returns string.        
  39     PHPDoc tag @return with type int is incompatible with native type string.  

I faktycznie – w linii 24 jako parametr metody getCaseThree() podajemy string, gdzie zgodnie z jej definicją parametrem powinien być integer. Kolejny poważny błąd, który bez statycznej analizy mógłby zostać wdrożony na produkcję. Jak na darmowe, proste w obsłudze i szybkie narzędzie – całkiem nieźle!

Podsumowanie

Jak widać powyżej, statyczna analiza kodu może dostarczyć nam wiele cennych informacji dotyczących stanu naszej aplikacji. Łatwa możliwość integracji z narzędziami CI może zaoszczędzić wielu wpadek oraz zdecydowanie przyśpieszyć poszukiwanie błędów. Z mojego doświadczenia dodam, że narzędzie doskonale uzupełnia się z PHPStormem – w powyższym kodzie część błędów była widoczne już podczas edycji pliku, natomiast PHPStan nic nam nie powiedział o nieużywanej zmiennej $text. Mimo to warto uznać PHPStana za swojego przyjaciela.

Więcej informacji o PHPStan odnajdziecie w oficjalnym repozytorium, natomiast tutaj znajdziecie repozytorium kodu z artykułu.

0 0 votes
Article Rating
Subscribe
Powiadom o
guest

0 komentarzy
najstarszy
najnowszy oceniany
Inline Feedbacks
View all comments