Tag Archives: wzorce projektowe

webmastering

PDO poprzez Dependency Injection Container [cz. 2/2]

Dzisiaj kończę temat wstrzy­ki­wa­nia zależ­no­ści. Punk­tem wyjścia będzie poprzedni kod. Konte­ner posłuży do skon­fi­gu­ro­wa­nia jesz­cze dwóch obiek­tów. Napi­sa­łem sobie 2 proste klasy: SelectQuery, który odpyta bazę danych na podane zapy­ta­nie oraz SelectQueryCache, który zachowa wynik zapy­ta­nia do pliku. Wynik będzie ważny tylko przez podany czas.

Przy okazji zmie­ni­łem trochę forma­to­wa­nie składni na bardziej „Zend-Frameworkowe”. Mam nadzieję, że bardziej czytelne. Kolejną zmianą jest używa­nie w nazwach klas/zmiennych wyłą­cze­nie języka angiel­skiego. Wam to nie będzie prze­szka­dzać, a mnie oszczę­dzi masę czasu gdybym chciał coś kiedyś rzucić na szero­kie wody.

W meto­dzie loadDefaults() dorzu­camy zmienne konfi­gu­ru­jące obiekt cachu­jący i dwa domknię­cia potra­fiące wypro­du­ko­wać gotowe obiekty:

//zmienne konfigurujące pdo z poprzedniego wpisu
 
$this->cache_filename = 'queries.cache';
$this->cache_interval = 30;
$this->cache_class = 'SelectQueryCache';
 
$this->selectQueryCache = function (DBContainer $k)
{
  return new $k->cache_class($k->cache_filename , k->cache_interval);
};
 
$this->selectQuery = function (DBContainer $k)
{
  $q = new SelectQuery($k->pdo_getPDO);
  // wstrzykujemy zaleznosc
  $q->setCache($k->selectQueryCache);
  return $q;
};

Właści­wie to wpis można by teraz zamknąć. Widać jak cache tworzony jest na podsta­wie zmien­nych oraz widać wstrzyk­nię­cie zależ­no­ści w postaci metody setCache(). Innym sposo­bem wstrzy­ki­wa­nia jest konstruk­tor. Nie zrobi­łem tak z uwagi na to, że SelectQuery może sobie radzić bez obiektu cachu­ją­cego, więc po co wymuszać.

Poni­żej załą­czam kod dwóch wspo­mnia­nych wyżej klas.

class SelectQuery
{
  /**
   * @var PDO
   */
  protected $pdo;
 
  /**
   * @var SelectQueryCache 
   */
  protected $cache = null;
 
  public function __construct(PDO $pdo)
  {
    $this->pdo = $pdo;
  }
 
  /**
   * @param SelectQueryCache $sqc dependency injection
   */
  public function setCache(SelectQueryCache $sqc)
  {
    $this->cache = $sqc;
  }
 
  /**
   * @param string $query
   * @return array
   */
  public function getResults($query)
  {
    if ($this->cache)
    {
      $cachedResults = $this->cache->getCachedResults($query);
      if ($cachedResults !== null)
      {
        return $cachedResults;
      }
    }
    $q = $this->pdo->prepare($query);
    $q->execute();
    $rows = array();
    while ($row = $q->fetch(PDO::FETCH_ASSOC))
    {
      $rows[] = $row;
    }
    if ($this->cache)
    {
      $this->cache->cacheQuery($query , $rows);
    }
    return $rows;
  }
}
 
class SelectQueryCache
{
  /**
   * @var array contains all cached queries
   */
  protected $cachedQueries = array();
 
  /**
   * @var SPLFileInfo
   */
  protected $cacheFile;
 
  /**
   * @var int time interval in secs to check if cache is still valid
   */
  protected $validTime;
 
  public function __construct($filename , $validtime = 60)
  {
    $this->cacheFile = new SplFileInfo(__DIR__ . '/' . $filename);
    $this->validTime = (int) $validtime;
    $this->loadCache();
  }
 
  protected function loadCache()
  {
    if (!file_exists($this->cacheFile))
    {
      touch($this->cacheFile);
    }
    $file = $this->cacheFile->openFile('r');
    if ($file->flock(LOCK_SH))
    {
      $this->cachedQueries = json_decode(file_get_contents($file) , true);
      $file->flock(LOCK_UN);
    }
    else
    {
      throw new Exception('Cannot acquire file lock to read file');
    }
  }
 
  public function cacheQuery($query , $result)
  {
    $this->cachedQueries[$query] = array(
      'time' => time() ,
      'result' => $result);
  }
 
  public function getCachedResults($query)
  {
    if ($this->isValidCache($query))
    {
      return $this->cachedQueries[$query]['result'];
    }
    else
    {
      return null;
    }
  }
 
  protected function isValidCache($query)
  {
    return ($this->cachedQueries !== null
      && array_key_exists($query , $this->cachedQueries)
      && $this->isValidTime($this->cachedQueries[$query]['time']));
  }
 
  protected function isValidTime($timestamp)
  {
    return ((time() - $this->validTime) < $timestamp);
  }
 
  protected function cleanUpOldCache()
  {
    foreach ($this->cachedQueries as $key => $cq)
    {
      if (!$this->isValidTime($cq['time']))
      {
        unset($this->cachedQueries[$key]);
      }
    }
  }
 
  protected function save()
  {
    $serializedData = json_encode($this->cachedQueries);
    $file = $this->cacheFile->openFile('w');
    if ($file->flock(LOCK_EX))
    {
      $file->fwrite($serializedData);
      $file->flock(LOCK_UN);
    }
    else
    {
      throw new Exception('Cannot acquire exclusive file lock to save file');
    }
  }
 
  public function __destruct()
  {
    $this->cleanUpOldCache();
    $this->save();
  }
}

Myślę, że w SelectQuery nie ma nic specjal­nego, no może poza getResults(), które najpierw spraw­dza czy jest cache i jakiś wynik, a jak nie to odpy­tuje bazę danych i zapi­suje wynik do cache.

Za to w SelectQueryCache znaj­dzie się trochę mięska:

  1. Do seria­li­za­cji danych używam json_encode, które jest ponoć trochę szyb­sze od zwykłej seria­li­za­cji, a zapi­sane dane mają formę straw­niej­szą dla humanoidów.
  2. Dane w pliku trzy­mam w postaci tablicy asocja­cyj­nej, której kluczem jest zapy­ta­nie, a warto­ściami wynik zapy­ta­nia i czas utworzenia.
  3. Do spraw­dza­nia czy cache nie jest prze­ter­mi­no­wany odej­muję liczbę sekund od obec­nego czasu i spraw­dzam czy jest wcze­śniej­sza od czasu utwo­rze­nia danego klucza.
  4. Oczysz­czam cache spraw­dza­jąc czy poszcze­gólne klucze nie są już prze­ter­mi­no­wane i w razie czego wywa­lam cały klucz.
  5. Do zapisu do pliku używam destruk­tora. Mam pewność, że obiekt zaraz przed zakoń­cze­niem żywota zapi­sze dane do pliku.

Na koniec przy­kład użycia całości:

$c = new DBContainer();
$query = $c->selectQuery;
$results = $query->getResults('SELECT nazwa_pola FROM jakas_tabela');

Za pierw­szym razem dane zostaną pobrane z bazy, a następ­nie przez 30 sekund z pliku cache.

webmastering

PDO poprzez Dependency Injection Container [cz. 1/2]

Ostat­nio mnie ostro zjecha­li­ście. Dzięki za komen­ta­rze, szcze­gól­nie te nega­tywne (yyy wszyst­kie?). Wszyst­kie staran­nie prze­czy­ta­łem. Poczy­ta­łem co nieco i zdecy­do­wa­łem się uderzyć z tema­tem jesz­cze raz. Tym razem uwzględ­nia­jąc zada­nia takie jak „a co jak będę miał kilka serwe­rów: testowy, produk­cyjny, itp.”, „a co jak chcę połą­czyć się z dwiema bazami na raz”?

Punk­tem wyjścia stał się Twit­tee, czyli konte­ner stwo­rzony w 2009 r. przez Fabiena Poten­ciera zajmu­jący 140 znaków (tyle żeby całość dała się prze­słać w postaci poje­dyn­czej wiado­mo­ści w serwi­sie Twit­ter). Podstawą konte­nera jest magia __set()__get(), czyli to co Zyx lubi najbar­dziej :-) Całość została przeze mnie mocno zmody­fi­ko­wana. Doda­łem np. rzuca­nie wyjąt­kami jeżeli wyma­gana wartość nie została usta­wiona plus obsługę domknięć w przy­padku gdy usta­wiona wartość jest funk­cją anonimową.

Parę lini­jek doty­czą­cych usta­wie­nia PDO wcisną­łem do funk­cji anoni­mo­wej plus doda­łem możli­wość trzy­ma­nia poje­dyn­czej instan­cji PDO w razie potrzeby (zwrócę potem uwagę na static w domknię­ciu). Obiekt PDO „nie wie”, że jest w konte­ne­rze i dobrze. Istotą DI jest to żeby klas nie trzeba było specjal­nie dosto­so­wy­wać do współ­pracy z kontenerem.

Obsługę wyjąt­ków w cało­ści zrzu­cam na klien­tów nie miesza­jąc kompe­ten­cji konte­nera, który ma ustawiać/zwracać zmienne/fabrykować obiekty.

class DBContainer {
 
  protected $values = array();
 
  public function __construct() {
        $this->loadDefaults();
  }
 
  protected function loadDefaults() {
    $this->pdo_driver = 'mysql';
    $this->pdo_host = 'localhost';
    $this->pdo_dbname = 'nazwabazy';
    $this->pdo_user = 'user';
    $this->pdo_pass = 'haslo';
    $this->pdo_charset = 'SET NAMES utf8';
    $this->pdo_persist = false;
 
    $this->pdo_getpdo = function(DBContainer $cont) {
 
      // static w kontekscie funkcji anonimowej
      static $persistentPDO;
 
      $pdoCreator = function() use ($cont) {
        if (!extension_loaded('PDO')) throw new Exception('Brak modulu PDO');
        $pdo = new PDO(
                    $cont->pdo_driver . ':host=' . $cont->pdo_host . ';dbname=' . $cont->pdo_dbname,
                    $cont->pdo_user,
                    $cont->pdo_pass,
                    array(PDO::MYSQL_ATTR_INIT_COMMAND => $cont->pdo_charset));
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
        return $pdo;
      };
 
      if ($cont->pdo_persist && $persistentPDO instanceof PDO) {
        return $persistentPDO;
      }
      elseif ($cont->pdo_persist) {
        $persistentPDO = $pdoCreator();
        return $persistentPDO;
      }
      else {
        return $pdoCreator();
      }
    };
 
    public function __set($key,$val) {
        $this->values[$key] = $val;
    }
 
    public function __get($key) {
        if (!isset($this->values[$key])) {
            throw new Exception("Wartosc {$key} nie istnieje");
        }
        if ($this->values[$key] instanceof Closure) {
            return $this->values[$key]($this);
        }
        else {
            return $this->values[$key];
        }
    }
}

Przy­kłady użycia:

$c = new DBContainer();
 
// PDO na domyslnych ustawieniach
$pdo = $c->pdo_getpdo;
 
// przestawiam baze danych
$c->pdo_dbname = 'testowa baza';
$nowePDOdlaBazyTestowa = $c->pdo_getpdo;
 
// znowu przestawiam baze danych, przestawiam na zapis PDO na stale
$c->pdo_dbname = 'baza produkcyjna';
$c->pdo_persist = true;
 
// sprawdzam czy na pewno obiekty PDO sa tej samej instancji
echo spl_object_hash($c->pdo_getpdo) . '<br>' . spl_object_hash($c->pdo_getpdo);
// zwroci taki sam hash

Na raz następny pokażę jak można fajnie korzy­stać z tego dla obiek­tów korzy­sta­ją­cych z pdo wewnętrznie.

Osoby nielu­biące magii upra­sza się o powstrzy­ma­nie od wyle­wa­nia żalu. Po to zrobili __get(), __set() i dyna­miczne typy zmien­nych żeby z nich korzy­stać. Dobra doku­men­ta­cja wg mnie zała­twia sprawę.

webmastering

A Ty w jaki sposób łączysz się z bazą danych?

Tytuł zabrzmiał jak w Matrik­sie. Chodzi oczy­wi­ście o łącze­nie apli­ka­cji PHP z bazą danych. Dzisiaj pokażę w jaki sposób w niewiel­kich projek­tach radzę sobie z przy­go­to­wa­niem obiektu PDO do pracy z bazą danych. Trudno żebym w niewiel­kiej „stronce” zaprzę­gał jakiś Zend Framework.

Napi­sa­łem sobie dawno temu klasę narzę­dziową do tworze­nia obiektu PDO. Imple­men­tuję w niej wzorzec projek­towy single­ton, a więc mam pewność, że gdzie­kol­wiek w kodzie żądam PDO, zawsze dostaję ten sam obiekt. Jeżeli chodzi o sam single­ton, to na jego temat można prze­czy­tać zarówno we Wzor­cach Projek­to­wych1 jak i w Design Patterns.2 Zresztą to w Googlu wysko­czy pier­dy­lion wyników ;-)

Używa­nie klasy jest dzie­cin­nie proste. Należy sobie jedno­ra­zowo prze­edy­to­wać stałe klasy doty­czące połą­cze­nia z bazą, a następ­nie w kodzie wywo­ły­wać metodę DBHandler::getPDO()

Poni­żej podaję kod klasy. Bierz­cie i jedzcie ;-)

<?php
class DBHandlerException extends PDOException {
}
 
/**
 * @author Dawid 'Spiechu' Spiechowicz
 * @license see http://spiechu.pl/o-publikowanym-kodzie/
 */
class DBHandler {
 
  /**
   * Dane bazy danych
   */
  const DB_HOST = 'localhost';
  const DB_NAME = 'nazwa bazy';
  const DB_USER = 'nazwa usera';
  const DB_PASS = 'haslo';
 
  /**
   * Sterownik bazy danych
   */
  const DB_DRIVER = 'mysql';
 
  /**
   * Czy wyswietlac dokladne komunikaty bledow
   */
  const DEBUG_MODE = true;
 
  /**
   * @var PDO singleton PDO
   */
  private static $pdo = null;
 
  /**
   * Zwraca singleton PDO lub wyswietla komunikat bledu i zwraca null.
   * @return PDO|null
   */
  public static function getPDO() {
    try {
      if (self::$pdo === null) {
        self::$pdo = self::createPDO();
      }
      return self::$pdo;
    }
    catch (DBHandlerException $e) {
      echo $e->getMessage();
      return null;
    }
  }
 
  /**
   * @return PDO zwraca nowa instancje PDO
   * @throws DBHandlerException
   */
  private static function createPDO() {
    if (!extension_loaded('PDO')) throw new DBHandlerException('Brak modulu PDO');
    try {
      $pdo = new PDO(
         self::DB_DRIVER . ':host=' . self::DB_HOST . ';dbname=' . self::DB_NAME, 
         self::DB_USER, 
         self::DB_PASS,
         array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'));
      $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
      return $pdo;
    }
    catch(PDOException $e) {
      if (self::DEBUG_MODE == true) {
        throw new DBHandlerException("Blad bazy danych : {$e->getMessage()}");
      }
      else {
        throw new DBHandlerException('Blad bazy danych');
      }
    }
  }
 
  /**
   * Zapobiega tworzeniu obiektu.
   */
  private function __construct() {
    throw new Exception('Nie mozna stworzyc tego obiektu!');
  }
 
  /**
   * Zapobiega klonowaniu obiektu.
   */
  private function __clone() {
    throw new Exception('Nie mozna klonowac tego obiektu!');
  }
}

Powy­żej widać kilka sztu­czek. Przede wszyst­kim konstruk­tor ma zasięg prywatny, co zapo­biega stwo­rze­niu instan­cji klasy z zewnątrz. Mało tego, próba wywo­ła­nia go z wnętrza klasy spowo­duje wyrzu­ce­nie wyjątku. To samo z metodą __clone().

Przy konfi­gu­ra­cji obiektu PDO usta­wiam tryb błędów na wyjątki, które wyła­puję i w zależ­no­ści od usta­wio­nej stałej DEBUG_MODE wyświe­tlam komu­ni­katy błędów PDO lub nie. Frag­ment $pdo->query('SET NAMES utf8') usta­wia kodo­wa­nie znaków na Unicode. Niestety nie znala­złem lepszej metody. Rezul­taty zapy­tań domyśl­nie będą dostępne w postaci tablicy asocjacyjnej.

  1. E. Gamma (i in.) : Wzorce projek­towe. Elementy opro­gra­mo­wa­nia obiek­to­wego wielo­krot­nego użytku. Gliwice : Helion, 2010, s. 130–136.
  2. E. Freeman (i in.) : Head First Design Patterns. Gliwice : Helion, 2005, s. 197–216.