インタラクティブなコンポーネント

コンポーネントは、ページに埋め込む独立した再利用できるオブジェクトです。フォーム、データグリッド、アンケートなど、繰り返し使う意味のあるものなら何でもかまいません。ここでは次のことを扱います。

  • コンポーネントの使い方
  • その書き方
  • シグナルとは何か

Nette には組み込みのコンポーネントのしくみがあります。Delphi や ASP.NET Web Forms の熟練者には似たものが馴染み深いかもしれません。React や Vue.js も、遠く似た考えの上に築かれています。とはいえ PHP のフレームワークの世界では、これは他にない機能です。

同時にコンポーネントは、アプリケーション開発への向き合い方を根本から変えます。あらかじめ用意された部品からページを組み立てられます。管理画面にデータグリッドが必要ですか。Nette 向けのオープンソースのアドオン(コンポーネントに限りません)を集めた Componetteで見つけて、プレゼンターに差し込むだけです。

プレゼンターにはいくつでもコンポーネントを組み込めます。そしてコンポーネントの中にほかのコンポーネントを埋め込めます。こうしてプレゼンターを根とするコンポーネントの木ができます。

ファクトリメソッド

コンポーネントはどうプレゼンターに差し込まれ、どう使われるのでしょうか。ふつうはファクトリメソッドを通じてです。

コンポーネントのファクトリは、本当に必要になったときにだけコンポーネントを作る(遅延、オンデマンド)優雅な方法です。魔法のすべては createComponent<Name>() という名前のメソッドを実装することにあります。<Name> は作られるコンポーネントの名前で、このメソッドがそれを作って返します。

class DefaultPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentPoll(): PollControl
	{
		$poll = new PollControl;
		$poll->items = $this->items;
		return $poll;
	}
}

すべてのコンポーネントが別々のメソッドで作られるので、コードが分かりやすくなります。

コンポーネントの名前は、メソッド名では大文字で始まっていても、常に小文字で始まります。

ファクトリを直接呼ぶことは決してありません。コンポーネントを最初に使ったときに自動的に呼ばれます。おかげでコンポーネントは適切な瞬間に、しかも本当に必要な場合にだけ作られます。コンポーネントを使わなければ(ページの一部だけを転送する AJAX のリクエストや、テンプレートをキャッシュする場合など)まったく作られないので、サーバーの性能を節約できます。

// コンポーネントにアクセスします。それが最初なら
// createComponentPoll() が呼ばれて作られます
$poll = $this->getComponent('poll');
// 別の書き方: $poll = $this['poll'];

テンプレートでは {control}タグでコンポーネントを描けます。ですからコンポーネントを手でテンプレートに渡す必要はありません。

<h2>投票してください</h2>

{control poll}

数が変わるコンポーネントを動的に作るには Multiplierを使います。

createComponent<Name>() のファクトリメソッドはプレゼンターだけのものではありません。同じやり方でコンポーネントの中にコンポーネントを入れ子にし、木に組み立てられます。たとえばコンポーネントの中で別々に描かれるフォームに便利です。

ハリウッド流

コンポーネントはふつう、私たちがハリウッド流と呼びたくなる新鮮な手法を使います。映画のオーディションの参加者がよく耳にする決まり文句をご存じでしょう。「こちらから連絡します、あなたからはしないでください。」まさにそういうことです。

Nette では、絶えず問い続ける(「フォームは送信されたか」「それは正しかったか」「ユーザーはこのボタンを押したか」)代わりに、フレームワークに「これが起きたら、このメソッドを呼んで」と伝えて、あとは任せます。JavaScript でプログラムしているなら、この書き方はよくご存じでしょう。ある出来事が起きたときに呼ばれる関数を書き、言語が適切なパラメータをそこに渡してくれます。

これはアプリケーションを書くときの見方をすっかり変えます。フレームワークに任せられる仕事が多いほど、あなたの手間は減ります。そして見落としも減ります。

コンポーネントを書く

コンポーネントという語は、ふつう Nette\Application\UI\Controlクラスの子孫を指します。(「control」という語のほうが正確ですが、言語によっては別の意味を持つので、「component」のほうが定着しました。)プレゼンター Nette\Application\UI\Presenter自身も Control クラスの子孫です。

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

描画

コンポーネントを描くのに {control componentName} タグを使うことはすでに見ました。これは実際にはコンポーネントの render() メソッドを呼び、そこで描画の面倒を見ます。プレゼンターと同じく $this->template 変数に Latte のテンプレートがあり、そこにパラメータを渡します。プレゼンターと違うのは、テンプレートのファイルを指定して描かせる必要がある点です。

public function render(): void
{
	// テンプレートにいくつかのパラメータを入れます
	$this->template->param = $value;
	// そして描きます
	$this->template->render(__DIR__ . '/poll.latte');
}

{control} タグは render() メソッドにパラメータを渡せます。

{control poll $id, $message}
public function render(int $id, string $message): void
{
	// ...
}

コンポーネントが、別々に描きたいいくつかの部分から成ることもあります。それぞれについて独自の描画メソッドを作ります。この例では renderPaginator() です。

public function renderPaginator(): void
{
	// ...
}

テンプレートでは次のように呼び出します。

{control poll:paginator}

理解を深めるために、このタグが PHP のコードにどう変換されるかを知っておくとよいでしょう。

{control poll}
{control poll:paginator 123, 'hello'}

は次に変換されます。

$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');

getComponent() メソッドが poll コンポーネントを返し、そのコンポーネントで render() メソッド、あるいはタグのコロンのあとに別の描画メソッドが指定されていれば renderPaginator() が呼ばれます。

注意してください。パラメータの中で角かっこの外に => が現れると、すべてのパラメータが配列に包まれ、第 1 引数として渡されます。

{control poll, id: 123, message: 'hello'}

は次に変換されます。

$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);

サブコンポーネントの描画:

{control cartControl-someForm}

は次に変換されます。

$control->getComponent("cartControl-someForm")->render();

コンポーネントもプレゼンターと同じく、いくつかの役立つ変数をテンプレートに自動的に渡します。

  • $basePath はルートディレクトリへの絶対 URL パスです(たとえば /eshop
  • $baseUrl はルートディレクトリへの絶対 URL です(たとえば http://localhost/eshop
  • $userユーザーを表すオブジェクトです
  • $presenter は現在のプレゼンターです
  • $control は現在のコンポーネントです
  • $flashesflashMessage() 関数で送られたメッセージの配列です

シグナル

Nette のアプリケーションでの移動が、Presenter:action の組へのリンクやリダイレクトから成ることはすでに見ました。しかし現在のページで何か処理をしたいだけの場合はどうでしょうか。たとえば表の列の並べ替えを変える、項目を削除する、ライト/ダークモードを切り替える、フォームを送信する、アンケートに投票する、などです。

この種のリクエストをシグナルと呼びます。アクションが action<Action>()render<Action>() メソッドを呼ぶのと同じように、シグナルは handle<Signal>() メソッドを呼びます。アクション(やビュー)の考え方が純粋にプレゼンターに関わるのに対し、シグナルはすべてのコンポーネントに関わります。UI\PresenterUI\Control の子孫なので、プレゼンターにも関わります。

public function handleClick(int $x, int $y): void
{
	// ... シグナルの処理 ...
}

シグナルを呼ぶリンクはいつもどおりに作ります。つまりテンプレートでは n:href 属性か {link} タグ、コードでは link() メソッドです。詳しくは URL リンクの作成の章をご覧ください。

<a n:href="click! $x, $y">ここをクリック</a>

シグナルは常に現在のプレゼンターとアクションで呼ばれます。別のプレゼンターやアクションで呼ぶことはできません。

ですからシグナルは、もとのリクエストと同じようにページを読み込み直しつつ、加えてシグナルを処理するメソッドを適切なパラメータで呼びます。そのメソッドがなければ Nette\Application\UI\BadSignalException例外が投げられ、ユーザーには 403 Forbidden のエラーページとして表示されます。

スニペットと AJAX

シグナルは AJAX を少し思い起こさせるかもしれません。現在のページで呼ばれるハンドラだからです。そのとおりで、シグナルは実際に AJAX で呼ばれることが多く、そのあとページの変わった部分だけがブラウザに転送されます。これをスニペットと呼びます。詳しくは AJAX のページをご覧ください。

フラッシュメッセージ

コンポーネントは、プレゼンターとは独立した自分のフラッシュメッセージの保管場所を持ちます。これはたとえば操作の結果を知らせるメッセージです。フラッシュメッセージの大事な性質は、リダイレクト後もテンプレートで使えることです。一度表示されたあとも、さらに 30 秒は有効なままです。たとえば通信のエラーでユーザーがページを再読み込みしても、メッセージがすぐ消えることはありません。

送信は flashMessageメソッドが担当します。第 1 パラメータはメッセージの本文(stringStringable)か、メッセージを表す stdClass オブジェクトです。省略可能な第 2 パラメータはその種類(error、warning、info など)です。flashMessage() メソッドはフラッシュメッセージのインスタンスを stdClass オブジェクトとして返すので、さらに情報を足せます。

$this->flashMessage('項目を削除しました。');
$this->redirect(/* ... */); // そしてリダイレクト

これらのメッセージは、テンプレートの $flashes 変数に stdClass オブジェクトとして入っていて、message(メッセージの本文)、type(メッセージの種類)のプロパティを持ち、先ほど触れたユーザーの情報を含むこともあります。たとえば次のように描きます。

{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}

シグナルの処理後のリダイレクト

コンポーネントのシグナルの処理のあとには、リダイレクトが続くことがよくあります。フォームと同じで、送信後にはリダイレクトして、ブラウザでページを再読み込みしてもデータが再送信されないようにします。

$this->redirect('this'); // 現在のプレゼンターとアクションにリダイレクトします

コンポーネントは再利用できる部品で、ふつう特定のプレゼンターへの直接のつながりを持つべきではないので、redirect()link() メソッドはパラメータを自動的にコンポーネントのシグナルと解釈します。

$this->redirect('click'); // 同じコンポーネントの 'click' シグナルにリダイレクトします

別のプレゼンターやアクションにリダイレクトする必要があるなら、プレゼンターを通して行えます。

$this->getPresenter()->redirect('Product:show'); // 別のプレゼンター/アクションにリダイレクトします

永続パラメータ

永続パラメータは、コンポーネントの状態をリクエストをまたいで保つために使います。その値はリンクをクリックしたあとも変わりません。セッションのデータと違い、URL で運ばれます。しかもそれは完全に自動的に起こり、同じページのほかのコンポーネントで作られたリンクにも及びます。

たとえば内容をページ分けするコンポーネントがあるとします。そうしたコンポーネントがページに複数あるかもしれません。そしてリンクをクリックしたあとも、すべてのコンポーネントが今のページにとどまってほしいとします。ですからページ番号(page)を永続パラメータにします。

Nette で永続パラメータを作るのはきわめて簡単です。public のプロパティを作り、アトリビュートで印を付けるだけです(以前は /** @persistent */ が使われていました)。

use Nette\Application\Attributes\Persistent;  // この行が大事です

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // public でなければなりません
}

プロパティにはデータ型(int など)を指定することをおすすめしますし、既定値も与えられます。パラメータの値は検証できます

リンクを作るとき、永続パラメータの値は変えられます。

<a n:href="this page: $page + 1">次へ</a>

あるいはリセットして URL から取り除けます。その場合は既定値になります。

<a n:href="this page: null">リセット</a>

永続コンポーネント

パラメータだけでなく、コンポーネントも永続にできます。その永続パラメータは、プレゼンターの異なるアクションのあいだや、複数のプレゼンターのあいだでも引き継がれます。永続コンポーネントには、プレゼンターのクラスにアトリビュートで印を付けます。たとえば calendarpoll のコンポーネントには次のように印を付けます。

use Nette\Application\Attributes\Persistent;

#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

これらのコンポーネントの中のサブコンポーネントには印を付ける必要がありません。それらも永続になります。

古いアノテーション @persistent もまだ動きますが、非推奨で警告を出します。

/**
 * @persistent(calendar, poll)
 */
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

依存関係を持つコンポーネント

依存関係を持つコンポーネントを、それを使うプレゼンターを「散らかさず」に作るにはどうすればよいでしょうか。Nette の DI コンテナの賢い機能のおかげで、通常のサービスと同じく、ほとんどの仕事をフレームワークに任せられます。

PollFacade サービスに依存するコンポーネントを例に取りましょう。

class PollControl extends Control
{
	public function __construct(
		private int $id, // コンポーネントを作る対象のアンケートの ID
		private PollFacade $facade,
	) {
	}

	public function handleVote(int $voteId): void
	{
		$this->facade->vote($this->id, $voteId);
		// ...
	}
}

通常のサービスを書いているなら、議論の余地はありません。DI コンテナがすべての依存関係の受け渡しを見えないところでこなしてくれます。しかしコンポーネントの場合、ふつうはファクトリメソッド createComponent…() の中で、プレゼンターの中に新しいインスタンスを作って扱います。とはいえ、すべてのコンポーネントのすべての依存関係を、コンポーネントに渡すためだけにプレゼンターへ渡すのは面倒です。しかも書くコードの量ときたら……。

当然の疑問として、コンポーネントを通常のサービスとして登録し、プレゼンターに渡して createComponent…() メソッドで返せばよいのでは、と思うかもしれません。しかしこのやり方はふさわしくありません。必要ならコンポーネントを何度も作れるようにしたいからです。

正しい解は、コンポーネントのファクトリ、つまりコンポーネントを作ってくれるクラスを書くことです。

class PollControlFactory
{
	public function __construct(
		private PollFacade $facade,
	) {
	}

	public function create(int $id): PollControl
	{
		return new PollControl($id, $this->facade);
	}
}

このファクトリを設定でコンテナに登録します。

services:
	- PollControlFactory

そして最後にプレゼンターで使います。

class PollPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private PollControlFactory $pollControlFactory,
	) {
	}

	protected function createComponentPollControl(): PollControl
	{
		$pollId = 1; // 自分のパラメータを渡せます
		return $this->pollControlFactory->create($pollId);
	}
}

素晴らしいのは、Nette DI がこうした単純なファクトリを生成できることです。ですからそのコード全体を書く代わりに、インターフェースを書くだけで済みます。

interface PollControlFactory
{
	public function create(int $id): PollControl;
}

これだけです。Nette が内部でこのインターフェースを実装し、プレゼンターに注入してくれるので、そこで使えます。$id パラメータと PollFacade クラスのインスタンスを、魔法のようにコンポーネントに足してくれます。

コンポーネントの詳細

Nette Application のコンポーネントは、ページに埋め込むウェブアプリケーションの再利用できる部分で、この章はまるごとそれに捧げられてきました。そうしたコンポーネントには、正確には何ができるのでしょうか。

  1. テンプレートに描ける
  2. AJAX のリクエストのときに自分のどの部分を描くべきかを知っている(スニペット)
  3. 自分の状態を URL に保存できる(永続パラメータ)
  4. ユーザーの操作に反応できる(シグナル)
  5. 階層的な構造を作る(根はプレゼンター)

これらの機能は、それぞれ継承の系列の中のいずれかのクラスが担当します。描画(1 + 2)は Nette\Application\UI\Controlが、ライフサイクルへの統合(3、4)は Nette\Application\UI\Componentクラスが、階層的な構造の生成(5)は Container と Componentのクラスが担当します。

Nette\ComponentModel\Component  { IComponent }
|
+- Nette\ComponentModel\Container  { IContainer }
	|
	+- Nette\Application\UI\Component  { SignalReceiver, StatePersistent }
		|
		+- Nette\Application\UI\Control  { Renderable }
			|
			+- Nette\Application\UI\Presenter  { IPresenter }

コンポーネントのライフサイクル

コンポーネントのライフサイクル

永続パラメータの検証

URL から受け取った永続パラメータの値は、loadState() メソッドがプロパティに書き込みます。あわせてプロパティに指定されたデータ型と合うかも確認し、合わなければ 404 のエラーで応え、ページは表示されません。

永続パラメータを決して盲信しないでください。ユーザーに URL で簡単に書き換えられます。たとえばページ番号 $this->page が 0 より大きいかを次のように確かめます。適切な方法が、先ほどの loadState() メソッドの上書きです。

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1;

	public function loadState(array $params): void
	{
		parent::loadState($params); // ここで $this->page が設定されます
		// 続いて独自の値のチェック:
		if ($this->page < 1) {
			$this->error();
		}
	}
}

逆の処理、つまり永続プロパティから値を集めるのは saveState() メソッドが担当します。

プレゼンターへの接続

コンポーネントがプレゼンターの階層の一部になった瞬間、$onAnchor 配列に入っているコールバックが呼ばれます。その時点からコンポーネントはプレゼンターを使えるようになり、安全にリンクを作ったり、永続パラメータを読んだりできます。

$control->onAnchor[] = function ($control): void {
	// コンポーネントがプレゼンターを使えるようになりました
};

シグナルの詳細

シグナルは(AJAX で呼ばれる場合を除き)もとのリクエストとまったく同じようにページを読み込み直し、signalReceived($signal) メソッドを呼びます。Nette\Application\UI\Component クラスでのその既定の実装は、handle<Signal> という語を組み合わせたメソッドを呼ぼうとします。そのあとの処理はそのオブジェクト次第です。Component を継承したオブジェクト(つまり ControlPresenter)は、handle<Signal> メソッドを適切なパラメータで呼ぼうとして反応します。

言い換えると、handle<Signal> 関数の定義を取り、リクエストとともに来たすべてのパラメータと、URL のパラメータを名前で引数に割り当てて、メソッドを呼ぼうとします。たとえば URL の id パラメータの値は $id 引数として、URL の something$something として渡されます。そしてメソッドがなければ、signalReceived メソッドが例外を投げます。

URL のパラメータのほかに、シグナルはリクエストの POST 本体で送られたパラメータも読みます。シグナルは JavaScript から呼ばれることが多く、そこでは POST メソッドでデータを送るのが自然なので、これは便利です。ただし同じ名前のパラメータが URL と POST 本体の両方から来た場合、URL の値が優先されます。ですから POST のフィールドに URL やルートのパラメータと同じ名前を付けるのは避けてください。さもないと URL の値が黙ってそれを上書きしてしまいます。シグナルのパラメータは、アクションのパラメータや永続パラメータと共通の空間を持ちます。共有されるパラメータの空間をご覧ください。

シグナルは、SignalReceiver インターフェースを実装してコンポーネントの木につながっている任意のコンポーネント、プレゼンター、オブジェクトが受け取れます。

シグナルの主な受け手は Presenter と、Control を継承した画面のコンポーネントになるでしょう。シグナルは、オブジェクトに何かをすべきだと知らせる合図として働きます。アンケートはユーザーの票を数えるべき、ニュースの欄は広がって 2 倍のニュースを表示すべき、フォームが送信されたのでデータを処理すべき、といった具合です。

シグナルの URL は Component::link()メソッドで作ります。$destination パラメータには文字列 {signal}! を、$args にはシグナルに渡したい引数の配列を渡します。シグナルは常に現在のプレゼンターとアクションで、現在のパラメータとともに呼ばれ、シグナルのパラメータが足されるだけです。さらにシグナルを指定するパラメータ ?do が足されます。

その形式は {signal}{signalReceiver}-{signal} です。{signalReceiver} はプレゼンターの中のコンポーネントの名前です。ですからコンポーネント名にハイフンは使えません。コンポーネント名とシグナルを分けるのに使われるからです。とはいえ、この方法で複数のコンポーネントを入れ子にできます。

isSignalReceiver()メソッドは、コンポーネント(第 1 引数)がシグナル(第 2 引数)の受け手かどうかを調べます。第 2 引数は省略でき、その場合はそのコンポーネントが何らかのシグナルの受け手かを調べます。第 2 パラメータを true にすると、指定したコンポーネントかその子孫のいずれかが受け手かどうかを確かめます。

handle<Signal> より前のどの段階でも、processSignal()メソッドを呼んでシグナルを手動で実行できます。このメソッドがシグナルの処理を引き受け、シグナルの受け手とされたコンポーネント(受け手が指定されていなければプレゼンター自身)を取って、そこへシグナルを送ります。

例を挙げます。

if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
	$this->processSignal();
}

これでシグナルが前倒しで実行され、もう一度呼ばれることはありません。

バージョン: 4.x