Na navigaci | Klávesové zkratky

Readonly vlastnosti v PHP a jejich skrytá úskalí

Představte si, že byste mohli svým datům dát pevnou půdu pod nohama – jednou je nastavíte a pak si můžete být jistí, že je nikdo nezmění. Přesně to přineslo PHP 8.1 s readonly vlastnostmi. Je to jako dát vašim objektům neprůstřelnou vestu – chrání jejich data před nechtěnými změnami. Pojďme se podívat, jak vám tento mocný nástroj může usnadnit život a na co si při jeho používání dát pozor.

Začněme jednoduchým příkladem:

class User
{
    public readonly string $name;

    public function setName(string $name): void
    {
        $this->name = $name;  // První nastavení - vše OK
    }
}

$user = new User;
$user->setName('John');      // Paráda, máme jméno
echo $user->name;            // "John"
$user->setName('Jane');      // BOOM! Výjimka: Cannot modify readonly property

Jakmile jednou jméno nastavíte, je to jako vytesané do kamene. Žádné náhodné přepsání, žádné nechtěné změny.

Kdy je uninitialized opravdu uninitialized?

Často se setkávám s mýtem, že readonly vlastnosti musí být nastaveny v konstruktoru. Ve skutečnosti je PHP mnohem flexibilnější – můžete je inicializovat kdykoliv během života objektu, ale pouze jednou! Před prvním přiřazením jsou ve speciálním stavu ‚uninitialized‘, což je takový limbo stav mezi nebytím a bytím.

A tady přichází zajímavý detail – readonly vlastnosti nemohou mít výchozí hodnotu. A proč? Kdyby měly výchozí hodnotu, staly by se de facto konstantami – hodnota by byla nastavena při vytvoření objektu a už by nešla změnit.

Vyžadují se 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 si nejste jistí typem, můžete použít mixed.

Readonly třídy: Když jeden zámek nestačí

S PHP 8.2 přišla možnost posunout zabezpečení na další úroveň. Představte si, že místo zamykání jednotlivých místností můžete zamknout celou budovu. Tedy celou třídu:

readonly class User
{
    public string $name;     // Automaticky readonly!
    public string $email;    // Taky readonly!
}

Ale pozor, s velkou mocí přichází velká omezení:

  • Žádné vlastnosti bez typu
  • Statické vlastnosti jsou tabu
  • Dynamické vlastnosti? Ani s atributem #[AllowDynamicProperties]
  • A v PHP 8.2 mohla readonly třída dědit jen od readonly třídy (PHP 8.3 už je v tomto benevolentnější)

Kdo může inicializovat a kdy?

Tady je to zajímavé – podívejte se na tento kód:

$user = new User;
$user->name = 'John';  // BUUM! Cannot initialize readonly property from global scope

Překvapení? I když jde o první přiřazení, PHP řekne ne. Stejně tak potomek třídy nemůže inicializovat readonly vlastnost svého rodiče:

class Employee extends User
{
    public function setName(string $name): void
    {
        $this->name = 'EMP: ' . $name;  // BUUM! Ani potomek nemůže!
    }
}

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 vlastnosti můžou inicializovat i potomci (konečně!)
  • modifikátorem public(set) můžete povolit inicializaci i zvenčí:
class User
{
    public(set) readonly string $name;  // Nová svoboda v PHP 8.4
}

$user = new User;
$user->name = 'John';  // Teď už to funguje!

Když readonly neznamená „opravdu neměnné“

Představte si readonly jako zámek na dveřích – zamkne dveře, ale co se děje uvnitř místnosti, to už neuhlídá. 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!

Vidíte? Samotný objekt $settings je uzamčený, ale jeho vnitřnosti můžeme měnit, jak se nám zlíbí.

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.

class Configuration
{
    public readonly array $settings;

    public function initialize(): void
    {
        $this->settings = ['debug' => true];
        $this->settings['cache'] = true;  // BUUM! Tohle neprojde
    }
}

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
    {
        // Trik s referencí
        $mode = 'development';
        $this->settings = [
            'debug' => true,
            'mode' => &$mode,  // Reference je naše tajná zbraň!
        ];

        $mode = 'production';  // Tohle projde!
    }
}

Wither metody a readonly: Jak na to?

Při práci s neměnnými objekty často potřebujeme implementovat metody pro změnu stavu. Tyto „wither“ metody (na rozdíl od klasických setterů) nemodifikují původní objekt, ale vrací jeho novou instanci s požadovanou změnou. Tento pattern využívá například specifikace PSR-7 pro HTTP požadavky.

Když chceme tyto objekty nebo jejich vlastnosti označit jako readonly, narazíme na technické omezení – readonly vlastnost nelze změnit ani ve wither metodě, a to ani v kopii objektu. I když PHP 8.3 umožňuje měnit readonly vlastnosti v metodě __clone(), samotné klonování nestačí, protože v něm nemáme přístup k nové hodnotě. Můžeme to ale 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 praktický problém – readonly vlastnosti (podobně 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 testovacími frameworky je přímočará:

// bootstrap.php nebo začátek test souboru
DG\BypassFinals::enable();

// Pokud chceme zachovat readonly a odstranit jen final:
DG\BypassFinals::enable(bypassReadOnly: false);

Shrnutí: Co si odnést

Readonly vlastnosti jsou mocný nástroj pro zvýšení bezpečnosti a předvídatelnosti vašeho kódu. Zapamatujte si klíčové body:

  • Readonly vlastnosti můžete inicializovat kdykoliv, ale pouze jednou
  • Musíte explicitně definovat datový typ
  • Do PHP 8.4 byla inicializace možná pouze ve scope třídy, která je definuje
  • Readonly nezaručuje úplnou neměnnost – zejména u vnořených objektů
  • Od PHP 8.2 můžete označit celé třídy jako readonly
  • Pro testování máte k dispozici BypassFinals
  • PHP 8.4 přináší větší flexibilitu s modifikátorem public(set)

před měsícem v rubrice PHP | blog píše David Grudl | nahoru

Mohlo by vás zajímat

Komentáře

  1. Adam #1

    avatar

    Díky za článek, pomohl nyní na jednom webu.

    před 2 lety | odpovědět

Napište komentář

Text komentáře
Kontakt

(kvůli gravataru)



*kurzíva* **tučné** "odkaz":http://example.com /--php phpkod(); \--

phpFashion © 2004, 2024 David Grudl | o blogu

Ukázky zdrojových kódů smíte používat s uvedením autora a URL tohoto webu bez dalších omezení.