Niebezpieczne użycie stałych w PHP

📅   22. 06. 2021
👤   Jan Barášek

Przy używaniu stałych w PHP należy pamiętać o dwóch ważnych rzeczach.

Stałe dynamiczne i statyczne

Stałą można zdefiniować w PHP albo statycznie bezpośrednio w klasie (najlepsze rozwiązanie), na przykład w następujący sposób:

class Region
{
	public const PREFIX = 420;
}

A sposób wykorzystania jest dość jasny. W czasie kompilacji klasy wartość stałej jest ustalona i można się do niej dostać, wywołując nazwę klasy i samą stałą. Najczęściej przez napisanie Region::PREFIX.

Innym (znacznie gorszym) sposobem jest zdefiniowanie stałej dynamicznie w czasie uruchamiania (najczęściej gdzieś w skrypcie konfiguracyjnym), gdzie jest to coś w rodzaju

define('BASE_DIR', __DIR__ . '/../');

Główną wadą definiowania stałej za pomocą funkcji define jest to, że skrypt definiujący stałą mógł nie zostać wywołany, więc stała nie będzie istniała przy próbie jej odczytania.

W połączeniu z użyciem stałej dynamicznej w definicji stałej statycznej w klasie, może to nawet spowodować błąd krytyczny:

class InvoiceGenerator
{
	// To jest całkowicie błędne!
	public const DATA_DIR = BASE_DIR . '/dane/faktura';
}

Wyjaśnienie:

Użycie stałej dynamicznej wewnątrz stałej statycznej ma tę poważną wadę, że wartości stałej dynamicznej nie można odczytać w czasie kompilacji. Skrypt ten musi być zatem przetwarzany ponownie w każdym żądaniu (tzn. nie może być przechowywany w pamięci podręcznej OPCache w celu optymalizacji szybkości), a jeśli stała w ogóle nie istniała, wyrzucany jest fatalny błąd czasu kompilacji i aplikacja nie może w ogóle działać.

Jeśli używasz programu PhpStan, może on automatycznie ostrzec Cię o tym problemie:

Reflection error: Could not locate constant "BASE_DIR" while
evaluating expression in InvoiceGenerator at line 6

Kształcenie:

Wartość wszystkich stałych powinna być zawsze stała.

Dziedziczenie stałych podczas używania statycznych

W niektórych przypadkach sensowne jest użycie dziedziczenia w celu nadpisania wartości stałej. Jednak w takim przypadku przodek nie może odczytać wartości od potomka (a przynajmniej nie powinien).

Przykładem może być definiowanie krajów i regionów:

abstract class Region
{
	public function getPrefix(): int
	{
		// Fatalny błąd!
		return static::REGION;
	}
}

final class CzechRepublic extends Region
{
	public const REGION = 420;
}

Paradoks polega na tym, że powyższy kod nie musi wyrzucać błędu, ale może zostać wyrzucony przez niewłaściwe użycie dziedziczenia.

Jeśli wywołamy metodę getPrefix() na potomku CzechRepublic, wszystko będzie w porządku, ponieważ wartość stałej zostanie poprawnie odczytana. Jeśli jednak potomek nie ustawi wartości stałej, zostanie wyrzucony błąd fatalny nieistniejącej stałej. Najgorsze w tym wszystkim jest to, że jest to ukryta zależność, która jest tworzona w implementacji metody, a programista dziedziczący klasę może nawet nie wiedzieć o tej zależności.

Najlepszym rozwiązaniem w tym przypadku jest albo zdefiniowanie stałej bezpośrednio w przodku z wartością domyślną (tak aby logika zawsze działała), albo przynajmniej rzucenie wyjątku w getterze.

abstract class Region
{
	public const REGION = null;

	public function getPrefix(): int
	{
		if (static::REGION === null) {
			throw new \LogicException('Region nie został określony.');
		}
		return static::REGION;
	}
}

final class CzechRepublic extends Region
{
	public const REGION = 420;
}

PhpStan odpowiada na ten błąd w następujący sposób:

Access to undefined constant static(Region):REGION.

Jan Barášek     Więcej o autorze

Autor pracuje jako starszy programista i architekt oprogramowania w Pradze. Projektuje i zarządza dużymi aplikacjami internetowymi, które znasz i używasz. Od 2009 r. zdobył bogate doświadczenie, które przekazuje za pośrednictwem tej strony internetowej.

Chętnie pomogę:

Kontakt