Creating Extensions for Nette DI
Besides configuration files, the generation of the DI container is also influenced by extensions. We
activate them in the configuration file in the extensions
section.
This is how you add an extension, represented by the BlogExtension
class, under the name blog
:
extensions:
blog: BlogExtension
Each compiler extension inherits from Nette\DI\CompilerExtension and can implement the following methods, which are called sequentially during the DI container compilation process:
- getConfigSchema()
- loadConfiguration()
- beforeCompile()
- afterCompile()
getConfigSchema()
This method is called first. It defines the schema for validating configuration parameters.
You configure the extension in a section named after the extension, in this case blog
:
# same name as the extension
blog:
postsPerPage: 10
allowComments: false
We create a schema describing all configuration options, including their types, allowed values, and optional default values:
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),
]);
}
}
Refer to the Schema page for documentation. Additionally, you can specify which
options can be dynamic using
dynamic()
, for example Expect::int()->dynamic()
.
We access the configuration via the $this->config
variable, which is an stdClass
object:
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
$num = $this->config->postPerPage;
if ($this->config->allowComments) {
// ...
}
}
}
loadConfiguration()
This method is used to add services to the container. The Nette\DI\ContainerBuilder is used for this:
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']);
}
}
The convention is to prefix services added by the extension with its name to avoid name conflicts. The prefix()
method does this, so if the extension is named blog
, the service will be named blog.articles
.
If we need to rename a service, we can create an alias with the original name for backward compatibility. Nette does this
similarly, e.g., for the routing.router
service, which is also available under the former name
router
.
$builder->addAlias('router', 'routing.router');
Loading Services from File
Services can be defined not only using the ContainerBuilder API but also using the familiar NEON syntax within the
extension's configuration file or a separate NEON file. The prefix @extension
represents the current extension.
services:
articles:
create: MyBlog\ArticlesModel(@connection)
comments:
create: MyBlog\CommentsModel(@connection, @extension.articles)
articlesList:
create: MyBlog\Components\ArticlesList(@extension.articles)
Load the services using Compiler::loadDefinitionsFromConfig()
:
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
$builder = $this->getContainerBuilder();
// load the configuration file for the extension
$this->compiler->loadDefinitionsFromConfig(
$this->loadFromFile(__DIR__ . '/blog.neon')['services'],
);
}
}
beforeCompile()
This method is called once the container builder holds all service definitions loaded from extensions
(loadConfiguration
methods) and user configuration files. At this stage, you can modify existing service definitions
or add relationships between them (e.g., using method calls). You can find services using findByTag()
or
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()
At this stage, the container class has been generated as a ClassType object. It includes all the factory methods for creating services and is ready to be written to the cache file. You can still modify the generated class code at this point.
class BlogExtension extends Nette\DI\CompilerExtension
{
public function afterCompile(Nette\PhpGenerator\ClassType $class)
{
$method = $class->getMethod('__construct');
// ...
}
}
$initialization
The Configurator
executes initialization code after the container is created. This code is built by adding
statements to the $this->initialization
object using its addBody() method.
Here's an example showing how to start a session or instantiate services tagged with run
using
initialization code:
class BlogExtension extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
// automatic session startup
if ($this->config->session->autoStart) {
$this->initialization->addBody('$this->getService("session")->start()');
}
// services with tag 'run' must be created after the container is instantiated
$builder = $this->getContainerBuilder();
foreach ($builder->findByTag('run') as $name => $foo) {
$this->initialization->addBody('$this->getService(?);', [$name]);
}
}
}