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 (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ś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)
{
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 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 DBContainer();
$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.
Ostatnio mnie ostro zjechaliście. Dzięki za komentarze, szczególnie te negatywne (yyy wszystkie?). Wszystkie starannie przeczytałem. Poczytałem co nieco i zdecydowałem się uderzyć z tematem jeszcze raz. Tym razem uwzględniając zadania takie jak „a co jak będę miał kilka serwerów: testowy, produkcyjny, itp.”, „a co jak chcę połączyć się z dwiema bazami na raz”?
Punktem wyjścia stał się Twittee, czyli kontener stworzony w 2009 r. przez Fabiena Potenciera zajmujący 140 znaków (tyle żeby całość dała się przesłać w postaci pojedynczej wiadomości w serwisie Twitter). Podstawą kontenera jest magia __set() i __get(), czyli to co Zyx lubi najbardziej
Całość została przeze mnie mocno zmodyfikowana. Dodałem np. rzucanie wyjątkami jeżeli wymagana wartość nie została ustawiona plus obsługę domknięć w przypadku gdy ustawiona wartość jest funkcją anonimową.
Parę linijek dotyczących ustawienia PDO wcisnąłem do funkcji anonimowej plus dodałem możliwość trzymania pojedynczej instancji PDO w razie potrzeby (zwrócę potem uwagę na static w domknięciu). Obiekt PDO „nie wie”, że jest w kontenerze i dobrze. Istotą DI jest to żeby klas nie trzeba było specjalnie dostosowywać do współpracy z kontenerem.
Obsługę wyjątków w całości zrzucam na klientów nie mieszając kompetencji kontenera, 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];
}
}
}
Przykł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 korzystać z tego dla obiektów korzystających z pdo wewnętrznie.
Osoby nielubiące magii uprasza się o powstrzymanie od wylewania żalu. Po to zrobili __get(), __set() i dynamiczne typy zmiennych żeby z nich korzystać. Dobra dokumentacja wg mnie załatwia sprawę.
Tytuł zabrzmiał jak w Matriksie. Chodzi oczywiście o łączenie aplikacji PHP z bazą danych. Dzisiaj pokażę w jaki sposób w niewielkich projektach radzę sobie z przygotowaniem obiektu PDO do pracy z bazą danych. Trudno żebym w niewielkiej „stronce” zaprzęgał jakiś Zend Framework.
Napisałem sobie dawno temu klasę narzędziową do tworzenia obiektu PDO. Implementuję w niej wzorzec projektowy singleton, a więc mam pewność, że gdziekolwiek w kodzie żądam PDO, zawsze dostaję ten sam obiekt. Jeżeli chodzi o sam singleton, to na jego temat można przeczytać zarówno we Wzorcach Projektowych jak i w Design Patterns. Zresztą to w Googlu wyskoczy pierdylion wyników
Używanie klasy jest dziecinnie proste. Należy sobie jednorazowo przeedytować stałe klasy dotyczące połączenia z bazą, a następnie w kodzie wywoływać metodę DBHandler::getPDO()
Poniżej podaję kod klasy. Bierzcie 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 sztuczek. Przede wszystkim konstruktor ma zasięg prywatny, co zapobiega stworzeniu instancji klasy z zewnątrz. Mało tego, próba wywołania go z wnętrza klasy spowoduje wyrzucenie wyjątku. To samo z metodą __clone().
Przy konfiguracji obiektu PDO ustawiam tryb błędów na wyjątki, które wyłapuję i w zależności od ustawionej stałej DEBUG_MODE wyświetlam komunikaty błędów PDO lub nie. Fragment $pdo->query('SET NAMES utf8') ustawia kodowanie znaków na Unicode. Niestety nie znalazłem lepszej metody. Rezultaty zapytań domyślnie będą dostępne w postaci tablicy asocjacyjnej.