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 three methods that are called during DI compilation:

  1. loadConfiguration()
  2. beforeCompile()
  3. afterCompile()

loadConfiguration()

This method is called first. Is used to validate configuration parameters and 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'])
			->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.

Configuring Extensions

Extensions can be 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 access the defined values in the extension through the $this->config variable.

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		$config = $this->config;
		// ['postsPerPage' => 10, 'comments' => false]
	}
}

Default Configuration Values

If this is possible, it is useful to provide default values for each configuration element according to the convention over configuration principle so that the user does not have to configure everything.

You can use the validateConfig() method to merge user configuration with default values. As a parameter, we pass it an array of all available keys and their default values, and method expands $this->config. At the same time, if there are keys in the configuration that are not available, an exception will be throwed. This prevents from mistakes in key names in the configuration file.

blog:
	postsPerPage: 10
class BlogExtension extends Nette\DI\CompilerExtension
{
	private $defaults = [
		'postsPerPage' => 5,
		'comments' => true
	];

	public function loadConfiguration()
	{
		$this->validateConfig($this->defaults);
		// ['postsPerPage' => 10, 'comments' => true]

		$builder->addDefinition($this->prefix('articles'))
			->setFactory(App\Model\HomepageArticles::class, ['@connection'])
			->addSetup('setPostsPerPage', [$this->config['postsPerPage']]);

		if (!$this->config['comments']) {
			$builder->getDefinition($this->prefix('articles'))
				->addSetup('disableComments');
		}
	}
}

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.

services:
	articles:
		factory: MyBlog\ArticlesModel(@connection)

	comments:
		factory: MyBlog\CommentsModel(@connection, @extension.articles)

	articlesList:
		factory: MyBlog\Components\ArticlesList(@extension.articles)

We will add services this way:

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		// load configuration parameters
		$config = $this->validateConfig($this->defaults);
		$builder = $this->getContainerBuilder();

		// load the configuration file for the extension
		$this->compiler->loadDefinitions(
			$builder,
			$this->loadFromFile(__DIR__ . '/blog.neon')['services'],
			$this->name
		);
	}
}

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.

For example, Nette Framework itself adds the initialize method. This method then calls when container instance is created.

We will show a piece of code how Nette Framework uses the initialize to startup session and automatically run services that have the run tag.

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function afterCompile(Nette\PhpGenerator\ClassType $class)
	{
		$container = $this->getContainerBuilder();

		// method initialize()
		$initialize = $class->getMethod('initialize');

		// automatic session startup
		if ($this->config['session']['autoStart']) {
			$initialize->addBody('$this->getService("session")->start()');
		}

		// services with tag 'run' must be created after the container is instantiated
		foreach ($container->findByTag('run') as $name => $foo) {
			$initialize->addBody('$this->getService(?);', [$name]);
		}
	}
}
version: 3.x 2.x