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:

  1. getConfigSchema()
  2. loadConfiguration()
  3. beforeCompile()
  4. 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]);
		}
	}
}
version: 3.x 2.x