プレゼンター

Nette でプレゼンターとテンプレートをどう書くのかを見ていきます。読み終えると次のことが分かります。

  • プレゼンターがどう動くのか
  • 永続パラメータとは何か
  • テンプレートがどう描かれるのか

プレゼンターがウェブアプリケーションの特定のページ、たとえばトップページ、ネットショップの商品、ログインフォーム、サイトマップのフィードなどを表すクラスであることは、すでに見てきました。アプリケーションはプレゼンターをひとつから何千まで持てます。ほかのフレームワークではコントローラとも呼ばれます。

ふつうプレゼンターという語は、ウェブのインターフェースを生成するのに向いた Nette\Application\UI\Presenterクラスの子孫を指し、この章の以降もそれを中心に扱います。より一般的な意味では、プレゼンターとは Nette\Application\IPresenterインターフェースを実装した任意のオブジェクトです。

プレゼンターのライフサイクル

プレゼンターの役目は、リクエストを処理してレスポンス(HTML のページ、画像、リダイレクトなど)を返すことです。

ですからまずリクエストが渡されます。これは直接の HTTP リクエストではなく、ルーターの助けを借りて HTTP リクエストが変換された Nette\Application\Requestオブジェクトです。このオブジェクトを直接扱うことはふつうありません。プレゼンターがリクエストの処理をほかのメソッドに巧みに委ねるからです。それをこれから見ていきます。

プレゼンターのライフサイクル

図は、存在すれば上から下へ順に呼ばれるメソッドの一覧を示しています。どれも必須ではありません。メソッドがひとつもない、まったく空のプレゼンターを作り、その上に単純な静的サイトを築くこともできます。

__construct()

コンストラクタは、オブジェクトが作られる瞬間に呼ばれるので、厳密にはプレゼンターのライフサイクルには属しません。それでもその重要さゆえに触れておきます。コンストラクタは(inject メソッドとともに)依存関係を渡すために使います。

プレゼンターは、アプリケーションのビジネスロジックを扱ったり、データベースに読み書きしたり、計算したりすべきではありません。それはモデルと呼ばれる層のクラスの責務です。たとえば ArticleRepository クラスが記事の読み込みと保存を担うでしょう。プレゼンターがそれを使うには、依存性注入で渡してもらう必要があります。

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articles,
	) {
	}
}

startup()

リクエストを受け取った直後に startup() メソッドが呼ばれます。プロパティの初期化やユーザーの権限の確認などに使えます。このメソッドは必ず親を呼ぶ必要があります: parent::startup()

action<Action>(args...)

render<View>() メソッドと似ています。render<View>() がこのあと描かれる特定のテンプレートのためにデータを用意するものであるのに対し、action<Action>() はリクエストを処理するもので、そのあとにテンプレートを描くとは限りません。たとえばデータを処理したり、ユーザーをログイン・ログアウトさせたりしてから、別の場所へリダイレクトすることもあります。

大事なのは、action<Action>()render<View>() より先に呼ばれることです。おかげで、アクションのメソッドの中でリクエストの流れを変えられます。たとえば setView('otherView') を使って、描かれるテンプレートや、呼ばれる render<View>() メソッドさえ変えられます。

switch('otherAction') メソッドを使えば、まったく別のアクションに切り替えることもできます。現在のメソッドを中断し、代わりに新しいアクションの action<Action>()render<View>() メソッドを実行します(そして自動的な正規化を無効にします)。リクエスト自体は続き、いま走っているメソッドだけが中断されます。

リクエストのパラメータがメソッドに渡されます。これらのパラメータには型を指定でき、そうすることをおすすめします。たとえば actionShow(int $id, ?string $slug = null) です。id パラメータがない、あるいは整数でない場合、プレゼンターは 404 エラーを返して終わります。

handle<Signal>(args...)

このメソッドは、いわゆるシグナルを処理します。シグナルについてはコンポーネントの章で学びます。主にコンポーネントと AJAX リクエストの処理のためのものです。

action<Action>() と同じく、型チェックも含めてリクエストのパラメータがメソッドに渡されます。

beforeRender()

beforeRender メソッドは、その名のとおり、すべての render<View>() メソッドの前に呼ばれます。テンプレートの共通の設定、レイアウトへの変数の受け渡しなどに使います。

render<View>(args...)

ここでは、このあと描かれるテンプレートを準備し、データを渡したりします。

action<Action>() と同じく、型チェックも含めてリクエストのパラメータがメソッドに渡されます。

public function renderShow(int $id): void
{
	// モデルからデータを取得してテンプレートに渡します
	$this->template->article = $this->articles->getById($id);
}

afterRender()

afterRender メソッドは、これもまた名のとおり、すべての render<View>() メソッドのあとに呼ばれます。使われることはむしろ稀です。

shutdown()

プレゼンターのライフサイクルの終わりに呼ばれます。

イベント

プレゼンターのライフサイクルの一部として呼ばれる startup()beforeRender()shutdown() メソッドのほかに、自動的に呼ばれる関数を定義できます。プレゼンターはいわゆるイベントを定義していて、そのハンドラを $onStartup$onRender$onShutdown の配列に足します。

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct()
	{
		$this->onStartup[] = function () {
			// ...
		};
	}
}

$onStartup 配列のハンドラは startup() メソッドの直前に、$onRender のハンドラは beforeRender()render<View>() のあいだに、そして $onShutdown のハンドラは shutdown() の直前に呼ばれます。

先へ進む前にひとつ助言を。 ご覧のとおり、プレゼンターは複数のアクション/ビューを扱えます。つまり render<View>() メソッドを複数持てます。とはいえ、プレゼンターはアクションひとつ、あるいはできるだけ少ない数で設計することをおすすめします。

レスポンスの送信

プレゼンターのレスポンスはふつうテンプレートを HTML のページに描くことですが、ファイルや JSON を送ることも、別のページへリダイレクトすることもできます。

ライフサイクルのどの時点でも、次のいずれかのメソッドでレスポンスを送り、同時にプレゼンターを終わらせられます。

これらのメソッドはいずれも、静かな終了の例外 Nette\Application\AbortException を投げて、ただちにプレゼンターを終わらせます。

これらのメソッドをどれも呼ばなければ、プレゼンターは自動的にテンプレートの描画に進みます。なぜでしょうか。99 % の場合、私たちはテンプレートを描きたいからです。ですからプレゼンターは、私たちの手間を省くためにこの振る舞いを既定としています。

リンクの作成

プレゼンターには、ほかのプレゼンターへの URL リンクを作る link() メソッドがあります。第 1 パラメータは行き先のプレゼンターとアクションで、そのあとに引数が続きます。引数は配列としても渡せます。

$url = $this->link('Product:show', $id);

$url = $this->link('Product:show', [$id, 'lang' => 'en']);

テンプレートでは、ほかのプレゼンターやアクションへのリンクを次のように作ります。

<a n:href="Product:show $id">商品の詳細</a>

実際の URL の代わりに、見慣れた Presenter:action の組を書き、必要なパラメータを添えるだけです。仕掛けは n:href にあり、これがこの属性を処理して本当の URL を生成するよう Latte に伝えます。Nette では URL のことを考える必要はまったくなく、プレゼンターとアクションのことだけを考えればよいのです。

詳しくは URL リンクの作成の章をご覧ください。

リダイレクト

別のプレゼンターに切り替えるには redirect()forward() メソッドを使います。link()メソッドとよく似た構文です。

forward() メソッドは、HTTP のリダイレクトなしにただちに新しいプレゼンターへ切り替えます。

$this->forward('Product:show');

HTTP コード 302(現在のリクエストのメソッドが POST なら 303)の一時的なリダイレクトの例です。

$this->redirect('Product:show', $id);

HTTP コード 301 の恒久的なリダイレクトには、次を使います。

$this->redirectPermanent('Product:show', $id);

アプリケーションの外の別の URL へは redirectUrl() メソッドでリダイレクトできます。HTTP コードは第 2 パラメータで指定でき、既定は 302(現在のリクエストのメソッドが POST なら 303)です。

$this->redirectUrl('https://nette.org');

リダイレクトは、いわゆる静かな終了の例外 Nette\Application\AbortException を投げて、ただちにプレゼンターの動作を終わらせます。

リダイレクトの前に、フラッシュメッセージ、つまりリダイレクト後のテンプレートに表示されるメッセージを送れます。

フラッシュメッセージ

これはふつう、何らかの操作の結果を知らせるメッセージです。フラッシュメッセージの大事な性質は、リダイレクト後もテンプレートで使えることです。一度表示されたあとも、さらに 30 秒は有効なままです。たとえば通信のエラーでユーザーがページを再読み込みしても、メッセージがすぐ消えることはありません。

flashMessage()メソッドを呼ぶだけで、テンプレートへの受け渡しはプレゼンターが担当します。第 1 パラメータはメッセージの本文、省略可能な第 2 パラメータはその種類(error、warning、info など)です。flashMessage() メソッドはフラッシュメッセージのインスタンスを返すので、追加の情報を足せます。

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

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

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

404 などのエラー

たとえば表示したい記事がデータベースにないなど、リクエストに応えられない場合は、error(string $message = '', int $httpCode = 404) メソッドで 404 のエラーを投げます。

public function renderShow(int $id): void
{
	$article = $this->articles->getById($id);
	if (!$article) {
		$this->error();
	}
	// ...
}

HTTP のエラーコードは第 2 パラメータで渡せ、既定は 404 です。このメソッドは Nette\Application\BadRequestException を投げることで働き、そのあと Application が制御をエラー用のプレゼンターに渡します。これは、起きたエラーを知らせるページを表示する役目のプレゼンターです。エラー用のプレゼンターはアプリケーションの設定で指定します。

JSON の送信

sendJson($data) メソッドは、渡されたデータを JSON にエンコードして HTTP のレスポンスとして送り、プレゼンターを終わらせます。例を挙げます。

public function actionData(): void
{
	$data = ['hello' => 'nette'];
	$this->sendJson($data);
}

リクエストのパラメータ

プレゼンターも、各コンポーネントも、そのパラメータを HTTP のリクエストから得ます。値は getParameter($name)getParameters() メソッドで取り出せます。値は文字列か文字列の配列で、要するに URL から直接得た生のデータです。

もっと便利にするために、プロパティ経由でパラメータにアクセスすることをおすすめします。#[Parameter] アトリビュートで印を付けるだけです。

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

class HomePresenter extends Nette\Application\UI\Presenter
{
	#[Parameter]
	public string $theme; // public でなければなりません
}

プロパティにはデータ型(string など)を指定することをおすすめします。そうすれば Nette が値を自動的にキャストします。パラメータの値は検証することもできます。

リンクを作るとき、パラメータの値を直接設定できます。

<a n:href="Home:default theme: dark">クリック</a>

永続パラメータ

永続パラメータは、リクエストをまたいで状態を保つために使います。その値はリンクをクリックしたあとも変わりません。セッションのデータと違い、URL で運ばれます。しかもそれは完全に自動的に起こるので、link()n:href で明示的に書く必要はありません。

使いどころの例を挙げましょう。多言語のアプリケーションがあるとします。現在の言語は、常に URL の一部でなければならないパラメータです。しかしそれをすべてのリンクに書くのは、途方もなく面倒です。ですからそれを永続パラメータ lang にすれば、自動的に運ばれていきます。素敵ですね。

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

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

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang; // public でなければなりません
}

$this->lang'en' のような値を持っていれば、link()n:href で作られたリンクにもパラメータ lang=en が入ります。そしてリンクをクリックしたあと、$this->lang はまた 'en' になります。

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

永続パラメータはふつう、あるプレゼンターのすべてのアクションのあいだで引き継がれます。複数のプレゼンターをまたいで引き継ぐには、次のいずれかで定義する必要があります。

  • プレゼンターが継承する共通の祖先で
  • あるいはプレゼンターが使うトレイトで:
trait LanguageAware
{
	#[Persistent]
	public string $lang;
}

class ProductPresenter extends Nette\Application\UI\Presenter
{
	use LanguageAware;
}

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

<a n:href="Product:show $id, lang: cs">チェコ語での詳細</a>

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

<a n:href="Product:show $id, lang: null">クリック</a>

共有されるパラメータの空間

リクエストのパラメータ、永続パラメータ、そして actionrenderhandle(シグナル)メソッドのパラメータは、ひとつの空間を共有していて、それぞれが名前で識別されます。同じ名前が複数に現れれば、それらはまったく同じ値を指します。

これはしばしば都合よく使われます。たとえば永続パラメータ lang とアクションやシグナルのメソッドの引数 $lang は同じものなので、メソッドのシグネチャに並べるだけで永続パラメータの現在の値を読めます。

#[Persistent]
public string $lang;

public function handleSearch(string $query, string $lang): void
{
	// $lang には永続パラメータ lang の現在の値が入ります
}

この空間は共有されているので、意図的に値を共有したい場合を除き、パラメータの名前は一意に保ってください。これはシグナルにも当てはまり、シグナルはさらにリクエストの POST 本体からもパラメータを読みます。シグナルの詳細をご覧ください。

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

プレゼンターには組み込みのコンポーネントのしくみがあります。コンポーネントは、プレゼンターに埋め込む独立した再利用できる部品です。フォーム、データグリッド、メニューなど、繰り返し使う意味のあるものなら何でもかまいません。

コンポーネントはどうプレゼンターに埋め込まれ、どう使われるのでしょうか。それはコンポーネントの章で学べます。ハリウッドとの共通点まで見つかります。

コンポーネントはどこで手に入るのでしょうか。Componetteには、フレームワークのコミュニティの有志が寄せたオープンソースのコンポーネントと、Nette のためのそのほか多くのアドオンがあります。

さらに深く

この章でここまで扱った内容で、たいていの用途には十分でしょう。以降の節は、プレゼンターをもっと深く知りたい、何もかも知りたいという方のためのものです。

パラメータの検証

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

URL から受け取ったパラメータを決して盲信しないでください。ユーザーに簡単に書き換えられます。たとえば言語 $this->lang が対応しているものの中にあるかを、次のように確かめます。そのための適切な方法が、先ほどの loadState() メソッドの上書きです。

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang;

	public function loadState(array $params): void
	{
		parent::loadState($params); // ここで $this->lang が設定されます
		// 続いて独自の値のチェック:
		if (!in_array($this->lang, ['en', 'cs'])) {
			$this->error();
		}
	}
}

リクエストの保存と復元

プレゼンターが処理するリクエストは Nette\Application\Requestオブジェクトで、プレゼンターの getRequest() メソッドが返します。

現在のリクエストはセッションに保存でき、逆にそこから復元してプレゼンターにもう一度実行させられます。これは、たとえばユーザーがフォームに入力している最中にログインのセッションが切れたときに役立ちます。データを失わないよう、ログインページにリダイレクトする前に $reqId = $this->storeRequest() で現在のリクエストをセッションに保存します。これは短い文字列の識別子を返すので、それをパラメータとしてログインのプレゼンターに渡します。

ログイン後に $this->restoreRequest($reqId) メソッドを呼ぶと、セッションからリクエストを取り出します。POST のリクエストはそこへ forward され、それ以外(GET)はリクエストの URL にリダイレクトされます。このメソッドは、そのリクエストがいまログインしているのと同じユーザーによって作られたかを確認します。別のユーザーがログインした場合やキーが正しくない場合は何もせず、プログラムはいつもどおり続きます。

ガイド前のページに戻るにはをご覧ください。

正規化

プレゼンターには、SEO(検索エンジン最適化)の向上に寄与する本当に優れた機能があります。異なる URL に同じ内容が存在するのを自動的に防ぐのです。たとえば /index/index?page=1 のように複数の URL が特定の行き先に通じている場合、フレームワークはそのひとつを主要(正規)なものと定め、ほかを HTTP コード 301 でそこへリダイレクトします。おかげで検索エンジンがページを二重に登録して、そのページランクを薄めることがなくなります。

この過程を正規化と呼びます。正規の URL はルーターが生成するもので、ふつうはコレクションの中で最初に一致するルートのものです。

正規化は既定で有効で、$this->autoCanonicalize = false で無効にできます。

AJAX や POST のリクエストではリダイレクトは起こりません。データを失いかねませんし、SEO 上の利点もないからです。

canonicalize() メソッドを使えば、正規化を手動で起こすこともできます。link() メソッドと同じように、プレゼンター、アクション、パラメータを渡します。リンクを生成して現在の URL アドレスと比べ、違っていれば生成したリンクへリダイレクトします。

public function actionShow(int $id, ?string $slug = null): void
{
	$realSlug = $this->facade->getSlugForId($id);
	// $slug が $realSlug と違えばリダイレクトします
	$this->canonicalize('Product:show', [$id, $realSlug]);
}

ルートのフィルタと canonicalize() を組み合わせて SEO に強い URL を作る完全なパターンは、スラッグを使った読みやすい URLをご覧ください。

レスポンス

プレゼンターが返すレスポンスは、Nette\Application\Responseインターフェースを実装したオブジェクトです。あらかじめ用意されたレスポンスがいくつかあります。

レスポンスは sendResponse() メソッドで送ります。

use Nette\Application\Responses;

// 素のテキスト
$this->sendResponse(new Responses\TextResponse('Hello Nette!'));

// ファイルを送ります
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));

// コールバックを送ります
$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) {
	if ($httpResponse->getHeader('Content-Type') === 'text/html') {
		echo '<h1>Hello</h1>';
	}
};
$this->sendResponse(new Responses\CallbackResponse($callback));

独自のレスポンスを書くこともできます。Nette\Application\Response インターフェースを実装するだけです。このインターフェースには、HTTP のリクエストとレスポンスを受け取る send() メソッドがひとつだけあります。たとえばメモリに保持したくないデータをストリームで送るときに役立ちます。

class CsvResponse implements Nette\Application\Response
{
	public function __construct(
		private string $fileName,
		private iterable $rows,
	) {
	}

	public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void
	{
		$response->setContentType('text/csv', 'utf-8');
		$response->sendAsFile($this->fileName);

		$handle = fopen('php://output', 'w');
		foreach ($this->rows as $row) {
			fputcsv($handle, $row);
		}

		fclose($handle);
	}
}

あとはプレゼンターでいつもどおり送ります: $this->sendResponse(new CsvResponse('export.csv', $rows));

HTTP キャッシュ

lastModified() メソッドを使うと、HTTP のキャッシュを簡単に活用できます。内容が最後に変更された日時(タイムスタンプ、文字列、DateTimeInterface オブジェクト)を渡し、必要なら ETag の検証子(内容の現在の版を表す短い文字列。そのハッシュなど)と有効期限も渡します。ブラウザがすでに一致する版を持っていれば、プレゼンターは 304 Not Modified のレスポンスを送って終わるので、ページが無駄に描かれたり転送されたりしません。

public function renderArticle(int $id): void
{
	$article = $this->articles->getById($id);
	$this->lastModified($article->updatedAt);
	// ...
}

テンプレートの仕上げ

プレゼンターがテンプレートを描くとき、sendTemplate() メソッドは描画の直前に completeTemplate() を呼びます。このメソッドは #[TemplateVariable] アトリビュートで印の付いた変数を埋め、テンプレートのファイルを探します(既定の変数は、テンプレートが作られるときに TemplateFactory がすでに設定しています)。この protected のメソッドを上書きすれば、すべてのビューで共通の変数を足したり、別のファイルを指定したりできます。

protected function completeTemplate(Nette\Application\UI\Template $template): void
{
	parent::completeTemplate($template);
	$template->siteName = 'My App';
}

#[Requires] によるアクセスの制限

#[Requires] アトリビュートは、プレゼンターとそのメソッドへのアクセスを制限する高度な選択肢を提供します。HTTP のメソッドを指定する、AJAX のリクエストを要求する、同一オリジンに限る、forward 経由のアクセスだけを許すといったことができます。このアトリビュートは、プレゼンターのクラスにも、action<Action>()render<View>()handle<Signal>()createComponent<Name>() といった個々のメソッドにも付けられます。

次の制限を指定できます。

  • HTTP のメソッドについて: #[Requires(methods: ['GET', 'POST'])]
  • AJAX のリクエストを要求する: #[Requires(ajax: true)]
  • 同一オリジンからのアクセスだけ: #[Requires(sameOrigin: true)]
  • forward 経由のアクセスだけ: #[Requires(forward: true)]
  • 特定のアクションへの制限: #[Requires(actions: 'default')]

バージョン 3.3 以降、同一オリジンの判定はブラウザの Sec-Fetch-Site ヘッダーで行われます(以前は SameSite の cookie 経由でした)。こちらのほうが確実で、スキーム、ドメイン、ポートの厳密な一致を確認します。

詳しくはガイド Requires アトリビュートの使い方をご覧ください。

HTTP メソッドのチェック

Nette のプレゼンターは、主に安全のために、届いたすべてのリクエストの HTTP メソッドを自動的に確認します。既定では GETPOSTHEADPUTDELETEPATCH のメソッドが許されます。

たとえば OPTIONS メソッドも追加で許したい場合は、#[Requires] アトリビュートを使います(Nette Application v3.2.3 以降)。

#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

バージョン 3.1.13 以降、確認は checkHttpMethod() で行われ、リクエストで指定されたメソッドが $presenter->allowedMethods 配列に含まれるかを調べます。バージョン 3.2.3 以降、この方法は非推奨で、#[Requires] が推奨されます。このメソッドは次のように上書きできます。

class MyPresenter extends Nette\Application\UI\Presenter
{
	protected function checkHttpMethod(): void
	{
		$this->allowedMethods[] = 'OPTIONS';
		parent::checkHttpMethod();
	}
}

強調しておくべきなのは、OPTIONS メソッドを有効にしたら、プレゼンターの中でそれを適切に扱わなければならない、ということです。このメソッドはいわゆるプリフライトのリクエストとしてよく使われ、CORS(Cross-Origin Resource Sharing)のポリシーに照らしてリクエストが許されるかを判断する必要があるとき、ブラウザが実際のリクエストの前に自動的に送ります。メソッドを有効にしながら正しい応答を実装しないと、食い違いや潜在的なセキュリティの問題につながりかねません。

非推奨のアクションへの印

#[Deprecated] アトリビュートは、アクション、シグナル、あるいはプレゼンター全体を非推奨で将来削除予定と印を付けます。アプリケーションの非推奨の部分へのリンクを生成すると、Nette は警告を出して開発者に知らせます。

このアトリビュートは、プレゼンターのクラス全体にも、個々の action<Action>()render<View>()handle<Signal>() メソッドにも付けられます。

関連情報

バージョン: 4.x