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

コンポーネントは、ページに挿入する独立した再利用可能なオブジェクトです。フォーム、データグリッド、投票など、繰り返し使用する意味のあるものであれば何でもかまいません。ここでは以下について説明します。

  • コンポーネントの使用方法
  • コンポーネントの作成方法
  • シグナルとは何か

Netteには組み込みのコンポーネントシステムがあります。DelphiやASP.NET Web Formsを知っている古い世代の方々には馴染みがあるかもしれません。ReactやVue.jsも、遠いながらも似たようなものに基づいています。しかし、PHPフレームワークの世界では、これはユニークな機能です。

一方、コンポーネントはアプリケーション開発へのアプローチに根本的な影響を与えます。事前に準備されたユニットからページを組み立てることができます。管理画面にデータグリッドが必要ですか?Nette用のオープンソースアドオン(コンポーネントだけではありません)のリポジトリであるComponetteで見つけて、Presenterに簡単に追加できます。

Presenterには任意の数のコンポーネントを含めることができます。そして、一部のコンポーネントには他のコンポーネントを挿入できます。これにより、Presenterをルートとするコンポーネントツリーが作成されます。

ファクトリメソッド

コンポーネントはどのようにPresenterに挿入され、その後使用されるのでしょうか?通常はファクトリメソッドを使用します。

コンポーネントファクトリは、コンポーネントが実際に必要になったときにのみ作成する(遅延/オンデマンド)エレガントな方法です。全体の魔法は、createComponent<Name>() という名前のメソッドを実装することにあります。ここで <Name> は作成されるコンポーネントの名前であり、このメソッドがコンポーネントを作成して返します。

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

すべてのコンポーネントが個別のメソッドで作成されるため、コードがより明確になります。

コンポーネント名は常に小文字で始まりますが、メソッド名では大文字で記述されます。

ファクトリは直接呼び出すことはありません。コンポーネントを初めて使用するときに自動的に呼び出されます。これにより、コンポーネントは適切なタイミングで、実際に必要な場合にのみ作成されます。コンポーネントを使用しない場合(例えば、ページの一部のみが転送されるAJAXリクエストの場合や、テンプレートのキャッシュの場合)、コンポーネントはまったく作成されず、サーバーのパフォーマンスを節約できます。

// コンポーネントにアクセスし、初めての場合は
// それを作成する createComponentPoll() が呼び出されます
$poll = $this->getComponent('poll');
// 代替構文: $poll = $this['poll'];

テンプレートでは、{control} タグを使用してコンポーネントを描画できます。したがって、コンポーネントを手動でテンプレートに渡す必要はありません。

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

{control poll}

ハリウッドスタイル

コンポーネントは通常、私たちがハリウッドスタイルと呼ぶのが好きな新鮮なテクニックを使用します。映画のオーディション参加者がよく聞く決まり文句をきっとご存知でしょう:「こちらから連絡しますので、電話しないでください」。まさにそれです。

Netteでは、常に何かを尋ねる(「フォームは送信されましたか?」、「有効でしたか?」または「ユーザーはこのボタンを押しましたか?」)代わりに、フレームワークに「それが起こったら、このメソッドを呼び出して」と伝え、残りの作業を任せます。JavaScriptでプログラミングしている場合、このプログラミングスタイルには精通しているでしょう。特定のイベントが発生したときに呼び出される関数を記述します。そして、言語は適切なパラメータを渡します。

これはアプリケーションの作成方法を完全に変えます。フレームワークに任せられるタスクが多ければ多いほど、あなたの作業は少なくなります。そして、見落とす可能性のあることも少なくなります。

コンポーネントの作成

コンポーネントという用語は、通常、Nette\Application\UI\Control クラスの子孫を意味します。(したがって、「コントロール」という用語を使用する方が正確ですが、日本語では「コントロール」は他の意味合いを持つことがあり、「コンポーネント」の方が一般的になりました。)Presenter自体 Nette\Application\UI\Presenter も、ちなみに Control クラスの子孫です。

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

レンダリング

コンポーネントをレンダリングするために {control componentName} タグが使用されることはすでに知っています。これは実際にはコンポーネントの render() メソッドを呼び出し、そこでレンダリングを処理します。Presenterとまったく同じように、$this->template 変数に Latteテンプレート があり、それにパラメータを渡します。Presenterとは異なり、テンプレートファイルを指定してレンダリングさせる必要があります。

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() メソッドを呼び出します。

注意:パラメータのどこかに => が現れると、すべてのパラメータが配列にラップされ、最初の引数として渡されます。

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

は次のように変換されます。

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

サブコンポーネントのレンダリング:

{control cartControl-someForm}

は次のように変換されます。

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

コンポーネントは、Presenterと同様に、いくつかの便利な変数を自動的にテンプレートに渡します。

  • $basePath はルートディレクトリへの絶対URLパスです(例:/eshop
  • $baseUrl はルートディレクトリへの絶対URLです(例:http://localhost/eshop
  • $userユーザーを表すオブジェクトです
  • $presenter は現在のPresenterです
  • $control は現在のコンポーネントです
  • $flashesflashMessage() 関数によって送信されたメッセージの配列です

シグナル

Netteアプリケーションのナビゲーションは、Presenter:action のペアへのリンクまたはリダイレクトに基づいていることはすでに知っています。しかし、現在のページでアクションを実行したいだけの場合はどうでしょうか?例えば、テーブルの列の並び替えを変更する、項目を削除する、ライト/ダークモードを切り替える、フォームを送信する、投票するなどです。

この種のリクエストはシグナルと呼ばれます。そして、アクションが action<Action>() または render<Action>() メソッドを呼び出すのと同様に、シグナルは handle<Signal>() メソッドを呼び出します。アクション(またはビュー)という概念は純粋にPresenterに関連していますが、シグナルはすべてのコンポーネントに関係します。したがって、UI\PresenterUI\Control の子孫であるため、Presenterにも関係します。

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

シグナルを呼び出すリンクは、通常の方法で作成します。つまり、テンプレートでは n:href 属性または {link} タグを使用し、コードでは link() メソッドを使用します。詳細については、URLリンクの作成の章を参照してください。

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

シグナルは常に現在のPresenterとアクションで呼び出され、別のPresenterや別のアクションで呼び出すことはできません。

したがって、シグナルは元のリクエストとまったく同じようにページの再読み込みを引き起こしますが、さらに適切なパラメータを持つシグナル処理メソッドを呼び出します。メソッドが存在しない場合、Nette\Application\UI\BadSignalException 例外がスローされ、ユーザーには403 Forbiddenエラーページとして表示されます。

スニペットとAJAX

シグナルはAJAXを少し思い出させるかもしれません:現在のページで呼び出されるハンドラです。そして、その通りです。シグナルは実際にはAJAXを使用して呼び出されることが多く、その後、変更されたページの部分のみがブラウザに転送されます。つまり、いわゆるスニペットです。詳細については、AJAX専用ページを参照してください。

フラッシュメッセージ

コンポーネントには、Presenterとは独立した独自のフラッシュメッセージストレージがあります。これらは、例えば操作の結果を通知するメッセージです。フラッシュメッセージの重要な特徴は、リダイレクト後もテンプレートで利用できることです。表示後もさらに30秒間有効です。例えば、転送エラーのためにユーザーがページを更新した場合でも、メッセージはすぐには消えません。

送信は flashMessage メソッドによって処理されます。最初のパラメータはメッセージのテキストまたはメッセージを表す 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') // 現在のPresenterとアクションにリダイレクトします

コンポーネントは再利用可能な要素であり、通常は特定のPresenterへの直接的な依存関係を持つべきではないため、redirect() および link() メソッドはパラメータを自動的にコンポーネントのシグナルとして解釈します。

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

別のPresenterやアクションにリダイレクトする必要がある場合は、Presenterを介して行うことができます。

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

パーシステントパラメータ

パーシステントパラメータは、異なるリクエスト間でコンポーネントの状態を維持するために使用されます。その値は、リンクをクリックした後も同じままです。セッションデータとは異なり、URLで転送されます。そして、これは完全に自動的に行われ、同じページの他のコンポーネントで作成されたリンクも含みます。

例えば、コンテンツをページ分割するためのコンポーネントがあるとします。このようなコンポーネントはページ上に複数存在する可能性があります。そして、リンクをクリックした後、すべてのコンポーネントが現在のページにとどまるようにしたいとします。したがって、ページ番号(page)をパーシステントパラメータにします。

Netteでパーシステントパラメータを作成するのは非常に簡単です。パブリックプロパティを作成し、属性でマークするだけです。(以前は /** @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>

パーシステントコンポーネント

パラメータだけでなく、コンポーネントもパーシステントにすることができます。このようなコンポーネントでは、そのパーシステントパラメータはPresenterの異なるアクション間、または複数のPresenter間でも転送されます。パーシステントコンポーネントは、Presenterクラスのアノテーションでマークします。例えば、このようにして calendar および poll コンポーネントをマークします。

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

これらのコンポーネント内のサブコンポーネントをマークする必要はありません。それらもパーシステントになります。

PHP 8では、属性を使用してパーシステントコンポーネントをマークすることもできます。

use Nette\Application\Attributes\Persistent;

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

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

それらを使用する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($id, $voteId);
		// ...
	}
}

従来のサービスを作成する場合、問題はありませんでした。すべての依存関係の受け渡しは、DIコンテナによって目に見えない形で処理されます。しかし、コンポーネントの場合、通常はPresenterのファクトリメソッド createComponent…() で直接新しいインスタンスを作成します。しかし、すべてのコンポーネントのすべての依存関係をPresenterに渡してからコンポーネントに渡すのは面倒です。そして、書かれたコードの量も…

論理的な疑問は、なぜコンポーネントを従来のサービスとして登録し、Presenterに渡してから createComponent…() メソッドで返さないのかということです。しかし、このアプローチは不適切です。なぜなら、コンポーネントを複数回作成できるようにしたいからです。

正しい解決策は、コンポーネントのファクトリ、つまりコンポーネントを作成するクラスを作成することです。

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

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

このようにして、ファクトリを構成内のコンテナに登録します。

services:
	- PollControlFactory

そして最後に、Presenterで使用します。

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は内部的にこのインターフェースを実装し、Presenterに渡します。そこで使用できます。魔法のように、パラメータ $idPollFacade クラスのインスタンスをコンポーネントに追加します。

コンポーネントの詳細

Nette Applicationのコンポーネントは、Webアプリケーションの再利用可能な部分であり、ページに挿入され、この章全体で扱われています。そのようなコンポーネントには、具体的にどのような機能があるのでしょうか?

  1. テンプレートでレンダリング可能
  2. AJAXリクエスト時にどの部分をレンダリングするかを知っている(スニペット)
  3. 状態をURLに保存する機能がある(パーシステントパラメータ)
  4. ユーザーアクションに応答する機能がある(シグナル)
  5. 階層構造を作成する(ルートはPresenter)

これらの各機能は、継承ラインのいずれかのクラスによって処理されます。レンダリング(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() メソッドが担当します。

シグナルの詳細

シグナルは、元のリクエストとまったく同じようにページの再読み込みを引き起こし(AJAXで呼び出された場合を除く)、signalReceived($signal) メソッドを呼び出します。Nette\Application\UI\Component クラスのデフォルト実装は、handle{signal} という単語で構成されるメソッドを呼び出そうとします。その後の処理は、特定のオブジェクト次第です。Component から継承するオブジェクト(つまり ControlPresenter)は、適切なパラメータを持つ handle{signal} メソッドを呼び出そうとすることで応答します。

言い換えれば、handle{signal} 関数の定義とリクエストで渡されたすべてのパラメータが取得され、URLのパラメータが名前に基づいて引数に割り当てられ、そのメソッドを呼び出そうとします。例えば、$id パラメータとしてURLの id パラメータの値が渡され、$something としてURLの something が渡されます。そして、メソッドが存在しない場合、signalReceived メソッドは例外をスローします。

シグナルは、SignalReceiver インターフェースを実装し、コンポーネントツリーに接続されている任意のコンポーネント、Presenter、またはオブジェクトが受信できます。

シグナルの主な受信者は、Presenter および Control から継承するビジュアルコンポーネントになります。シグナルは、オブジェクトに何かをするように指示する合図として機能することを目的としています。投票はユーザーからの投票をカウントする必要があり、ニュースブロックは展開して2倍のニュースを表示する必要があり、フォームは送信されてデータを処理する必要がある、などです。

シグナルのURLは、Component::link() メソッドを使用して作成します。$destination パラメータとして文字列 {signal}! を渡し、$args としてシグナルに渡したい引数の配列を渡します。シグナルは常に現在のPresenterとアクションで現在のパラメータとともに呼び出され、シグナルパラメータのみが追加されます。さらに、最初にシグナルを指定するパラメータ ?do が追加されます。

その形式は {signal} または {signalReceiver}-{signal} のいずれかです。{signalReceiver} はPresenter内のコンポーネントの名前です。したがって、コンポーネント名にハイフンを使用することはできません。ハイフンはコンポーネント名とシグナルを区切るために使用されますが、このようにして複数のコンポーネントをネストすることが可能です。

isSignalReceiver() メソッドは、コンポーネント(最初の引数)がシグナル(2番目の引数)の受信者であるかどうかを検証します。2番目の引数は省略できます。その場合、コンポーネントが任意のシグナルの受信者であるかどうかを判断します。2番目のパラメータとして true を指定すると、指定されたコンポーネントだけでなく、その子孫のいずれかが受信者であるかどうかも検証できます。

handle{signal} に先行する任意の段階で、processSignal() メソッドを呼び出すことでシグナルを手動で実行できます。このメソッドはシグナルの処理を担当します。シグナルの受信者として指定されたコンポーネント(受信者が指定されていない場合はPresenter自体)を取得し、それにシグナルを送信します。

例:

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

これにより、シグナルは早期に実行され、再度呼び出されることはありません。

バージョン: 4.0