SmartObject
SmartObject po léta vylepšoval chování objektů v PHP. Od verze PHP 8.4 jsou již všechny jeho funkce součástí samotného PHP, čímž završil svou historickou misi být průkopníkem moderního objektového přístupu v PHP.
Instalace:
composer require nette/utils
SmartObject vznikl v roce 2007 jako revoluční řešení nedostatků tehdejšího objektového modelu PHP. V době, kdy PHP trpělo řadou problémů s objektovým návrhem, přinesl výrazné vylepšení a zjednodušení práce pro vývojáře. Stal se legendární součástí frameworku Nette. Nabízel funkcionalitu, kterou PHP získalo až o mnoho let později – od kontrolu přístupu k vlastnostem objektů až po sofistikované syntaktické cukrátka. S příchodem PHP 8.4 završil svou historickou misi, protože všechny jeho funkce se staly nativní součástí jazyka. Předběhl vývoj PHP o pozoruhodných 17 let.
Technicky prošel SmartObject zajímavým vývojem. Původně byl implementován jako třída Nette\Object
, od
které ostatní třídy dědily potřebnou funkcionalitu. Zásadní změna přišla s PHP 5.4, které přineslo podporu trait. To
umožnilo transformaci do podoby traity Nette\SmartObject
, což přineslo větší flexibilitu – vývojáři mohli
funkcionalitu využít i ve třídách, které již dědily od jiné třídy. Zatímco původní třída
Nette\Object
zanikla s příchodem PHP 7.2 (které zakázalo pojmenování tříd slovem Object
),
traita Nette\SmartObject
žije dál.
Pojďme si projít vlastnosti, které kdysi Nette\Object
a později Nette\SmartObject
nabízeli.
Každá z těchto funkcí ve své době představovala významný krok vpřed v oblasti objektově orientovaného programování
v PHP.
Konzistentní chybové stavy
Jedním z nejpalčivějších problémů raného PHP bylo nekonzistentní chování při práci s objekty.
Nette\Object
přinesl do tohoto chaosu řád a předvídatelnost. Podívejme se, jak vypadalo původní
chování PHP:
echo $obj->undeclared; // E_NOTICE, později E_WARNING
$obj->undeclared = 1; // projde tiše bez hlášení
$obj->unknownMethod(); // Fatal error (nezachytitelný pomocí try/catch)
Fatal error ukončil aplikaci bez možnosti jakkoliv reagovat. Tichý zápis do neexistujících členů bez upozornění mohl
vést k závažným chybám, které šly obtížné odhalit. Nette\Object
všechny tyto případy zachytával a
vyhazoval výjimku MemberAccessException
, což umožnilo programátorům na chyby reagovat a řešit je.
echo $obj->undeclared; // vyhodí Nette\MemberAccessException
$obj->undeclared = 1; // vyhodí Nette\MemberAccessException
$obj->unknownMethod(); // vyhodí Nette\MemberAccessException
Od PHP 7.0 již jazyk nezpůsobuje nezachytitelné fatal error a od PHP 8.2 je přístup k nedeklarovaným členům považován za chybu.
Nápověda „Did you mean?“
Nette\Object
přišel s velmi příjemnou funkcí: inteligentní nápovědou při překlepech. Když vývojář
udělal chybu v názvu metody nebo proměnné, nejen oznámil chybu, ale také nabídl pomocnou ruku v podobě návrhu
správného názvu. Tato ikonická hláška, známá jako „did you mean?“, ušetřila programátorům hodiny hledání
překlepů:
class Foo extends Nette\Object
{
public static function from($var)
{
}
}
$foo = Foo::form($var);
// vyhodí Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"
Dnešní PHP sice nemá žádnou podobu „did you mean?“, ale tento dovětek umí do chyb doplňovat Tracy. A dokonce takové chyby i samo opravovat.
Properties s kontrolovaným přístupem
Významnou inovací, kterou SmartObject přinesl do PHP, byly properties s kontrolovaným přístupem. Tento koncept, běžný v jazycích jako C# nebo Python, umožnil vývojářům elegantně kontrolovat přístup k datům objektu a zajistit jejich konzistenci. Properties jsou mocným nástrojem objektově orientovaného programování. Fungují jako proměnné, ale ve skutečnosti jsou reprezentovány metodami (gettery a settery). To umožňuje validovat vstupy nebo generovat hodnoty až v momentě čtení.
Pro používání properties musíte:
- Přidat třídě anotaci ve tvaru
@property <type> $xyz
- Vytvořit getter s názvem
getXyz()
neboisXyz()
, setter s názvemsetXyz()
- Zajistit, aby getter a setter byly public nebo protected. Jsou volitelné – mohou tedy existovat jako read-only nebo write-only property
Ukažme si praktický příklad na třídě Circle, kde properties využijeme k zajištění, že poloměr bude vždy
nezáporné číslo. Nahradíme původní public $radius
za property:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // není public!
// getter pro property $radius
protected function getRadius(): float
{
return $this->radius;
}
// setter pro property $radius
protected function setRadius(float $radius): void
{
// hodnotu před uložením sanitizujeme
$this->radius = max(0.0, $radius);
}
// getter pro property $visible
protected function isVisible(): bool
{
return $this->radius > 0;
}
}
$circle = new Circle;
$circle->radius = 10; // ve skutečnosti volá setRadius(10)
echo $circle->radius; // volá getRadius()
echo $circle->visible; // volá isVisible()
Od PHP 8.4 lze dosáhnout stejné funkcionality pomocí property hooks, které nabízí mnohem elegantnější a stručnější syntaxi:
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
Extension methods
Nette\Object
přinesl do PHP další zajímavý koncept inspirovaný moderními programovacími jazyky –
extension methods. Tato funkce, převzatá z C#, umožnila vývojářům elegantně rozšiřovat existující třídy o nové
metody bez nutnosti je upravovat nebo od nich dědit. Třeba jste si mohli do formuláře přidat metodu
addDateTime()
, která přidá vlastní DateTimePicker:
Form::extensionMethod(
'addDateTime',
fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);
$form = new Form;
$form->addDateTime('date');
Extension metody se ukázaly jako nepraktické, protože jejich názvy nenapovídaly editory, naopak hlásily, že metoda neexistuje. Proto byla jejich podpora ukončena. Dnes je běžnější využívat kompozici nebo dědičnost pro rozšíření funkcionality tříd.
Zjištění názvu třídy
Pro zjištění názvu třídy nabízel SmartObject jednoduchou metodu:
$class = $obj->getClass(); // pomocí Nette\Object
$class = $obj::class; // od PHP 8.0
Přístup k reflexi a anotacím
Nette\Object
nabízel přístup k reflexi a anotacím pomocí metod getReflection()
a
getAnnotation()
. Tento přístup významně zjednodušil práci s metainformacemi tříd:
/**
* @author John Doe
*/
class Foo extends Nette\Object
{
}
$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // vrátí 'John Doe'
Od PHP 8.0 je možné přistupovat k metainformacím v podobě atributů, které nabízí ještě větší možnosti a lepší typovou kontrolu:
#[Author('John Doe')]
class Foo
{
}
$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];
Method gettery
Nette\Object
nabízel elegantní způsob, jak předávat metody jako kdyby šlo o proměnné:
class Foo extends Nette\Object
{
public function adder($a, $b)
{
return $a + $b;
}
}
$obj = new Foo;
$method = $obj->adder;
echo $method(2, 3); // 5
Od PHP 8.1 je možné využít tzv. first-class callable syntax, která tento koncept posouvá ještě dál:
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
Události
SmartObject nabízí zjednodušenou syntax pro práci s událostmi. Události umožňují objektům informovat ostatní části aplikace o změnách svého stavu:
class Circle extends Nette\Object
{
public array $onChange = [];
public function setRadius(float $radius): void
{
$this->onChange($this, $radius);
$this->radius = $radius;
}
}
Kód $this->onChange($this, $radius)
je ekvivalentní následujícímu cyklu:
foreach ($this->onChange as $callback) {
$callback($this, $radius);
}
Kvůli srozumitelnosti doporučujeme se magické metodě $this->onChange()
vyhnout. Praktickou náhradou je
třeba funkce Nette\Utils\Arrays::invoke:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);