Strict types w PHP 7

strict types w php 7

PHP to język używający typowania dynamicznego, inaczej zwanego słabym. W skrócie oznacza to tyle, że typ zmiennej jest ustalany w czasie działania programu, a sam interpreter spróbuje czasem dostosować go według pewnych zasad. Przykładowe działanie w języku PHP dla początkujących może być małym zdziwieniem: "3" + null + 2 w języku PHP równa się 5.

Tego trzeba się nauczyć i być przede wszystkim świadomym. Gorzej wygląda to w przypadku takiej samej transformacji typów dla argumentów funkcji. Rezultat także może być nieoczekiwany, ale już nieco trudniej go kontrolować.

<?php

function calculate(int $num1, int $num2): void
{
    echo $num1 + $num2;
}

calculate(true, false);

Wynik z takich danych wejściowych to 1. Wartości typu bool zostały transformowane do typu int, gdzie odpowiednio true = 1, a false = 0.

<?php

declare(strict_types=1);

function calculate(int $num1, int $num2): void
{
    echo $num1 + $num2;
}

calculate(true, true);

Na całe szczęście od wersji 7 PHP można wymusić restrykcyjne traktowanie typów dla takiego przypadku. Jedyną różnicą między dwoma blokami kodu jest deklaracja strict_types=1. Tym razem skończy się to fatal errorem, co jest jak najbardziej zasadne.

Fatal error: Uncaught TypeError: Argument 1 passed to calculate() must be of the type integer, boolean given, called in […][…] on line 10 and defined in […][…]:5
Stack trace: 0 […]: calculate(true, false) 1 {main} thrown in […][…] on line 5

Nie taka nowość jakby się wydawało…

Mam wrażenie, że wszyscy programiści o tym wiedzą. Zazwyczaj śledzi się nowości wchodzące wraz z nową wersją, a szczególnie przejście z PHP5.6 na PHP7.0. Tak więc pewnie nie wielka część z Was dowie się czegoś istotnego. Cel jest jednak inny – przekonać do używania tego mechanizmu.

Patrząc na kod open source raczej rzadko można doszukać się używania tej funkcjonalności. Rozumiem starsze rozwiązania, które są tylko wspierane i dostosowywane najniższym kosztem do kolejnych wersji języka, ale próżno szukać chociażby w najnowszej wersji Symfony, czy Laminas. Oczywiście zakładam, że programiści są świadomi i nie pozwalają sobie na tego rodzaju przypadkowe działanie kodu, ale po co samemu się pilnować skoro możemy to wymusić.

Szczególnie trudne do rozpoznania przypadki to te, w których dane wejściowe pochodzą od użytkownika. Jasna sprawa, że trzeba je poddać sanityzacji i walidacji, ale to i tak może nie wystarczyć. A przecież to jedna z najważniejszych zasad: znaj wejście i wyjście.

Kiedy ja zaczynałem używać restrykcyjnego typowania to bardzo przeszkadzał mi pewien fakt. Niestety nie da się włączyć tego globalnie dla całego projektu. No a przynajmniej nie tak łatwo – są pewne obejścia, ale nie polecam takich rozwiązań, dlatego też nie będę ich prezentował. Wspominam o nich tylko dla kompletności wpisu. Zresztą teraz już rozumiem, że nawet gdyby natywnie istniała taka opcja to mało komu udałoby się z niej skorzystać – a na pewno nie w tych projektach, w których wykorzystywane są zewnętrzne zależności. Czyli pewnie w większości, a we wszystkich tych, w których ja brałem udział.

Wynika z tego więc, że nawet startując z nowym projektem, nie można zastosować restrykcyjnego podejścia dla całego projektu, bo biblioteki nie są na to gotowe. Co nie oznacza, że nie są pisane w sposób restrykcyjny, ale kto wie skoro interpreter tego nie pilnuje.

PhpStorm jak zawsze ogromne ułatwienie

Może się więc okazać, że dodawanie tego w każdym pliku jest uciążliwe. Ja znalazłem na to rozwiązanie, którym podzielę się poniżej. Teraz już nie ma możliwości, że zdarzy mi się zapomnieć, a Wam odpada jeden argument przeciw temu mechanizmowi. W końcu w dzisiejszych czasach mamy IDE. Ja korzystam z PhpStorm, więc pokażę jak załatwić w nim sprawę. Wystarczy wejść w File > Settings > Editor > File and Code Templates i w szablonach wrzucić jedną linię: declare(strict_types=1); dla klas, interfejsów i traitów. Na screenie i w bloku z kodem prezentuję mój template dla klasy:

<?php

declare(strict_types=1);

#if (${NAMESPACE})
namespace ${NAMESPACE};
#end

class ${NAME}
{

}

Teraz przy tworzeniu nowej klasy nie trzeba pamiętać o ręcznym wrzucaniu deklaracji. Konfiguracja jest globalna, więc uważajcie z tym w starych projektach, gdzie typowanie jest słabe. Tyle, że tworząc nowy plik i tak możecie narzucić konwencję – mały plus tego że można tylko dla niektórych plików używać.

Warto kontrolować dane wejściowe do każdej metody. To zawsze była biznesowa przewaga Javy i innych języków silnie typowanych nad PHP, więc warto ją zniwelować. Takie przesłanie płynie z tego wpisu, a dodatkowo wrzucam Wam rozwiązanie, które mi ułatwia to osiągnąć. Przy okazji tak samo zalecam używanie w testach asercji assertSame() zamiast assertEquals() – jako, że typ jest istotny w całej układance.

Krystian Żądło
Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan angielskiej piłki nożnej, dobrego humoru oraz podcastów.