Představte si, že by vaše PHP objekty mohly být čistší, přehlednější a lépe použitelné. Dobrá zpráva – už nemusíte snít! PHP 8.4 přichází s revoluční novinkou v podobě property hooks a asymetrické viditelnosti, které kompletně mění pravidla hry v objektově orientovaném programování. Zapomeňte na neohrabané gettery a settery – konečně máme k dispozici moderní a intuitivní způsob, jak kontrolovat přístup k datům objektů. Pojďme se podívat na to, jak tyto novinky mohou změnit váš kód k nepoznání.
Property hooks představují promyšlený způsob, jak definovat chování
při čtení a zápisu vlastností objektu – a to mnohem čistěji a
výkonněji než dosavadní magické metody __get/__set
. Je to jako
byste dostali k dispozici sílu magických metod, ale bez jejich typických
nevýhod.
Podívejme se na jednoduchý příklad z praxe, který vám ukáže, proč
jsou property hooks tak užitečné. Představme si běžnou třídu
Person
s veřejnou property age
:
class Person
{
public int $age = 0;
}
$person = new Person;
$person->age = 25; // OK
$person->age = -5; // OK, ale to je přece nesmysl!
PHP sice díky typu int
zajistí, že věk bude celé číslo
(to lze od PHP 7.4), ale co s tím záporným věkem? Dříve bychom museli
sáhnout po getterech a setterech, property by musela být private, museli
bychom doplnit spoustu kódu… S hooks to vyřešíme elegantně:
class Person
{
public int $age = 0 {
set => $value >= 0 ? $value : throw new InvalidArgumentException;
}
}
$person->age = -5; // Ups! InvalidArgumentException nás upozorní na nesmysl
Krása tohoto řešení spočívá v jeho jednoduchosti – navenek se
property chová úplně stejně jako dřív, můžeme číst i zapisovat
přímo přes $person->age
. Ale máme plnou kontrolu nad tím,
co se při zápisu děje. A to je teprve začátek!
Můžeme jít ještě dál a vytvořit třeba hook pro čtení. Hookům lze přidat atributy. A samozřejmě mohou obsahovat složitější logiku než jednoduchý výraz. Podívejte se na tento příklad práce se jménem:
class Person
{
public string $first;
public string $last;
public string $fullName {
get {
return "$this->first $this->last";
}
set(string $value) {
[$this->first, $this->last] = explode(' ', $value, 2);
}
}
}
$person = new Person;
$person->fullName = 'James Bond';
echo $person->first; // vypíše 'James'
echo $person->last; // vypíše 'Bond'
A něco důležitého: kdykoliv se přistupuje k proměnné (i uvnitř samotné třídy Person), vždy se využijí hooks. Jediná výjimka je přímý přístup k reálné proměnné uvnitř kódu samotného hooku.
Ohlédnutí do minulosti: Co nás naučil SmartObject?
Pro uživatele Nette může být zajímavé ohlédnout se do minulosti. Framework totiž podobnou funkcionalitu nabízel už před 17 lety ve formě SmartObject, který výrazně vylepšoval práci s objekty v době, kdy PHP v této oblasti značně zaostávalo.
Pamatuju si, že tehdy přišla vlna bezbřehého nadšení, kdy se properties používaly prakticky všude. Tu pak vystřídala vlna opačná – nepoužívat je nikde. Důvod? Chybělo jasné vodítko, kdy je lepší použít metody a kdy property. Ale dnešní nativní řešení je kvalitativně úplně jinde.Property hooks a asymetrická viditelnost jsou plnohodnotné nástroje, které nám dávají stejnou úroveň kontroly jako máme u metod. Proto dnes můžeme mnohem lépe rozlišit, kdy je property skutečně tím správným řešením.
Backed nebo Virtual? Dobrá otázka!
Podívejte se na tento kód a zkuste si rychle odpovědět – je to vlastně jednoduchý kvíz:
- Můžeme do
$age
zapisovat? - A co
$adult
– můžeme číst i zapisovat?
class Person
{
public int $age = 0 {
set => $value >= 0 ? $value : throw new InvalidArgumentException;
}
public bool $adult {
get => $this->age >= 18;
}
}
Samozřejmě $age
je, jak jsme si řekli už dříve, property
pro čtení i pro zápis. Ale $adult
je jen pro čtení!
A tady narážíme na první zapeklitost v designu property hooks. Ze signatury property vůbec nepoznáme, jestli do ní můžeme zapisovat nebo ji číst!
Odpověď se totiž skrývá v kódu, v implementaci hooků. Property totiž mohou být dvojího druhu: backed (se skutečným úložištěm v paměti) a virtuální (které pouze simulují existenci property). To, zda je property backed nebo virtuální, rozhoduje, zda se v kódu hooku na ni odkazujeme.
Property je backed (má vlastní úložiště), když:
- v těle hooku se na ni odkazujeme přes
$this->propertyName
- nebo má zkrácený
set
, který automaticky znamená zápis do$this->propertyName
V našem příkladu tedy:
- Property
$age
je backed, protože používá zkrácenýset
(což automaticky znamená zápis do$this->age
) - Property
$adult
je virtuální, protože žádný její hook neobsahuje$this->adult
Je to sice mazané řešení, ale ne zrovna šťastné. Tak zásadní informaci, jako zda lze property číst nebo do ní zapisovat, má prozradit API a signatura na první pohled, ne až studium implementace.
Když reference, tak bezpečně!
Reference existují v PHP od jeho počátků. Pomocí znaku
&
můžete propojit dvě proměnné tak, aby ukazovaly na
stejné místo v paměti. Je to jako mít dva dálkové ovladače k jedné
televizi – ať zmáčknete kterýkoliv, ovládáte tu samou obrazovku.
Ale co kdyby někdo mohl získat referenci na property s set
hookem? Mohl by její hodnotu měnit přímo a kompletně tak obejít veškerou
validaci. Podívejte se na tento příklad:
class Person
{
public int $age = 0 {
set => $value >= 0 ? $value : throw new InvalidArgumentException;
}
}
$person = new Person;
$ref = &$person->age; // Fatal error: Tohle neprojde!
$ref = -5; // Kdyby to prošlo, validace by byla k ničemu
PHP na to myslelo a elegantně to vyřešilo (tedy pardon, mysleli na to
Ilija Tovilo a Larry Garfield, autoři hooků). Získat referenci na takovou
property prostě není možné (myšleno na backed proměnnou se
set
hookem). Je to správné řešení – property hook má
zajistit, že se do property dostane jen platná hodnota, a reference by tuto
kontrolu obcházely.
Když pole potká property hooks – zajímavá výzva!
Práce s poli v PHP je obvykle příjemně přímočará. Do pole v property můžeme přidávat prvky různými způsoby:
class Person
{
public array $phones = [];
}
$person = new Person;
$person->phones[] = '777 123 456'; // přidá číslo na konec pole
$person->phones['bob'] = '777 123 456'; // přidá číslo s konkrétním klíčem
A právě tady narážíme na zajímavý problém s property hooks. Představme si, že chceme vytvořit třídu Person, která bude obsahovat seznam telefonních čísel, a chceme, aby se u nich automaticky ořezávaly mezery na začátku a konci:
class Person
{
public array $phones = [] {
set => array_map('trim', $value);
}
}
$person = new Person;
$person->phones[] = '777 123 456'; // Překvapení! Error: Indirect modification of Person::$phones is not allowed
Proč to nefunguje? Operace $person->phones[]
totiž v PHP
funguje ve dvou krocích:
- Nejdřív získá referenci na pole pomocí
get
- Pak do získaného pole přidá novou hodnotu
Tedy vůbec se nevolá set
hook. Ba co víc, jak už víme
z předchozí kapitoly, nelze získat referenci na backed proměnnou se
set
hookem (tedy udělat první krok). Proto ta chybová
hláška.
Ani metoda addPhone()
, která by volala
$this->phones[] = $phone
, nám nepomůže – všechny
přístupy k property (i uvnitř třídy) totiž procházejí
přes hooky.
Tak jak z toho ven? Pojďme si projít možná řešení. První, které vás možná napadne:
$phones = $person->phones; // načteme pole
$phones[] = ' 777 123 456 '; // přidáme číslo
$person->phones = $phones; // uložíme zpět
Funguje to, ale… představte si pole s tisíci čísly. Náš
set
hook by musel provést trim()
na všech číslech
znovu, i když se přidalo jediné. To není zrovna ukázka efektivity.
Existuje lepší cesta – uvědomit si, že pokud má pole nějak specificky pracovat se svými prvky (třeba ořezávat mezery), mělo by to být jeho zodpovědností, ne úkolem třídy, která ho jen drží. Jasně, pole samo o sobě nenaučíme novým trikům, ale můžeme ho „zabalit“ do objektu s rozhraním ArrayAccess:
class Phones implements ArrayAccess
{
private array $data = [];
public function __construct(array $data = [])
{
$this->data = array_map('trim', $data);
}
public function offsetSet(mixed $offset, mixed $value): void
{
$value = trim($value);
if ($offset === null) {
$this->data[] = $value;
} else {
$this->data[$offset] = $value;
}
}
// implementace dalších metod pro ArrayAccess...
}
class Person
{
function __construct(
public Phones $phones = new Phones,
) {}
}
$person = new Person;
$person->phones[] = ' 777 123 456 '; // Hurá! Číslo se uloží pěkně ořezané
A teď třešnička na dortu – můžeme využít hook k tomu, aby do
$person->phones
šlo zapsat i obyčejné pole:
class Person
{
function __construct(
public Phones $phones = new Phones {
set(array|Phones $value) => is_array($value) ? new Phones($value) : $value;
},
) {}
}
$person = new Person;
$person->phones = [' 888 999 000 ', '777 888 999']; // Magicky se převede na Phones a ořeže řetězce
Jak vidíte, hooks mohou obsahovat i promoted properties.
Ještě se podívejme na alternativní řešení. Vzpomeňte si, že kromě
backed property máme ještě virtuální property – ty, které
nepoužívají $this->propertyName
v těle hooku. A tady se
skýtá druhé řešení:
class Person
{
private array $_phones = []; // skutečné úložiště čísel
public array $phones { // virtuální property pro veřejný přístup
get => $this->_phones;
set {
$this->_phones = array_map('trim', $value);
}
}
public function addPhone(string $phone): void
{
$this->_phones[] = trim($phone);
}
}
$person = new Person;
$person->addPhone(' 777 123 456 '); // Přidá ořezané číslo
echo $person->phones[0]; // Vypíše "777 123 456"
$person->phones = [' 888 999 000 ']; // Nastaví nové pole s ořezanými čísly
Tady jsme zůstali u klasického pole, ale schovali jsme ho za privátní proměnnou. Na venek nabízíme virtuální property pro čtení celého pole a jeho kompletní přepsání, plus specializovanou metodu pro přidávání jednotlivých čísel.
Hooks a dědičnost: Když potomci přebírají žezlo
Potomci mohou nejen přidávat hooks k vlastnostem, které je dosud neměly, ale také předefinovat ty existující. Podívejme se na příklad:
class Person
{
public string $email;
public int $age {
set => $value >= 0
? $value
: throw new InvalidArgumentException('Věk nemůže být záporný');
}
}
class Employee extends Person
{
// Přidá hook k vlastnosti, která žádný neměla
public string $email {
set => strtolower($value); // Emaily vždy převedeme na malá písmena
}
// Rozšíří existující validaci věku
public int $age {
set => $value <= 130
? parent::$age::set($value) // Nejdřív ověříme původní podmínku
: throw new InvalidArgumentException('130 let? Tomu nevěřím!');
}
}
Všimněte si té zajímavé syntaxe parent::$age::set($value)
.
Na první pohled možná vypadá zvláštně, ale dává perfektní smysl –
nejdřív se odkážeme na vlastnost v rodiči a pak na její hook. Je to jako
bychom řekli „hej, zavolej set hook na age
vlastnosti mého
rodiče“.
A co víc – můžeme hooks označit jako final
, pokud chceme
zabránit jejich přepsání v potomcích. Dokonce můžeme jako
final
označit celou property – pak ji potomci nemohou změnit
žádným způsobem (ani přidat hooks, ani rozšířit její viditelnost).
class Person
{
// Tenhle hook už nikdo nepřepíše
public int $age {
final set => $value >= 0 ? $value : throw new InvalidArgumentException;
}
// A tuhle property už vůbec nikdo nezmění
final public string $id;
}
Property v rozhraních
Překvapivou novinkou je podpora property v rozhraních a abstraktních třídách. Představte si, že vytváříte rozhraní pro entity, které obsahují řetězec se jménem. Doteď jsme museli psát něco takového:
interface Named
{
public function getName(): string;
public function setName(string $name): void;
}
Nuda, že? S property hooks můžeme být mnohem elegantnější! V rozhraní teď můžeme deklarovat přímo property, a to dokonce asymetricky – můžeme říct zvlášť, co má být čitelné a co zapisovatelné:
interface Named
{
// Říkáme: "Implementující třída musí mít veřejně čitelnou property name"
public string $name { get; }
}
A teď to zajímavé – jak můžeme takové rozhraní implementovat? Máme hned několik možností:
class Person implements Named
{
public string $name; // Nejjednodušší řešení - obyčejná property
}
class Employee implements Named
{
public string $name { // Pokročilejší - složené jméno
get => $this->firstName . ' ' . $this->lastName;
}
private string $firstName;
private string $lastName;
}
Všimněte si zajímavého detailu – rozhraní Named
požaduje property pouze pro čtení, ale třída Person
nabízí
property čitelnou i zapisovatelnou. A to je naprosto v pořádku –
rozhraní totiž definuje jen minimální požadavky. Je to jako když řeknete
„potřebuju auto, co jede dopředu“ a dostanete auto, co umí i couvat –
splňuje to vaše minimální požadavky a přidává něco navíc.
Pro puntičkáře: V rozhraní musíme u property použít klíčové slovo
public
, i když je to vlastně nadbytečné – vše v rozhraní
je ze své podstaty veřejné. U metod uvádět public
je
hloupost, ale u property je to vyžadováno kvůli konzistenci syntaxe.
A ještě jedna věc stojí za zmínku – všimli jste si té zvláštní
syntaxe { get; set; }
? Zatímco ve třídě můžeme napsat
jednoduše public string $name
, v rozhraní musíme explicitně
říct, jaké operace property podporuje. Je to sice trochu pracnější, ale
dává to smysl – u rozhraní chceme být maximálně explicitní v tom, co
požadujeme.
Property v abstraktních třídách: To nejlepší z obou světů
Abstraktní třídy si vezmou to nejlepší z rozhraní a přidají vlastní šťávu. Mohou nejen deklarovat property, ale také nabídnout výchozí implementaci některých hooků:
abstract class Person
{
// Čistě abstraktní property - implementaci dodá potomek
abstract public string $name { get; }
// Protected property s oběma operacemi
abstract protected int $age { get; set; }
// Tady už nabízíme hotovou validaci emailu
abstract public string $email {
get; // tento hook je abstraktní a potomek ho musí implementovat
set => Nette\Utils\Validators::isEmail($value)
? $value
: throw new InvalidArgumentException('Tohle nevypadá jako email...');
}
}
A teď něco opravdu zajímavého – kovarianci a kontravarianci!
Zní to jako zaklínadlo, ale je to vlastně jednoduchá věc. Podívejte se:
class Animal {}
class Dog extends Animal {}
interface PetShop
{
// Property jen pro čtení může vracet specifičtější typ
public Animal $pet { get; }
}
class DogShop implements PetShop
{
// Vrací psa místo zvířete - to je v pohodě!
public Dog $pet { get; }
}
Když má property pouze hook get
, může v potomkovi vracet
specifičtější typ (tomu se říká kovariance). Představte si to jako:
„Slíbil jsem ti zvíře, a pes je přece taky zvíře, ne?“
Naopak property pouze s hookem set
může v potomkovi
přijímat obecnější typ (kontravariance). Je to logické – když umím
pracovat s konkrétním typem, zvládnu i jeho předka.
Jakmile má property oba hooky get
i set
, musí typ
zůstat stejný. Proč? Protože by to mohlo vést k nekonzistencím –
nemůžeme slíbit, že vrátíme psa, když nám někdo může přes setter
podstrčit kočku!
Asymetrická viditelnost: Každému, co jeho jest
Představte si, že vytváříte třídu Person a chcete, aby datum narození mohl číst kdokoliv, ale měnit ho mohla jen samotná třída. Dřív byste museli sáhnout po getterech a setterech, ale teď? Teď máme elegantní řešení:
class Person
{
public private(set) DateTimeImmutable $dateOfBirth;
}
Tenhle zápis říká: „Číst může každý, zapisovat jen třída
sama.“ První modifikátor public
určuje viditelnost pro
čtení, druhý private(set)
pro zápis. A protože veřejné
čtení je default, můžeme ho vynechat a psát prostě:
class Person
{
private(set) DateTimeImmutable $dateOfBirth;
}
Samozřejmě platí logické pravidlo – viditelnost pro zápis nemůže
být širší než pro čtení. Nemůžeme použít třeba
protected public(set)
– to by bylo jako říct „číst můžou
jen potomci, ale zapisovat může každý“. Trochu podivné, ne?
A co dědičnost? V PHP platí, že potomek může viditelnost buď zachovat, nebo rozšířit z protected na public. To samé platí i pro asymetrickou viditelnost:
class Person
{
public protected(set) string $name; // Číst může každý, zapisovat jen potomci
}
class Employee extends Person
{
public public(set) string $name; // Potomek může rozšířit práva zápisu
}
Zajímavý je případ private(set)
. Taková property je
automaticky final
– když řekneme, že zapisovat může jen
třída sama, logicky to znamená, že ani potomci nemají právo
to měnit.
A nejlepší na tom je, že asymetrickou viditelnost můžeme kombinovat s hooks:
class Person
{
private(set) DateTimeImmutable $birthDate {
set => $value > new DateTimeImmutable
? throw new InvalidArgumentException('Narození v budoucnosti? Sci-fi!')
: $value;
}
}
Tahle property má všechno: je veřejně čitelná, zapisovat do ní může jen třída sama, a ještě kontroluje, jestli datum není v budoucnosti. Hooks řeší „co se má stát“, asymetrická viditelnost „kdo to může udělat“. Perfektní tým!
Asymetrická viditelnost a pole: Elegantní řešení starého problému
Pamatujete na naše trápení s telefonními čísly? Asymetrická viditelnost nám nabízí ještě jedno řešení:
class Person
{
private(set) array $phones = [];
public function addPhone(string $phone): void
{
$this->phones[] = trim($phone);
}
}
$person = new Person;
var_dump($person->phones); // OK: můžeme číst
$person->addPhone('...'); // OK: můžeme přidat číslo
$person->phones = []; // CHYBA: nemůžeme přepsat celé pole
Pole je veřejně čitelné, ale nikdo zvenčí ho nemůže přepsat. Pro přidání nového čísla máme specializovanou metodu. Žádné složité objekty simulující pole, žádné virtual properties – jen čistá, jasná kontrola přístupu.
Pro úplnost dodejme, že u vlastnosti s omezeným zápisem nemůžete získat referenci zvenku:
$ref = &$person->phones; // Fatal error: Takhle ne!
Reference jsou povolené jen ze scope, ze kterého je property zapisovatelná. Je to logické – reference by mohla obejít naše omezení pro zápis.
Když to shrneme, pro práci s polem v property máme teď několik možností:
- Chytrý objekt simulující pole (přináší víc možností, ale taky víc kódu)
- Backed property s hookem (znemožňuje přímou modifikaci pole)
- Virtual property s privátním úložištěm (vyžaduje metody pro úpravy)
- Asymetrická viditelnost (přesouvá logiku do metod)
Který přístup vybrat? Jak už to bývá – záleží na konkrétním případu. Prostě si zkusit, které API nejlépe sedne do ruky.
Readonly a asymetrická viditelnost: Konečně svoboda volby!
Modifikátor
readonly
jsou ve skutečnosti dva modifikátory v jednom:
zakazuje vícenásobný zápis a zároveň omezuje zápis na private. Vlastně
to není žádný readonly, je to spíš writeonce zkřížený s
private(set)
.
To druhé mi přišlo vždy zbytečně přísné a nepraktické. Proč by readonly vlastnost nemohla být zapisovatelná třeba v potomcích?
PHP 8.4 to konečně změnilo. Teď readonly
dělá property
defaultně protected(set)
, tedy zapisovatelnou i v potomcích.
A když potřebujeme jinou viditelnost? Jednoduše si ji nastavíme:
class Person
{
// Readonly přístupná jen uvnitř třídy (staré chování)
public private(set) readonly string $name;
// Readonly přístupná i v potomcích (nové výchozí chování)
public readonly string $dateOfBirth;
// Readonly zapisovatelná kdekoliv (ale jen jednou!)
public public(set) readonly string $id;
public function rename(string $newName): void
{
$this->name = $newName; // Uvnitř třídy můžeme měnit
}
}
class Employee extends Person
{
public function setBirthDate(DateTimeImmutable $date): void
{
$this->dateOfBirth = $date; // V potomkovi můžeme měnit
}
}
$person = new Person;
$person->id = 'abc123'; // Tohle projde
$person->id = 'xyz789'; // Ale tohle už ne - readonly!
Což nám dává přesně tu flexibilitu, kterou potřebujeme.
Když si terminologie protiřečí…
Pojďme se podívat na malý terminologický zmatek v PHP:
- Pořád mluvíme o čtení a zápisu vlastností
- Máme modifikátor
readonly
- V phpDoc najdeme anotace
@property-read
a@property-write
- Ale v hooks a asymetrické viditelnosti najednou
používáme
get/set
Bylo by logičtější používat termíny read
a
write
, ne?
U hooks bych ještě pochopil použití get/set
– jde
o akce a navíc to navazuje na magické metody __get/__set
. Ale
u asymetrické viditelnosti? To je přece koncepčně odlišná věc –
neřeší „co se má stát“ jako hooks, ale „kdo to může udělat“.
Proto by dávalo mnohem větší smysl použít termín write
, tedy
například private(write)
:
class Person
{
private(set) string $name; // takhle to je
private(write) string $name; // takhle by to dávalo větší smysl
}
Druhá varianta by byla mnohem intuitivnější. Navíc by lépe ladila
s existujícím modifikátorem readonly
.
Vypadá to, že PHP ve snaze o syntaktickou konzistenci mezi hooks a asymetrickou viditelností obětovalo sémantickou konzistenci s již existujícími koncepty v jazyce.
Nová éra v PHP: Revoluce v objektovém návrhu
V PHP světě jsme byli dlouho odkázáni na jediný správný způsob objektově orientovaného návrhu: všechny vlastnosti private a přístup k nim výhradně přes gettery a settery. Nebyla to rozmazlenost vývojářů – public property prostě byly problematické:
- Každý do nich mohl strkat nos bez jakékoliv kontroly
- Byly součástí veřejného API, takže každá změna (třeba přidání validace) znamenala rozbití zpětné kompatibility
- V rozhraních jste je mohli tak akorát zmínit v komentářích
Kdo chtěl programovat správně, používat rozhraní a dependency injection, musel sáhnout po getterech a setterech. Byla to jediná cesta, jak mít plnou kontrolu nad tím, co se v objektech děje.
Ale s PHP 8.4 přichází nová doba! Property hooks a asymetrická viditelnost nám konečně dávají nad vlastnostmi stejnou kontrolu jako nad metodami. Property se stávají plnohodnotnou součástí veřejného API, protože:
- Kdykoliv můžeme přidat validaci nebo transformaci hodnot
- Máme pod palcem, kdo může hodnoty měnit
- Můžeme je deklarovat v rozhraních
V podstatě můžete property hooks brát jako elegantní náhradu getterů a setterů bez zbytečného boilerplate kódu. Nebo naopak – gettery a settery byly jen takové provizorní řešení, než PHP dospělo k něčemu lepšímu.
Ze své vlastní zkušenosti s Nette můžu mluvit velmi konkrétně – jak jsem říkal, framework podobnou funkcionalitu nabízel už před 17 lety. To znamená, že jsem měl možnost s property přístupem pracovat dlouho. A musím říct, že to bylo nesmírně návykové. Porovnejte:
// Starý svět
$this->getUser()->getIdentity()->getName()
// Nový svět
$this->user->identity->name
Druhý zápis není jen kratší a čitelnější – je taky přirozenější. Je to jako rozdíl mezi „Prosím, mohli byste mi laskavě podat informaci o vašem jméně?“ a normálním „Jak se jmenuješ?“.
Jasně, možná namítnete, že přímý přístup k datům může svádět k porušování principů objektově orientovaného návrhu. Že místo ptaní se objektu na data bychom ho měli požádat o akci (Tell-Don't-Ask). To je pravda – ale hlavně pro objekty s bohatým chováním, které implementují business logiku. Pro datové transfer objekty, value objects nebo konfigurační třídy je přímý přístup k datům naprosto přirozený.
Zároveň nám tu vzniká pořádné dilema. Co s existujícími projekty? Pokud máte knihovnu nebo framework, který důsledně používá gettery a settery, bylo by možná kontraproduktivní do něj najednou zavádět property. Rozbili byste tím konzistenci API – uživatel by musel hádat, kde použít metodu a kde vlastnost.
Časem se určitě vytvoří nové styly a konvence. Některé projekty možná zůstanou u getterů a setterů, jiné budou hledat cesty jak začlenit property. Hlavně že máme na výběr.
Důležité je i pojmenování
Jak vlastně property pojmenovat? Zejména u boolean hodnot to není tak přímočaré, jak by se mohlo zdát.
U metod se běžně používají prefixy is
nebo
has
:
class Article {
public function isPublished(): bool { ... }
public function hasComments(): bool { ... }
}
Ale u properties by tyto prefixy působily krkolomně a redundantně. Místo nich je lepší používat přídavná jména nebo podstatná jména:
class Article {
public bool $published; // lepší než $isPublished
public bool $commented; // lepší než $hasComments
public bool $draft; // lepší než $isDraft
}
if ($article->published) { // čte se přirozeně
// ...
}
Pro počty položek je lepší použít množné číslo:
class Article {
public int $views; // lepší než $viewCount
public array $tags; // jasně říká, že jde o kolekci
}
Jde o to, aby kód byl čitelný jako běžná věta. Když píšeme
if ($article->published)
, čte se to mnohem přirozeněji než
if ($article->isPublished)
. Property by měly vypadat jako
vlastnosti, ne jako zapomenuté závorky u metody.
Kdy použít property a kdy metody?
Výborná otázka! Tady si můžeme vzít inspiraci z jazyků jako C# nebo Kotlin, které s property pracují už roky. Property se skvěle hodí pro:
Value objects a DTO:
class Money {
public readonly float $amount;
public readonly string $currency;
}
Jednoduché entity:
class Article {
public string $title;
public string $content;
public DateTimeImmutable $publishedAt;
public bool $published {
get => $this->publishedAt <= new DateTimeImmutable;
}
}
Computed hodnoty závislé na jiných vlastnostech:
class Rectangle {
public float $width;
public float $height;
public float $area {
get => $this->width * $this->height;
}
}
Metody jsou lepší pro:
- operace pracující s více property najednou
- operace s vedlejšími efekty (logování, notifikace)
- akce, které něco dělají (save, send, calculate…)
- komplexní validace nebo business logiku
- operace, které mohou selhat z více důvodů
- situace, kde oceníme fluent interface
Všechna tato doporučení se nám snaží říct jednu základní věc: za použitím property by se neměl skrývat žádný složitý proces nebo něco, co má vedlejší efekty. Složitost operace by měla zhruba odpovídat tomu, co intuitivně očekáváme od čtení či zápisu do proměnné.
I když… vzpomeňte si na innerHTML
v JavaScriptu. Když
napíšete element.innerHTML = '<p>Ahoj</p>'
, spustí
se složitý proces parsování HTML, vytvoření DOM stromu, překreslení
stránky… A přesto to všichni považují za přirozené!
Takže možná důležitější než samotná složitost implementace je to, jestli daná operace _konceptuálně_ odpovídá vlastnosti. Je to jako s autem – tlačítko start/stop může spustit složitou sekvenci kroků, ale pro řidiče je to pořád jen „zapnout/vypnout“.
Komentáře
Miloš #1
Takže se máme na co těšit, díky za skvělý článek na úvod do problematiky property hooks.
Tomáš Jacík #2
Zapomněl jsi zmínil, že u
get
hooku můžeš povolit získání reference uvedením&get
. Docela to pomůže hlavně u virtuálních property kde nepotřebuješ set 🙂Napište komentář