Konfigurace prostředí
Systémový kontejner je DI kontejner, ve kterém se nacházejí všechny služby a parametry potřebné pro běh aplikace. Řekneme si:
- jak systémový kontejner vytvořit
- jak se konfiguruje pomocí NEON souborů
- jak na produkční a vývojářský režim
- jak používat a vytvářet rozšíření pro kontejner
Omlouváme se, tato stránka je ještě v přípravě.
Systémový kontejner je statický Dependency Injection kontejner, ve kterém se nacházejí všechny služby a parametry potřebné pro běh aplikace. Tedy nejen služby frameworku samotného, ale i všech knihoven, které se rozhodnete použít. Jak takový základní kontejner vypadá, se můžete podívat třeba tady.
V praxi se ukázalo, že programování kontejnerů je značně rutinní a nezáživná činnost. Navíc s tím, jak aplikace roste, je stále těžší se v něm orientovat. Pohodlným řešením je nechat si kontejner vygenerovat. Což Nette Framework umí. Stručným konfiguračním jazykem popíšeme, jaké služby má obsahovat a framework sám PHP kód vytvoří. S pravdou ven: i kód výše odkazovaného kontejneru byl vygenerován.
Configurator
Generování kontejneru je úkolem třídy Nette\Config\Configurator. Samotný PHP kód se vytváří jen jednou a poté uloží do cache, proto musíme určit složku pro dočasné soubory:
$configurator = new Nette\Config\Configurator;
$configurator->setTempDirectory(__DIR__ . '/../temp');
Poté stačí uvést cestu ke konfiguračnímu souboru:
$configurator->addConfig(__DIR__ . '/config/config.neon');
A vytvoření instance je už maličkost:
// vrací objekt třídy SystemContainer
$container = $configurator->createContainer();
Režim prostředí
Configurator se pokouší detekovat, zda aplikace běží na ostrém
(production) nebo vývojářském (development) serveru. Činí tak podle IP
adresy serveru, pokud je 127.0.0.1, považuje jej za vývojářský. Zjistit
režim můžeme metodou isProductionMode() a přenastavit pomocí
setProductionMode().
RobotLoader
Pro sestavení kontejneru je potřeba načíst všechny třídy, které se
v konfiguračním souboru uvádějí. K tomu je obvykle třeba aktivovat auto-loading. Třída Configurator nám
vychází vstříc metodou, která vyrobí RobotLoader, nám pak stačí uvést
indexované adresáře a robota aktivovat. Nezapomeňte tak učinit ještě
před samotným voláním createContainer().
$configurator->createRobotLoader()
->addDirectory(APP_DIR)
->addDirectory(LIBS_DIR)
->register();
Konfigurace v NEON souboru
Se syntaxí si můžete pohrát na stránce http://ne-on.org.
Definice služeb
services:
database:
class: Nette\Database\Connection(%dsn%, %user%, %password%)
#nebo ve dvou řádcích:
class: Nette\Database\Connection
arguments: [%dsn%, %user%, %password%]
Vygeneruje:
function createServiceDatabase()
{
return new Nette\Database\Connection(
$this->parameters['dsn'],
$this->parameters['user'],
$this->parameters['password']
);
}
Definice služby:
services:
database:
class: Nette\Database\Connection
factory: DbFactory::createConnection
DbFactory::createConnection:
class DbFactory
{
static function createConnection(Nette\DI\Container $container)
{
...
}
}
Vygeneruje:
function createServiceDatabase()
{
return DbFactory::createConnection($this);
}
Setup
services:
database:
class: Nette\Database\Connection(%dsn%, %user%, %password%)
setup:
- setCacheStorage(@cacheStorage)
Vygeneruje:
function createServiceDatabase()
{
$service = new Nette\Database\Connection(...);
$service->setCacheStorage($this->cacheStorage);
return $service;
}
Autowiring umí odkazy na jiné služby doplnit automaticky, takže lze parametry úplně vynechat:
setup:
- setCacheStorage
Pokud služba cacheStorage neexistuje, můžeme jako parametr
uvést výsledek volání funkce:
setup:
- setCacheStorage( Factory::createStorage() )
# nebo metody jiné služby:
- setCacheStorage( @factory::createStorage() )
Případně nově vytvořenou třídu:
setup:
- setCacheStorage( Nette\Caching\Storages\FileStorage(%tempDir%) )
# vygeneruje: $service->setCacheStorage(new Nette\Caching\Storages\FileStorage(...));
Lze nastavovat i hodnoty proměnných:
substitutions
setup:
- $substitutions( [db: test] )
# vygeneruje: $service->substitutions = array('db' => 'test');
Kompletní příklad:
parameters:
database:
driver: mysql
host: localhost
dbname: test
user: jim
password: beam
substitutions:
db: test
services:
database:
class: Nette\Database\Connection(
'%database.driver%:host=%database.host%;dbname=%database.dbname%',
%database.user%, %database.password%, null,
Nette\Database\Reflection\DiscoveredReflection()
)
setup:
- setCacheStorage
- $substitutions( %database.substitutions% )
Dědičnost služeb
services:
dev_database < database
setup:
- Diagnostics\ConnectionPanel::initialize
Továrny
Továrny se definují takřka stejně jako služby:
factories:
article:
class: Article
Vygeneruje:
function createArticle()
{
return new Article($this->database);
}
Továrnu pak voláme přímo $article =
$container->createArticle();
Továrnám lze narozdíl od služeb předávat parametry:
factories:
article:
parameters: [id, title: null]
class: Article(..., %id%)
setup:
- $title(%title%)
Vygeneruje:
function createArticle($id, $title = NULL)
{
$service = new Article($this->database, $id);
$service->title = $title;
return $service;
}
Auto-wiring
Auto-wiring umí automaticky předávat do konstruktoru a dalších metod
požadované služby. Řídí se podle type hintů a anotací
@return. Služba odpovídající hledané třídě musí být
v kontejneru právě jedna, jinak se vyhodí výjimka.
Pokud potřebujeme definovat více služeb stejného typu, můžeme je z auto-wiringu vyřadit:
services:
cacheStorage:
class: Nette\Caching\Storages\FileStorage(%tempDir%)
tempCacheStorage:
class: Nette\Caching\Storages\DevNullStorage
autowired: no
Pokud upravujeme základní služby Nette Frameworku, nesmíme se zapomenout
ujistit, že kontejner zná třídy naší implementace. Pokud tedy nastavujeme
vlastní factory pro službu, tak to znamená mít správně
absolutní název třídy v annotaci @return, nebo nastavovat
vždy i třídu do class.
Více konfiguračních souborů
Vkládané soubory se uvádějí v sekci includes.
includes:
- parameters.php
- services.neon
- presenters.neon
Konfigurace se slučují tak, že nejvyšší prioritu má soubor
obsahující sekci includes a nejnižší první vkládaný soubor.
Slučování polí lze zabránit uvedením vykřičníku za název pole:
argument!: [1, 2, 3]
Rozšíření pro kontejner
Configurator sám však žádný kód negeneruje, o to se
stará Nette\Config\Compiler
a Nette\DI\ContainerBuilder.
Nejdříve se spustí Nette\Config\Loader,
který načte konfigurační soubory a předá je Compileru. Do
Compileru si můžeme připojit vlastní rozšíření.
$configurator->onCompile[] = function ($configurator, $compiler) {
$compiler->addExtension('blog', new MyBlogExtension);
};
Každé rozšíření Compileru musí dědit od Nette\Config\CompilerExtension
a může implementovat tři různé metody, které jsou postupně volány,
během sestavování Containeru.
CompilerExtension::loadConfiguration()
Metoda je nad všemi rozšířeními volána jako první a je určena k načtení dodatečných konfiguračních souborů, vytvoření dalších služeb pomocí Nette\DI\ContainerBuilder a hlavně zpracování konfigurace aplikace.
V configu je možné uvést sekci, která se jmenuje stejně jako naše rozšíření. Pokud tedy budeme uvažovat předchozí příklad, mohlo by v konfigu přibýt například toto
blog: # stejné jméno jako má extension
postsPerPage: 10
comments: FALSE
Nastavení si lze přečíst zavoláním metody getConfig() v rozšíření.
class MyBlogExtension extends Nette\Config\CompilerExtension
{
public function loadConfiguration()
{
$config = $this->getConfig();
// array(2) [ 'postsPerPage' => 10, 'comments' => FALSE ]
Protože uznáváme princip konvence před
konfigurací, nastavíme si výchozí hodnoty, aby aplikace fungovala i bez
toho, že budeme něco nastavovat. V prvním argumentu metody
getConfig() můžeme poskytnout výchozí hodnoty, do kterých bude
sloučena příslušná sekce konfiguračního souboru.
class MyBlogExtension extends Nette\Config\CompilerExtension
{
public $defaults = array(
'postsPerPage' => 5,
'comments' => TRUE
);
public function loadConfiguration()
{
$config = $this->getConfig($this->defaults);
Nastavení máme v poli $config, takže je aplikujeme na
služby, které vytvoříme. Použijeme k tomu ContainerBuilder,
který umožňuje to stejné, jako zápis služby v konfiguračním
souboru.
$builder = $this->getContainerBuilder();
Vytvoříme si službu blog_articles, která bude reprezentovat
model pro práci s články a jejich načítání.
Konvence je, prefixovat služby v rozšíření jeho názvem,
aby nevznikaly konflikty. Pokud konvenci dodržíme, budeme moct místo
$container->blog_articles volat služby jako
$container->blog->articles. Pomůže nám s tím metoda
prefix().
$builder->addDefinition($this->prefix('articles'))
->setClass('MyBlog\ArticlesModel', array('@connection'));
Dále si vytvoříme továrničku na komponentu, které předáme
blog_articles a nastavíme počet zobrazených příspěvků na
stránku. Jak používat továrničky na komponenty si ukážeme později.
$builder->addDefinition($this->prefix('articlesList'))
->setClass('MyBlog\Components\ArticlesList', array($this->prefix('@articles')))
->addSetup('setPostsPerPage', $config['postsPerPage'])
->setShared(FALSE)->setAutowired(FALSE); // ze služby se stane továrnička
Budeme potřebovat i modelovou třídu pro komentáře
blog_comments, které předáme připojení k databázi a
blog_articles a pokud jsou vyplé komentáře, tak je
zakážeme.
$comments = $builder->addDefinition($this->prefix('comments'))
->setClass('MyBlog\CommentsModel', array('@connection', $this->prefix('@articles')));
if (!$config['comments']) { // volitelné vypnutí komentářů
$comments->addSetup('disableComments');
}
Komentáře se musí také někde zobrazovat a psát, takže přidáme
ještě jednu továrničku na komponentu blog_commentsControl,
která nám bude komentáře zobrazovat a dovolí nám psát nové, pokud
nebudou vyplé.
$builder->addDefinition($this->prefix('commentsControl'))
->setClass('MyBlog\Components\CommentsControl', array($this->prefix('@comments')))
->setShared(FALSE)->setAutowired(FALSE); // ze služby se stane továrnička
Toto rozdělení na modely a komponenty je pouze ilustrativní.
Načítání dodatečné konfigurace
Pokud se nám nelíbí vytvářet všechny služby v rozšíření, můžeme jeho část přesunout do samostatného konfiguračního souboru.
services:
blog_articles:
class: MyBlog\ArticlesModel(@connection)
blog_comments:
class: MyBlog\CommentsModel(@connection, @blog_articles)
factories:
blog_articlesList:
class: MyBlog\Components\ArticlesList(@blog_articles)
blog_commentsControl:
class: MyBlog\Components\CommentsControl(@blog_comments)
Který načteme a dodatečně služby nastavíme
public function loadConfiguration()
{
$config = $this->getConfig($this->defaults);
$builder = $this->getContainerBuilder();
// načtení konfiguračního souboru pro rozšíření
$this->compiler->parseServices($builder, $this->loadFromFile(__DIR__ . '/blog.neon'));
// počet článků na stránku v komponentě
$builder->getDefinition('blog_articlesList')
->addSetup('setPostsPerPage', $config['postsPerPage']);
// volitelné vypnutí komentářů
if (!$config['comments']) {
$builder->getDefinition('blog_comments')
->addSetup('disableComments');
}
}
Rozšíření se nám krásně vyčistilo a díky syntaxi Neon, jsou definice služeb mnohem lépe čitelné.
CompilerExtension::beforeCompile()
V této fázi sestavování už by neměly přibývat další služby. Můžeme ovšem upravovat existující a doplnit některé potřebné vazby mezi službami, například pomocí tagů.
CompilerExtension::afterCompile(Nette\Utils\PhpGenerator\ClassType $class)
V této fázi už je třída Containeru vygenerována,
obsahuje všechny metody, které vytváří služby a je připravena na zápis
do cache. Díky api třídy Nette\Utils\PhpGenerator\ClassType
můžeme přidávat vlastní kód do kontejneru a upravovat tak výsledný kód,
který se stará o vytvoření služeb.
Samotný Nette Framework například přidává metodu
initialize, ve které zpracovává některá
uživatelská nastavení. Tuto metodu pak sám vždy volá po
instanciování kontejneru.
Ukážeme si kousek kódu, kde Nette Framework doplňuje do metody
initialize startování session a automatické spouštění
služeb, které mají tag run.
public function afterCompile(Nette\Utils\PhpGenerator\ClassType $class)
{
$container = $this->getContainerBuilder();
$config = $this->getConfig($this->defaults);
// metoda initialize
$initialize = $class->methods['initialize'];
// automatické startování session
if ($config['session']['autoStart']) {
$initialize->addBody('$this->session->start();');
}
// služby s tagem run musejí být spouštěny po vytvoření kontejneru
foreach ($container->findByTag('run') as $name => $foo) {
$initialize->addBody('$this->getService(?);', array($name));
}
}
Metody beforeCompile() a afterCompile() se liší
pouze v tom, že jedna má přístup ke kódu výsledného
Containeru a druhá ne.
Komentáře 
semtex.989 | 20. 1. 2012, 8:09 | comment
Díky za návod Rozšíření pro kontejner, Filipe!
bojovyletoun | 20. 1. 2012, 14:35 | comment
Taky díky za návod, nesledoval jsem fórum, ale při pullnutí jsem našel právě změny v nette extension $ defaults, tak tuším, že se peče něco velkého.


mcmatak | 19. 1. 2012, 18:56 | comment
nevím jestli to tady někde je napsané v době psaní tohoto příspěvku, ale trvalo mi asi dvě hodiny, než jsem přišel na to, že když chci nahradit nějaký nette service (builtin) vlastním, třeba jako usera, tak si ho musím také v neonu pojmenovat!
user: class: Nette\Security\User factory: [@model, createServiceUser]bez toho řádku class to nebude fungovat