Nette & Doctrine
V tomto tutoriáli sa naučíte sprevádzkovať spoluprácu Nette s ORM Doctrine. Doctrine bude následne vo vašej aplikácii vystupovať ako Model.
Predhovor
Doctrine je pokročilý ORM framework, ktorý posúva možnosti a pohodlie práce s Modelom o veľký kus dopredu. V tomto tutoriáli sa naučíte „dosadiť“ Doctrine do pozície Modelu, vyskúšate si prácu s konzolovým rozhraním doctrine-cli a sprevádzkujete napojenie Doctrine na vstavaný profiler.
Aby konzolové rozhranie správne pracovalo aj v prostredí Windows, je potrebné správne nakonfigurovať niekoľko tzv. „environment premenných“ prostredia Windows. V tomto tutoriáli nájdete podrobný postup ako PHP na konzoli sprevádzkovať. Užívateľov unixových operačných systémov sa táto poznámka netýka.
Čo budete potrebovať?
- Stiahnite si aktuálne stabilné vydanie Nette Framework (tutoriál je určený pre verziu PHP 5.2 ale s malými úpravami samozrejme pobeží aj na PHP 5.3). Z tohto balíka budete potrebovať knižnice Nette a tzv. skeleton, ktorý nájdete v zložke tools\Skeleton.
- Zo stránok projektu Doctrine si stiahnite archív verzie 1.2. Sťahujte súbor s názvom Doctrine-1.2.x.tgz, tzv. sandbox nie je pre tento tutoriál vhodný.
- Vytvorte si pracovnú zložku pre projekt, napr. nette-doctrine a príp. sprevádzkujte nový VirtualHost.
Štruktúra projektu
- Do zložky projektu nakopírujte obsah tools/Skeleton z distribučného archívu Nette Framework. Nezabudnite nastaviť práva na zápis nad zložkami nette-doctrine/app/log a nette-doctrine/app/temp.
- Vyprázdnite zložku nette-doctrine/app/models
- Do zložky nette-doctrine/libs nakopírujte knižnice Nette Framework.
- Pripravte zložku pre knižnice Doctrine – vytvorte nette-doctrine/libs/Doctrine.
- Rozbaľte archív s Doctrine a vyhľadajte zložku lib. Z tejto zložky zoberte adresár Doctrine a súbor Doctrine.php a nakopírujte ich do zložky, ktorú ste vytvorili v predošlom kroku (nette-doctrine/libs/Doctrine).
- Vytvorte zložku nette-doctrine/scripts
- V zložke nette-doctrine/app vytvorte adresár doctrine a túto štruktúru podadresárov:
doctrine/
data/
fixtures/
sql/
migrations/
schema
tieto zložky slúžia na skladovanie súborov pre konzolový program doctrine-cli. Jedná sa predovšetkým o konfiguračné súboory YAML a generované SQL skripty
Fungovanie konzolového rozhrania nie je nutným požiadavkom prevádzky Doctrine v Nette Framework. Výraznou mierou však spríjemňuje prácu a vývoj aplikácií. Preto vám odporúčame, aby ste si konzolové rozhranie sprevádzkovali.
Tým je projekt pripravený. Výslednú štruktúru znázorňuje následujúci obrázok:

Doctrine v rámci Nette Framework
Knižnice Doctrine bude „naťahovať“ RobotLoader, preto skontrolujte jeho konfiguráciu v app/config.ini. Okrem toho, do súboru zapíšte konfiguráciu databázovehého pripojenia. V tejto chvíli ju zapíšte do sekcie common, hneď za konfiguráciu RobotLoadera:
; == database == database.driver = mysql database.host = localhost database.username = php database.password = php database.database = nette_doctrine database.profiler = true
Teraz sa zamerajte na súbor app/bootstrap.php. Do tohto súboru sa zvyčajne ukladá kód, ktorý inicializuje databázové spojenie – databázovému enginu je predaná konfigurácia zapísaná v app/config.ini. Na konci súboru bootstrap je potom aplikácia spustená.
Kedže konzolové rozhranie nepracuje s aplikáciou, musíte sa nejakým spôsobom vyhnúť volaniu
$application->run();
Najjednoduchšie to spravíte tak, že konfigurácia DB enginu sa vykoná v inom súbore a tento v bootstrap.php „nainkludujete“. Aby ste však mohli pristupovať ku config.ini tak ako ste zvyknutý ( pomocou Environment::loadConfig() ) je nutné Nette/loader.php natiahnuť už v tomto separátnom súbore. Vytvorte teda nový súbor app/bootstrap.db.php a na jeho začiatok vložte kód
<?php
require LIBS_DIR . '/Nette/loader.php';
Zároveň rovnaký riadok zmažte z app/bootstrap.php a na jeho miesto vložte kód
require(dirname(__FILE__) . '/bootstrap.db.php');
Do nového súboru teraz treba vložiť kód, ktorý predá Doctrine databázovú konfiguráciu. Táto musí byť vo forme pomerne nepekného, tzv. dsn reťazca. Je to obyčajný String, v ktorom je každá časť konfigurácie (DB driver, server, meno, heslo) oddelená nejakým separátorom. Schéma tohto stringu je približne takáto
dbDriver://dbUsername:dbPassword@server/database
Na mieste dbDriver je možné použiť niektorý z podporovaných driverov
- fbsql (FrontBase)
- ibase (InterBase / Firebird)
- mssql (Microsoft SQL Server)
- mysql (MySQL)
- oci (Oracle 7/8/9/10)
- pgsql (PostgreSQL)
- querysim (QuerySim)
- sqlite
Ostatné sekcie dsn reťazca sú samovysvetľujúce. Do app/bootstrap.db.php je teda potrebné vložiť kód, ktorý dsn reťazec zostaví z konfigurácie uloženej v app/config.ini
Environment::loadConfig();
$dbConfig = Environment::getConfig('database');
$conn = Doctrine_Manager::connection($dbConfig->driver . '://' . $dbConfig->username . ':' . $dbConfig->password . '@' . $dbConfig->host . '/' . $dbConfig->database);
if ($dbConfig->profiler) {
}
riadok s kódom Environment::loadConfig() zmažte z app/bootstrap.php!
Nakoniec je nutné niekam uložiť konfiguráciu (umiestnenie) pracovných zložiek pre konzolové rozhranie. doctrine-cli si ich vo vhodný moment z tohto miesta vytiahne a využije pre svoje fungovanie. Ako najvhodnejší kandidát na úschovu týchto údajov sa javí globálne dostupná statická trieda Nette\Environment, ktorá je na takéto účely priamo určená. Pridajte do app/bootstrap.db.php následujúci kód (súbor môžete následne zavrieť)
Environment::setVariable('doctrine_config',
array(
'data_fixtures_path' => dirname(__FILE__) . '/doctrine/data/fixtures',
'models_path' => dirname(__FILE__) . '/models',
'migrations_path' => dirname(__FILE__) . '/doctrine/migrations',
'sql_path' => dirname(__FILE__) . '/doctrine/data/sql',
'yaml_schema_path' => dirname(__FILE__) . '/doctrine/schema'
)
);
Súbor app/bootstrap.db.php môžete využiť pri unit testoch ako bootstrapový súbor, ktorý vás pripojí k testovacej databáze.
Konzolové rozhranie doctrine-cli
Do súboru app/config.ini pridajte sekciu console, ktorá bude dediť od sekcie development.
Ak pracujete na unixovom operačnom systéme, vytvorte v zložke nette-doctrine/scripts súbor doctrine-cli a nastavte mu spustiteľný príznak
touch scripts/doctrine-cli chmod +x scripts/doctrine-cli
Následne do neho vložte kód
#!/usr/bin/env php
<?php
define('APP_DIR', dirname(__FILE__) . '/../app');
define('LIBS_DIR', dirname(__FILE__) . '/../libs');
require dirname(__FILE__).'/../app/bootstrap.db.php';
$cli = new Doctrine_Cli(Environment::getVariable('doctrine_config'));
$cli->run($_SERVER['argv']);
Skúste teraz skript spustiť bez parametrov

doctrine-cli a Windows
Na dosiahnutie rovnakej funkčnosti pod operačným systémom Windows musíte vyvinúť trocha viac úsilia. Predpokladajme, že máte Windows XP (pod Windows Vista a Windows 7 je postup len veľmi málo odlišný) a PHP máte nainštalované v zložke C:\php. Túto zložku je nutné pridať do environment premennej PATH:
- Klepnite pravým tlačidlom na ikonu „Môj počítač“ a z menu vyberte na samom konci „Vlastnosti“
- V novootvorenom okne klepnite na záložku „Pokročilé“
- Nájdite tlačidlo „Premenné prostredia“ a klepnite na neho
- V novootvorenom okne v spodnej časti nalistujte premennú „Path“ a klepnite na tlačidlo „Editovať“
- K hodnote premennej pripíšte na koniec ;C:\php (vrátane bodkočiarky)

- Posledné okno zavrite klepnutím na „OK“ (ostatné okná ešte nezatvárajte)
- V okne „Premenné prostredia“ (druhé) rovnakým spôsobom nalistujte a editujte premennú „PATHEXT“
- K jej hodnote na koniec pripíšte ;.PHP

- Všetky okná teraz pozatvárajte klepnutím na „OK“
- Spustite konzolu s právomocami administrátora
- Do konzoly zadajte tieto dva príkazy
assoc .php=phpfile ftype phpfile="C:\php\php.exe" -f "%1" -- %~2
Gratulujeme, práve ste asociovali súbory s príponou .php s vaším PHP interpretom. To znamená, že všetky súbory .php sú odteraz spustiteľné a predané do C:\php\php.exe na spracovanie.
Ostáva už iba vytvoriť konzolové rozhranie doctrine-cli vo vašom Windows operačnom systéme. Vytvorte súbor nette-doctrine/scripts/doctrine-cli.php. Jeho obsah je veľmi, veľmi podobný ako v prípade unixovej verzie
<?php
define('APP_DIR', dirname(__FILE__) . '/../app');
define('LIBS_DIR', dirname(__FILE__) . '/../libs');
require dirname(__FILE__).'/../app/bootstrap.db.php';
$cli = new Doctrine_Cli(Environment::getVariable('doctrine_config'));
$cli->run($_SERVER['argv']);
Skúste spustiť skript bez parametrov

doctrine-cli v akcii (vygeneruj mi Model)
Už letmý pohľad na výpis doctrine-cli prezrádza silu tohto skriptu. Doctrine prostredníctvom neho poskytuje silné nástroje automatizácie práce. Štruktúru vašej biznisovej logiky je možné zapísať (reprezentovať) troma rôznymi entitami
- DB schéma
- php Model
- YAML konfiguračný súbor
Prvé dva sú pre vašu aplikáciu nepostrádateľné a budú jej nedeliteľnou súčasťou. A hlavným kúzlom doctrine-cli je, že dokáže z jednej entity vygenerovať zvyšné dve

doctrine-cli dokáže napr. z existujúcej databázy vygenerovať php kód Modelov. Alebo naopak, z YAML konfiguračného súboru dokáže pripraviť databázovú schému a php Modely (najčastejší model vývoja). Poďme sa na kúzlo tohto Doctrine triptychu pozrieť bližšie.
V zložke app/doctrine/schema vytvorte súbor schema.yml. Súbory YAML (prípona .yml) sú konfiguračné súbory s formátom vysoko čitateľným pre človeka. Na rozdiel od XML súborov sa v nich človek dokáže zorientovať omnoho jednoduchšie a to dokonca keď sa jedná o stromové štruktúry. Súbor app/doctrine/schema/schema.yml definuje schému našej databázy (tabuľky a ich stĺpce). Poďme si takú jednoduchú schému vygenerovať
Employee:
columns:
name: string(50)
relations:
Phonenumbers:
type: many
class: Phonenumber
local: id
foreign: employee_id
Phonenumber:
columns:
employee_id: integer
phonenumber: string(50)
relations:
Employee:
local: employee_id
foreign: id
Ako vidíte, naša aplikácia bude obsahovať dve tabuľky s prepojením 1:N. Všimnite si definíciu tabuľky alebo ich prepojenia. Všetko je zreteľné na prvý pohľad. Skúste ešte definovať obsah týchto tabuliek, aby ste inštaláciu Doctrine mohli otestovať s nejakými demo údajmi
app/doctrine/data/fixtures/data.yml
Employee:
Employee_1:
name: John Doe
Phonenumbers: [john_home, john_work, john_desk]
Employee_2:
name: Betty Lee
Phonenumbers: [betty_home, betty_work]
Phonenumber:
john_home:
phonenumber: 555 123 456
john_work:
phonenumber: 332 546 789
john_desk:
phonenumber: 555 456 879
betty_home:
phonenumber: 555 649 879
betty_work:
phonenumber: 332 456 546
Opäť veľmi čitateľný zápis. Teraz už iba ostáva konfiguračné súbory predhodiť konzolovému rozhraniu doctrine-cli a nechať ho urobiť svoju prácu
./scripts/doctrine-cli build-all-load

Hoďte očkom prosím na vašu databázu, alebo do zložky app/models


Zapojte vaše vygenerované Modely do práce – nech vám napr. vylistujú všetky telefónne čísla, ktoré patria Johnovi. Dopíšte do app/models/Employee.php túto statickú metódu
public static function getByName($name)
{
$q = Doctrine_Query::create()
->from('Employee e')
->leftJoin('e.Phonenumbers p')
->where('e.name = ?', $name);
$result = $q->execute();
return ($result) ? $result : null;
}
Práve ste použili DQL (Doctrine query language) – veľmi mocný dotazovací jazyk, ktorým vás Doctrine odtieňuje od rôznych enginov databáz. Využite túto metódu v niektorom presenteri a zobrazte si výsledok v šablóne. Editujte app/presenters/HomepagePresenter.php a do metódy renderDefault dopíšte
$this->template->employee = Employee::getByName('John Doe');
Do šablóny tohto View dopíšte (do druhého DIVu)
<pre>{? print_r($employee->toArray())}</pre>
a otvorte domovskú stránku projektu

Gratulujeme, vaša prvá stránka vydolovala údaje z databázy v spolupráci s Doctrine. Možnosti samotnej práce s Modelmi ďaleko presahujú rámec tohto tutoriálu, preto si všetky pokročilé techniky prosím naštudujte v oficiálnej dokumentácii.
Profiler
V úvode sme vám sľúbili aj postup ako Doctrine napojiť na profiler Nette Framework. Všímavejší istotne postrehli, že v súbore app/bootstrap.db.php sme si pre profiler pripravili miesto
$dbConfig = Environment::getConfig('database');
$conn = Doctrine_Manager::connection($dbConfig->driver . '://' . $dbConfig->username . ':' . $dbConfig->password . '@' . $dbConfig->host . '/' . $dbConfig->database);
if ($dbConfig->profiler) {
// tu príde kód
}
Do prázdneho miesta dopíšte tento kód
$profiler = new Doctrine_Connection_Profiler();
$conn->setListener($profiler);
Debug::enableProfiler();
Debug::addColophon('fetchDoctrineEvents');
Na poslednom riadku sme do metódy Debug::addColophon() predali callback na funkciu fetchDoctrineEvents. Musíme ju teda doplniť do kódu. Napr. na samotný koniec suboru app/bootstrap.db.php (mimo horeuvedený IF!)
function fetchDoctrineEvents()
{
$profiler = Doctrine_Manager::getInstance()->getCurrentConnection()->getListener();
$queries = 0;
$out = '<br />';
foreach ($profiler as $event) {
$evName = $event->getName();
if ($evName == 'execute') {
$queries++;
$out .= '[' . number_format($event->getElapsedSecs() * 1000, 3) . 'ms]<br />'. $event->getQuery() . '<br />';
}
$params = $event->getParams();
if(!empty($params)) {
$out .= print_r($params, true) . '<br /><br />';
}
}
return array(
$profiler->count() . ' Doctrine events',
$queries . ' sql queries',
$out
);
}
Spustite hlavnú stránku ešte raz

Zhrnutie
Náš tutoriál je na konci. naučili ste sa v ňom naprogramovať podporu ORM frameworku Doctrine do Nette Framework. Sprevádzkovali ste konzolové rozhramie doctrine-cli (užívatelia Windows okrem toho sprevádzkovali vykonávanie skriptov PHP v konzoli), zoznámili ste sa s konfiguráciou DB schémy a demo údajov pomocou súborov YAML. Pomocou doctrine-cli ste z týchto údajov vygenerovali DB schému a PHP Modely. Modely ste následne dokázali využiť v presenteroch Nette. A nakoniec ste prepojili Nette a Doctrine profiler a jeho údaje vykreslili na stránke.
Celý projekt si môžete stiahnuť vo forme archívu (neobsahuje knižnice Nette a Doctrine!)
Připojené soubory
- nette-doctrine.zip 28 kB
Komentáře 
Acnnair | 24. 5. 2010, 22:32 | question
Najskôr by som chcel poďakovať za výborný tutoriál. Je to moje prvé
stretnutie s Doctrine a musím uznať, že som ani nevedel, že existuje niečo
takto komplexné. Inak všetko funguje naozaj nádherne, ale mám jeden menší
problém. Pred prvým spustením príkazu „build-all-load“ si vytvorím
tabuľku s kódovaním utf8. Podarilo sa mi nájsť ako nastaviť kódovanie na
utf8:
$conn->setCharset(‚utf8‘);
$conn->setCollate(‚utf8_general_ci‘);
Takto sa mi vytvoria v mojej db tabuľky s utf8. Problém je keď chcem niečo
zmením v schema.yml a dám do konzoly „build-all-reload“ alebo
„rebuild-db“. Vytvorí mi to totiž db s kódovaním latin1, čiže sa mi
to všetko rozhádže. Nevie mi niekto poradiť ako nastavím aj tu kódovanie
utf8? Ďakujem.
LuKo | 15. 6. 2010, 11:41 | comment
Acnnair: V YML souboru by měla fungovat direktiva:
options:
type: INNODB
collate: utf8unicodeci
charset: utf8
Employee:
…
Jinak zajímavý článek, díky za něj. Bohužel dám vale Dibi, protože potřebuji provázané tabulky, které Ormion zatím neumí :-/
pesu | 3. 9. 2010, 10:28 | comment
Pokud snad někdo z vás narazí na chybu „Couldn't locate driver named mysql“, pak je to tím, že PHP/Win v konzolovém systému hledá konfigurační soubor php.ini v natvrdo definovaných adresářích a tudíž nemusí zrovna ten váš php.ini najít, zvláště pokud ho jako já máte v nějakém non-std adresáři, viz. (http://www.php.net/…ion.file.php).
Pak se stane, že se php-cli spustí v default konfiguraci, kde není PDO a vyhazuje to tuhle chybu. Nejjednodušší řešení je vzít php.ini a nakopírovat ho do C:\Windows.
mayo2000 | 29. 9. 2010, 20:25 | comment
Pridávam $conn string pre Oracle: $conn =
Doctrine_Manager::connection(‚oci://‘. $dbConfig->username . ‚:‘
. $dbConfig->password
. ‚@‘ . $dbConfig->host
. ‚/(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=‘
. $dbConfig->host
. ‚)(PORT=1521))) (CONNECT_DATA=(SID=‘
. $dbConfig->database . ‚)))‘);
larry | 4. 10. 2010, 10:09 | comment
V Mac OS X v souboru doctrine-cli misto
#!/usr/bin/env php
zadat
#!/usr/bin/php
PJK | 4. 10. 2010, 18:08 | comment
Proč inkludovat nastavení DB z externího souboru?
if (! Environment::isConsole()) {
// whatever
}

perun | 23. 3. 2010, 12:16 | comment
Pekny clanok. Mam len jednu poznamku v config.ini som musel z common naliat nastavenie pre console (pouzil som 0.9.3 pre PHP 5.3)
takze moj ini vyzeral asi tak:
[common]; PHP configuration
php.date.timezone = "Europe/Prague"
php.iconv.internal_encoding = "%encoding%"
php.mbstring.internal_encoding = "%encoding%"
; services
service.Nette-Security-IAuthenticator = UsersModel
service.Nette-Loaders-RobotLoader.option.directory[] = %appDir%
service.Nette-Loaders-RobotLoader.option.directory[] = %libsDir%
service.Nette-Loaders-RobotLoader.run = TRUE
; == database ==
database.driver = mysql
database.host = localhost
database.username = root
database.password =
database.database = xpm_n
database.profiler = true
[console < common]
[production < common]
[development < production]