Dzisiaj kończę temat wstrzykiwania zależności. Punktem wyjścia będzie poprzedni kod. Kontener posłuży do skonfigurowania jeszcze dwóch obiektów. Napisałem sobie 2 proste klasy: SelectQuery
, który odpyta bazę danych na podane zapytanie oraz SelectQueryCache
, który zachowa wynik zapytania do pliku. Wynik będzie ważny tylko przez podany czas.
Przy okazji zmieniłem trochę formatowanie składni na bardziej „Zend-Frameworkowe”. Mam nadzieję, że bardziej czytelne. Kolejną zmianą jest używanie w nazwach klas/zmiennych wyłączenie języka angielskiego. Wam to nie będzie przeszkadzać, a mnie oszczędzi masę czasu gdybym chciał coś kiedyś rzucić na szerokie wody.
W metodzie loadDefaults()
dorzucamy zmienne konfigurujące obiekt cachujący i dwa domknięcia potrafiące wyprodukować 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 (DBKontener $k)
{
return new $k->cache_class($k->cache_filename , k->cache_interval);
};
$this->selectQuery = function (DBKontener $k)
{
$q = new SelectQuery($k->pdo_getPDO);
// wstrzykujemy zaleznosc
$q->setCache($k->selectQueryCache);
return $q;
};
Właściwie to wpis można by teraz zamknąć. Widać jak cache tworzony jest na podstawie zmiennych oraz widać wstrzyknięcie zależności w postaci metody setCache()
. Innym sposobem wstrzykiwania jest konstruktor. Nie zrobiłem tak z uwagi na to, że SelectQuery
może sobie radzić bez obiektu cachującego, więc po co wymuszać.
Poniżej załączam kod dwóch wspomnianych 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)
{
if ($this->cachedQueries !== null
&& array_key_exists($query , $this->cachedQueries)
&& $this->isValidTime($this->cachedQueries[$query]['time']))
{
return true;
}
else
{
return false;
}
}
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 specjalnego, no może poza getResults()
, które najpierw sprawdza czy jest cache i jakiś wynik, a jak nie to odpytuje bazę danych i zapisuje wynik do cache.
Za to w SelectQueryCache
znajdzie się trochę mięska:
- Do serializacji danych używam
json_encode
, które jest ponoć trochę szybsze od zwykłej serializacji, a zapisane dane mają formę strawniejszą dla humanoidów.
- Dane w pliku trzymam w postaci tablicy asocjacyjnej, której kluczem jest zapytanie, a wartościami wynik zapytania i czas utworzenia.
- Do sprawdzania czy cache nie jest przeterminowany odejmuję liczbę sekund od obecnego czasu i sprawdzam czy jest wcześniejsza od czasu utworzenia danego klucza.
- Oczyszczam cache sprawdzając czy poszczególne klucze nie są już przeterminowane i w razie czego wywalam cały klucz.
- Do zapisu do pliku używam destruktora. Mam pewność, że obiekt zaraz przed zakończeniem żywota zapisze dane do pliku.
Na koniec przykład użycia całości:
$c = new DBKontener();
$query = $c->selectQuery;
$results = $query->getResults('SELECT nazwa_pola FROM jakas_tabela');
Za pierwszym razem dane zostaną pobrane z bazy, a następnie przez 30 sekund z pliku cache.