Tvorba rozšíření pro Nette DI
Generování DI kontejneru kromě konfiguračních souborů ovlivňují ještě tzv rozšíření.
Aktivujeme je v konfiguračním souboru v sekci extensions
.
Takto přidáme rozšíření reprezentované třídou BlogExtension
pod názvem blog
:
extensions:
blog: BlogExtension
Každé rozšíření kompileru dědí od Nette\DI\CompilerExtension a může implementovat následující metody, které jsou postupně volány během sestavování DI kontejneru:
- getConfigSchema()
- loadConfiguration()
- beforeCompile()
- afterCompile()
getConfigSchema()
Tato metoda se volá jako první. Definuje schema pro validaci konfiguračních parametrů.
Rozšíření konfigurujeme v sekci, jejíž název je stejný jako ten, pod kterým bylo rozšíření přidáno, tedy
blog
:
# stejné jméno jako má extension
blog:
postsPerPage: 10
allowComments: false
Vytvoříme schema popisující všechny konfigurační volby včetně jejich typů, povolených hodnot a případně i výchozích hodnot:
use Nette\Schema\Expect;
class BlogExtension extends Nette\DI\CompilerExtension
{
public function getConfigSchema(): Nette\Schema\Schema
{
return Expect::structure([
'postsPerPage' => Expect::int(),
'allowComments' => Expect::bool()->default(true),
]);
}
}
Dokumentaci najdete na stránce Schema. Navíc lze určit, které volby mohou
být dynamické pomocí
dynamic()
, např. Expect::int()->dynamic()
.
Ke konfiguraci se dostaneme přes proměnnou $this->config
, což je objekt stdClass
:
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
$num = $this->config->postPerPage;
if ($this->config->allowComments) {
// ...
}
}
}
loadConfiguration()
Používá se přidání služeb do kontejneru. K tomu slouží Nette\DI\ContainerBuilder:
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
$builder = $this->getContainerBuilder();
$builder->addDefinition($this->prefix('articles'))
->setFactory(App\Model\HomepageArticles::class, ['@connection']) // or setCreator()
->addSetup('setLogger', ['@logger']);
}
}
Konvence je prefixovat služby přidané rozšířením jeho názvem, aby nevznikaly jmenné konflikty. To dělá metoda
prefix()
, takže pokud se rozšíření jmenuje blog
, služba ponese název
blog.articles
.
Pokud potřebujeme přejmenovat službu, můžeme kvůli zachování zpětné kompatibility vytvořit alias s původním
názvem. Podobně to dělá Nette např. u služby routing.router
, která je dostupná i pod dřívějším názvem
router
.
$builder->addAlias('router', 'routing.router');
Načtení služeb ze souboru
Služby nemusíme vytvářet jen pomocí API třídy ContainerBuilder, ale i známým zápisem používaným
v konfiguračním souboru NEON v sekci services. Prefix @extension
představuje aktuální extension.
services:
articles:
create: MyBlog\ArticlesModel(@connection)
comments:
create: MyBlog\CommentsModel(@connection, @extension.articles)
articlesList:
create: MyBlog\Components\ArticlesList(@extension.articles)
Služby načteme:
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
$builder = $this->getContainerBuilder();
// načtení konfiguračního souboru pro rozšíření
$this->compiler->loadDefinitionsFromConfig(
$this->loadFromFile(__DIR__ . '/blog.neon')['services'],
);
}
}
beforeCompile()
Metoda se volá ve chvíli, kdy kontejner obsahuje všechny služby přidané jednotlivými rozšířeními v metodách
loadConfiguration
a taktéž uživatelskými konfiguračními soubory. V této fázi sestavování tedy můžeme
definice služeb upravovat nebo doplnit vazby mezi nimi. Pro vyhledávání služeb v kontejneru podle tagů lze využít metodu
findByTag()
, podle třídy či rozhraní zase metodu findByType()
.
class BlogExtension extends Nette\DI\CompilerExtension
{
public function beforeCompile()
{
$builder = $this->getContainerBuilder();
foreach ($builder->findByTag('logaware') as $serviceName => $tagValue) {
$builder->getDefinition($serviceName)->addSetup('setLogger');
}
}
}
afterCompile()
V této fázi už je třída kontejneru vygenerována v podobě objektu ClassType, obsahuje všechny metody, které vytváří služby, a je připravena na zápis do cache. Výsledný kód třídy můžeme v této chvíli ještě upravit.
class BlogExtension extends Nette\DI\CompilerExtension
{
public function afterCompile(Nette\PhpGenerator\ClassType $class)
{
$method = $class->getMethod('__construct');
// ...
}
}
$initialization
Třída Configurator po vytvoření kontejneru volá
inicializační kód, který se vytváří zápisem do objektu $this->initialization
pomocí metody addBody().
Ukážeme si příklad, jak třeba inicializačním kódem nastartovat session nebo spustit služby, které mají tag
run
:
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
// automatické startování session
if ($this->config->session->autoStart) {
$this->initialization->addBody('$this->getService("session")->start()');
}
// služby s tagem run musejí být vytvořeny po instancování kontejneru
$builder = $this->getContainerBuilder();
foreach ($builder->findByTag('run') as $name => $foo) {
$this->initialization->addBody('$this->getService(?);', [$name]);
}
}
}