PHP Tutoriál - IV. pokročilejšie programovanie v OOP

Publikované 02.2.2014 17:46   |  Tutoriály, PHP, OOP 0

V tejto časti sa pozrieme ešte hlbšie do OOP v PHP. Teraz si ukážeme preťažovanie, iterátory, reflexiu, návrhové vzory ako napr. Singleton, traits a prácu s anotáciami. Máme sa na čo tešiť.


Preťažovanie

Preťažovanie vlastností a metód

Preťažovanie, resp. prekrývanie vlastností a metód u PHP je možné vďaka vložením špeciálnych prototypov metód, ktoré sa spustia automaticky, keď sa napr. snažíme vyvolať vlastnosť alebo metódu, ktorá vlastne neexistuje. Takto môžeme využiť veľkú rozmanitosť v PHP na prácu s takýmito akciami. Teda jedná sa o tieto prototypy metód:

Metóda Parametre Popis
__get() $vlastnost Spustí sa, ak sa snažíme vyvolať takú vlastnosť, aká neexistuje a mala by vrátiť hodnotu
__set() $vlastnost, $hodnota Spustí sa, ak sa snažíme nastaviť hodnotu takej vlastnosti, ktoré neexistuje
__call() $metoda, $argumenty Spustí sa, ak sa snažíme vyvolať takú metódu s argumentami, ktorá neexistuje. Argumenty vkládame pomocou číselne usporiadaného poľa, kde prvý index má hodnotu 0

V nasledujúcom príklade si ukážeme využitie metódy __get() a __set() pri práci s poľami:

class Suradnice
{
  private $suradnice = Array('x' => null, 'y' => null);
  
  public function __get($vlastnost)
  {
    if(array_key_exists($vlastnost, $this->suradnice)) { // overíme, či daná vlastnosť, resp. súradnica sa nachádza v našom poli
      return $this->suradnice[$vlastnost] . "\n"; // vrátime existujúcu súradnicu
    } else {
      return "Je možné čítať len súradnice X a Y!\n";
    }
  }
  
  public function __set($vlastnost, $hodnota)
  {
    if(array_key_exists($vlastnost, $this->suradnice)) {
      $this->suradnice[$vlastnost] = $hodnota; // nastavíme novú hodnotu
    } else {
      echo "Je možné pracovať len so súradnicami X a Y!\n";
    }
  }
}

$suradnice = new Suradnice; // vytvoríme si objekt
$suradnice->x = 35; // nastavíme súradnicu X na hodnotu 35
echo $suradnice->x; // vypíšeme súradnicu X

$suradnice->z = 10; // pokúšame sa nastaviť neexistujúcu súradnicu
echo $suradnice->z; // pokúšame sa vypísať neexistujúcu súradnicu

Output:

35
Je možné pracovať len so súradnicami X a Y!
Je možné čítať len súradnice X a Y!

Funkcia array_key_exists() sa stará o to, že kontroluje či daný index sa nachádza v našom poli. Inak môžete vidieť, že naša trieda sa správa tak, ako sme potrebovali. Síce vlastnosti x a y nemáme, ale máme vlastnosť suradnice, kde zahrňuje indexy pod týmito menami. V metóde __get() sme sa pýtali prv, či sa naša vlasťnosť, resp. v tomto prípade ako index nachádza v našom poli. Ak áno, vrátili sme jeho hodnotu. Tak aj v metóde __set() sme sa znova pýtali, či sa index nachádza v poli a ak áno, nastavíme mu novú hodnotu.

Poďme si ukázať príklad aj s metódou __call(). Táto metóda má mnoho využití a my si ukážejme jedno z nich. Vytvoríme si 2 triedy, kde jedna trieda sa bude snažiť vyvolať metódu tej druhej metódy:

// tvoríme si triedu na Pozdrav
class Pozdrav
{
  public function pozdravMa($meno) // a jednoduchú metódu
  {
    echo "Ahoj " . $meno . "!\n";
  }
}

class PozdravHo
{
  private $objekt;
  
  public function __construct()
  {
    $this->objekt = new Pozdrav; // v konštruktore si do vlastnosti $objekt vložíme objekt triedy Pozdrav
  }
  
  public function __call($metoda, array $argumenty)
  {
    return call_user_func_array(array($this->objekt, $metoda), $argumenty); // vyvolávame metódu daného objektu s argumentami
  }
}

$pozdravik = new PozdravHo(); // vytvoríme si objekt triedy PozdravHo

$pozdravik->pozdravMa('Peter'); // vyvolávame metódu, ktorá ale sa nenachádza v trieda PozdravHo, čo spustí metódu __call()

Output:

Ahoj Peter!

Aj teraz sme opäť použili nejakú novú funkciu. Teraz je to ale call_user_func_array(), ktorá pri dosadení za parametre ako metóda, daný objekt a argumenty vyvolá metódu objektu, ktorú sme ako parameter vložili. Teda v našom prípade je to metóda pozdravMa() v objekte Pozdrav, ktorý sme si už nastavili v konštruktore.

Preťažovanie syntaxe pre prístup k poli

Občas sa vám stane, že máte objekt povedzme používateľov registrovaných na vašom webe a vám práve chýba tá jednoduchosť poli, kde si len zadáte meno používateľa a vyhodia sa vám podstatné informácie. Jedna z možných metód bez preťažovania je si dať všetkých používateľov do 1 poľa a to upraviť, ale nie je to efektívne riešenie ak máte na stránke tisícky registrovaných používateľov. Aj v OOP je to možné, aby sme napr. na vypísanie ID používateľa použli následovný tvar:

echo "Petrovo (nick peter6633) identifikačné číslo " . $uzivatelskeIDcka['peter6633'];

K tomuto ale je potrebné využiť rozhranie ArrayAccess. Jeho tvar je následovný:

ArrayAccess {
  /* Methods */
  abstract public boolean offsetExists ( mixed $offset )
  abstract public mixed offsetGet ( mixed $offset )
  abstract public void offsetSet ( mixed $offset , mixed $value )
  abstract public void offsetUnset ( mixed $offset )
}

Ukážme si to na nasledujúcom príklade, ako takéto rozranie využiť:

class UzivatelskeIDcka implements ArrayAccess // vkládame vzor ArrayAccess
{
  private $mysql; // vlastnosť, kde si uložíme objekt na prácu s MySQL(i) databázou
  
  public function offsetExists($nick) // metóda na zistenie, či daný užívateľ existuje
  {
    return $this->mysql->nickExistuje($nick);
  }
  
  public function offsetGet($nick) // získame ID užívateľa
  {
    return $this->mysql->vratIDuzivatela($nick);
  }
  
  public function offsetSet($nick, $id) // nastavíme nové ID užívateľa
  {
    return $this->mysql->nastavIDuzivatela($nick, $id);
  }
  
  public function offsetUnset($nick) // vymažeme užívateľa
  {
    return $this->mysql->odstranUzivatela($nick);
  }
}

$uzivatelskeIDcka = new UzivatelskeIDcka;

echo "Petrovo (nick peter6633) identifikačné číslo " . $uzivatelskeIDcka['peter6633'];

Tento príklad je samozrejme nedostačujúci, keďže tam chýba aj práca s databázou ako nájdenie daného používateľa, úprava a následne vymazanie. Nie je to tu, keďže je to zbytočný kód navyše, keď sa snažíme si ukázať príncip využitia implementácie ArrayAccess.

Iterátory

Iterátory v OOP slúžia na to, aby sme mohli prechádzať vlastnosti objektu počas celého behu skriptu, ktoré sa menia, kým platí nejaká podmienka. Predstavte si, že chcete vypísať toľko výsledkov, kým druhá mocnina daného čísla neprekročí hodnotu 1000. Využijeme rozhranie Iterator:

Iterator extends Traversable {
  /* Methods */
  abstract public mixed current ( void )
  abstract public scalar key ( void )
  abstract public void next ( void )
  abstract public void rewind ( void )
  abstract public boolean valid ( void )
}

Budeme samozrejme využívať každú metódu tohto vzoru, čo je zrejme jasné, keďže v minulej časti sme si už spomínali, že pri rozhraní je potrebné využiť všetky jej metódy. Tak pusťme sa teda do príkladu:

class Mocnina implements Iterator
{
  private $start = 2; // štarovacie číslo bude 2
  private $aktualne;
  
  public function current()
  {
    return pow($this->aktualne, 2); // dané číslo umocníme na druhú
  }
  
  public function key()
  {
    return $this->aktualne; // vrátime aktuálnu hodnotu
  }
  
  public function next()
  {
    $this->aktualne++; // zvýšime hodnotu nášho čísla
  }
  
  public function rewind()
  {
    $this->aktualne = $this->start; // nastavili sme, že prvá hodnota bude obsahovať hodnotu z vlastnosti $start
  }
  
  public function valid()
  {
    return pow($this->aktualne, 2) < 1000;
  }
}

$mocnina = new Mocnina;

foreach ($mocnina as $key => $value)
{
  echo "Druhá mocnina čísla " . $key . " je " . $value . "\n";
}

Output:

Druhá mocnina čísla 2 je 4
Druhá mocnina čísla 3 je 9
Druhá mocnina čísla 4 je 16
Druhá mocnina čísla 5 je 25
Druhá mocnina čísla 6 je 36
Druhá mocnina čísla 7 je 49
Druhá mocnina čísla 8 je 64
Druhá mocnina čísla 9 je 81
Druhá mocnina čísla 10 je 100
Druhá mocnina čísla 11 je 121
Druhá mocnina čísla 12 je 144
Druhá mocnina čísla 13 je 169
Druhá mocnina čísla 14 je 196
Druhá mocnina čísla 15 je 225
Druhá mocnina čísla 16 je 256
Druhá mocnina čísla 17 je 289
Druhá mocnina čísla 18 je 324
Druhá mocnina čísla 19 je 361
Druhá mocnina čísla 20 je 400
Druhá mocnina čísla 21 je 441
Druhá mocnina čísla 22 je 484
Druhá mocnina čísla 23 je 529
Druhá mocnina čísla 24 je 576
Druhá mocnina čísla 25 je 625
Druhá mocnina čísla 26 je 676
Druhá mocnina čísla 27 je 729
Druhá mocnina čísla 28 je 784
Druhá mocnina čísla 29 je 841
Druhá mocnina čísla 30 je 900
Druhá mocnina čísla 31 je 961

Ako môžeme vidieť, implementovali sme rozhranie Iterator, ktorý nám umožňuje si pri prechádzaní objektu vypísať všetky hodnoty, ktoré sa umocnili na druhú a nepresiahli umocnenú hodnotu 1000.

Iterátor sa dá aj využiť iným spôsobom. Zrejme si budete chcieť do triedy podávať aj vlastné metódy a takto môžete mať v tom dosť veľký neporiadok. To ale nie je povinnosťou, ale ak o tom už zvažujete, máme riešenie. Namiesto rozhrania Iterator si do našej triedy implementujeme rozhranie IteratorAggregate, kde vďaka metódy getIterator() vrátime objekt Iterátora, ktorý bude prechádzať tú našu dotyčnú triedu. Pomocou tejto techniky si spracujme náš predchádzajúci príklad do tejto podoby:

class IteratorMocniny implements Iterator
{
  private $aktualne;
  private $objekt;
  
  public function __construct($objekt)
  {
    $this->objekt = $objekt; // načítame si objekt triedy, s ktorou budeme pracovať
  }
  
  public function current()
  {
    return pow($this->aktualne, 2); // dané číslo umocníme na druhú
  }
  
  public function key()
  {
    return $this->aktualne; // vrátime aktuálnu hodnotu
  }
  
  public function next()
  {
    $this->aktualne++; // zvýšime hodnotu nášho čísla
  }
  
  public function rewind()
  {
    $this->aktualne = $this->objekt->getStart(); // načítame si začiatočné číslo z nášho objektu
  }
  
  public function valid()
  {
    return pow($this->aktualne, 2) < 1000;
  }
}

class Mocnina implements IteratorAggregate
{
  private $start = 2;
  
  public function getIterator()
  {
    return new IteratorMocniny($this); // vrátime inštanciu triedy IteratorMocniny, kde parameter bude naša celá trieda
  }
  
  public function getStart()
  {
    return $this->start; // vrátime naše štartovacie číslo
  }
}

$mocnina = new Mocnina;

foreach ($mocnina as $key => $value)
{
  echo "Druhá mocnina čísla " . $key . " je " . $value . "\n";
}

Output je samozrejme rovnaký. Vytvorili sme si triedu, ktorá bude sa zaoberať spracovaním hodnôt a vďaka implementácie IteratorAggregate sme mohli v našej hl. triede iba vložiť metódu getIterator(), ktorá vrátila objekt triedy IteratorMocniny, kde sme ako parameter vložili našu triedu ($this).

Akú techniku si zvolíte je len na vás. Či vám vyhovuje to mať v jednej triede alebo si to chcete odvodiť z dvoch sa už rozhodnete Vy. Ja sám preferujem druhú metódu.

Návrhové vzory

O návrhových vzoroch sa dá písať toho veľa. Dôležité určite je to, že z väčšiny programátorov sa zhodneme, že sú veľmi dôležitou súčasťou pokročilejšieho programovanie v OOP. Tieto vzory vznikli na základe skúsených programátorov a boli vytvorené na často sa opakujúcich požiadavkách používateľov preto aj nesú pomenovanie bežné návrhové vzory. Nemusíte ich písať nanovo, lebo už vytvorené sú, stačí len využiť ich implementáciu a dosť nám môžu uľahčiť život. Výhodou týchto vzorov nie je len to, že šetria mnoho času, ale stretneme sa s nimi takmer vo všetkých objektovo orientovaných jazykoch. Každý z týchto návrhových vzorov, čo si ukážeme má aj svoje vlastné pomenovanie ako napr. Singleton, alebo vzor Výrobná trieda. Poďme teda pekne poporiadku.

Vzor stratégia (angl. strategy)

Vzor stratégia funguje na jednoduchom princípe. Máme nejakú abstraktnú triedu, od ktorej sa odvádzajú ďalšie a tie vykonávajú svoju logickú časť. Nevykonávajú ich všetky naraz, ale samotný skript sa rozhodne, ktorú stratégiu si vyberie. Predstavme si, že máme na našej webstránke možnosť stiahnutia si archívu nejakého dokumentu, poprípade aplikácie. V prípade, že používateľ príde ale s operačným systémom Windows, zrejme mu neponukneme súbor typu tar.gz, ale zip. Podľa toho si vytvoríme triedy StrategyZip a StrategyTarGz, ktoré budú odvodené z triedy StrategyFilename a skript sa rozhodne, ktorú z týchto tried pri vygenerovaní výslednej URL adresy použije. Hierachia by vyzerala následovne:

Strategy design pattern

Poďme si toto znázorniť na našom príklade. Na zistenie, či daný klient používa Windows pre zjednodušenie budeme predpokladať, že v hlavičke HTTP_USER_AGENT sa nachádza časť Win:

<?php

abstract class StrategyFilename
{
  abstract function generateURL($subor);
}

class StrategyZip extends StrategyFilename
{
  public function generateURL($subor)
  {
    return "http://mojastranka.sk/download/" . $subor . ".zip";
  }
}

class StrategyTarGz extends StrategyFilename
{
  public function generateURL($subor)
  {
    return "http://mojastranka.sk/download/" . $subor . ".zip";
  }
}

if(strstr($_SERVER['HTTP_USER_AGENT'], 'Win')) {
  $objektPomenovania = new StrategyZip;
} else {
  $objektPomenovania = new StrategyTarGz;
}

$alien_channel = $objektPomenovania->generateURL('alien_channel');
$fotky_svadba = $objektPomenovania->generateURL('fotky_svadba');

?>
<h1>Neuveriteľné súbory už dnes!</h1>
<h2>SŤAHUJ!</h2>
<p>
  <a href="<?= $alien_channel ?>">Aplikácia, ktorá vás spojí s mimozemšťanmi z druhého sveta priamo LIVE!</a>
  <br />
  <a href="<?= $fotky_svadba ?>">Nenechajte si ujsť super fotky z Jožkovej svadby</a>
</p>

Keďže sme stránku navštívili s OS Windows, output je teda následovný:

<h1>Neuveriteľné súbory už dnes!</h1>
<h2>SŤAHUJ!</h2>
<p>
  <a href="http://mojastranka.sk/download/alien_channel.zip">Aplikácia, ktorá vás spojí s mimozemšťanmi z druhého sveta priamo LIVE!</a>
  <br />
  <a href="http://mojastranka.sk/download/fotky_svadba.zip">Nenechajte si ujsť super fotky z Jožkovej svadby</a>
</p>

Takže je vidieť, že tento vzor je jednoduchý aj na vysvetlenie. Máme abstraktnú triedu StrategyFilename, od ktorej sa odvádzajú StrategyZip a StrategyTarGz. Skript, pri overení pomocou funkcie strstr() overí, či sa daný podreťazec nachádza v danom reťazci (v našom prípade sa jedná o Win). Potom podľa toho vyvolá buď triedu na zip alebo tar.gz súbory. Následne už len vypíšeme vygenerované URL adresy do nášho outputu.

Vzor singleton

Singleton je jeden z najznámenších vzorov orientovaného programovania. Predstavme si, že máme aplikáciu s mnoha triedami a taktiež triedu Logovanie. Táto trieda bude slúžiť na to, aby sme mohli zapísať určité záznamy, ktoré budeme chcieť, aby sa niekde ukládali. Lenže načo vytvárať N inštancií tejto triedy, keď každá chce pracovať iba 1 centrálnou? Tým pádom si do tejto triedy pridáme statickú metódu getInstance(), ktorá vráti existujúcu inštanciu danej triedy a tým nám zabráni vytvoriť si novú. Takto môžeme odkiaľkoľvek k danej inštancii pristupovať bez toho, aby sme museli vytvárať nové. Hierarchický návrh by mohol vyzerať napr. takto:

Singleton design pattern

Poďme si to ukázať na nasledujúcom príklade:

class Logovanie
{
  static private $instance = null;
  
  static public function getInstance()
  {
    if(null === self::$instance) { // ak inštanciu ešte nemáme, tak ju vytvoríme
      self::$instance = new Logovanie();
    }
    return self::$instance; // vrátime inštanciu
  }
  
  public function log($text)
  {
    // sem bude kód na spracovania záznamu a uloženie ho niekam
  }
  
  private function __construct()
  {
  }
  
  private function __clone()
  {
  }
}

class User
{
  // ...
  
  public function emailChange($newEmail)
  {
    // ...
    Logovanie::getInstance()->log('Užívateľ si zmenil e-mailovú adresu'); // zaznamenáme, že si zmenil e-mailovku
  }
}

$user = new User();
// ...
$user->emailChange('novy@jozko.com');

Singleton je dostatočne jednoduchý príncip na pochopenie. Stretávame sa s ním takmer všade a aj tu vidíte, ako možno spraviť systém logovania. V triede Logovanie sme pridali súkromné metódy __construct() a __clone(), čo zabráni omylnému vytvoreniu novej inštancii triedy. Potom je možné pristúpiť k triede jedine pomocou Logovanie::getInstance() a môžeme spokojne a efektívne pracovať s týmto vzorom.

Vzor výrobná trieda (angl. factory method)

Tento vzor funguje na princípe polymorfizmu, len sa trošku viac okorení ako v našom minulom článku. V tomto vzore sa využíva naplno bázová trieda, ktorá stojí za všetkými jej potomkami. Následne sa nám tu už objaví aj trieda Tovarna, ktorá obsahuje statickú metódu a tá bude rozhodovať akého typu bude ďalší objekt, resp. objekt, ktorý je potomkom danej bázovej triedy. Predstavme si, že máme abstraknú triedu Uzivatel a od nej budú odvodené triedy ako UzivatelHost, UzivatelZakaznik a UzivatelSpravca. Každá z týchto tried zdedí východzie hodnoty triedy Uzivatel a ak sa niečo nezhoduje, tak ich len prepíše. Potom trieda Tovarna bude obsahovať užívateľov a ich typy a podľa typu sa zistí, ktorá inštancia sa pre daného používateľa spustí. Hierarchicky by to mohlo vyzerať takto:

Factory method design pattern

No, ale poďme si to aj teraz ukázať na konkrétnom príklade:

abstract class Uzivatel
{
  public $meno = null;
  
  public function __construct($meno)
  {
    $this->meno = $meno;
  }
  
  public function __toString()
  {
    return $this->meno;
  }
  
  /* Teraz si nastavíme východzie hodnoty naších oprávnení */
  
  public function mozeCitat()
  {
    return true;
  }
  
  public function mozePridavat()
  {
    return false;
  }
  
  public function mozeUpravovat()
  {
    return false;
  }
  
  public function mozeMazat()
  {
    return false;
  }
}

/* Vytvárame potomkov bázovej triedy s tým, že aj nastavujeme pre
 * každý typ používateľa nové oprávnenia
 */

class UzivatelHost extends Uzivatel
{
}

class UzivatelZakaznik extends Uzivatel
{
  public function mozePridavat()
  {
    return true;
  }
  
  public function mozeUpravovat()
  {
    return true;
  }
}

class UzivatelSpravca extends Uzivatel
{
  public function mozePridavat()
  {
    return true;
  }
  
  public function mozeUpravovat()
  {
    return true;
  }
  
  public function mozeMazat()
  {
    return true;
  }
}

// trieda, ktorá bude spravovať všetkých používateľov
class TovarnaUzivatelov
{
  private static $uzivatelia = Array('Jožko' => 'uzivatel', 'Janko' => 'spravca', 'Anonymous' => 'host');
  
  static public function vytvor($meno)
  {
    if(!isSet(self::$uzivatelia[$meno]))
    {
      // užívateľ neexistuje, spracujeme nejakú výnimku
    }
    else
    {
      switch(self::$uzivatelia[$meno])
      {
        case 'host': return new UzivatelHost($meno); // break nám nie je už potrebné zadávať, lebo použitím return sa nám na tomto mieste už metóda zastaví
        case 'uzivatel': return new UzivatelZakaznik($meno);
        case 'spravca': return new UzivatelSpravca($meno);
        default: // tu bude nejaká chyba, lebo toto oprávnenie neexistuje
      }
    }
  }
}

// spravíme si funkciu, ktorá nám tieto oprávnenia prenesú do našej čitateľnej podoby
function toString($bool)
{
  return $bool === true ? 'Áno' : 'Nie';
}

// funkcia, ktorá nám vypíše oprávnenia daného užívateľa
function zobrazOpravnenia(Uzivatel $uzivatel)
{
  echo "Oprávnenie užívateľa " . $uzivatel . ":\n";
  echo "Môže čítať: " . toString($uzivatel->mozeCitat()) . "\n";
  echo "Môže pridávať: " . toString($uzivatel->mozePridavat()) . "\n";
  echo "Môže upravovať: " . toString($uzivatel->mozeUpravovat()) . "\n";
  echo "Môže mazať: " . toString($uzivatel->mozeMazat()) . "\n";
  echo "\n";
}

// vytvoríme si pole, naších užívateľov
$uzivatelia = array('Anonymous', 'Jožko', 'Janko');

// prechádzame každého používateľa
foreach ($uzivatelia as $uzivatel)
{
  zobrazOpravnenia(TovarnaUzivatelov::vytvor($uzivatel)); // vypisujeme oprávnenia
}

Output:

Oprávnenie užívateľa Anonymous:
Môže čítať: Áno
Môže pridávať: Nie
Môže upravovať: Nie
Môže mazať: Nie

Oprávnenie užívateľa Jožko:
Môže čítať: Áno
Môže pridávať: Áno
Môže upravovať: Áno
Môže mazať: Nie

Oprávnenie užívateľa Janko:
Môže čítať: Áno
Môže pridávať: Áno
Môže upravovať: Áno
Môže mazať: Áno

Aby sme si to trocha zrekapitulovali, tak pozrime sa na to ešte raz. Už ako to máme podľa hierachie, trieda Uzivatel je abstraktná a triedy UzivatelHost, UzivatelZakaznik a UzivatelSpravca sú jej potomkami. Trieda TovarnaUzivatelov sa zaoberá tým, že vo svojom poli má zoznam skutočných používateľov a ku každému z nich je pridané oprávnenie. Následne podľa oprávnenia vytvorí inštanciu danej triedy.

Potom sme si vytvorili funkcie ako toString(), ktorý prevedie boolean na našu čitateľnú formu (Áno/Nie). Následne sme si vytvorili funkciu zobrazOpravnenia(), ktorá vypísala oprávnenia daného používateľa. Všimnime si ale túto časť:

function zobrazOpravnenia(Uzivatel $uzivatel)

Parameter v tomto tvare Uzivatel $uzivatel znamená, že to musí byť objekt, ktorý je inštanciou triedy Uzivatel.

Následne sme si vytvorili náš zoznam užívateľov, ktorým sme vypísali oprávnenia prechádzaním príkazu foreach.

Vzor pozorovateľ (angl. observer)

Vzor porovateľ už ako z názvu vyplýva, že sleduje určité udalosti. My mu nastavíme, čo má sledovať a ak sa niečo zmení, určite chceme o tom informovať užívateľa. Preto si musím dovoliť to ukázať na jednoduchom príklade, aký sa nachádza v mojom zdroji (viď celkom dole).

Predstavme si, že máme aplikáciu pre náš e-shop a používateľ vidí cenu produktu, ktorá sa môže kedykoľvek zmeniť závisiaca na kurze. Predpokládajme, že hodnotu súčasného kurzu získavame z iného zdroja a najpravdepodobnejšie nie je súčasťou databázy. Predpokladajme ešte, že každý objekt produktu má metódu zobrazit(), ktorá vráti HTML výstup daného produktu.

Náš vzor dokáže zachytiť nové informácie o našom kurze a ak došlo k zmene kurzu, môže o tom informovať používateľa a vytvoriť nový výstup metódy zobrazit() ešte skôr, ako sa vôbec táto metóda spustí. Takto sa môžu objekty samé aktualizovať a zmena sa prejaví na každom z nich. Keby sme to chceli vyjadriť hierarchicky na našom príklade, vyzeralo by to následovne:

Design pattern observer

Takže prv si vytvoríme rozhranie Pozorovatel, ktorý bude mať metódu oznamit(), ktorá nám bude slúžiť ako pozorovateľ:

interface Pozorovatel
{
  public function oznamit($obj);
}

Následne vytvoríme objekt, ktorý chce, aby bol pozorovaný. Ten bude obsahovať nejakú metódu registruj() a tá bude registrovať všetky objekt, ktoré chcú byť pozorované. Vyzeralo by to nejak takto:

interface Pozorovatel
{
  public function oznamit($obj);
}

class MennyKurz
{
  static private $instance = null; // vytvoríme si súkromnú vlastnosť, kde budeme ukladať našú inštanciu
  private $pozorovatelia = array();
  private $mennyKurz;
  
  static public function getInstance()
  {
    if(null === self::$instance) { // ak inštanciu ešte nemáme, tak ju vytvoríme
      self::$instance = new MennyKurz;
    }
    return self::$instance; // vrátime inštanciu
  }
  
  // vrátime súčasný menný kurz
  public function getMennyKurz()
  {
    return $this->mennyKurz;
  }
  
  // nastavíme nový menný kurz
  public function setMennyKurz($mennyKurz)
  {
    $this->mennyKurz = $mennyKurz;
    $this->upozorni(); // upozorníme o tom všetky objekty
  }
  
  // registrujeme objekty do nášho poľa, ktoré chcú pozorovať zmeny
  public function registruj($obj)
  {
    $this->pozorovatelia[] = $obj;
  }
  
  // upozorníme pozorovateľa
  public function upozorni()
  {
    foreach($this->pozorovatelia as $pozorovatel)
    {
      $pozorovatel->oznamit($this);
    }
  }
}

class Produkt implements Pozorovatel
{
  public function __construct()
  {
    MennyKurz::getInstance()->registruj($this); // zaregistrujeme tento objekt ako pozorovateľa
  }
  
  public function oznamit($obj)
  {
    if($obj instanceof MennyKurz)
    {
      // oznámime, že došlo k aktualizácii
      echo "POZOR: Aktualizácia kurzu. Nová hodnota je " . $obj->getMennyKurz() . "\n";
    }
  }
}

// vytvoríme si 2 objekty produktov, aby sme vedeli, či sa nám zmena oznámi obidvom objektom
$produkt1 = new Produkt();
$produkt2 = new Produkt();

MennyKurz::getInstance()->setMennyKurz(4.5); // nastavíme nový kurz, čo spôsobí oznámenie

Output:

POZOR: Aktualizácia kurzu. Nová hodnota je 4.5
POZOR: Aktualizácia kurzu. Nová hodnota je 4.5

Vysvetlenie je už samotne popísané aj v kóde, ale môžeme si to vysvetliť aj teraz. Máme rozhranie Pozorovatel, ktoré využíva trieda Produkt, keďže potrebuje pozorovať aktuálny kurz danej meny. Trieda MennyKurz obsahuje informácie o kurze a taktiež vlastnosť pozorovatelia, kde sa budú nachádzať všetke triedy, ktoré sa zaregistrovali ako pozorovateľ. Pri metóde setMennyKurz() máme po nastavení kurzu vyvolávanie metódy upozorni(), ktorá stojí za tým všetkým, teda upozorní každého pozorovateľa (v našom príklade produkty), že sa zmenil aktuálny kurz. V triede Produkt sme sa v konštruktore zaregistrovali do zoznamu pozorovateľov, teda ak sa zmení kurz, táto informácia nám neujde.

Na záver sme si vytvorili 2 rovnaké triedy produktu a zmenili kurz. Výsledkom bola 2x tá istá informácia o tom, že nastala zmena kurzu.

Využitie v reálnom prostredí ma tento vzor dosť veľký. V e-shope sa s tým okrem príkladu kurzu môžeme stretnúť, keď si zaškrtnete políčko o informovaní, kedy bude produkt na sklade. Ak sa produkt na sklade už nachádza, bude vám táto informácia odoslaná ihneď. Avšak sa s tým stretneme aj na sociálnych sieťach ako Facebook, kde po odoslaní nejakej správy (napr. správa na nástenke) sa táto informácia odošle všetkým osobám, resp. priateľom o tom, že daný používateľ si pridal novú správu do nástenky.

Reflexia

Reflexia alebo aj introspekcia sú novinkou, čo sa týka PHP 5. Vďaka relfexii dokážeme sledovať celý priebeh skriptu a ziskávať dostatočné informácie o tom, čo sa v tomto čase deje. Môžeme skúmať funkcie, triedy alebo nejaké ďalšie entity. Ukážeme si aplikačné rozhranie reflexie (API), ktoré je už dosť výkonné a zároveň ponúka veľkú škálu nástrojov pre prácu s našou aplikáciou.

Aplikačné rozhranie reflexie (alebo aj reflection API) obsahuje mnoho tried s ktorými budeme pri reflexii pracovať. Pozrime sa na stručný výpis týchto tried, s ktorými budeme v nasledujúcich príkladoch pracovať:

interface Reflector
static export(...)
 
class ReflectionFunction implements Reflector
__construct(string $name)
string __toString()
static mixed export(string $name [,bool $return = false])
bool isInternal()
bool isUserDefined()
string getName()
string getFileName()
int getStartLine()
int getEndLine()
string getDocComment()
mixed[] getStaticVariables()
mixed invoke(mixed arg0, mixed arg1, ...)
bool returnsReference()
ReflectionParameter[] getParameters()
 
class ReflectionMethod extends ReflectionFunction implements
Reflector
bool isPublic()
bool isPrivate()
bool isProtected()
bool isAbstract()
bool isFinal()
bool isStatic()
bool isConstructor()
bool isDestructor()
int getModifiers()
ReflectionClass getDeclaringClass()
 
class ReflectionClass implements Reflector 
string __toString()
static mixed export(string $name [,bool $return = false])
string getName()
bool isInternal()
bool isUserDefined()
bool isInstantiable()
string getFileName()
int getStartLine()
int getEndLine()
string getDocComment()
ReflectionMethod getConstructor()
ReflectionMethod getMethod(string $name)
ReflectionMethod[] getMethods(int $filter)
ReflectionProperty getProperty(string $name)
ReflectionProperty[] getProperties(int $filter)
mixed[] getConstants()
mixed getConstant(string $name)
ReflectionClass[] getInterfaces()
bool isInterface()
bool isAbstract()
bool isFinal()
int getModifiers()
bool isInstance($obj)
object newInstance(mixed arg0, arg1, ...)
ReflectionClass getParentClass()
bool isSubclassOf(string $class)
bool isSubclassOf(ReflectionClass $class)
mixed[] getStaticProperties()
mixed[] getDefaultProperties()
bool isIterateable()
bool implementsInterface(string $ifc)
bool implementsInterface(ReflectionClass $ifc)
ReflectionExtension getExtension()
string getExtensionName()
 
class ReflectionParameter implements Reflector
static mixed export(mixed func, int/string $param [,bool $return = false])
__construct(mixed func, int/string $param [,bool $return = false])
string __toString()
string getName()
bool isPassedByReference()
ReflectionClass getClass()
bool allowsNull()
 
class ReflectionExtension implements Reflector
static export(string $ext [,bool $return = false])
__construct(string $name)
string __toString()
string getName()
string getVersion()
ReflectionFunction[] getFunctions()
mixed[] getConstants()
mixed[] getINIEntries()
ReflectionClass[] getClasses()
String[] getClassNames()
 
class ReflectionProperty implements Reflector
static export(string/object $class, string $name, [,bool $return = false])
__construct(string/object $class, string $name)
string getName()
mixed getValue($object)
setValue($object, mixed $value)
bool isPublic()
bool isPrivate()
bool isProtected()
bool isStatic()
bool isDefault()
int getModifiers()
ReflectionClass getDeclaringClass()
 
class Reflection
static mixed export(Reflector $r [, bool $return = 0])
static array getModifierNames(int $modifier_value)
 
class ReflectionException extends Exception

Ako z výpisu je možné vidieť, rozhranie API na reflexiu je bohaté a umožňuje nám zo skriptu získať mnoho informácií. Využitie reflexie je veľké a preto si to aj ukážeme na 2 rôznych príkladoch. Jeden príklad sa bude zaoberať načítaním dynamických informácii o triede a druhý sa bude zaoberať implementáciou vzorom delegácie, kde využijeme reflexiu.

Prvý jednoduchý príklad reflexie bude využitia metódy ReflectionClass::export() na triedu ReflectionParameter, aby sme mohli vidieť, ako táto trieda z pohľadu reflexie vyzerá:

// získame dostupné inforácie o našej triede
echo ReflectionClass::export('ReflectionParameter');

Output:

Class [ <internal:Reflection> class ReflectionParameter implements Reflector ] {

  - Constants [0] {
  }

  - Static properties [0] {
  }

  - Static methods [1] {
    Method [ <internal:Reflection> static public method export ] {

      - Parameters [3] {
        Parameter #0 [ <required> $function ]
        Parameter #1 [ <required> $parameter ]
        Parameter #2 [ <optional> $return ]
      }
    }
  }

  - Properties [1] {
    Property [ <default> public $name ]
  }

  - Methods [14] {
    Method [ <internal:Reflection> final private method __clone ] {

      - Parameters [0] {
      }
    }

    Method [ <internal:Reflection, ctor> public method __construct ] {

      - Parameters [2] {
        Parameter #0 [ <required> $function ]
        Parameter #1 [ <required> $parameter ]
      }
    }

    Method [ <internal:Reflection> public method __toString ] {

      - Parameters [0] {
      }
    }

    Method [ <internal:Reflection> public method getName ] {

      - Parameters [0] {
      }
    }

    Method [ <internal:Reflection> public method isPassedByReference ] {

      - Parameters [0] {
      }
    }

    Method [ <internal:Reflection> public method getDeclaringFunction ] {

      - Parameters [0] {
      }
    }

    Method [ <internal:Reflection> public method getDeclaringClass ] {

      - Parameters [0] {
      }
    }

    Method [ <internal:Reflection> public method getClass ] {

      - Parameters [0] {
      }
    }

    Method [ <internal:Reflection> public method isArray ] {

      - Parameters [0] {
      }
    }

    Method [ <internal:Reflection> public method allowsNull ] {

      - Parameters [0] {
      }
    }

    Method [ <internal:Reflection> public method getPosition ] {

      - Parameters [0] {
      }
    }

    Method [ <internal:Reflection> public method isOptional ] {

      - Parameters [0] {
      }
    }

    Method [ <internal:Reflection> public method isDefaultValueAvailable ] {

      - Parameters [0] {
      }
    }

    Method [ <internal:Reflection> public method getDefaultValue ] {

      - Parameters [0] {
      }
    }
  }
}

Ako môžeme vidieť, export nám do podrobna zobrazil všetky potrebné informácie o danej triede vrátane metód, vlastností a konštant.

Poďme teraz na druhý príklad využitia reflexxie v praxi. Využijeme vzor delegácie, teda to, že budeme vyvolávať metódy jednej triedy z inej triedy a k tomu využijeme reflexiu. Možno sa pýtate na čo, keď sme už raz pomocou __call() vyvolávali cez funkciu call_user_func_array(). Áno, je možné aj týmto postupom, ale pomocou reflexie je to efektívnejšie a profesionalnejšie. Z dôvodu, že ak by sme sa pokúšali spustiť chránenú alebo súkromnú metódu, bežným spôsobom by nám to vyhodilo nepekný PHP error, ale vďaka reflexie vieme zachytiť túto výnimku a spracovať ju tak, ako by sme ju chceli mať spracovanú. Ďalším dôvodom je aj to, aby sme si mohli ukázať, ako môžeme reflexiu využiť aj v tomto prípade:

class PrvaTrieda
{
  public function volaniePrvejTriedy() // špecifická metóda pre prvú triedu
  {
    echo "Toto je prvá trieda\n";
  }
}

class DruhaTrieda
{
  public function volanieDruhejTriedy() // to isté aj pre druhú
  {
    echo "Toto je druhá trieda\n";
  }
}

class Delegator
{
  private $ciele = array(); // budeme si ukládať cieľové objekty
  
  public function __construct()
  {
    $this->ciele[] = new PrvaTrieda; // už na začiatok si pridáme do poľa triedu PrvaTrieda
  }
  
  public function addObject($obj)
  {
    $this->ciele[] = $obj; // keď budeme pridávať ďalšie, tak si ich uložíme
  }
  
  // spustí sa, ak vyvoláme metódu, ktorá táto trieda neobsahuje
  public function __call($nazov, $args)
  {
    foreach($this->ciele as $obj)
    {
      $r = new ReflectionClass($obj); // priprávíme si náš objekt do reflexie
      try
      {
        if($metoda = $r->getMethod($nazov)) {
          if($metoda->isPublic() && !$metoda->isAbstract()) {
            return $metoda->invoke($obj, $args); // vyvoláme metódu
          }
        }
      }
      catch (Exception $e)
      {
        /*
         * Sem si spracujeme výnimku, ktorá nastane, ak metóda neexistuje
         * alebo daná metóda nie je verejná, poprípade je abstraktná.
         * Potom je možné kód spracovať na ďalšie podmienky a pod.
         */
      }
    }
  }
}

$delegator = new Delegator();
$delegator->addObject( new DruhaTrieda );
$delegator->volaniePrvejTriedy();
$delegator->volanieDruhejTriedy();

Output:

Toto je prvá trieda
Toto je druhá trieda

Tak pekne postupne. Máme triedy PrvaTrieda a DruhaTrieda (to je snáď všetkým jasné) s aj špecifickými metódami. Ďalej máme triedu Delegator, ktorá obsahuje špeciálnu metódu __call(), ktorú sme už v úvode v sekcii preťažovania zistili, že sa vyvoláva vtedy, ak daná metóda v danej triede neexistuje.
V tejto metóde sme si vytvorili inštanciu triedy ReflectionClass, kde sme ako parameter vložili náš objekt pomocou prechádzania všetkych uložených objektov cez foreach. Tam sme si vytvorili blok try, lebo ak metóda neexistuje, ktorú sa snažíme vyvolať, tak nám vyhodí výnimku. Tiež, potom zisťujeme, či je nájdená metóda verejná a nie je abstraktná (to by sme nemohli vôbec vyvolať). Ak nesplňuje túto podmienku, tiež sa vyvolá výnimka.
Následne ak sú všetky podmienky splnené, vyvoláme túto metódu pomocou metódy invoke(). Potom pomocou catch zachytíme výnimku (ak nejaká vznikla) a môžeme ju ďalej spracovať podľa nášho uváženia.

Traits

Veľkou novinkou v PHP 5.4 sú traits. Je to niečo, čo zvýši úroveň OOP v PHP o niekoľko levelov vyššie. Ide síce v PHP o novinku, v iných jazykoch ako Java sú traits už dlho zaužívané. Deklarácia je podobá ako u deklarovaní tried, ale namiesto class použijeme kľúčové slovo trait. Pozrime sa na nasledujúcu ukážku:

trait Pozdrav
{
  public function pozdravSvet()
  {
    echo "Ahoj Svet!";
  }
}

A ako sa to používa? Jednoducho, stačí len kľúčové slovíčko use a názov traitu a stane sa kúzlo!

class TriedaPozdrav
{
  use Pozdrav;
}

$pozdrav = new TriedaPozdrav;
$pozdrav->pozdravSvet();

Output:

Ahoj Svet!

Je toto možné? Áno, to je! A ešte k tomu, je možné ich využívať viac ako jeden:

trait Ahoj
{
  public function ahoj()
  {
    return "Ahoj";
  }
}

trait Svet
{
  public function svet()
  {
    return "Svet!";
  }
}

class TriedaPozdrav
{
  use Ahoj, Svet; // oddeľujeme čiarkami
  
  public function pozdravSvet()
  {
    echo $this->ahoj() . " " . $this->svet();
  }
}

$pozdrav = new TriedaPozdrav;
$pozdrav->pozdravSvet();

Output je ten istý. Je vidieť, že je to skvelá vec v PHP 5 a už teraz je možné premyslieť X nápadov, ako tieto traits využiť.

Namespace..use

Ďalšou super novinkou, ktorá sa z časti podobá traits-om (lebo tam máme slovíčku use) je využitie kombinácie namespace..use. Ide o jednoduchý príncip, ktorý sa ihneď začal využívať takmer u všetkých známych frameworkov. V súbore, kde deklarujeme jeden objekt, použijeme na začiatku kľúčové slovo namespace <balik(y)>, kde za balík dosadíme nejakú miesto, kde daná trieda patrí, nar. Doplnky. Potom v ďalšom súbore môžeme použiť use <balik(y)>\<nazov_triedy> a je tým sme dali najavo, že využijeme danú triedu (funguje to na princípe vkládania súborov).

Ukážme si to na jednoduchom príklade:

Súbor nieco.php

<?php

namespace Doplnky;

class Nieco
{
  public function pozdrav()
  {
    return "Ahoj Svet!";
  }
}

Súbor ine.php

<?php

namespace Doplnky;

class Ine
{
  public function dopln()
  {
    return " A je to skvelé!";
  }
}

Súbor index.php

<?php

require_once('nieco.php');
require_once('ine.php');

use Doplnky\Nieco;
use Doplnky\Ine;

$nieco = new Nieco;
$ine = new Ine;

echo $nieco->pozdrav() . $nieco->ine();

Output:

Ahoj Svet! A je to skvelé!

Síce tento príklad nie je najefektívnejší, lebo môžeme už vidieť, že by stačilo len require_once a môžeme vytvárať inštancie. Ale pri veľkých projektov je dobré navrhnúť špeciálny autoloader, ktorý spracuje dané balíky a bude vedieť, kde každý súbor sa nachádza. Balíkov môžeme mať viacero, malo by to takýto tvar:

namespace Ine\Super\Doplnky;
[...]
use Ine\Super\Doplnky\URLprekladac;

Takto si použijeme len tie triedy, ktoré v danom súbore potrebujeme a ešte k tomu to máme prehľadné. Napr. z pozadia Symfony2 frameworku v jednom controlleru na blog to vyzerá takto:

namespace Acme\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Acme\BlogBundle\Entity\Article;
use Acme\BlogBundle\Entity\Comment;
use Acme\BlogBundle\Entity\Category;
use Acme\BlogBundle\Form\Type\CommentType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

Anotácie

Poslednou časťou tohto článku sú anotácie. Čo sú to vlastne zač? Sú to povedzme metadata triedy/metódy/vlastnosti, ktoré určujú jej charakteristiku. Napr. máme anotáciu o tom, že táto metóda (tá čo je pod anotáciou) sa bude spúšťať ako prvá pred tým, než prebehne validácia formulára. Alebo naopak. Iný príklad využíva napr. ORM Doctrine2 systém, kde cez anotácie na samotné vlastnosti určíme, že táto vlastnosť bude typu integer, bude unikátna a nemôže byť nulová. Ako si také anotácie vôbec vytvoriť? Na anotácie je už vytvorených mnoho profesionálnych tried, ale na jednoduchú ukážku si ukážeme vlastnú:

class Zaokruhlene
{
  /**
   * @zaokruhlit true
   */
  public function cislo($a)
  {
    return $a;
  }
}

class Cele
{
  /**
   * @zaokruhlit false
   */
  public function cislo($a)
  {
    return $a;
  }
}

// funkcia na spracovanie anotácií pomocou reflexie
function getAnnotations($class, $method) {
  $c = new ReflectionClass($class);
  $m = $c->getMethod($method);
  $s = $m->getDocComment();        
  $s = str_replace('/*', '', $s);
  $s = str_replace('*/', '', $s);
  $s = str_replace('*', '', $s);
  $aTags = explode('@', $s);
  array_shift($aTags);
  for($i=0;$i<=count($aTags)-1;$i++)
  {
    $data = explode(' ', $aTags[$i]);
    $aTags[$i] = array(trim($data[0]) => trim($data[1]));
  }
  return $aTags;
}

$triedy = array('Zaokruhlene', 'Cele'); // uložíme si triedy

foreach($triedy as $trieda)
{
  $trieda = new $trieda;
  if(getAnnotations($trieda, 'cislo')[0]['zaokruhlit'] == 'true') // môžeme zaokrúhliť?
  {
    echo round($trieda->cislo(2.4));
  }
  else
  {
    echo $trieda->cislo(2.4);
  }
  echo "\n";
}

Output:

2
2.4

Na načítanie anotácií sme využili Reflexiu, ktorú sme už spomínali vyššie. Načítali sme si metadata metódy, odstránili znaky ako /*, */ a * a zvyšok uložili ich do poľa, kde sme uložili dáta bez znaku @. Následne sme ešte opäť pre zjednodušenie rozdelili daný string na 2 polovice. Teda výsledný tvar v dvojrozmerného poľa je:

Array (
  [0] = Array(
    'zaokruhlit' => 'true'
  )
)

Pre zjednodušenie sme aj vedeli, že anotácia @zaokruhlenie bude na prvom mieste, preto sme už mohli jasne rozhodnuť na tomto riadku:

  if(getAnnotations($trieda, 'cislo')[0]['zaokruhlit'] == 'true') // môžeme zaokrúhliť?

Následne sme podľa toho vrátili zaokrúhlenú, alebo celú hodnotu.

Samozrejme, na prácu s anotáciami odporúčam používať už profesionálne annotation parsery, aký napr. má Doctrine2 systém. Napr. v tejto ukážke je možné vidieť príklad využitia anotácie o tom, že ak sa daný objekt vymaže, odstrání sa aj obrázok, ktorý objekt obsahuje:

    /**
    * Called before entity removal
    *
    * @ORM\PostRemove()
    */
    public function removeUpload()
    {
        if ($file = $this->getAbsolutePath()) {
            unlink($file);
        }
    }

Stojí za tým anotácia @ORM\PostRemove(), ktorá o tom rozhodla, že táto metóda sa vykoná hneď po vymazaní objektu, resp. po tom, čo povieme, že sa má vymazať.

Použitá literatúra: Mistrovství v PHP 5, ISBN: 978-80-251-1519-0

Komentáre

Žiadne komentáre. Budte prvý, kto prispeje.

Odoslať komentár

Posledné Referencie

Deffender User Control Panel Deffender BG & FUN private server Eduard Karpiel - portfólio VideoVizitky.SK Apartmány Maladinovo Lymfoštúdia Eva

O Mne

Mám 23 rokov. Študujem Informatiku v odbore programovanie a taktiež sa venujem tvorbe nových webstránok. Pracujem s jazykmi

  • PHP (OOP) & MySQL
  • HTML & CSS (len drobné zmeny)
  • Symfony2 Framework (začiatočník, ale učím sa rýchlo - viď tento web)

Kontakt

V prípade záujmu alebo nejakého dotazu ma neváhajte kontaktovať na nižšie uvedených možnostiach. Poprípade môžete využiť kontaktný formulár

ICQ: 357-248-017
Skype: lkopo__ (upřednostňujte)
E-mail: