Rozšíření objektů v PHP

Nette Framework rozšiřuje objektové a vyjadřovací schopnosti PHP o několik syntaktických cukrátek. Máte rádi cukrátka? Čtěte a dozvíte se

  • proč je dobré používat Nette\SmartObject
  • co jsou to property
  • jak vyvolávat události

V této kapitole se budeme věnovat převážně traitě Nette\SmartObject, která rozšiřuje objektové schopnosti PHP. Tuto traitu používají takřka všechny třídy Nette Framework. Zároveň je natolik transparentní, abyste ji mohli používat jako základ pro vaše třídy. Zkusme si říct, proč byste to měli dělat.

Striktní třídy

PHP je jazyk na sekání chyb jako stvořený, neboť dává vývojáři značnou volnost. Jak tomu udělat přítrž a začít psát programy bez těžko odhalitelných chyb? Stačí si nastavit přísnější laťku a dodržovat určitá pravidla.

Najdete v tomto příkladu chybu?

class Circle
{
    public $radius;

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

}

$circle = new Circle;
$circle->raduis = 10;
echo $circle->getArea(); // 10² * π ≈ 314

Zdá se, že kód by měl vypsat přibližně 314, nicméně vypíše nulu. Jak je to možné? Omylem se místo $circle->radius do kódu dostalo raduis, drobný překlep. Záludnost chyby tkví hlavně v tom, že na ni PHP nijak neupozorní, nepomůže ani sledování chyb Warning nebo Notice. Z hlediska jazyka to není chyba, ačkoliv stojí hodně námahy ji vypátrat.

Uvedenou chybu bychom ihned odhalili, pokud by třída Circle používala Nette\SmartObject:

class Circle
{
    use Nette\SmartObject;
    ...

Zatímco dosud kód tiše (ale chybně) proběhl, teď už je situace jiná:

Traita Nette\SmartObject učinila Circle striktnější a na použití nedeklarované proměnné reagoval vyhozením výjimky, kterou Tracy\Debugger zobrazil. Řádek s fatálním překlepem je odhalen a označen, textová zpráva situaci popisuje slovy: Cannot write to an undeclared property Circle::$raduis, did you mean $radius? (nelze zapsat do nedeklarované proměnné Circle::$raduis, myslel jste $radius?). Programátor zvolá „áhá“ a může promptně zareagovat. Chybu, které by si dlouho nemusel ani všimnout a jen za velkého úsilí by ji odhaloval, dostal srozumitelně naservírovanou na červeném podnose.

První schopnost traity Nette\SmartObject, kterou jsme si ukázali, je vyhazování výjimek při přístupu k nedeklarovanému členu třídy.

$circle = new Circle;
echo $circle->undeclared; // vyhodí Nette\MemberAccessException
$circle->undeclared = 1; // vyhodí Nette\MemberAccessException
$circle->unknownMethod(); // také vyhodí Nette\MemberAccessException

Tím její možnosti zdaleka nekončí.

Properties, gettery a settery

Termínem property (česky vlastnost) se v moderních objektově orientovaných jazycích 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ž v případě potřeby.

Každá třída, která používá traitu Nette\SmartObject, umí property imitovat. Není potřeba je nastavovat, stačí dodržet jednoduchou konvenci:

  • Přidej anotaci @property <type> $name
  • Getter i setter musí být public nebo protected metoda.
  • Getter má název ve tvaru getXyz() nebo isXyz(), setter má název ve tvaru setXyz()
  • Getter i setter je 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:

/**
 * @property float $radius
 * @property-read float $area
 * @property-read bool $visible
 */
class Circle
{
    use Nette\SmartObject;

    private $radius; // už není public!

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

    protected function setRadius($radius)
    {
        // hodnotu před uložením sanitizujeme
        $this->radius = max(0.0, (float) $radius);
    }

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

    protected function isVisible()
    {
        return $this->radius > 0;
    }

}

$circle = new Circle;
$circle->radius = 10; // ve skutečnosti volá setRadius()
echo $circle->area; // volá getArea()
echo $circle->visible; // volá $circle->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.

Události

Vytvoříme si frontu funkcí, které se zavolají, když se změní poloměr kruhu. Změně poloměru říkejme událost change a jednotlivým funkcím handlery události:

class Circle
{
    use Nette\SmartObject;

    /** @var array */
    public $onChange;

    public function setRadius($radius)
    {
        // vyvoláme callbacky v poli $onChange
        $this->onChange($this, $this->radius, $radius);

        $this->radius = max(0.0, (float) $radius);
    }
}

$circle = new Circle;

// přidám handler události
$circle->onChange[] = function (Circle $circle, $oldValue, $newValue) {
    echo 'došlo ke změně';
};

$circle->setRadius(10);

V kódu metody setRadius vidíte další syntaktický cukr – místo iterace nad polem $onChange a voláním jednotlivých metod pomocí nespolehlivé funkce call_user_func stačí napsat stručné onChange(...) a uvedené parametry jsou předány každému jednotlivému handleru.