Je známo, že objekty se do jazyka PHP dostaly spíš jako nezvaní hosté. Docela příznačným rozdílem mezi jazykem, který byl jako objektový již navrhován, a jazykem, který se jím stal k velkému překvapení samotných tvůrců, je v absenci resp. přítomnosti společného předka všech tříd. Object Pascal deklaruje TObject, DOT.NET má System.Object a v Javě nebo Ruby stojí v hierarchii nejvýše Object (je delikátní, že nejzákladnější třída bývá pojmenována objekt). V PHP nic takového neexistuje.

Základní třída obvykle deklaruje metody pro sebereflexi. Například metodu vracející název třídy. Je to sice detail, ale vždycky mi připadalo tuze ošklivé mixovat hezký objektový kód s voláním pro-objektové funkce get_class:

$a = $obj->myMethod();
$class = $obj->getClass(); // hezké
$class = get_class($obj); // ošklivé

Nekonzistentní generování chyb

Teď trošku odbočím. Objektový model a vůbec chování jazyka PHP mi v mnoha směrech nevyhovuje. Třeba takový přístup k nedeklarovaným členům. To je téměř vždy známka vážné chyby v programu, způsobené třeba překlepem v kódu. Jenže PHP na ni reaguje značně nekonzistentně:

$obj->undeclared = 1; // projde bez hlášení
echo $obj->undeclared2; // generuje Notice
MyClass::$undeclared = 1; // generuje Fatal error
$obj->undeclared(); // generuje Fatal error

Chyby úrovně Notice patří k těm nejdůležitějším a nesmí se při ladění vypínat. Druhým extrémem je Fatal error, který by se spíš měl jmenovat Fatal disaster. Proč? Protože jej nelze obsloužit uživatelským skriptem. Nemáme možnost vygenerovat zprávu pro uživatele. Ten zírá na prázdnou stránku a netuší, co se děje. A hlavně, PHP nemá ani tolik slušnosti, aby odeslalo HTTP kód 500, takže rozbitou stránku (v nejhorším i s chybovou hláškou) zaindexují vyhledávače. PHP fůůůj!

Velice by se hodilo, kdyby přístup k nedeklarovaným členům vygeneroval výjimku. Takovou, která se nechá probublat až nahoru, kde ji zachytí krizová bariéra a promění v korektní hlášení.

Skvělé „vlastnosti“

Zase odbočím. Termínem property se označují speciální členy tříd, které umožňují pracovat s metodami tak, jako by to byly proměnné. Mám na mysli tzv. gettery a settery:

$obj->caption = 'Hello World';
// přeloží se na $obj->setCaption('Hello World')

echo $obj->caption;
// přeloží se na volání $obj->getCaption();

To je nesmírně šikovná věc. Zvenku to vypadá jako obyčejná proměnná, ale přístup k ní máme plně pod kontrolou. Můžeme validovat vstupy. Můžeme generovat výstupy, až když jsou skutečně potřeba. A navíc, pokud neexistuje setter, tak máme read-only proměnnou, což nelze u obyčejné proměnné zajistit.

Property podporují všechny moderní objektově orientované jazyky, v PHP však nejsou a na jejich brzkou implementaci bych si ani nesázel.

Extension method

Ještě jednou odbočím. Naposled. Fakt.

Novinkou třetí verze C# jsou extension method. Jedná se o obdobu prototypování, které znáte z JavaScriptu nebo Ruby. Umožňují připsat nové metody do již existující třídy (JavaScript a Ruby dovoluje metody nejen připsat, ale i přepsat, což však považuji za zlo).

K čemu je to dobré? Máme třeba třídu List reprezentující seznam položek, která implementuje metody pro jejich přidávání, rušení a podobně. Z této třídy je odvozena hierarchie podtříd. V praxi zjistíme, že by se nám docela hodila metoda List::shuffle() pro náhodné zamíchání pořadí položek. Jenže kód třídy List nemůžeme modifikovat (je třeba součástí cizí knihovny). Vytvořit potomka, který by zmíněnou metodu měl, také nepomůže, protože bychom museli upravit stávající kód, co objekty List vytváří. A navíc by to neřešilo absenci metod v hierarchii tříd z List odvozených.

Řešením je vytvořit samostatnou funkci, které objekt předáme a ona na něm operaci vykoná. Jen místo $list->suffle() je třeba volat Tools::suffleList($list).

Extension methods jsou rozšíření jazyka, které doplňování metod do tříd na ryze syntaktické úrovni umožní. Můžeme v programu psát $list->suffle() a interpreter bude tiše volat Tools::suffleList($list).

Nette\Object

Po několika odbočkách jsem zpět u diskutované nejvyšší třídy hierarchie. Myslíte, že něco takového PHP potřebuje? S přihlédnutím k celkové koncepci jazyka bych řekl, že ani ne. Ale co kdyby existence této třídy dokázala vyřešit všechny zde zmíněné problémy a rozšířit objektový model jazyka o property a extension method? V tom případě – sem s ní!

Nette\Object je onen zázračný táta všech objektů (dříve NObject). Nenechte se zmást prefixem, nejde o základní třídu jen pro Nette. Používám ji v Texy, dibi a vůbec ve všech svých aplikacích, u tříd, z nichž lze vytvářet objekty.

Samozřejmostí je podpora sebereflexe:

class MyClass extends Object {}
// včetně jmenného prostoru:
// class MyClass extends Nette\Object

$obj = new MyClass();
$class = $obj->getClass();
$has = $obj->getReflection()->hasMethod('test');

Následuje příklad použití vlastností (property):

class Circle extends Object
{
	private $radius;

	public function getRadius()
	{
		return $this->radius;
	}

	public function setRadius($radius)
	{
		// validate value
		$this->radius = max(0, (float) $radius);
	}

	public function getArea()
	{
		return $this->radius * $this->radius * M_PI;
	}
}


$circle = new Circle;
$circle->radius = 12; // as $circle->setRadius(12);

echo "Radius: $circle->radius"; // as $circle->getRadius();
echo "Area: $circle->area";

// but $circle->area = XXX; throws Exception

Technickou stránku implementace jsem se snažil co nejlépe promyslet. Špatně navržená cesta by mohla narušit zapouzdření objektu, mohla by veřejně zpřístupnit protected a private metody a podobně. Toho se v případě Nette\Object obávat nemusíte. Property fungují pouze jako syntactic sugar, jako něco, co může zpřehlednit kód a těšit programátora, ale pokud nechcete, tak to používat nemusíte. Proto platí, že

  • getter i setter musí být veřejná metoda
  • getter je povinný, setter volitelný
  • názvy jsou citlivé na velikost písmen (case-sensitive)

Možná vás napadá, jestli i metody sebereflexe lze volat „oslazené“. Jasně:

echo "Class: $obj->class"; // $obj->getClass();
$has = $obj->reflection->hasMethod('test');

A nakonec extension method:

class MyClass extends Object
{
	public $a;
	public $b;
}


// declare method MyClass::join()
function MyClass_prototype_join(MyClass $_this, $separator)
{
	return $_this->a . $separator . $_this->b;
}

echo $obj->join(' ');

I zde jsem uvažoval nad několika způsoby implementace, nakonec jsem zvolil tento vycházející ze zvyklostí JavaScriptu. Deklarace v podobě globální funkce má tu výhodu, že unikátnost je zaručena přímo jazykem a existenci lze ověřovat klasickými prostředky.

Nette\Object kultivuje jazyk

Používám Nette\Object jako předka všech tříd, protože mi to usnadňuje vývoj (překlep vyhodí výjimku atd). Jde o třídu dostatečně transparentní, neměla by způsobovat žádné kolize, proto ji můžete používat také. Cílem článku bylo především poukázat na vyšší standard a nové možnosti využití knihoven Texy, dibi a (brzy již) Nette.

Doplnění: o deset let později

Když jsem před deseti lety psal tento článek, sotva by mě napadlo, že tato třída bude mít smysl ještě dnes. Ale samozřejmě některé věci už jsou jinak.

Ze třídy Nette\Object se stala traita, což lépe odpovídá jejímu smyslu. A název se změnil na SmartObject, protože slovo Object už nebude možné používat v PHP 7.2. Tuhle změnu jsme nějakou dobu tušili, proto Nette už od verze 2.4 přestalo třídu Object používat a nahradilo ji zmíněnou traitou. Extension method nebo reflexi už nepodporuje a properties vyžadují uvést je v dokumentačním komentáři. Přesný popis najdete v dokumentaci.

Doplnění: o sedmnáct let později

Nedávno vyšlo PHP 8.4, které už disponuje všemi vlastnostmi, které dříve zajišťoval Nette\Object, čímž se završila jeho historická mise býti průkopníkem moderního objektového přístupu v PHP.