S PHP 8.1 přichází mocný nástroj, který výrazně zlepšuje způsob, jakým můžeme zajistit integritu dat – readonly proměnné. Jednou nastavíte, navždy platí. Pojďme se podívat, jak vám mohou usnadnit život a na co si při jejich používání dát pozor.
Začněme jednoduchým příkladem, který ilustruje základní použití:
class User
{
public readonly string $name;
public function setName(string $name): void
{
$this->name = $name; // povolená inicializace
}
}
$user = new User;
$user->setName('John'); // první volání - OK
echo $user->name; // vypíše "John"
$user->setName('John'); // druhé volání - CHYBA: pokus o druhý zápis vyhodí výjimku
// Error: Cannot modify readonly property User::$name
Jak vidíte, jakmile je readonly proměnná jednou nastavena, nelze její hodnotu již změnit.
Inicializace a stav „uninitialized“
Jedním z častých nedorozumění je představa, že readonly proměnné musí být nastaveny pouze v konstruktoru. Ve skutečnosti je možné je inicializovat kdykoliv během životního cyklu objektu – ale pouze jednou. Před prvním přiřazením se nacházejí ve speciálním stavu ‚uninitialized‘. Logicky také nemohou mít výchozí hodnotu – to by popíralo samotný princip jednorázové inicializace.
Vyžaduje typy
Readonly proměnné vyžadují explicitní definici datového typu. Je to
proto, že stav ‚uninitialized‘, který využívají, existuje pouze
u typovaných proměnných. Bez uvedení typu tedy readonly proměnnou nelze
definovat. Pokud typ neznáte, použijte mixed
.
Readonly třídy
PHP 8.2 přineslo možnost posunout koncept readonly na vyšší úroveň – můžeme označit celou třídu jako readonly. Tím se automaticky všechny její instanční vlastnosti stanou readonly:
readonly class User
{
public string $name; // automaticky readonly
public string $email; // automaticky readonly
}
Readonly třídy mají několik důležitých omezení:
- nemohou obsahovat neotypované vlastnosti
- nemohou obsahovat statické vlastnosti
- nemohou používat dynamické vlastnosti (ani s atributem #[AllowDynamicProperties])
- v PHP 8.2 platilo, že od readonly třídy mohla dědit pouze readonly třída, což od PHP 8.3 neplatí
Omezení scope a inicializace
Zajímavé je chování při pokusu o inicializaci readonly proměnné z různých kontextů. Následující kód překvapivě vyhodí výjimku, i když se jedná o první přiřazení:
$user = new User;
$user->name = 'John'; // CHYBA: Cannot initialize readonly property User::$name from global scope
Stejně tak není možné inicializovat readonly proměnnou ani z potomka třídy:
class Employee extends User
{
public function setName(string $name): void
{
$this->name = 'EMP: ' . $name;
// vyhodí výjimku: Cannot initialize readonly property User::$name from scope Employee
}
}
$employee = new Employee;
$employee->setName('John Smith');
Readonly proměnnou lze tedy inicializovat výhradně ze třídy, která ji definovala. Přesněji řečeno šlo. PHP 8.4 totiž přináší dvě důležité změny:
- Readonly proměnnou lze inicializovat i z potomka, tedy z třídy Employee
- Pomocí modifikátoru
public(set)
lze povolit zápis do readonly proměnné i mimo scope třídy:
class User
{
public(set) readonly string $name;
}
$user = new User;
$user->name = 'John'; // OK
echo $user->name; // OK
Readonly vs. skutečná neměnnost dat
Při práci s readonly proměnnými je klíčové pochopit, že samotné readonly nezaručuje úplnou neměnnost dat. Pokud do readonly proměnné uložíme objekt, jeho vnitřní stav zůstává modifikovatelný, objekt se automaticky nestává immutable (neměnným):
class Settings
{
public string $theme = 'light';
}
class Configuration
{
public function __construct(
public readonly Settings $settings = new Settings,
) {
}
}
$config = new Configuration;
$config->settings->theme = 'dark'; // toto je povoleno, přestože $settings je readonly!
U polí je situace specifická. Přímá modifikace prvků pole není možná, protože PHP to považuje za změnu celého pole. Existuje však výjimka – pokud pole obsahuje reference, jejich obsah měnit můžeme, protože PHP to nepovažuje za změnu samotného pole. Toto chování je konzistentní s běžným fungováním PHP:
class Configuration
{
public readonly array $settings;
public function initialize(): void
{
$dynamicValue = 'development';
$this->settings = ['mode' => 'debug', 'environment' => &$dynamicValue];
var_dump($this->settings); // ['mode' => 'debug', 'environment' => 'development']
$dynamicValue = 'production'; // povolená změna
var_dump($this->settings); // ['mode' => 'debug', 'environment' => 'production']
}
}
Naopak, přímá modifikace prvků pole není možná:
class Configuration
{
public readonly array $settings;
public function initialize(): void
{
$this->settings = ['debug' => true, 'cache' => false];
$this->settings['cache'] = true; // vyhodí výjimku!
}
}
Problém s „wither“ metodami
Při práci s neměnnými objekty (ať už kompletně nebo částečně) se
často setkáváme s potřebou implementovat metody pro změnu stavu. Tyto
metody, tradičně označované předponou with
(oproti
get
), nemění původní objekt, ale vrací novou instanci (klon)
objektu s požadovanou úpravou. Tento vzor využívá například specifikace
PSR-7 pro práci s HTTP požadavky.
Samozřejmě by dávalo smysl označit takové objekty nebo jejich vlastnosti
jako readonly. Narazíme však na technické omezení – readonly vlastnost
nelze změnit ani ve wither metodě, a to ani v kopii objektu. PHP 8.3 sice
přineslo možnost měnit readonly vlastnosti v metodě __clone()
,
ta ale sama o sobě nestačí, protože v ní nemáme přístup k nové
hodnotě určené ke změně. Můžeme to však vyřešit pomocí následující
obezličky:
class Request
{
private array $changes = [];
public function __construct(
public readonly string $method = 'GET',
public readonly array $headers = [],
) {}
public function withMethod(string $method): self
{
$this->changes['method'] = $method;
$dolly = clone $this;
$this->changes = [];
return $dolly;
}
public function __clone()
{
foreach ($this->changes as $property => $value) {
$this->$property = $value;
}
$this->changes = [];
}
}
$request = new Request('GET');
$newRequest = $request->withMethod('POST'); // Původní $request zůstává s GET
Testování a BypassFinals
Při psaní testů můžeme narazit na problém, že readonly (stejně jako final) komplikují mockování a testování. Naštěstí existuje elegantní řešení v podobě knihovny BypassFinals.
Tato knihovna dokáže za běhu odstranit klíčová slova final
a readonly
z vašeho kódu, což umožňuje mockovat i třídy a
metody, které jsou takto označené. Integrace s PHPUnit a dalšími
testovacími frameworky je snadná:
// bootstrap.php nebo začátek test souboru
DG\BypassFinals::enable();
// Pokud chceme zachovat readonly a odstranit jen final:
DG\BypassFinals::enable(bypassReadOnly: false);
Shrnutí
Readonly proměnné představují mocný nástroj pro zlepšení bezpečnosti a předvídatelnosti vašeho kódu. Pamatujte na klíčové body:
- Readonly proměnné lze inicializovat kdykoliv, ale pouze jednou
- Vyžadují explicitní definici datového typu
- Inicializace je možná pouze ve scope třídy, která je definuje (do PHP 8.4)
- Readonly neznamená automaticky immutable – zejména u objektů
- S příchodem PHP 8.2 můžeme jako readonly označit celé třídy
- Pro testování lze použít BypassFinals
- Od PHP 8.4 přichází větší flexibilita díky modifikátoru public(set)
Komentáře
Adam #1
Díky za článek, pomohl nyní na jednom webu.
Napište komentář