Erstellen von Erweiterungen für Nette DI

Die Generierung des DI-Containers wird neben den Konfigurationsdateien auch durch sogenannte Erweiterungen beeinflusst. Wir aktivieren sie in der Konfigurationsdatei im Abschnitt extensions.

So fügen wir eine Erweiterung hinzu, die durch die Klasse BlogExtension repräsentiert wird, unter dem Namen blog:

extensions:
	blog: BlogExtension

Jede Compiler-Erweiterung erbt von Nette\DI\CompilerExtension und kann die folgenden Methoden implementieren, die während der Erstellung des DI-Containers nacheinander aufgerufen werden:

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

getConfigSchema()

Diese Methode wird zuerst aufgerufen. Sie definiert das Schema zur Validierung der Konfigurationsparameter.

Wir konfigurieren die Erweiterung im Abschnitt, dessen Name mit dem Namen übereinstimmt, unter dem die Erweiterung hinzugefügt wurde, also blog:

# gleicher Name wie die Extension
blog:
	postsPerPage: 10
	allowComments: false

Wir erstellen ein Schema, das alle Konfigurationsoptionen beschreibt, einschließlich ihrer Typen, erlaubten Werte und gegebenenfalls auch Standardwerte:

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),
		]);
	}
}

Die Dokumentation finden Sie auf der Seite Schema. Zusätzlich kann festgelegt werden, welche Optionen dynamisch sein können, mittels dynamic(), z.B. Expect::int()->dynamic().

Auf die Konfiguration greifen wir über die Variable $this->config zu, die ein stdClass-Objekt ist:

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		$num = $this->config->postPerPage;
		if ($this->config->allowComments) {
			// ...
		}
	}
}

loadConfiguration()

Wird verwendet, um Dienste zum Container hinzuzufügen. Dazu dient 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']) // oder setCreator()
			->addSetup('setLogger', ['@logger']);
	}
}

Die Konvention ist, Dienste, die durch eine Erweiterung hinzugefügt werden, mit deren Namen zu präfixieren, um Namenskonflikte zu vermeiden. Dies macht die Methode prefix(), sodass, wenn die Erweiterung blog heißt, der Dienst den Namen blog.articles trägt.

Wenn wir einen Dienst umbenennen müssen, können wir aus Gründen der Abwärtskompatibilität einen Alias mit dem ursprünglichen Namen erstellen. Ähnlich macht es Nette z. B. beim Dienst routing.router, der auch unter dem früheren Namen router verfügbar ist.

$builder->addAlias('router', 'routing.router');

Laden von Diensten aus einer Datei

Dienste müssen nicht nur über die API der ContainerBuilder-Klasse erstellt werden, sondern auch mit der bekannten Schreibweise, die in der NEON-Konfigurationsdatei im Abschnitt services verwendet wird. Das Präfix @extension repräsentiert die aktuelle Extension.

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

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

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

Wir laden die Dienste:

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		$builder = $this->getContainerBuilder();

		// Laden der Konfigurationsdatei für die Erweiterung
		$this->compiler->loadDefinitionsFromConfig(
			$this->loadFromFile(__DIR__ . '/blog.neon')['services'],
		);
	}
}

beforeCompile()

Die Methode wird aufgerufen, wenn der Container alle Dienste enthält, die von den einzelnen Erweiterungen in den loadConfiguration-Methoden sowie durch die Benutzer-Konfigurationsdateien hinzugefügt wurden. In dieser Phase der Erstellung können wir also die Dienstdefinitionen ändern oder Abhängigkeiten zwischen ihnen hinzufügen. Zum Suchen von Diensten im Container nach Tags kann die Methode findByTag() verwendet werden, nach Klasse oder Schnittstelle die Methode 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()

In dieser Phase ist die Containerklasse bereits in Form eines ClassType-Objekts generiert, enthält alle Methoden, die Dienste erstellen, und ist bereit, in den Cache geschrieben zu werden. Der resultierende Klassencode kann zu diesem Zeitpunkt noch geändert werden.

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function afterCompile(Nette\PhpGenerator\ClassType $class)
	{
		$method = $class->getMethod('__construct');
		// ...
	}
}

$initialization

Die Configurator-Klasse ruft nach der Erstellung des Containers Initialisierungscode auf, der durch Schreiben in das $this->initialization-Objekt mittels der Methode addBody() erstellt wird.

Wir zeigen ein Beispiel, wie man z. B. mit Initialisierungscode die Session startet oder Dienste startet, die das Tag run haben:

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function loadConfiguration()
	{
		// automatisches Starten der Session
		if ($this->config->session->autoStart) {
			$this->initialization->addBody('$this->getService("session")->start()');
		}

		// Dienste mit dem Tag run müssen nach der Instanziierung des Containers erstellt werden
		$builder = $this->getContainerBuilder();
		foreach ($builder->findByTag('run') as $name => $foo) {
			$this->initialization->addBody('$this->getService(?);', [$name]);
		}
	}
}
Version: 3.x