Presenter

NetteでPresenterとテンプレートを作成する方法について学びます。読み終えた後、あなたは知っているでしょう:

  • Presenterがどのように機能するか
  • パーシステントパラメータとは何か
  • テンプレートがどのようにレンダリングされるか

すでに知っているように、PresenterはWebアプリケーションの特定のページ(ホームページ、eコマースの製品、ログインフォーム、サイトマップフィードなど)を表すクラスです。アプリケーションは1つから数千のPresenterを持つことができます。他のフレームワークでは、コントローラーとも呼ばれます。

通常、Presenterという用語は、Webインターフェースの生成に適したNette\Application\UI\Presenterクラスの子孫を意味し、この章の残りの部分で扱います。一般的な意味では、PresenterはNette\Application\IPresenterインターフェースを実装する任意のオブジェクトです。

Presenterのライフサイクル

Presenterのタスクは、リクエストを処理し、応答(HTMLページ、画像、リダイレクトなど)を返すことです。

したがって、最初にリクエストが渡されます。これは直接のHTTPリクエストではなく、ルーターの助けを借りてHTTPリクエストが変換されたNette\Application\Requestオブジェクトです。Presenterはリクエストの処理をこれから示す他のメソッドに賢く委任するため、通常はこのオブジェクトに直接触れることはありません。

Presenterのライフサイクル

この図は、存在する場合に上から下に順に呼び出されるメソッドのリストを表しています。これらのメソッドのいずれも存在する必要はなく、単一のメソッドを持たない完全に空のPresenterを持ち、それに基づいて単純な静的Webサイトを構築できます。

__construct()

コンストラクタは、オブジェクト作成時に呼び出されるため、Presenterのライフサイクルには完全には属しません。しかし、その重要性のために記載しています。コンストラクタ(injectメソッドとともに)は、依存関係を渡すために使用されます。

Presenterは、アプリケーションのビジネスロジックを処理したり、データベースへの書き込みや読み取りを行ったり、計算を実行したりすべきではありません。これらは、モデルと呼ばれるレイヤーのクラスの仕事です。例えば、ArticleRepository クラスは記事の読み込みと保存を担当するかもしれません。Presenterがそれを使用できるようにするには、依存性注入を使用して渡してもらいます。

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>() より前に呼び出されるため、そこで将来のイベントの流れを変更できることです。つまり、レンダリングされるテンプレートと呼び出される render<View>() メソッドを setView('jineView') を使用して変更できます。

メソッドにはリクエストからのパラメータが渡されます。パラメータに型を指定することが可能であり、推奨されます。例えば actionShow(int $id, ?string $slug = null) – パラメータ id が欠落している場合、または整数でない場合、Presenterは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()

Presenterのライフサイクルの最後に呼び出されます。

先に進む前に、良いアドバイス。ご覧のとおり、Presenterは複数のアクション/ビューを処理できます。つまり、複数の render<View>() メソッドを持つことができます。ただし、1つまたはできるだけ少ないアクションを持つPresenterを設計することをお勧めします。

応答の送信

Presenterの応答は通常、HTMLページを含むテンプレートのレンダリングですが、ファイルの送信、JSON、または別のページへのリダイレクトなども可能です。

ライフサイクルのいつでも、次のいずれかのメソッドを使用して応答を送信し、同時にPresenterを終了できます。

これらのメソッドのいずれも呼び出さない場合、Presenterは自動的にテンプレートのレンダリングに進みます。なぜですか?なぜなら、99%の場合、テンプレートをレンダリングしたいので、Presenterはこの動作をデフォルトと見なし、作業を楽にしたいからです。

リンクの作成

Presenterには link() メソッドがあり、これを使用して他のPresenterへのURLリンクを作成できます。最初のパラメータはターゲットのPresenterとアクションであり、その後に渡される引数が続きます。引数は配列として指定できます。

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

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

テンプレートでは、他のPresenterとアクションへのリンクは次のように作成されます。

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

単に実際のURLの代わりに、既知の Presenter:action ペアを記述し、必要に応じてパラメータを指定します。トリックは n:href にあり、これはこの属性がLatteによって処理され、実際のURLが生成されることを示します。Netteでは、URLについて考える必要はまったくなく、Presenterとアクションについて考えるだけです。

詳細については、URLリンクの作成の章を参照してください。

リダイレクト

別のPresenterに移動するには、redirect() および forward() メソッドを使用します。これらはlink()メソッドと非常によく似た構文を持っています。

forward() メソッドは、HTTPリダイレクトなしで即座に新しいPresenterに移動します。

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

HTTPコード302(または現在のリクエストメソッドがPOSTの場合は303)を持ついわゆる一時的なリダイレクトの例:

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

HTTPコード301を持つ永続的なリダイレクトは、次のように実現できます。

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

アプリケーション外の別のURLにリダイレクトするには、redirectUrl() メソッドを使用します。2番目のパラメータとしてHTTPコードを指定できます。デフォルトは302(または現在のリクエストメソッドがPOSTの場合は303)です。

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

リダイレクトは、いわゆるサイレント終了例外 Nette\Application\AbortException をスローすることで、Presenterの動作を即座に終了します。

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

フラッシュメッセージ

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

flashMessage() メソッドを呼び出すだけで、Presenterがテンプレートへの受け渡しを処理します。最初のパラメータはメッセージのテキストであり、オプションの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 = null, 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 は制御をエラーPresenterに渡します。これは、発生したエラーを通知するページを表示するタスクを持つPresenterです。 エラーPresenterの設定は、application設定で行われます。

JSONの送信

JSON形式でデータを送信し、Presenterを終了するアクションメソッドの例:

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

リクエストパラメータ

Presenterおよび各コンポーネントは、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でパーシステントパラメータを作成するのは非常に簡単です。パブリックプロパティを作成し、属性でマークするだけです。(以前は /** @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)を指定することをお勧めします。また、デフォルト値を指定することもできます。パラメータ値は検証できます。

パーシステントパラメータは、通常、特定のPresenterのすべてのアクション間で転送されます。複数のPresenter間で転送するには、次のいずれかで定義する必要があります。

  • Presenterが継承する共通の祖先で
  • Presenterが使用するトレイトで:
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>

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

Presenterには組み込みのコンポーネントシステムがあります。コンポーネントは、Presenterに挿入する独立した再利用可能なユニットです。フォーム、データグリッド、メニューなど、繰り返し使用する意味のあるものであれば何でもかまいません。

コンポーネントがどのようにPresenterに挿入され、その後使用されるのでしょうか?それはコンポーネントの章で学びます。ハリウッドと共通点があることさえ発見するでしょう。

そして、どこでコンポーネントを入手できますか?Componetteページでは、オープンソースコンポーネントや、フレームワーク周辺のコミュニティのボランティアによってここに配置されたNette用の他の多くのアドオンを見つけることができます。

深く掘り下げる

この章でこれまで見てきたことで、おそらく完全に十分でしょう。以下の行は、Presenterについて深く掘り下げ、すべてを知りたい人のためのものです。

パラメータの検証

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();
		}
	}
}

リクエストの保存と復元

Presenterが処理するリクエストはNette\Application\Requestオブジェクトであり、Presenterの getRequest() メソッドによって返されます。

現在のリクエストはセッションに保存したり、逆にそこから復元してPresenterに再度実行させたりすることができます。これは、例えばユーザーがフォームに入力していてログインが期限切れになった場合に便利です。データを失わないように、ログインページにリダイレクトする前に現在のリクエストを $reqId = $this->storeRequest() を使用してセッションに保存します。これは短い文字列の形式でその識別子を返し、それをログインPresenterにパラメータとして渡します。

ログイン後、$this->restoreRequest($reqId) メソッドを呼び出します。これはセッションからリクエストを取得し、それにフォワードします。メソッドは、リクエストが現在ログインしているユーザーと同じユーザーによって作成されたことを検証します。別のユーザーがログインした場合、またはキーが無効な場合、何もしませんでプログラムは続行します。

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

カノニカル化

Presenterには、より良いSEO(インターネットでの検索エンジンの最適化)に貢献する本当に素晴らしい機能が1つあります。異なるURLで重複コンテンツが存在するのを自動的に防ぎます。特定のターゲットに複数のURLアドレス(例:/index/index?page=1)がある場合、フレームワークはそのうちの1つをプライマリ(カノニカル)として決定し、HTTPコード301を使用して他のアドレスをそれにリダイレクトします。これにより、検索エンジンはページを2回インデックス付けせず、ページランクを希釈しません。

このプロセスはカノニカル化と呼ばれます。カノニカルURLは、ルーターによって生成されるURLであり、通常はコレクション内の最初の一致するルートです。

カノニカル化はデフォルトで有効になっており、$this->autoCanonicalize = false を介して無効にできます。

AJAXまたはPOSTリクエストの場合、データが失われたり、SEOの観点から付加価値がなかったりするため、リダイレクトは発生しません。

カノニカル化は、canonicalize() メソッドを使用して手動で呼び出すこともできます。このメソッドには、link() メソッドと同様に、Presenter、アクション、およびパラメータが渡されます。リンクを作成し、現在のURLアドレスと比較します。異なる場合は、生成されたリンクにリダイレクトします。

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

イベント

Presenterのライフサイクルの一部として呼び出される startup()beforeRender()shutdown() メソッドに加えて、自動的に呼び出されるように他の関数を定義することもできます。Presenterはいわゆるイベントを定義し、そのハンドラを $onStartup$onRender$onShutdown 配列に追加します。

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

$onStartup 配列のハンドラは startup() メソッドの直前に呼び出され、次に $onRenderbeforeRender()render<View>() の間に呼び出され、最後に $onShutdownshutdown() の直前に呼び出されます。

応答

Presenterが返す応答は、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));

#[Requires] を使用したアクセス制限

#[Requires] 属性は、Presenterとそのメソッドへのアクセスを制限するための高度なオプションを提供します。HTTPメソッドの指定、AJAXリクエストの要求、同一オリジン(same origin)への制限、およびフォワーディング経由のアクセスのみに使用できます。属性は、Presenterクラスと個々の action<Action>()render<View>()handle<Signal>()createComponent<Name>() メソッドの両方に適用できます。

これらの制限を指定できます。

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

詳細については、Requires属性の使用方法のガイドをご覧ください。

HTTPメソッドのチェック

NetteのPresenterは、各受信リクエストのHTTPメソッドを自動的に検証します。このチェックの理由は主にセキュリティです。標準では、GETPOSTHEADPUTDELETEPATCH メソッドが許可されています。

例えば OPTIONS メソッドを追加で許可したい場合は、#[Requires] 属性を使用します(Nette Application v3.2以降)。

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

バージョン3.1では、検証は checkHttpMethod() で行われます。これは、リクエストで指定されたメソッドが $presenter->allowedMethods 配列に含まれているかどうかを判断します。メソッドを追加するには、次のようにします。

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

OPTIONS メソッドを許可する場合、その後Presenter内で適切に処理する必要があることを強調することが重要です。このメソッドは、いわゆるプリフライトリクエストとしてよく使用されます。これは、CORS(Cross-Origin Resource Sharing)ポリシーの観点からリクエストが許可されているかどうかを判断する必要がある場合に、ブラウザが実際のリクエストの前に自動的に送信します。メソッドを許可しても正しい応答を実装しない場合、不整合や潜在的なセキュリティ問題につながる可能性があります。

その他の読み物