tiny 'n' smart
database layer

Edit

Quick Start

Cíle layeru dibi

  • maximálně ulehčit práci programátorům. Jak?
    • zjednodušit zápis SQL příkazů, co to jen půjde
    • snadný přístup k metodám, i bez globálních proměnných
    • funkce pro několik rutinních úkonů
  • eliminovat výskyt chyby. Jak?
    • přehledný zápis SQL příkazů
  • přenositelnost mezi databázovými systémy
    • automatická podpora konvencí (escapování/slashování, uvozování identifikátorů)
    • automatické formátování spec. typů, např. datum, řetězec
    • sjednocení základních fcí (připojení k db, vykonání příkazu, získání výsledku)
  • a především KISS (Keep It Simple, Stupid)
    • zachovat maximální jednoduchost
    • raději jeden geniální nápad, než 10.000 hloupých řádků kódu

Připojení k databázi

Každé spojení je reprezentováno objektem Dibi\Connection. To komunikuje s databází přes ovladač (třída implementující Dibi\Driver). Který ovladač použít zvolíme při vytváření objektu:

$options = [
    'driver'   => 'mysql',
    'host'     => 'localhost',
    'username' => 'root',
    'password' => '***',
    'database' => 'table',
];

// v případě chyby vyhodí Dibi\Exception
$connection = new Dibi\Connection($options);
$connection->query('TRUNCATE `table`');

Ale na tento způsob můžete klidně zapomenout :-) Je tu totiž statický registr dibi. Ten má za úkol udržovat v globálně dostupném úložišti objekt spojení a nad ním volat potřebné funkce:

dibi::connect([
    'driver'   => 'mysql',
    'host'     => 'localhost',
    'username' => 'root',
    'password' => '***',
    'database' => 'test',
    'charset'  => 'utf8',
]);

Statická třída dibijednu báječnou výhodu – kdekoliv je po ruce. Nemusíte získávat instanci připojení, prostě napíšete dibi:: a máte vystaráno.

Poznámka: připojování ve stylu DSN, kdy popis připojení je uložen v řetězci připomínajícím URI, se v praxi ukázalo jako nepraktické. Používám raději pole, přípustný je však i řetězec, a to ve standardizovaném formátu HTTP query.

SQL příkazy – tak to je bomba!

Přiznám se, že způsob zápisu SQL příkazů jsem hledal šíleně dlouho. Nakonec jsem dospěl k technice, která je nesmírně prostá, intuitivní a doslova návyková:

dibi::query('SELECT * FROM [table] WHERE [id] = %i', $id);

$arr = [
    'pole' => 'hodnota',
    'bit'  => TRUE,
];
dibi::query('INSERT INTO [table]', $arr);

dibi::query('UPDATE `table` SET ', $arr, 'WHERE `id`=%i', $x);

Jak vidíte, SQL příkaz se zapisuje jako série parametrů a před vložením proměnné uvedeme modifikátor (např. %i). Pokud ho neuvedeme, zjistí se typ automaticky (samozřejmě nelze zjistit typy jako je datum apod).

Proměnná na naformátuje do výsledného SQL podle pravidel aktivní databáze. Tak třeba TRUE bude v MS SQL jako –1, jinde jako ‚1‘. Stejně tak se zformátují řetězce, časové údaje, atd.

Modifikátory jsou následující:

%s string
%sN string, ale '' se přeloží jako NULL
%bin binární data
%b boolean
%i %u integer
%iN integer, ale 0 se přeloží jako NULL
%f float
%d datum (očekává string nebo integer)
%t datum & čas (také string či integer)
%n identifikátor (tedy název tabulky či sloupce)
%sql SQL – řetězec ponechá beze změny
%lmt speciální – určuje limit
%ofs speciální – určuje offset
%ex speciální – expanduje pole

Pokud za modifikátorem následuje NULL, vloží se do databáze NULL. Pokud následuje pole, tak se modifikátor aplikuje na všechny jeho prvky. Ty se pak vloží do SQL oddělené čárkama.

Vždy používejte modifikátor %s před proměnnou s řetězcem. Dibi by pak nemohlo rozlišit, co je SQL příkaz (tzv. embedded SQL) a co řetězec. V tomto příkladu je funkce dibi::query volána s dvěma argumenty, první je řetězec představující (embedded) SQL, druhý je řetězec představující řetězec. Modifikátor %s to odliší:

$text = "I'm fine";
dibi::query('UPDATE `table` SET `text`=%s', $text);
// MySQL: UPDATE `table` SET `text`='I\'m fine'
// ODBC:  UPDATE [table] SET [text]='I''m fine'

Proč používám termín embedded SQL? Protože jak vidno, i toto SQL prochází zpracováním, aby vyhovovalo konvencím dané databáze. Identifikátory (jména tabulek a sloupců) uvozuji do hranatých závorek nebo zpětných uvozovek (je to jedno), dále řetězce značím jednoduchými či dvojitými uvozovkami, ale na výstup se dostane vždy to, co databáze žádá. Příklad

dibi::query("UPDATE `table` SET [text]='I''m fine'");

// MySQL: UPDATE `table` SET `text`='I\'m fine'
// ODBC:  UPDATE [table] SET [text]='I''m fine'

Ještě doplním, že uvozovka se uvnitř řetězce v embedded SQL zapisuje zdvojením. Lomítko má totiž v PHP řetězci zvláštní význam, muselo by se tedy použít dvojité, což leda komplikuje život a cílem dibi je opak.

Operátor LIKE

Pro operátor LIKE jsou k dispozici tři speciální výrazy:

%like~ výraz začíná řetězcem
%~like výraz končí řetězcem
%~like~ výraz obsahuje řetězec

Příklad použítí – hledej jména začínající na určitý řetězec:

dibi::query("SELECT * FROM table WHERE name LIKE %like~", $query);

Formátování polí

Jak jsem už psal, modifikátor je možné aplikovat také na všechny prvky pole, které se pak oddělené čárkami vloží do SQL. Ovšem můžeme využít také dvou speciálních modifikátorů %a nebo %v.

%and   [key]=val AND [key2]="val2" AND ...
%or   [key]=val OR [key2]="val2" OR ...
%a assoc [key]=val, [key2]="val2", ...
%l %in list (val, "val2", ...)
%v values ([key], [key2], ...) VALUES (val, "val2", ...)
%m multivalues ([key], [key2], ...) VALUES (val, "val2", ...), (val, "val2", ...), ...
%by ordering [key] ASC, [key2] DESC ...
%n identifikátory [key], [key2] AS alias, ...
jiný val, val2, ...

Také si můžeme dovolit luxus žádný modifikátor před polem neuvést. V tom případě dibi použije tuto dedukci: jde-li o příkaz INSERT či REPLACE, zvol %v, jinak %a (platí pro asociativní pole).

Takže příklad:

$arr = [
    'a' => 'hello',
    'b'  => TRUE,
];
dibi::query('INSERT INTO [table]', $arr);
// INSERT INTO `table` (`a`, `b`) VALUES ('hello', 1)

dibi::query('UPDATE `table` SET ', $arr);
// UPDATE `table` SET `a`='hello', `b`=1

Složitější výrazy v polích

Přímo v polích (v klíčích) je možné používat modifikátory:

dibi::query('UPDATE `table` SET ', [
    'number%sql' => 'RAND()', // %sql means SQL ;)
]);
// UPDATE `table` SET 'number' = RAND()

Také hodnoty mohou být složitější výrazy:

dibi::query('UPDATE `table` SET ', [
    'title' => array('SHA1(%s)', 'tajneheslo'),
]);
// UPDATE `table` SET 'title' = SHA1('tajneheslo')

Klíče je možné zcela vynechat:

dibi::query('SELECT * FROM `table` WHERE %and', [
    array('number > %i', 10),
    array('number < %i', 100),
]);
// SELECT * FROM `table` WHERE (number > 10) AND (number < 100)

Speciální typ pro datum a čas

Parametrem může být také objekt DateTime.

dibi::query('UPDATE `table` SET ', [
    'time' => new DateTime,
]);
// UPDATE `table` SET ('2008-01-01 01:08:10')

Postupné skládání dotazu

Dibi disponuje také podporou pro postupné skládání SQL dotazu:

$query[] = 'SELECT * FROM [table]';
if ($where){
    array_push($query, 'WHERE [id]=%d', $where);
}

// a nyní předáme pole
$result = dibi::query($query);

Nebo lze použít expanzi pole přes speciální modifikátor %ex.

Podmíněné SQL příkazy

Podmíněné SQL příkazy jsou velmi silným nástrojem. Ovládají se pomocí tří klíčových slov %if, %else a %end. První z nich %if se musí, obdobně jako modifikátor, nacházet zcela na konci řetězce představujícího SQL:

$user = ???

dibi::query('
SELECT *
FROM [table]
%if', isset($user), 'WHERE [user]=%s', $user
);

Závěrečné %end je možno vynechat (nebo bude lepší na něm trvat?).

Podmínku lze rozšířit o část %else:

dibi::query('
SELECT *
FROM %if', $cond, '[one_table] %else [second_table]'
);

Podmínky můžete zanořovat do libovolné hloubky!

Prefixy & substituce

Názvy tabulek a sloupců mohou obsahovat proměnné části. Ty si nejprve nadefinujeme:

// create new substitution :blog:  ==>  wp_
dibi::addSubst('blog', 'wp_');

a poté použijeme v SQL. Všimněte si, že v SQL jsou uvozeny dvojtečkama:

dibi::test("UPDATE [:blog:items] SET [text]='Hello World'");
// UPDATE `wp_items` SET `text`='Hello World'

Testování query()

Abyste si mohli trošku s dibi hrát, je tu připravena funkce dibi::test(), které předáte parametry stejně jako dibi::query(), ovšem místo provedení SQL příkazu se tento barevně vypíše na obrazovku.

Možná by vás zajímalo, co celé to parsování a skládání dotazu stojí. Napsal jsem tyto funkce co nejoptimálněji a situace je taková, že zaberou jen zlomek času, který si ukousne samotné vykonání SQL příkazu. Můžete si ověřit.

Získávání výsledků

Nejjednodušší cesta vede přes klasickou iteraci

$result = dibi::query('SELECT * FROM table');

foreach ($result as $n => $row) {
    print_r($row);
}

unset($result);

Všimněte si, že zdroje se uvolní automaticky při zrušení objektu.

Je možné také nastavit offset a eventuálně i limit

$result = dibi::query('SELECT * FROM table');

$offset = 10;
$limit = 3;

foreach ($result->getIterator($offset, $limit)
          as $n => $row) {
    print_r($row);
}

Můžeme získat jen první políčko výsledku

$value = $result->fetchSingle();

Nebo celou tabulku do indexovaného pole:

$all = $result->fetchAll();

A pak tu máme k dispozici jednu mocnou funkci:

$assoc = $result->fetchAssoc('id');

Získá celou tabulku do asociativního a klíčem je políčko ‚id‘. Největší síla funkce se projeví tehdy, pokud provedete asociaci podle více políček. Takto lze nesmírně elegantně získávat data z dotazů, ve kterých spojujeme více tabulek. Příklad si nechám na příště.

Užitečná je také funkce pro získávání dat v podobě asociativního pole klíč ⇒ hodnota

$pairs = $result->fetchPairs('customerID', 'name');

Počet řádků zjistíme voláním:

$rows = count($result);

// přesun kurzoru:
$result->seek($row);

Datové typy

Stále to není všechno, jedeme dále. Při získávání záznamů můžeme specifikovat datový typ jednotlivých sloupců a dibi je bude automaticky převádět.

$result->setType('id', Dibi\Type::INTEGER);
$record = $res->fetch();

if (is_int($record['id']))
    echo 'yes, it is integer';

Nebo lze nechat všechny typy automaticky detekovat:

$result->detectTypes();
...

Případně nastavit v metodě connect() parameter 'resultDetectTypes' => TRUE a typy se budou detekovat vždy.

Vrácený záznam

Dibi vrací záznamy jako objekty Dibi\Row. Ty mají tu vlastnost, že k hodnotám atributů lze přistupovat dvěma způsoby – buď jako k hodnotám objektů anebo jako k asociativnímu poli.

Výjimky, logování chyb a profiler

Jakákoliv chyba vzniklá během operace s databázovým serverem vyhodí výjimku Dibi\Exception nebo potomka Dibi\DriverException. Pokud dojde k chybě během vykonávání SQL příkazu, je i tento předán jako výjimce.

Užitečnou vlastností je logování provozu:

dibi::getProfiler()->setFile('log.sql');

Pokud používáte Firefox, Firebug a FirePHP, bude se provoz logovat i do Firebug konzole. Což se hodí při ladění. Tehdy se uplatní i velmi jednoduchý profiler:

echo dibi::$sql; // poslední SQL příklaz
echo dibi::$elapsedTime; // jeho doba trvání v sec
echo dibi::$numOfQueries; // celkem SQL příkazů
echo dibi::$totalTime; // celkový čas v sec

Dibi disponuje rozhraním pro připojení vlastního profileru nebo logovací knihovny.