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() nebo isXyz(), setter s názvem setXyz()
  • 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);
verze: 4.0 3.x 2.x