サービスの定義
設定は、個々のサービスをどう作り、依存関係とどう結びつけるかを DI コンテナに指示する場所です。Nette はそのための、とても明快で優雅な方法を提供します。
NEON の設定ファイルの services
セクションで、独自のサービスとその設定を定義します。PDO
クラスのインスタンスを表す database
というサービスを定義する簡単な例を見てみましょう。
services:
database: PDO('sqlite::memory:')
上の設定から、DI コンテナに次のファクトリメソッドが作られます。
public function createServiceDatabase(): PDO
{
return new PDO('sqlite::memory:');
}
サービス名を付けると、設定ファイルのほかの場所から @serviceName
の形で参照できます。サービスに名前を付ける必要がなければ、単に箇条書きの記号(-)を使えます。
services:
- PDO('sqlite::memory:')
DI コンテナからサービスを取り出すには、サービス名をパラメータに取る getService()
メソッドか、サービスの型を取る getByType() メソッドを使います。
$database = $container->getService('database');
$database = $container->getByType(PDO::class);
サービスの生成
ふつうサービスは、特定のクラスをインスタンス化するだけで作ります。たとえば次のようにです。
services:
database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret)
キーを足して設定を広げたい場合は、定義を複数行に分けられます。
services:
database:
create: PDO('sqlite::memory:')
setup: ...
create キーには factory
という別名があり、どちらの書き方もよく使われます。ただし create
の利用をおすすめします。
コンストラクタやファクトリメソッドの引数は、arguments
キーで指定することもできます。
services:
database:
create: PDO
arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret]
サービスは必ずしも単純なクラスのインスタンス化で作る必要はなく、静的メソッドやほかのサービスのメソッドを呼んだ結果でもかまいません。
services:
database: DatabaseFactory::create()
router: @routerFactory::create()
分かりやすさのために -> の代わりに ::
を使っていることに注意してください。式の言語をご覧ください。次のファクトリメソッドが生成されます。
public function createServiceDatabase(): PDO
{
return DatabaseFactory::create();
}
public function createServiceRouter(): RouteList
{
return $this->getService('routerFactory')->create();
}
DI コンテナは、作られるサービスの型を知る必要があります。戻り値の型が指定されていないメソッドでサービスを作る場合は、その型を設定で明示的に宣言しなければなりません。
services:
database:
create: DatabaseFactory::create()
type: PDO
引数
コンストラクタやメソッドへの引数は、PHP 自体とよく似たやり方で渡します。
services:
database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret)
読みやすさのために、引数を別々の行に並べることもできます。その場合、カンマは省略できます。
services:
database: PDO(
'mysql:host=127.0.0.1;dbname=test'
root
secret
)
引数に名前を付ければ、順序を気にする必要もなくなります。
services:
database: PDO(
username: root
password: secret
dsn: 'mysql:host=127.0.0.1;dbname=test'
)
一部の引数を省いて既定値を使いたい場合や、オートワイヤリングでサービスを注入させたい場合は、アンダースコア(_)を使います。
services:
foo: Foo(_, %appDir%)
引数にはサービスやパラメータなど、さまざまなものを書けます。式の言語をご覧ください。
Setup
setup セクションでは、サービスの生成時に呼ぶべきメソッドを定義します。
services:
database:
create: PDO(%dsn%, %user%, %password%)
setup:
- setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)
PHP では次のようになります。
public function createServiceDatabase(): PDO
{
$service = new PDO('...', '...', '...');
$service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $service;
}
メソッドの呼び出しのほかに、プロパティへの値の代入もできます。配列への要素の追加にも対応していますが、NEON の構文との衝突を避けるため、配列アクセスを引用符で囲む必要があります。
services:
foo:
create: Foo
setup:
- $value = 123
- '$onClick[]' = [@bar, clickHandler]
PHP のコードでは次のようになります。
public function createServiceFoo(): Foo
{
$service = new Foo;
$service->value = 123;
$service->onClick[] = [$this->getService('bar'), 'clickHandler'];
return $service;
}
さらに setup
では、静的メソッドやほかのサービスのメソッドも呼べます。現在のサービス自身を引数として渡す必要があるなら、@self
で参照します。
services:
foo:
create: Foo
setup:
- My\Helpers::initializeFoo(@self)
- @anotherService::setFoo(@self)
分かりやすさのために -> の代わりに ::
を使っていることに注意してください。式の言語をご覧ください。次のファクトリメソッドが生成されます。
public function createServiceFoo(): Foo
{
$service = new Foo;
My\Helpers::initializeFoo($service);
$this->getService('anotherService')->setFoo($service);
return $service;
}
式の言語
Nette DI はきわめて豊かな式の言語を備えていて、ほとんど何でも定義できます。設定ファイルではパラメータが使えます。
# パラメータ
%wwwDir%
# キーの下のパラメータの値
%mailer.user%
# 文字列の中のパラメータ
'%wwwDir%/images'
さらにオブジェクトの生成、メソッドや関数の呼び出しもできます。
# オブジェクトの生成
DateTime()
# 静的メソッドの呼び出し
Collator::create(%locale%)
# PHP の関数の呼び出し
::getenv(DB_USER)
サービスは名前でも型でも参照できます。
# 名前によるサービス
@database
# 型によるサービス
@Nette\Database\Connection
ファーストクラス callable 構文も使えます。
# コールバックの生成。[@user, logout] と同じです
@user::logout(...)
定数も使えます。
# クラス定数
FilesystemIterator::SKIP_DOTS
# PHP の関数 constant() でグローバル定数を取得
::constant(\PHP_VERSION)
サービスの公開プロパティと定数には @service::member
でアクセスします。その名前がプロパティを指すのか定数を指すのかは最初の文字で決まります。小文字で始まれば公開プロパティ、大文字で始まれば定数です。
# サービスの公開プロパティ(小文字で始まります)
@settings::apiUrl
# サービスのクラス定数(大文字で始まります)
@settings::Version
メソッドの呼び出しは PHP と同じように連ねられます。分かりやすさのために ->
の代わりに :: を使います。
DateTime()::format('Y-m-d')
# PHP: (new DateTime())->format('Y-m-d')
@http.request::getUrl()::getHost()
# PHP: $this->getService('http.request')->getUrl()->getHost()
これらの式は、サービスの生成、引数、setupセクション、パラメータなど、どこでも使えます。
parameters:
ipAddress: @http.request::getRemoteAddress()
services:
database:
create: DatabaseFactory::create( @anotherService::getDsn() )
setup:
- initialize( ::getenv('DB_USER') )
特別な関数
設定ファイルでは次の特別な関数が使えます。
not()は値を反転しますbool()、int()、float()、string()は指定した型への損失のないキャストですtyped()は指定した型のすべてのサービスの配列を作りますtagged()は指定したタグを持つすべてのサービスの配列を作ります
services:
- Foo(
id: int(::getenv('ProjectId'))
productionMode: not(%debugMode%)
)
(int) のような標準の PHP
のキャストと違い、損失のないキャストは数値でない値に対して例外を投げます。
typed()
関数は、指定した型(クラスまたはインターフェース)のすべてのサービスの配列を作ります。オートワイヤリングが無効にされたサービスは除かれます。カンマで区切って複数の型を指定することもできます。
services:
- BarsDependent( typed(Bar) )
ある型のサービスの配列は、オートワイヤリングで自動的に引数として渡すこともできます。
tagged()
関数は、特定のタグを持つすべてのサービスの配列を作ります。ここでもカンマで区切って複数のタグを指定できます。
services:
- LoggersDependent( tagged(logger) )
オートワイヤリング
autowired
キーを使うと、特定のサービスのオートワイヤリングの振る舞いを変えられます。詳しくはオートワイヤリングの章をご覧ください。
services:
foo:
create: Foo
autowired: false # foo サービスはオートワイヤリングから除かれます
遅延サービス
遅延読み込みは、サービスの生成を実際に必要になるまで先延ばしにする手法です。グローバルな設定では、すべてのサービスについて遅延生成を有効にできます。個々のサービスでは、その振る舞いを上書きできます。
services:
foo:
create: Foo
lazy: false
サービスが遅延として定義されていると、DI コンテナからそれを要求したとき、特別なプロキシオブジェクトを受け取ります。このプロキシは実際のサービスと見た目も振る舞いも同じですが、本当の初期化(コンストラクタの呼び出しと setup の実行)は、そのメソッドやプロパティに最初にアクセスしたときにはじめて起こります。
サービスが遅れて作られるので、設定の誤りも遅れて現れることを覚えておいてください。たとえばデータベースの認証情報の誤りは、アプリケーションの起動時ではなく、最初のクエリのときにはじめて明らかになります。
遅延生成は循環依存、つまりサービス A がサービス B を必要とし、同時に B が A
を必要とする状況も和らげます。遅延生成がなければ、コンテナは
Circular reference detected のエラーを報告します。遅延プロキシがあれば、サービス A は B
のプロキシだけを受け取り、それが実際に使われるとき、つまり A
がすでに存在する時点で自分を初期化します。とはいえ循環依存は設計の欠陥の兆しなので、取り除くほうがよいでしょう。
遅延読み込みには PHP 8.4
以上が必要で、クラスを直接インスタンス化して作られるサービス(create: Foo
など)にだけ働き、ファクトリメソッドで作られるサービスには働きません。最終的に PHP
の内部クラスを継承するクラスにも使えません。遅延読み込みを適用できない場合、lazy: true
のフラグは黙って無視されます。
タグ
タグはサービスに補足の情報を足すためのものです。サービスにはひとつ以上のタグを割り当てられます。
services:
foo:
create: Foo
tags:
- cached
タグは値を持つこともできます。
services:
foo:
create: Foo
tags:
logger: monolog.logger.event
特定のタグを持つすべてのサービスを取得するには、tagged() 関数が使えます。
services:
- LoggersDependent( tagged(logger) )
DI コンテナの中では、findByTag()
メソッドで特定のタグを持つすべてのサービスの名前を取得できます。
$names = $container->findByTag('logger');
// $names はサービス名をキー、タグの値を値とする配列です
// たとえば ['foo' => 'monolog.logger.event', ...]
Inject モード
inject: true フラグを使うと、Injectアトリビュートを付けた公開プロパティと
inject*()メソッドによる依存性注入が有効になります。
services:
articles:
create: App\Model\Articles
inject: true
既定では、inject モードはプレゼンターでのみ有効です。
サービスの変更
DI コンテナには、組み込みの拡張やユーザーの拡張によって追加された多くのサービスが入っています。こうした既存のサービスの定義は、設定で直接変更できます。たとえば
application.application サービスのクラスは既定で Nette\Application\Application
ですが、別のものに変えられます。
services:
application.application:
create: MyApplication
alteration: true
alteration
フラグは、既存のサービスを変更しているだけであることを示します。同時に安全装置としても働き、変更しようとしたサービスが存在しなければ、コンパイルは例外で失敗します。
setup を足すこともできます。
services:
application.application:
create: MyApplication
alteration: true
setup:
- '$onStartup[]' = [@resource, init]
サービスは内部の名前で特定しなくてもよく、型で参照することもできます。先ほどの例は次のようにも書けます。
services:
@Nette\Application\Application:
create: MyApplication
サービスを変更するとき、もとの引数、setup
の項目、タグを取り除きたいことがあります。そのときは reset キーを使います。
services:
application.application:
create: MyApplication
alteration: true
reset:
arguments: true
setup: true
tags: true
拡張が追加したサービスを取り除きたい場合は、次のようにします。
services:
cache.journal: false