Nette DI İçin Extension Yazma

Extension, DI container'ın derlenmesine kancalanan bir sınıftır. Programlı olarak servis kaydedebilir, kendi yapılandırma bölümünü doğrulayabilir, başkalarının tanımladığı servisleri değiştirebilir ve hatta üretilen container kodunu değiştirebilir. Bu sayfa size bir extension'ı nasıl yazacağınızı, ne zaman ne olduğunu ve nelere dikkat etmeniz gerektiğini öğretiyor.

Extension'lar, paketlerin Nette'ye doğal yoldan katılma biçimidir: tüm nette/* paketleri onları kullanır, sizinkiler de kullanabilir. Tipik bir extension şunlardan birini ya da birkaçını yapar:

  • bir kütüphaneyi tümleştirir – servislerini container'a kaydeder ve dost canlısı, doğrulanmış bir yapılandırma bölümü sunar (mail: ya da database: bölümleri buradan gelir)
  • kaydı otomatikleştirir – birbirine benzeyen çok sayıda servisi bir döngüde ya da bir kurala göre kaydeder; bunları services: bölümünde tek tek yazmak zahmetli olurdu
  • kesişen değişiklikler yapar – başkalarının kaydettiği servisleri bulup tamamlar, örneğin belirli bir etikete sahip her servise bir logger iliştirir

Günlük uygulama işlerinde extension'a ender ihtiyaç duyarsınız; sınıflarınızı kaydetmeyi ve bağlamayı yapılandırmanın services bölümü karşılar. Yapılandırma tek başına yetmemeye başladığında extension'a yönelin.

Extension extensions bölümünde etkinleştirilir. BlogExtension sınıfının temsil ettiği bir extension'ı blog adıyla böyle eklersiniz:

extensions:
	blog: BlogExtension

Yapıcısı argüman alıyorsa onları hemen orada verin:

extensions:
	blog: BlogExtension(%debugMode%)

Derleme Nasıl İşler

Extension yazarken kendinize güvenmek için bir şeyi bilmeniz gerekir: kodunuzun ne zaman çalıştığını. Nette, servisleri istekleri işlerken bağlamaz. Bunun yerine container'ı önceden derler: tüm yapılandırma dosyalarını okur, extension'ların işini yapmasına izin verir ve iyileştirilmiş bir PHP sınıfı üretip diske yazar. Sonraki her istek yalnızca bu bitmiş sınıfı yükler. Dolayısıyla extension kodunuz yalnızca container (yeniden) kurulurken çalışır, her istekte değil.

Bunun önemli bir sonucu var: derleme sırasında henüz hiçbir servis yoktur. Var olan şey tanımlardır; her servisin hangi sınıf olacağını, nasıl oluşturulacağını ve sonrasında üzerinde ne çağrılacağını anlatan tarifler. Tanımlar ContainerBuilder nesnesinde durur. Extension aslında betiklenebilir yapılandırmadır: services: bölümünde bildirebildiğiniz her şeyi PHP'de de kurabilirsiniz; koşullu olarak, döngülerle ya da başkalarının kaydettiklerine tepki vererek.

Derleme aşamalar hâlinde ilerler ve bir extension her aşamaya adım atabilir:

  1. tüm extension'ların yapılandırma bölümleri doğrulanır (getConfigSchema())
  2. her extension servislerini kaydeder (loadConfiguration()); kullanıcının services: bölümü en son işlenir, böylece son sözü her zaman uygulama söyler
  3. tüm tanımlar yerine oturup servis türleri çözüldükten sonra extension'lar onları değiştirebilir (beforeCompile())
  4. container sınıfı üretilir; extension'lar kodunu hâlâ ayarlayabilir (afterCompile()) ve uygulama başladığında çalışacak kod üretebilir (başlatma)

Geliştirici kipinde container, bir yapılandırma dosyasını ya da extension sınıfının kendisini değiştirdiğinizde otomatik olarak yeniden derlenir; her ikisi de bağımlılık olarak izlenir. Böylece extension'ları hiç önbellek temizlemeden geliştirebilirsiniz.

Her aşamada ne olduğuna daha derin bakmak için (parametreler ne zaman genişletilir, @service ne zaman referansa dönüşür ve servisleri türe göre aramak tam olarak ne zaman güvenlidir) bkz. Container derlemesi nasıl işler.

İlk Extension

İşte küçük ama eksiksiz bir extension. Onu aynı dosyada etkinleştirip yapılandırıyoruz:

extensions:
	blog: BlogExtension

blog:
	postsPerPage: 5

Ve sınıfın tamamı şu:

use Nette\Schema\Expect;

class BlogExtension extends Nette\DI\CompilerExtension
{
	public function getConfigSchema(): Nette\Schema\Schema
	{
		return Expect::structure([
			'postsPerPage' => Expect::int(10),
			'allowComments' => Expect::bool(true),
		]);
	}


	public function loadConfiguration(): void
	{
		$builder = $this->getContainerBuilder();

		$builder->addDefinition($this->prefix('articles'))
			->setFactory(Blog\Articles::class, ['postsPerPage' => $this->config->postsPerPage]);

		if ($this->config->allowComments) {
			$builder->addDefinition($this->prefix('comments'))
				->setFactory(Blog\Comments::class);
		}
	}
}

getConfigSchema(), blog: bölümünün (extension'ı kaydettiğimiz anahtarın adını taşır) neler içerebileceğini, türleri ve varsayılan değerleri de kapsayacak şekilde anlatır; doğrulanan değerler sonra $this->config içinde bulunur. loadConfiguration() içinde servisleri kaydederiz. Adlara dikkat edin: $this->prefix('articles'), blog.articles üretir; böylece farklı extension'ların servisleri çakışamaz.

Ve son birkaç satır, extension'ların neden var olduğunu gösterir: comments servisi yalnızca yorumlar açıkken kaydedilir. Düz bir yapılandırma dosyası böyle kararlar veremez.

Bu şekilde kaydedilen servisler tam olarak services: bölümüne yazılmış gibi davranır; istendiğinde tembel olarak oluşturulurlar ve autowiring onları Blog\Articles türünün bildirildiği her yere aktarır.

Sonraki bölümler önce extension yaşam döngüsünü ayrıntılı anlatıyor, sonra extension içinde kullanacağınız ContainerBuilder API'sini ve en sonunda bilinmeye değer tuzakları.

Extension Yaşam Döngüsü

Bir extension Nette\DI\CompilerExtension sınıfından türer ve derleyicinin derleme sırasında bu sırayla çağırdığı dört metottan (getConfigSchema(), loadConfiguration(), beforeCompile() ve afterCompile()) bazılarını geçersiz kılar.

getConfigSchema(): Nette\Schema\Schema

Extension'ın yapılandırma bölümünün şemasını tanımlar. Bu sayede kullanıcılar doğrulamayı ve anlaşılır hata mesajlarını bedava alır: blog: bölümündeki bir yazım hatası ya da yanlış tür, siz tek bir denetim yazmadan anlaşılır bir mesajla bildirilir.

Şema, Schema kütüphanesiyle anlatılır ve türleri, varsayılan değerleri, izin verilen değerleri ve çok daha fazlasını ifade edebilir:

public function getConfigSchema(): Nette\Schema\Schema
{
	return Expect::structure([
		'postsPerPage' => Expect::int(10),
		'storage' => Expect::anyOf('files', 'database')->firstIsDefault(),
	]);
}

Doğrulanan yapılandırma, $this->config içinde bir stdClass nesnesi olarak bulunur (şemaya castTo('array') eklerseniz dizi olarak).

Bir seçeneğin değeri derleme zamanında bilinemiyorsa (örneğin bir ortam değişkeninden geliyorsa), onu dynamic() ile işaretleyin, örneğin Expect::int()->dynamic(). Ayrıntısı dinamik parametreler bölümünde.

loadConfiguration()

Extension'ın servislerini ContainerBuilder kullanarak kaydettiği yer:

public function loadConfiguration(): void
{
	$builder = $this->getContainerBuilder();
	$builder->addDefinition($this->prefix('articles'))
		->setFactory(Blog\Articles::class);
}

Bir servis kısa bir adla da erişilebilir olmalıysa bir alias ekleyin. Uzlaşıya göre bu yalnızca extension olağan adıyla kaydedildiğinde yapılır; böylece extension'ın birden çok örneği bunun için çekişemez:

if ($this->name === 'blog') {
	$builder->addAlias('articles', $this->prefix('articles'));
}

Çok sayıda servis olduğunda, onları tanıdık services söz dizimiyle ayrı bir NEON dosyasında tanımlamak daha elverişli olabilir. @extension öneki geçerli extension'a başvurur:

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

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

Bu tanımları loadDefinitionsFromConfig() ile yükleriz; adlara önek otomatik eklenir ve dosya bağımlılık olarak izlenir, böylece değiştirilmesi yeniden derlemeyi tetikler:

public function loadConfiguration(): void
{
	$this->loadDefinitionsFromConfig(
		$this->loadFromFile(__DIR__ . '/services.neon')['services'],
	);
}

beforeCompile()

Bu metot çağrıldığında builder tüm tanımları çoktan tutuyordur: sizinkileri, diğer extension'larınkileri ve kullanıcının yapılandırma dosyalarından gelenleri. Servis türleri de çözülmüştür, dolayısıyla türe göre arama güvenilirdir. Bu da bu aşamayı, nihai servis grafiğini incelemek ve tamamlamak için ideal kılar.

Genellikle servisleri etikete ya da türe göre arar ve bulunan tanımları tamamlarsınız:

public function beforeCompile(): void
{
	$builder = $this->getContainerBuilder();

	foreach ($builder->findByTag('logaware') as $name => $attrs) {
		$builder->getDefinition($name)->addSetup('setLogger');
	}
}

setLogger() çağrısının açık argümanı yok; tıpkı factory'lerde olduğu gibi onları autowiring sağlayacak.

$this->compiler->getExtensions() ile alınan, isteğe bağlı olarak sınıfa ya da arayüze göre süzülen diğer kayıtlı extension'larla da işbirliği yapabilirsiniz:

foreach ($this->compiler->getExtensions(FooExtension::class) as $extension) {
	// ...
}

afterCompile (Nette\PhpGenerator\ClassType $class)

Son aşamada container sınıfı, bir ClassType nesnesi olarak üretilir; bu, PHP Generator kütüphanesinin bir parçasıdır. Her servis için bir factory metodu içerir ve önbelleğe yazılmak üzeredir. Kodunu hâlâ değiştirebilirsiniz:

public function afterCompile(Nette\PhpGenerator\ClassType $class): void
{
	$method = $class->getMethod('__construct');
	// ...
}

Bu aşamaya yalnızca ender ihtiyaç duyarsınız. Uygulama başladığında çalışacak kod eklemek için bunun yerine başlatmayı kullanın:

Başlatma Kodu

Önceki aşamaların hepsi container'ın nasıl kurulacağını etkiler. Buna ek olarak bir extension, çalışma zamanında, container oluşturulduktan hemen sonra çalışan kod üretebilir; örneğin bir oturum başlatmak ya da servisleri ayağa kaldırmak için. Kod, $this->initialization nesnesine onun addBody() metoduyla yazılır:

public function loadConfiguration(): void
{
	// 'run' etiketli servisler container başlar başlamaz oluşturulmalı
	$builder = $this->getContainerBuilder();
	foreach ($builder->findByTag('run') as $name => $attrs) {
		$this->initialization->addBody('$this->getService(?);', [$name]);
	}
}

Nette'nin kendisi başlatmayı örneğin oturumu otomatik başlatmak ya da güvenlikle ilgili HTTP header'ları göndermek için kullanır. Ve şunu unutmayın: extension'daki her şeyin aksine bu kod her istekte çalışır, bu yüzden onu küçük tutun.

ContainerBuilder

Nette\DI\ContainerBuilder, bir extension'ın derleyiciyle konuştuğu nesnedir. Tüm servislerin tanımlarını tutar ve onları eklemek, aramak ve değiştirmek için metotlar sunar. Onu loadConfiguration() ve beforeCompile() içinde alırsınız:

$builder = $this->getContainerBuilder();

Servis Ekleme

Bir servisi kaydetmek, NEON dosyasının services: bölümünde yaptığınızın aynısıdır; yalnızca PHP'de yazılmıştır. Her yapılandırma anahtarının tanım üzerinde bir karşılığı vardır, dolayısıyla şu iki yazım eşdeğerdir:

services:
	articles:
		create: Blog\Articles(@connection)
		setup:
			- setLogger(@logger)
		tags: [logaware]
$builder->addDefinition($this->prefix('articles'))
	->setFactory(Blog\Articles::class, ['@connection'])
	->addSetup('setLogger', ['@logger'])
	->addTag('logaware');

addDefinition() metodunun döndürdüğü tanım, yapılandırma anahtarlarının karşılıklarını sunan bir ServiceDefinition nesnesidir: setType() (servisin sınıfı), setFactory() (nasıl oluşturulacağı), setArguments(), addSetup(), addTag() ve setAutowired().

addSetup(), setup: listesini yansıtır ve aynı biçimleri kabul eder: bir metot çağrısı addSetup('setLogger', ['@logger']), bir özellik ataması addSetup('$cache', ['@cache']) ya da başka bir serviste çağrı addSetup('@Tracy\Bar::addPanel', [$panel]).

Builder, sıradan servislerin yanı sıra üretilen factory'leri, accessor'ları ve locator'ları da kaydedebilir; her birinin, ilgili tanım türünü döndüren kendi metodu vardır:

Metot Kaydettiği
addDefinition() sıradan bir servis (ServiceDefinition döndürür)
addFactoryDefinition() üretilen bir factory (create() metotlu arayüz)
addAccessorDefinition() üretilen bir accessor (get() metotlu arayüz)
addLocatorDefinition() birkaç factory'yi birleştiren bir çoklu factory / locator
addImportedDefinition() container'a çalışma zamanında dışarıdan aktarılan bir servis
addAlias() var olan bir servis için ikinci bir ad

Bir factory'de, oluşturduğu nesneyi getResultDefinition() ile yapılandırırsınız; accessor ise setReference() ile var olan bir servise işaret eder:

$builder->addFactoryDefinition($this->prefix('latteFactory'))
	->setImplement(LatteFactory::class)
	->getResultDefinition()
		->setFactory(Latte\Engine::class)
		->addSetup('setStrictTypes', [true]);

addLocatorDefinition() ve addImportedDefinition() metotlarına ender ihtiyaç duyulur; böyle servisler genellikle elle yazılmak yerine NEON'daki implement: ve içe aktarılan servis anahtarlarından gelir.

Servisleri Bulma ve Değiştirme

Var olan tanımları aramak ve dolaşmak için builder şunları sunar:

Metot Açıklama
getDefinition(string $name) verilen addaki tanım (yoksa istisna fırlatır)
hasDefinition(string $name) bu adda bir tanım ya da alias var mı
getDefinitions() tüm tanımlar
removeDefinition(string $name) bir tanımı kaldırır
getByType(string $type) o türdeki autowiring servisinin adı ya da null
getDefinitionByType(string $type) o türdeki autowiring tanımı
findByType(string $type) o türdeki tüm tanımlar, ad => tanım çiftleri olarak
findByTag(string $tag) etiketi taşıyan servisler, ad => etiket değeri çiftleri olarak
addExcludedClasses(array $types) sınıfları ve arayüzleri autowiring'den çıkarır

Kullanışlı bir kalıp, bir servisin var olup olmadığını öğrenmek için getByType() kullanmaktır; örneğin yalnızca uygulamada varsa bir logger'a kancalanmak için:

if ($builder->getByType(Psr\Log\LoggerInterface::class)) {
	$builder->getDefinition($this->prefix('articles'))
		->addSetup('setLogger');
}

Tanım Türleri

Her add*Definition() metodu farklı türde bir tanım döndürür. Hepsi ortak ata Nette\DI\Definitions\Definition sınıfından türer:

  • ServiceDefinition – sıradan bir servis; setType(), setFactory(), addSetup(), addTag() ve setAutowired() ile yapılandırılır
  • FactoryDefinition – üretilen bir factory: create() metodu her çağrıda yeni bir nesne döndüren arayüz
  • AccessorDefinition – üretilen bir accessor: get() metodu var olan bir servisi döndüren arayüz
  • LocatorDefinition – birkaç factory ya da accessor'ı tek arayüzde birleştiren çoklu factory / locator
  • ImportedDefinition – container'ın kendisinin oluşturmadığı, çalışma zamanında dışarıdan aldığı bir servis

Şunu unutmayın: getDefinition(), verilen adın altında hangi türden tanım varsa onu döndürür. Kodunuz üretilen bir factory ile karşılaşabiliyorsa, önce türü denetleyin ve üretilen nesneyi getResultDefinition() ile yapılandırın:

$def = $builder->getDefinition($name);
if ($def instanceof Nette\DI\Definitions\FactoryDefinition) {
	$def = $def->getResultDefinition();
}
$def->addSetup('setLogger');

İpuçları ve Tuzaklar

Derleme Zamanı ve Çalışma Zamanı

En yaygın kafa karışıklığı kaynağı: extension kodu, uygulama istekleri işlerken değil, container derlenirken çalışır. Pratikte bu şu demektir:

  • Extension asla servis örnekleriyle çalışmaz; onlar henüz yoktur. Servisleri new ile örneklemeyin; bir tanım kaydedin ve onları container oluştursun.
  • Tüm yapılandırma değerleri üretilen koda gömülür. Ortamlar arasında değişebilen bir değer (bir yol, getenv() ile alınan bir parola) dinamik işaretlenmelidir; aksi hâlde derleme zamanında donar.
  • $this->initialization->addBody() metoduna verilen dizeler şimdi çalıştırılmaz; container'a üretilen ve her istekte çalışan PHP kodudur.

Dosya Bağımlılıkları

Container, yapılandırma dosyaları ya da extension sınıfları değiştiğinde yeniden derlenir. Ama extension'ınız başka bir dosyayı okuyorsa (bir varlık listesi, bir kütüphanenin XML yapılandırması), container'ın bundan haberi olmaz. Böyle dosyaları şununla kaydedin:

$builder->addDependency($file);

Aksi hâlde klasik bir gizemle karşılaşırsınız: dosyayı düzenlersiniz, ama uygulama eski biçimde davranmayı sürdürür; değişiklik ancak container başka bir nedenle yeniden kurulduğunda ortaya çıkar. (loadFromFile() ile okunan dosyalar otomatik izlenir.)

Koşullu Kayıt

Bir extension kendini ortamına uydurabilir. İsteğe bağlı tümleştirmeler tipik olarak class_exists() ile korunur:

if (class_exists(Symfony\Component\Console\Command\Command::class)) {
	$builder->addDefinition($this->prefix('command'))
		->setFactory(Blog\Console\SitemapCommand::class);
}

%debugMode% gibi değerleri de en iyi extension'ın yapıcısı üzerinden aktarırsınız:

extensions:
	blog: BlogExtension(%debugMode%)
class BlogExtension extends Nette\DI\CompilerExtension
{
	public function __construct(
		private bool $debugMode = false,
	) {}
}

Tipik bir kullanım, bir Tracy panelini yalnızca geliştirici kipinde kaydetmektir.

Karmaşık Argümanlar

Bazen bir factory'nin ya da setup çağrısının argümanı düz bir değer, bir sınıf adı ya da @service referansı değildir. Bu durumlar için şunlar vardır:

  • new Nette\DI\Definitions\Statement(Blog\Panel::class, [$args]) – yerinde oluşturulan bir nesne; argüman olarak kullanılan “anonim servis”
  • new Nette\DI\Definitions\Reference('blog.articles') – bir servise referans; @name dizesinin nesne karşılığı
  • $builder::literal('PHP_SAPI') – üretilen container'a olduğu gibi eklenen ham PHP kodu parçası

Örnek – bir Tracy paneli kaydetme:

$builder->getDefinition($this->prefix('articles'))
	->addSetup('@Tracy\Bar::addPanel', [
		new Nette\DI\Definitions\Statement(Blog\ArticlesPanel::class),
	]);

Dışa Aktarılan Etiketler ve Türler

Meta veri dışa aktarımı, derlenmiş container'ın yalnızca uygulamanın gerçekten kullandığı etiketleri ve autowiring türlerini tutması için yapılandırmada kısıtlanabilir. Extension'ınız çalışma zamanında $container->findByTag() ya da $container->getByType() ile servis alıyorsa, böyle bir kısıtlama tam da dayandığınız meta veriyi silebilir.

Bunu önlemek için derleyiciye hangi etiketlerin ve türlerin her zaman dışa aktarılması gerektiğini söyleyin:

public function loadConfiguration(): void
{
	// bu etiket, dışa aktarım kısıtlansa bile her zaman dışa aktarılır
	$this->compiler->addExportedTag('event.subscriber');

	// bu tür getByType() için her zaman kullanılabilir olur
	$this->compiler->addExportedType(Nette\Database\Connection::class);
}

Her iki metot da yalnızca dışa aktarılan meta veriye ekleme yapar; uygulamanın di › export yapılandırmasını asla geçersiz kılmaz. Yani uygulama dışa aktarımı bir listeyle kısıtladığında, extension'ınızın ihtiyaç duyduğu etiketler ve türler dahil kalır; yalnızca etiket dışa aktarımını tümüyle kapatmak (tags: false) onları da her şeyle birlikte atar.

versiyon: 3.x