Krótko: PHP jest typowany dynamicznie: typ wartości jest znany dopiero w czasie działania, a bez dalszych ustaleń PHP w razie potrzeby automatycznie przekształca wartości. Od PHP 7 parametry, wartości zwracane i właściwości można jednak opatrzyć typami, a declare(strict_types=1) czyni sprawdzanie rygorystycznym. Kto zna reguły i stosuje ścisłe porównania za pomocą ===, unika całej klasy błędów logicznych i błędów bezpieczeństwa.
Typowanie dynamiczne, ale nie bez typów
W PHP przy $preis = 19.90; nie trzeba podawać typu. PHP rozpoznaje, że jest to liczba zmiennoprzecinkowa. Jeśli wartość zostanie później połączona z ciągiem znaków, PHP ją przekształci. To zachowanie nazywa się type juggling, czyli automatyczna konwersja typów. Ułatwia ono start, ale może prowadzić do zaskakujących wyników.
Dla osób podejmujących decyzje ma to znaczenie, ponieważ wiele starszych aplikacji PHP napisano bez deklaracji typów. Taki kod trudniej sprawdzać i modernizować. Nowsze projekty konsekwentnie używają typów, co ujawnia błędy wcześniej i dopiero wtedy pozwala w pełni wykorzystać analizę statyczną za pomocą narzędzi takich jak PHPStan czy Psalm.
Typy PHP w skrócie
Podręcznik PHP wymienia dziewięć typów wewnętrznych, które może mieć każde wyrażenie. Zwykle dzieli się je na trzy grupy:
| Grupa | Typy | Przykład |
|---|---|---|
| Typy skalarne | bool, int, float, string |
true, 42, 3.5, "Text" |
| Typy złożone | array, object, callable |
[1, 2], new DateTime() |
| Typy szczególne | null, resource |
null, otwarty plik |
Do sprawdzania typów podręcznik zaleca funkcje is_*(), takie jak is_int(). Do analizy nadaje się get_debug_type(), które od PHP 8.0.0 zwraca powszechnie używane nazwy typów, podczas gdy starsze gettype() zwraca na przykład "integer" i "double".
Obok konwersji automatycznej istnieje konwersja jawna: za pomocą rzutowania, takiego jak (int) $wert, lub settype() sam kod określa, jaki typ ma mieć wartość. Jest to zwyczajne przy danych z formularzy, ponieważ wszystko, co dociera przez $_GET lub $_POST, jest początkowo ciągiem znaków lub tablicą. Rzutowanie nie sprawdza jednak, czy wartość miała sens: z "abc" po cichu robi się 0. Przy danych wejściowych lepszym wyborem jest więc zwykle prawdziwe sprawdzenie za pomocą filter_var().
Klasy i interfejsy są w PHP również typami. Funkcja może więc wymagać, aby parametr był obiektem określonej klasy albo spełniał określony interfejs. Jak współdziałają klasy, interfejsy i enumy, wyjaśnia strona Programowanie obiektowe w PHP.
Deklaracje typów: od PHP 7.1 do 8.3
Deklaracje typów określają, jakie wartości funkcja przyjmuje i zwraca. System typów rozwijał się przez wiele wersji. Według dziennika zmian w podręczniku doszły między innymi te możliwości:
| Wersja | Nowość |
|---|---|
| 7.1.0 | typy dopuszczające null (?int), void, iterable |
| 7.2.0 | object |
| 7.4.0 | typowane właściwości klas |
| 8.0.0 | typy unii (kilka dozwolonych typów w jednym zapisie), mixed, typ zwracany static |
| 8.1.0 | typy przecięcia (A&B), typ zwracany never |
| 8.2.0 | typy DNF, typ literalny true, null i false jako samodzielne typy |
| 8.3.0 | typowane stałe klas, interfejsów, traitów i enumów |
DNF oznacza dysjunktywną postać normalną i pozwala na kombinacje takie jak (A&B)|null. Kto planuje wersje PHP, widzi z tej tabeli także, jakich środków językowych wymaga aplikacja.
strict_types: konwersja albo TypeError
Bez dodatkowych ustaleń PHP pracuje w trybie z konwersją. Funkcja z parametrami int przyjmuje wtedy także 1.5 i robi z tego 1; od PHP 8.1 ta konwersja ze stratą jest oznaczona jako przestarzała. Z declare(strict_types=1) na początku pliku PHP zgłasza zamiast tego TypeError.
<?php
declare(strict_types=1);
function summe(int $a, int $b): int
{
return $a + $b;
}
echo summe(1, 2); // 3
echo summe(1.5, 2.5); // TypeError, zamiast po cichu 3
Ważne są dwie subtelności. Po pierwsze, strict_types obowiązuje dla wywołań z pliku, w którym się znajduje, a nie dla zdefiniowanych tam funkcji. Jeśli plik bez strict_types wywołuje tę samą funkcję, następuje konwersja. Po drugie, liczba całkowita tam, gdzie oczekiwana jest liczba zmiennoprzecinkowa, jest dozwolona także w trybie ścisłym.
Porównania luźne i ścisłe
Operator == porównuje po konwersji typów, a === porównuje wartość i typ bez konwersji. Podręcznik w większości przypadków zaleca === i !==, ponieważ reguły konwersji nie zawsze są oczywiste.
<?php
var_dump(0 == "a"); // PHP 7: true, PHP 8: false
var_dump("1" == "01"); // true, oba są ciągami numerycznymi
var_dump("1" === "01");// false, ten sam typ, ale inny ciąg znaków
var_dump(1 == true); // true
var_dump(1 === true); // false
W PHP 8.0 porównania między liczbami a ciągami nienumerycznymi stały się rozsądniejsze. Starsze aplikacje mogą po aktualizacji zachowywać się inaczej, na przykład w blokach switch. Wyrażenie match, również wprowadzone w PHP 8.0, porównuje zawsze ściśle. Jak luźne porównania prowadzą do luk bezpieczeństwa, na przykład przy sprawdzaniu tokenów lub haseł, opisuje artykuł Type juggling: luźne porównania jako ryzyko bezpieczeństwa.
Typy, aktualizacje i PHP2Shell
Rygorystyczny system typów czyni kod aplikacji bardziej odpornym, ale nie chroni przed błędami w samym interpreterze. Luki PHP2Shell CVE-2026-109618 i CVE-2026-109619 leżą w zarządzaniu pamięcią i żądaniami w PHP. Usunięto je w PHP 8.5.12, 8.4.27, 8.3.36 i 8.2.35; potem wszystkie procesy PHP muszą się zrestartować.
System typów mimo to odgrywa rolę, a mianowicie przy aktualizacji. Aplikacje, które nadal tkwią na PHP 8.1 lub starszym, nie otrzymują od projektu PHP oficjalnych aktualizacji. Dobrze otypowany kod znacznie łatwiej podnieść do wspieranej wersji, ponieważ narzędzia wcześnie wykrywają niezgodności. W starszych projektach pomaga nasza migracja do wspieranej wersji PHP.
Proszę włączać strict_types stopniowo w nowych i przerabianych plikach. W ten sposób bezpieczeństwo typów rośnie, a stary kod nie psuje się za jednym zamachem.
Jak konkretnie pomaga PHP SOS?
- Sprawdzamy, jakiej wersji PHP minimalnie wymaga Państwa aplikacja, i porównujemy ją z wersjami, w których luki usunięto.
- Uruchamiamy analizę statyczną na kodzie i oznaczamy miejsca z luźnymi porównaniami w sprawdzeniach istotnych dla bezpieczeństwa.
- Planujemy aktualizacje tak, aby zmienione reguły porównań zostały wcześniej przetestowane.
- Instalujemy aktualizację PHP, restartujemy wszystkie workery i dokumentujemy stan.
Częste pytania
Czy PHP jest typowany silnie, czy słabo?
PHP jest typowany dynamicznie: typ zmiennej jest znany dopiero w czasie działania. Bez strict_types PHP w razie potrzeby przekształca wartości. Dzięki deklaracjom typów i strict_types kod można jednak uczynić bardzo rygorystycznym.
Co daje declare(strict_types=1)?
Wyłącza automatyczną konwersję wartości skalarnych dla wywołań z danego pliku. Jeśli wartość nie pasuje do zadeklarowanego typu, PHP zgłasza TypeError. Jedynym wyjątkiem jest liczba całkowita tam, gdzie oczekiwana jest liczba zmiennoprzecinkowa.
Dlaczego 0 == "a" w PHP 8 jest fałszem, a w PHP 7 prawdą?
Do PHP 7 nienumeryczny ciąg znaków był przy porównaniu z liczbą zamieniany na liczbę, z „a” robiło się 0. Od PHP 8.0 PHP w takim przypadku nie porównuje już numerycznie, wynikiem jest false.
Czy typy chronią przed lukami bezpieczeństwa w samym PHP?
Nie. Typy zapobiegają błędom w kodzie Państwa aplikacji. Luki w interpreterze, takie jak PHP2Shell, zamyka wyłącznie aktualizacja PHP. Nowoczesne funkcje typów wymagają jednak aktualnych wersji PHP, więc są dobrym powodem do aktualizacji.