Tag Archives: dependency injection

Symfony

Symfony2 compiler pass with tags and custom attributes

It passed about half year since I scrat­ched Symfony2 Frame­work surface. Basics are behind me, now it’s time for middle-level stuff. I’m exci­ted to start a whole new Symfony rela­ted blog post series. I didn’t wanted to copy typi­cal tuto­rial topics. It would be waste of your time and my work. I thought it would be nice to show some code rela­ted to Symfony Depen­dency Injec­tion Conta­iner.

Tuto­rials usually show typi­cal conta­iner servi­ces defi­ni­tion. Boring stuff. I’m going to show you dyna­mic collec­tion of servi­ces passed into proces­sor service. The idea is that collec­tion doesn’t know its elements, so every bundle can define its own servi­ces to be added to collec­tion. The secret is tags. To add some spice, tagged service can be defi­ned with prio­rity. And we can specify that this proces­sor should be run at the begin­ning, and that at the end.

We’ll start with defi­ning common proces­sor inter­face and some basic imple­men­ta­tions.

Now it’s time for a service that proces­ses given argu­ment with all the proces­sors one by one.

We could define typi­cal conta­iner defi­ni­tions and pass proces­sors right into proces­sing service, but remem­ber about requ­ire­ment that exter­nal bundles can define their own proces­sors. We’ll use tags.

Now the key part: compi­ler pass defi­ni­tion.

Usage:

At the first sight you may think that it’s a lot of logic to run to define stupid proces­sor. What about perfor­mance? No worries. Remem­ber, the compi­la­tion outcome is dumped into cache file on prod envi­ron­ment. All these fore­ach loops and prio­rity queues are being run only once.

Solu­tion is pretty gene­ric, you can easily adapt to your own needs. Of course code is on MIT license, so grab it and use in your own (even commer­cial) projects!

Happy PHP conta­iner compiling :-)

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ę.