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.

Podobne wpisy:

  1. PDO poprzez Depen­dency Injec­tion Conta­iner [cz. 1/2]
  2. (W miarę) bezpieczne urucha­mia­nie skryp­tów PHP poprzez shell_exec()
  3. A Ty w jaki sposób łączysz się z bazą danych?
  4. Symfony2 compi­ler pass with tags and custom attributes

2 Comments

  • mailo
    8 maja 2011 - 20:19 | Permalink

    DBKon­te­ner ? Serio ;-) ?
    Czy ta pętla jest tam potrzebna ? a co z PDO->fetchAll() ?
    Nie zale­cam stoso­wa­nia array_key_exists, potrafi zamu­lic, lepiej:
    isset($this->cachedQueries[$query]),
    Poza tym, zapis „return true”, „return false” jest również zbyteczny (funk­cja isVa­lid­Ca­che):
    return $this->cachedQueries !== null
    && isset($this->cachedQueries[$query])
    && $this->isValidTime($this->cachedQueries[$query][’time’]);
    To takie pier­doly ode mnie.

  • Śpiechu
    9 maja 2011 - 07:36 | Permalink

    @mailo
    Dzięki za uwagi. W przy­padku cacho­wa­nia na pewno każda ms mniej w czasie wyko­ny­wa­nia skryptu jest cenna.

  • Dodaj komentarz

    Twój adres e-mail nie zostanie opublikowany.

    Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <p> <pre lang="" line="" escaped=""> <q cite=""> <strike> <strong>