Creating Extensions for Nette DI
Generating an DI container in addition to configuration files also affect the so-called extensions. We
activate them in the configuration file in the extensions
section.
This is how we add the extension represented by class BlogExtension
with name blog
:
extensions:
blog: BlogExtension
Each compiler extension inherits from Nette\DI\CompilerExtension and can implement following methods that are called during DI compilation:
- getConfigSchema()
- loadConfiguration()
- beforeCompile()
- afterCompile()
getConfigSchema()
This method is called first. It defines schema used to validate configuration parameters.
Extensions are configured in a section whose name is the same as the one under which the extension was added, eg
blog
.
# same name as my extension
blog:
postsPerPage: 10
comments: false
We will define a schema describing all configuration options, including their types, accepted values and possibly 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),
]);
}
}
See the Schema for documentation. Additionally, you can specify which options can
be dynamic using dynamic()
, for
example Expect::int()->dynamic()
.
We access configuration through the $this->config
, which is an object stdClass
:
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. This is done by 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']);
}
}
The convention is to prefix the services added by an extension with its name so that no name conflicts arise. This is done by
prefix()
, so if the extension is called ‘blog’, the service will be called blog.articles
.
If we need to rename a service, we can create an alias with its original name to maintain backward compatibility. Similarly
this is what Nette does for eg routing.router
, which is also available under the earlier name
router
.
$builder->addAlias('router', 'routing.router');
Retrieve Services from a File
We can create services using the ContainerBuilder API, but also we can add them via the familiar NEON configuration file and
its services
section. 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)
We will add services this way:
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()
The method is called when the container contains all the services added by individual extensions in
loadConfiguration
methods as well as user configuration files. At this phase of assembling, we can then modify
service definitions or add links between them. You can use the findByTag()
method to search for services by tags, or
findByType()
method to search by class or interface.
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 phase, the container class is already generated as a ClassType object, it contains all the methods that the service creates, and is ready for caching as PHP file. We can still edit the resulting 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 calls the initialization code after container creation, which is created by writing to an
object $this->initialization
using method addBody().
We will show an example of how to start a session or start services that have the run
tag 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]);
}
}
}