SmartObject

SmartObject opravovala chování objektů v mnoha směrech, ale dnešní PHP již obsahuje většinu vylepšení nativně. Stále však přidává podporu pro tzv. property.

Instalace:

composer require nette/utils

Properties, gettery a settery

Termínem property (česky vlastnost) se v moderních objektově orientovaných jazycích (např. C#, Python, Ruby, JavaScript) označují speciální členy tříd, které se tváří jako proměnné, ale ve skutečnosti jsou reprezentovány metodami. Při přiřazení nebo čtení hodnoty této „proměnné“ se zavolá příslušná metoda (tzv. getter nebo setter). Jde o velice šikovnou věc, díky ní máme přístup k proměnným plně pod kontrolou. Můžeme tak validovat vstupy nebo generovat výsledky až ve chvíli, kdy se property čte.

PHP property nepodporují, ale traita Nette\SmartObject je umí imitovat. Jak na to?

  • Přidejte třídě anotaci ve tvaru @property <type> $xyz
  • Vytvořte getter s názvem getXyz() nebo isXyz(), setter s názvem setXyz()
  • Getter a setter musí být public nebo protected a jsou volitelné, mohou tedy existovat read-only nebo write-only property

Property využijeme u třídy Circle, abychom zajistili, že do proměnné $radius se dostanou jen nezáporná čísla. Nahradíme 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()

Properties jsou především syntaktickým cukříkem, jehož smyslem je zpřehlednit kód a osladit tak programátorovi život. Pokud nechcete, nemusíte je používat.

Pohled do historie

SmartObject opravovala chování objektů v mnoha směrech, ale dnešní PHP již obsahuje většinu vylepšení nativně. Následující text je tak nostalgickým pohledem do historie a připomínkou toho, jak se věci vyvíjely.

Objektový model PHP trpěl od počátku celou řadou vážných nedostatků a necnostní. To byl důvod vzniku třídy Nette\Object (v roce 2007), která se je pokoušela napravovat a zlepšit komfort při používání PHP. Stačilo, aby ostatní třídy od ní dědily, a získaly výhody, které přinášela. Když PHP 5.4 přišlo s podporou trait, byla třída Nette\Object nahrazena traitou Nette\SmartObject. Nebylo tak nutné už dědit od společného předka. Navíc traita se dala použít i ve třídách, které již dědily od jiné třídy. Definitivní konec Nette\Object přišel s vydáním PHP 7.2, které zakázalo třídám jmenovat se Object.

Jak šel vývoj PHP dál, objektový model a schopnosti jazyka se vylepšovaly. Jednotlivé funkce třídy SmartObject se stávaly zbytečnými. Od vydání PHP 8.2 zůstala jediná feature, která ještě není v PHP přímo podporována, a to možnost používat tzv. property.

Jaké vlastnosti kdysi Nette\Object a potažmo Nette\Object nabízely? Nabízíme přehled. (V ukázkách se používá třída Nette\Object, ale většina vlastnosti se týká i traity Nette\SmartObject).

Nekonzistentní chyby

PHP mělo nekonzistentní chování při přístupu k nedeklarovaným členům. Stav v době vzniku Nette\Object byl následující:

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.

echo $obj->undeclared;   // vyhodí Nette\MemberAccessException
$obj->undeclared = 1;    // vyhodí Nette\MemberAccessException
$obj->unknownMethod();   // vyhodí Nette\MemberAccessException

PHP od verze PHP 7.0 už nezachytitelné fatal error nezpůsobuje a přístup k nedeklarovaným členům se stává chybou od PHP 8.2.

Did you mean?

Pokud došlo k vyhození chyby Nette\MemberAccessException, třeba z důvodu překlepu při přístupu k proměnné objektu nebo volání metody, pokusilo se Nette\Object v chybové hlášce napovědět, jak chybu opravit, a to v podobě ikonického dovětku „did you mean?“.

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.

Extension methods

Inspirací byly extension methods z jazyka C#. Dávaly možnost do existujících tříd přidávat nové metody. 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.

Zjištění názvu třídy:

$class = $obj->getClass(); // pomocí Nette\Object
$class = $obj::class;      // od PHP 8.0

Přístup k reflexi a anotacem

Nette\Object nabízel přístup k reflexi a anotacím pomocí metod getReflection() a getAnnotation():

/**
 * @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ů:

#[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:

$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5

Události

Nette\Object nabízel syntaktický cukr pro vyvolání události:

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:

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