Problém, ktorý property hooks riešia
Typická PHP trieda obsahuje množstvo kódu, ktorý nerobí nič zaujímavé. Vlastnosť je súkromná, k nej patrí getter a setter, setter niekedy niečo overí alebo normalizuje. Pri desiatich vlastnostiach je to dvadsať metód, ktoré nikto nečíta:
<?php
// pred PHP 8.4
final class Zakaznik
{
private string $email;
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): void
{
$email = strtolower(trim($email));
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Neplatný e-mail.');
}
$this->email = $email;
}
}
Volajúci kód musí používať $zakaznik->getEmail(). Keď sa rozhodnete vlastnosť sprístupniť priamo, musíte prepísať celú aplikáciu. Presne naopak: keď máte verejnú vlastnosť a potrebujete k nej pridať validáciu, tiež musíte prepísať celú aplikáciu. Property hooks túto väzbu rušia.
Property hooks: getter a setter priamo pri vlastnosti
Property hook je blok kódu, ktorý sa vykoná pri čítaní alebo zápise vlastnosti. Syntax je priamo pri deklarácii:
<?php
final class Zakaznik
{
public string $email {
set (string $hodnota) {
$hodnota = strtolower(trim($hodnota));
if (!filter_var($hodnota, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Neplatný e-mail.');
}
$this->email = $hodnota;
}
}
}
$zakaznik = new Zakaznik();
$zakaznik->email = ' Jan.Novak@Priklad.SK ';
echo $zakaznik->email; // jan.novak@priklad.sk
Volajúci kód pracuje s obyčajnou verejnou vlastnosťou. Validácia sa vykoná, no navonok to vyzerá ako prosté priradenie. Ak zajtra validáciu zmeníte, žiadny iný súbor sa meniť nemusí.
Vypočítané vlastnosti cez get
Hook get je ešte užitočnejší. Umožňuje vlastnosť, ktorá nemá vlastné úložisko a počíta sa z iných hodnôt:
<?php
final class Faktura
{
public function __construct(
public int $zakladCentov,
public float $sadzbaDph = 0.23,
) {}
public int $dphCentov {
get => (int) round($this->zakladCentov * $this->sadzbaDph);
}
public int $spoluCentov {
get => $this->zakladCentov + $this->dphCentov;
}
public string $spoluText {
get => number_format($this->spoluCentov / 100, 2, ',', ' ') . ' EUR';
}
}
$faktura = new Faktura(zakladCentov: 125000);
echo $faktura->spoluText; // 1 537,50 EUR
Skrátený zápis so šípkou (get => výraz) je vhodný pre jednoriadkové výpočty. Pre zložitejšiu logiku použijete plný blok s return. Takáto vlastnosť sa nedá zapísať — pokus o priradenie skončí chybou, čo je presne to, čo chcete.
{{ $faktura->spoluText }} namiesto {{ $faktura->getSpoluText() }}. Pri formátovaní cien a dátumov to z šablón odstráni značné množstvo šumu.
Hodnota pred zmenou cez $field
Vnútri hooku máte prístup k podkladovému úložisku vlastnosti. Pri použití plného bloku sa naň odkážete cez názov vlastnosti, čo umožňuje aj lenivé prepočítanie s cache:
<?php
final class Text
{
private ?int $pocetSlovCache = null;
public function __construct(public string $obsah) {}
public int $pocetSlov {
get {
return $this->pocetSlovCache
??= str_word_count(strip_tags($this->obsah));
}
}
}
Asymetrická viditeľnosť: čítanie verejne, zápis súkromne
Druhá veľká novinka sa dá zhrnúť jednou vetou: vlastnosť môže mať inú viditeľnosť pre čítanie a inú pre zápis. Dovtedy ste na to potrebovali súkromnú vlastnosť a verejný getter:
<?php
final class Objednavka
{
public function __construct(
public private(set) string $stav = 'nova',
public protected(set) int $sumaCentov = 0,
) {}
public function zaplatit(): void
{
if ($this->stav !== 'nova') {
throw new DomainException('Objednávku už nie je možné zaplatiť.');
}
$this->stav = 'zaplatena'; // vnútri triedy povolené
}
}
$objednavka = new Objednavka();
echo $objednavka->stav; // v poriadku — čítanie je verejné
$objednavka->stav = 'zaplatena'; // Error — zápis je súkromný
Toto je presne to, čo väčšina entít potrebuje: stav si môže prečítať ktokoľvek, meniť ho smie len samotný objekt cez metódy s biznis pravidlami. Bez asymetrickej viditeľnosti ste museli písať getter pre každú takúto vlastnosť.
Kombinácia s readonly
readonly a asymetrická viditeľnosť riešia podobný problém, ale nie ten istý. readonly dovolí zápis práve raz. private(set) dovolí ľubovoľný počet zápisov, ale len zvnútra triedy. Pre entitu so stavom, ktorý sa mení, potrebujete private(set). Pre hodnotový objekt, ktorý sa po vytvorení nemení, zostáva správnou voľbou readonly.
readonly sa nekombinujú. Vlastnosť s hookom nemôže byť readonly. Ak potrebujete nemennosť aj validáciu, validujte v konštruktore a vlastnosť nechajte readonly bez hooku.
Hooks v rozhraniach
Rozhranie môže odteraz vyžadovať vlastnosť, nie len metódu. To je významná zmena pre návrh kontraktov:
<?php
interface MaIdentifikator
{
public string $id { get; }
}
interface MaStav
{
public string $stav { get; set; }
}
final class Zariadenie implements MaIdentifikator
{
public function __construct(public readonly string $id) {}
}
Implementácia môže kontrakt naplniť obyčajnou vlastnosťou aj hookom — volajúceho to nezaujíma. Vďaka tomu sa dajú písať rozhrania, ktoré nenútia každú implementáciu do tvaru getId().
Lazy objekty: odložená inicializácia bez knižnice
PHP 8.4 pridalo do reflexie podporu lenivých objektov. Objekt existuje, dá sa odovzdať ďalej, ale jeho skutočná inicializácia prebehne až pri prvom prístupe k vlastnosti alebo metóde. Dovtedy to vedeli len knižnice ako Doctrine cez generované proxy triedy:
<?php
$reflektor = new ReflectionClass(TazkyReport::class);
$report = $reflektor->newLazyGhost(function (TazkyReport $objekt): void {
// spustí sa až pri prvom reálnom použití objektu
$objekt->__construct(nacitajMilionRiadkov());
});
// sem sa dostaneme okamžite, dáta sa ešte nenačítali
$sluzba->zaregistruj($report);
V bežnej aplikácii toto priamo nepoužijete. Význam to má pre autorov frameworkov a ORM — a znamená, že závislosti registrované v kontajneri sa dajú vytvárať skutočne lenivo bez réžie generovaných proxy tried.
Drobnosti, ktoré ušetria najviac písania
Volanie metódy na novom objekte bez zátvoriek
<?php
// pred 8.4
$hodnota = (new Formatovac())->format($cislo);
// od 8.4
$hodnota = new Formatovac()->format($cislo);
Nové funkcie pre polia
Doteraz ste hľadanie prvku v poli riešili cyklom alebo kombináciou array_filter a reset. PHP 8.4 pridalo štyri funkcie, ktoré to robia priamo:
<?php
$objednavky = [...];
// prvá zhoda alebo null
$nezaplatena = array_find(
$objednavky,
fn (Objednavka $o) => $o->stav === 'nova'
);
// existuje aspoň jedna zhoda?
$maNezaplatene = array_any(
$objednavky,
fn (Objednavka $o) => $o->stav === 'nova'
);
// spĺňajú podmienku všetky?
$vsetkyZaplatene = array_all(
$objednavky,
fn (Objednavka $o) => $o->stav === 'zaplatena'
);
Na kolekciách v Laraveli tieto funkcie nepotrebujete — first(), contains() a every() robia to isté. Užitočné sú v čistom PHP a v knižniciach bez závislosti na frameworku.
Atribút #[\Deprecated]
Vlastné funkcie a metódy sa dajú označiť ako zastarané tak, aby PHP samo vygenerovalo upozornenie:
<?php
final class PlatobnaSluzba
{
#[\Deprecated(
message: 'Použite metódu vytvorPlatbu().',
since: '3.2.0',
)]
public function createPayment(array $data): Platba
{
return $this->vytvorPlatbu($data);
}
}
Pri údržbe väčšieho projektu alebo internej knižnice je to lacný spôsob, ako upozorniť kolegov bez čítania dokumentácie.
Čo z toho použiť hneď a čo nie
Property hooks nie sú dôvod prepisovať existujúci kód. Fungujúca trieda s gettermi a settermi nemá žiadny problém, ktorý by hooky riešili — prepis len vytvorí príležitosť na chybu. Odporúčam:
- Nové triedy píšte rovno s hookmi a asymetrickou viditeľnosťou. Ušetríte polovicu riadkov.
- Existujúce triedy prerábajte len vtedy, keď ich aj tak otvárate kvôli zmene.
- Eloquent modely zatiaľ nechajte tak. Laravel má vlastný mechanizmus prístupov (
Attribute) napojený na castovanie a serializáciu — miešať ho s hookmi prináša viac zmätku ako úžitku. - DTO a hodnotové objekty sú naopak ideálny kandidát. Tam hooky vyniknú najviac.
Zhrnutie
- Property hooks nahrádzajú triviálne gettery a settery a umožňujú vypočítané vlastnosti bez vlastného úložiska.
- Asymetrická viditeľnosť (
public private(set)) dáva entitám verejné čítanie a chránený zápis bez jediného gettera. - Rozhrania s vlastnosťami umožňujú kontrakty, ktoré nevynucujú tvar
getX(). - Lazy objekty ocenia hlavne autori frameworkov a ORM.
- Drobnosti —
new Trieda()->metoda(),array_find,#[\Deprecated]— ušetria denne niekoľko riadkov. - Prepisovať fungujúci kód len kvôli novej syntaxi sa nevypláca. Začnite pri nových triedach.
Ak ešte len plánujete prechod na PHP 8.4, prečítajte si aj článok o rozdieloch medzi PHP 8.3 a 8.4 a o bezpečnostnom checkliste pre produkčné PHP aplikácie.