symfony book 日本語ドキュメント

第7章 - ビューレイヤーの内側

ビュー(view)は特定のアクションと関連づけされた出力をレンダリングすることを引き受けます。symfonyにおいて、ビューはいくつかの部分から構成され、それぞれの部分はとり組む人達が簡単に修正できるように設計されています。

しかしながら、あなたの役割が何であれ、アクションの結果を表現する退屈な作業を速くする便利なツールを見ることになります。この章ではこれらすべてのツールをカバーします。

テンプレートを利用する

リスト7-1はsymfonyの典型的なテンプレートを示します。HTMLコードと基本的なPHPコード、通常はアクション($this->name = 'foo';)とヘルパーで定義された変数への呼び出しを含みます。

リスト7-1 - サンプルのindexSuccess.phpテンプレート

[php]
<h1>ようこそ</h1>
<p>お帰りなさい、<?php echo $name ?>!</p>
<ul>何をなさりたいですか?
  <li><?php echo link_to('最新の記事を読む', 'article/read') ?></li>
  <li><?php echo link_to('新しい記事を書き始める', 'article/write') ?></li>
</ul>

4章で説明したように、テンプレートのなかではPHP開発者ではない人が読みやすいようにPHPの代替構文が望ましいです。テンプレートのなかではPHPコードは最小限に保つべきです。これらのファイルはアプリケーションのGUIを設計するために使われ、時々、別のチームによって作成とメンテナンスが行われ、アプリケーションのロジックではなくプレゼンテーションに特化されているからです。ロジックをアクション内部に保つことで、コードを重複せずに、1つのアクションを共有する複数のテンプレートを持つことが簡単になります。

ヘルパー

ヘルパーはPHP関数でHTMLコードを返し、テンプレートのなかで使うことができます。リスト7-1においてlink_to()関数はヘルパーです。時々、テンプレート内でよく使われるコードのスニペット(断片)をまとめることで、ヘルパーは時間を節約します。たとえば、つぎのヘルパーのための関数定義は簡単に想像がつきます。

[php]
<?php echo input_tag('nickname') ?>
 => <input type="text" name="nickname" id="nickname" value="" />

上記のコードはリスト7-2のようになります。

リスト7-2 - ヘルパー定義のサンプル

[php]
function input_tag($name, $value = null)
{
  return '<input type="text" name="'.$name.'" id="'.$name.'"value="'.$value.'" />';
}

実際のところ、symfonyに組み込まれているinput_tag()関数は、ほかの属性を<input>タグに追加する3番目のパラメーターを受けとるので、上記のコードよりも少々複雑です。完全な構文とオプションはAPIドキュメント(http://www.symfony-project.org/api/1_0/)で確認できます。

たいていの場合、ヘルパーは賢いので長くて複雑なコードを書かずにすみます。

[php]
<?php echo auto_link_text('我々のサイトにお越しください www.example.com') ?>
 => 我々のサイトにお越しください <a href="http://www.example.com">www.example.com</a>

ヘルパーはテンプレートを書く作業工程を円滑にし、パフォーマンスとアクセシビリティの観点から最高のHTMLコードを生み出します。つねに無地のHTMLを使うことができますが、ヘルパーのほうがより速く書けます。

TIP なぜヘルパーがsymfony内部のすべての場所で使われているcamelCase(キャメルケース)の規約ではなくアンダースコアの構文にしたがって命名されるのか疑問に思っていらっしゃるかもしれません。これはヘルパーが関数であり、PHPのすべてのコア関数がアンダースコアの構文の規約を利用するからです。

ヘルパーを宣言する

ヘルパーの定義を含むsymfonyのファイルはオートロードされません(これらがクラスではなく関数を含むからです)。ヘルパーは目的によって分類されます。たとえば、テキストを扱うすべてのヘルパー関数はTextHelper.phpという名前のファイルで定義され、Textヘルパーグループと呼ばれます。ですのでヘルパーをテンプレートのなかで使う必要がある場合、use_helper()関数でヘルパーを宣言することで最初の段階で関連のヘルパーグループをロードしなければなりません。リスト7-3は、Textヘルパーグループの一部である、auto_link_text()ヘルパーを使うテンプレートを示しています。

リスト7-3 - ヘルパーを使うことを宣言する

[php]
// このテンプレート内で特定のヘルパーグループを使う
<?php use_helper('Text') ?>
...
<h1>説明</h1>
<p><?php echo auto_link_text($description) ?></p>

TIP 複数のヘルパーグループを宣言する必要がある場合、より多くの引数をuse_helper()呼び出しに追加してください。たとえば、TextJavascriptヘルパーグループの両方をテンプレートにロードするには、<?php use_helper('Text', 'Javascript') ?>を呼び出します。

いくつかのヘルパーは、すべてのテンプレートのなかで、宣言を行わずにデフォルトで利用できます。ヘルパーグループのヘルパーはつぎのようなものがあります:

すべてのテンプレートのためにデフォルトでロードされる、標準的なヘルパーのリストはsettings.ymlファイルのなかで設定できます。Cacheグループのヘルパーを使わない、もしくはTextグループのヘルパーをつねに使うことがわかっている場合、standard_helpers設定をそれぞれ変更します。これによってアプリケーションの動作を少し加速します。前のリストにある最初の4つのヘルパーグループ(HelperTagUrlAsset)は削除できません。なぜなら、テンプレートエンジンが適切に動作するために必須だからです。結果として、標準ヘルパーのリストにも表示されません。

TIP テンプレートの外部でヘルパーを使う必要がある場合、sfLoader::loadHelpers($helpers)を呼び出すことでどこからでもヘルパーグループをロードすることができます。$helpersはヘルパーグループの名前もしくはヘルパーグループ名の配列です。たとえば、アクション内でauto_link_text()を使いたい場合、sfLoader::loadHelpers('Text')を最初に呼び出す必要があります。

よく使われるヘルパー

ヘルパーが助けしてくれる機能に関連する、いくつかのヘルパーに関する詳細な内容は後の章で学ぶことになります。リスト7-4では、ヘルパーが返すHTMLコードと一緒に、よく使われるデフォルトのヘルパーの手短な一覧を示しています。

リスト7-4 - デフォルトの共通ヘルパー

[php]
// Helperグループ
<?php use_helper('HelperName') ?>
<?php use_helper('HelperName1', 'HelperName2', 'HelperName3') ?>

// Tagグループ
<?php echo tag('input', array('name' => 'foo', 'type' => 'text')) ?>
<?php echo tag('input', 'name=foo type=text') ?>  // 代替のオプション構文
 => <input name="foo" type="text" />
<?php echo content_tag('textarea', 'ダミーの内容', 'name=foo') ?>
 => <textarea name="foo">ダミーの内容</textarea>

// Urlグループ
<?php echo link_to('クリックしてください', 'mymodule/myaction') ?>
=> <a href="/route/to/myaction">クリックしてください</a>  // ルーティングの設定による

// Assetグループ
<?php echo image_tag('myimage', 'alt=foo size=200x100') ?>
 => <img src="/images/myimage.png" alt="foo" width="200" height="100"/>
<?php echo javascript_include_tag('myscript') ?>
 => <script language="JavaScript" type="text/javascript" src="/js/myscript.js"></script>
<?php echo stylesheet_tag('style') ?>
 => <link href="/stylesheets/style.css" media="screen" rel="stylesheet"type="text/css" />

symfonyにはほかの多くのヘルパーが存在し、それらすべてを説明するには一冊の本が必要になります。ヘルパーの最良のリファレンスはオンラインのAPIドキュメント(http://www.symfony-project.org/api/1_0/)です。そこですべてのヘルパーの構文、オプションと例について十分な説明があります。

独自のヘルパーを追加する

symfonyはさまざまな目的のために多くのヘルパーを搭載していますが、APIドキュメントで必要なものが見つからない場合、新しいヘルパーを作りたいと思うでしょう。これはとても簡単に行うことができます。

ヘルパー関数(HTMLコードを返す通常のPHP関数)はFooBarHelper.phpという名前のファイルに保存されます。FooBarはヘルパーグループの名前です。ファイルをapps/myapp/lib/helper/ディレクトリ(もしくはプロジェクトのlib/フォルダーの1つのもとで作られたhelper/ディレクトリ)に保存すればインクルードするさいにuse_helper('FooBar')ヘルパーによって自動的に見つかります。

TIP このシステムによってsymfonyの既存のヘルパーをオーバーライドできます。たとえば、Textヘルパーグループのすべてのヘルパーを再定義するために、 TextHelper.phpファイルをapps/mypp/lib/helper/ディレクトリのなかに作ります。use_helper('Text')を呼び出すたびに、symfonyは固有のヘルパーよりあなたのヘルパーグループを使います。しかしつぎのことに気をつけてください: オリジナルのファイルがロードされない場合、これをオーバーライドするためにすべてのヘルパーグループの関数を再定義しなければなりません; さもなければ、いくつかのオリジナルのヘルパーがまったく利用できなくなります。

ページのレイアウト

リスト7-1で示されているテンプレートは有効なXHTMLのドキュメントではありません。DOCTYPEの定義と<html><body>タグは見つかりません。これらは、アプリケーションのほかの場所に存在する、ページのレイアウトを含むlayout.phpファイルに保存されるからです。このファイルは、グローバルレイアウト(global template)とも呼ばれますが、すべてのテンプレートのなかで繰り返しを避けるためにアプリケーションのすべてのページに共通なHTMLコードを保存します。テンプレートの内容は、レイアウトに統合されるか、考えを変える場合、レイアウトはテンプレートを"デコレート"(装飾)します。図7-1で説明されるように、これはDecoratorデザインパターンのアプリケーションです。

TIP Decoratorとほかのデザインパターンについて詳しい情報はMartin Fowler(マーチン・ファウラー)が執筆したPatterns of Enterprise Application Architecture(Addison-Wesley、ISBN: 0-32112-742-0)をご覧ください(邦訳は「エンタープライズアプリケーションアーキテクチャパターン」翔泳社、2005年 ISBN 4798105538)。

図7-1 - レイアウト内でテンプレートをデコレートする

レイアウト内でテンプレートをデコレートする

リスト7-5は、アプリケーションのtemplates/ディレクトリのなかに設置された、デフォルトのページレイアウトを示しています。

リスト7-5 - デフォルトのレイアウト(myproject/apps/myapp/templates/layout.php)

[php]
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/2000/REC-xhtml1-20000126/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <?php echo include_http_metas() ?>
  <?php echo include_metas() ?>
  <?php echo include_title() ?>
  <link rel="shortcut icon" href="/favicon.ico" />
</head>
<body>

<?php echo $sf_data->getRaw('sf_content') ?>

</body>
</html>

<head>セクションに呼び出されたヘルパーはレスポンスオブジェクトとビューの設定から情報を取得します。<body>タグはテンプレートの結果を出力します。このレイアウトによって、デフォルトの設定とリスト7-1のサンプルのテンプレート、と処理されたビューはリスト7-6のようになります。

リスト7-6 - 組み立てられたレイアウト、ビューの設定、とテンプレート

[php]
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/2000/REC-xhtml1-20000126/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <meta name="title" content="symfony project" />
  <meta name="robots" content="index, follow" />
  <meta name="description" content="symfony project" />
  <meta name="keywords" content="symfony, project" />
  <title>symfonyのプロジェクト</title>
  <link rel="stylesheet" type="text/css" href="/css/main.css" />
  <link rel="shortcut icon" href="/favicon.ico">
</head>
<body>

<h1>ようこそ</h1>
<p>おかえり、<?php echo $name ?>!</p>
<ul>何をなさいますか?
  <li><?php echo link_to('最近の記事を読む', 'article/read') ?></li>
  <li><?php echo link_to('新しい記事を書き始める', 'article/write') ?></li>
</ul>

</body>
</html>

それぞれのアプリケーションに対してグローバルテンプレートを完全にカスタマイズできます。必要なHTMLコードを追加してください。このレイアウトはサイトのナビゲーション、ロゴなどを保持するためによく使われます。複数のレイアウトを保有することが可能で、それぞれのアクションに対して使われるレイアウトを選ぶことができます。今のところはJavaScriptとスタイルシートのインクルージョンは気にしないでください; この章の"ビューのコンフィギュレーション"のセクションで扱う方法を説明します。

テンプレートのショートカット

テンプレートにおいて、いくつかのsymfonyの変数はつねに利用可能です。これらのショートカットは、symfonyのコアオブジェクトを通して、テンプレート内でもっとも共通に必要とされる情報にアクセスできます:

以前の章でsfRequestsfUserオブジェクトの便利なメソッドを詳細に説明しました。$sf_request$sf_user変数を通してテンプレートのこれらのメソッドを実際に呼び出すことができます。たとえば、リクエストがtotalパラメーターを含む場合、その変数はつぎのようにテンプレート内で利用できます:

[php]
// 長いバージョン
<?php echo $sf_request->getParameter('total'); ?>

// 短いバージョン
<?php echo $sf_params->get('total'); ?>

// つぎのアクションコードと同等
echo $this->getRequestParameter('total');

コードのフラグメント

いくつかのページでHTMLもしくはPHPコードを含めることが必要になることがよくあります。そのコードを繰り返すことを避けるには、多くの場合、PHPのinclude()ステートメントで十分です。

たとえば、アプリケーションの多くのテンプレートがコードの同じフラグメントを使うことが必要な場合、グローバルテンプレートのディレクトリ(myproject/apps/myapp/templates/)のなかに存在する、myFragment.phpという名前のファイルにフラグメントを保存し、つぎのようにそのファイルをテンプレートにインクルードしてください:

[php]
<?php include(sfConfig::get('sf_app_template_dir').'/myFragment.php') ?>

しかしこれはフラグメントをまとめるにはあまりきれいな方法ではありません。たいていの場合、フラグメントとそれを含むさまざまなテンプレートのあいだで異なる変数名を持つ可能性があるからです。加えて、symfonyのキャッシュシステム(12章で説明)はインクルードを検出する方法を持たないので、コードのフラグメントはテンプレートから独立してキャッシュされません。symfonyはincludeを置き換えるために3つのタイプの代わりになる賢いコードのフラグメントを提供します;

NOTE コンポーネントスロット(component slot)と呼ばれる、別のコードのフラグメントのタイプはフラグメントの性質が文脈に依存するときに使われます(たとえば、コードのフラグメントが任意のモジュールのアクションに対して異なることが必要な場合)。コンポーネントスロットはこの章の後のほうで説明します。

これらのコードのフラグメントのインクルードはPartialグループのヘルパーによって実現されます。これらのヘルパーは、初期化の宣言を行わずに、symfonyのどのテンプレートからも利用できます。

部分テンプレート

部分テンプレート(partial)は再利用可能なテンプレートコードの塊(チャンク)です。たとえば、情報公開を行うアプリケーションにおいて、記事を表示するテンプレートのコードは記事の詳細な内容のページに使われ、もっとも人気のある記事と最新の記事の一覧にも使われます。図7-2で示されるように、このコードは部分テンプレートのための完璧な候補です。

図7-2 - テンプレート内で部分テンプレートを再利用する

テンプレート内で部分テンプレートを再利用する

テンプレートのように、部分テンプレートはtemplates/ディレクトリに設置されたファイルで、これらはPHPが埋め込まれたHTMLコードを含みます。部分テンプレートのファイルの名前はつねにアンダースコア(_)で始まります。テンプレートが同じtemplatesフォルダーに設置されているので、これは部分テンプレートとテンプレートを区別するのに役立ちます。

部分テンプレートが同じモジュール、別のモジュール、もしくはグローバルなtemplates/ディレクトリにあったとしても、テンプレートは部分テンプレートをインクルードできます。リスト7-7に書かれているように、include_partial()ヘルパーを利用して部分テンプレートをインクルードして、モジュールと部分テンプレート名をパラメーターとして指定します(ただし先頭のアンダースコアと末尾の.phpを省略します)。

リスト7-7 - 部分テンプレートをmymoduleモジュールのテンプレートのなかに含める

[php]
// myapp/modules/mymodule/templates/_mypartial1.php部分テンプレートをインクルードする
// テンプレートと部分テンプレートは同じモジュールにあるので、
// モジュール名を省略できる
<?php include_partial('mypartial1') ?>

// myapp/modules/foobar/templates/_mypartial2.php部分テンプレートをインクルードする
// この場合モジュール名は必須
<?php include_partial('foobar/mypartial2') ?>

// myapp/templates/_mypartial3.php部分テンプレートをインクルードする
// 'global'モジュールの一部として見なされる
<?php include_partial('global/mypartial3') ?>

部分テンプレートはsymfonyの通常のヘルパーとテンプレートのショートカットにアクセスできます。しかし、部分テンプレートはアプリケーションのどこからでも呼び出すことができるので、部分テンプレートをインクルードするテンプレートを呼び出すアクションで定義された変数が明示的に引数として渡されないかぎり、部分テンプレートは変数に自動的にアクセスできません。たとえば、$total変数にアクセスできる部分テンプレートが欲しい場合、リスト7-8、7-9、7-10で示されるように、アクションは変数をテンプレートに渡さなければなりませんし、また、テンプレートは変数をinclude_partial()呼び出しの2番目の引数としてヘルパーに渡さなければなりません。

リスト7-8 - アクションが変数を定義する(mymodule/actions/actions.class.php)

[php]
class mymoduleActions extends sfActions
{
  public function executeIndex()
  {
    $this->total = 100;
  }
}

リスト7-9 - テンプレートが変数を部分テンプレートに渡す(mymodule/templates/indexSuccess.php)

[php]
<p>Hello, world!</p>
<?php include_partial('mypartial',
array('mytotal' => $total)
) ?>

リスト7-10 - 部分テンプレートは変数を利用できる(mymodule/templates/_mypartial.php)

[php]
<p>合計: <?php echo $mytotal ?></p>

TIP すべてのヘルパーはこれまで<?php echo functionName() ?>によって呼び出されてきました。通常のPHPのinclude()ステートメントと似たようなふるまいをするように、部分テンプレートのヘルパーは、echoすることなく、<?php include_partial() ?>によって簡単に呼び出されます。実際に表示しないで部分テンプレートの内容を返す関数が必要な場合、代わりにget_partial()を使います。この章で説明されたすべてのinclude_ヘルパーはechoステートメントで一緒に呼び出すことができるget_で始まる対のヘルパーを持ちます。

コンポーネント

2章において、最初のサンプルスクリプトはプレゼンテーションからロジックを分離するために2つの部分に分割されました。MVCパターンがアクションとテンプレートに適用されるように、部分テンプレートをロジックとプレゼンテーションの部分に分割する必要があるかもしれません。その場合、コンポーネント(component)を使うべきです。

コンポーネントは、はるかに速く動くこと以外は、アクションと似ています。コンポーネントのロジックは、action/components.class.phpファイルに設置されたsfComponentsから継承したクラスに保存されます。そのプレゼンテーションは部分テンプレートに保存されます。sfComponentsクラスのメソッドは、アクションと同じように、executeという単語で始まり、アクションが変数を渡す方法と同じように、変数をプレゼンテーションの対応物に渡すことができます。コンポーネントに対してプレゼンテーションとして役割を果たす部分テンプレートはコンポーネントによって(先頭のexecuteではなく、代わりにアンダースコアで)命名されます。テーブル7-1はアクションとコンポーネントの間の命名規約を比較しています。

テーブル7-1 - アクションとコンポーネントの命名規約

規約 |アクション |コンポーネント ------------------------------------|-------------------|-------------- ロジックのファイル |actions.class.php |components.class.php ロジッククラスの拡張 |sfActions |sfComponents メソッドの命名方法 |executeMyAction() |executeMyComponent() プレゼンテーションのファイルの命名方法|myActionSuccess.php|_myComponent.php

TIP アクションのファイルを分離できるので、sfComponentsクラスは同じタイプの構文で単独のコンポーネントを可能にするsfComponentの対応物を持ちます。

たとえば、任意の題目に対して最新のニュースの見出しを表示するサイドバーを持つことを仮定します。題目はユーザーのプロファイルに依存し、いくつかのページで再利用されます。ニュースの見出しを取得するために必要なクエリは単純な部分テンプレートで表示するには複雑すぎるので、これらをアクションのようなファイル、コンポーネントに移動させる必要があります。図7-3はこの例を図示しています。

リスト7-11と7-12で示された、この例に対して、コンポーネントは独自のモジュール(news)に保持されますが、ビューの機能上の観点から意味がある場合、コンポーネントとアクションを単独のモジュールに混ぜることができます。

図7-3 - テンプレート内でコンポーネントを使う

テンプレート内でコンポーネントを使う

リスト7-11 - コンポーネントクラス(modules/news/actions/components.class.php)

[php]
<?php

class newsComponents extends sfComponents
{
  public function executeHeadlines()
  {
    $c = new Criteria();
    $c->addDescendingOrderByColumn(NewsPeer::PUBLISHED_AT);
    $c->setLimit(5);
    $this->news = NewsPeer::doSelect($c);
  }
}

リスト7-12 - 部分テンプレート(modules/news/templates/_headlines.php)

[php]
<div>
  <h1>最新のニュース</h1>
  <ul>
  <?php foreach($news as $headline): ?>
    <li>
      <?php echo $headline->getPublishedAt() ?>
      <?php echo link_to($headline->getTitle(),'news/show?id='.$headline->getId()) ?>
    </li>
  <?php endforeach ?>
  </ul>
</div>

では、コンポーネントがテンプレートのなかで必要になるたびに、つぎのように呼び出します:

[php]
<?php include_component('news', 'headlines') ?>

部分テンプレートのように、コンポーネントは連想配列の形式で追加パラメーターを受けとります。パラメーターはこれらの名前のもとで部分テンプレート内および$thisオブジェクトを通してコンポーネントのなかで利用できます。例としてリスト7-13をご覧ください。

リスト7-13 - パラメーターをコンポーネントとテンプレートに渡す

[php]
// コンポーネントへの呼び出し
<?php include_component('news', 'headlines', array('foo' => 'bar')) ?>

// コンポーネント自身にて
echo $this->foo;
 => 'bar'

// _headlines.php部分テンプレートにて
echo $foo;
 => 'bar'

通常のテンプレートのように、コンポーネント内部、もしくはグローバルレイアウト内部でコンポーネントをインクルードできます。アクションのように、コンポーネントのexecuteメソッドは変数を関連する部分テンプレートに渡し、同じショートカットにアクセスできます。しかし、似ている点はここまでです。コンポーネントはセキュリティもしくはバリデーションを処理せず、インターネットから呼び出すことはできませんし(呼び出せるのはアプリケーション自身からのみ)、さまざまなものを返すことはできません。それがコンポーネントがアクションよりも実行が速い理由です。

スロット

部分テンプレートとコンポーネントは再利用のために優れたものです。しかし多くの場合、コードのフラグメントは複数の動的な領域を持つレイアウトの要件を満たすことが求められます。たとえば、カスタムタグをレイアウトの<head>セクションに追加することを考えます。レイアウトはアクションの内容に依存します。もしくは、レイアウトが主要な動的な領域を1つ持つことを想定します。その領域はアクションの結果によって内容が満たされます。加えて、レイアウトは別の小さな領域をたくさん持ちます。これらの領域はレイアウトで定義されたデフォルトの内容を持ちますが、テンプレートレベルでオーバーライドできます。

これらの状況に対して、解決方法はスロット(slot)です。基本的には、スロットはビューの要素(レイアウト、テンプレート、もしくは部分テンプレート)に設置できるプレースホルダーです。プレースホルダーの内容を満たすことは変数の設定に似ています。内容を満たすコードはレスポンスにグローバルに保存されるので、どこでもスロットを定義できます(レイアウト、テンプレート、もしくは部分テンプレート)。スロットをインクルードするまえにかならずスロットを定義してください。そしてレイアウトはテンプレートのあとで実行され(これはデコレーションプロセス)、部分テンプレートがテンプレート内部に呼び出されるときに部分テンプレートが実行されることを覚えておいてください。説明が抽象的でわかりにくいですか?例を見てみましょう。

1つのテンプレートと2つのスロットに対して1つの領域を持つレイアウトを想像してください。1つはサイドバー用で他はフッター用です。スロットの値はテンプレート内で定義されます。デコレーションプロセスの間、図7-4に示されるように、レイアウトのコードはテンプレートのコードを包み、スロットは以前定義された値で満たされます。サイドバーとフッターはメインのアクションに対して文脈依存の関係があります。これは複数の"穴"をともなうレイアウトを持つことと似ています。

図7-4 - テンプレートのなかで定義されたレイアウトのスロット

テンプレートのなかで定義されたレイアウトのスロット

いくつかのコードを読むことで理解が進みます。スロットを含めるには、include_slot()ヘルパーを使います。has_slot()ヘルパーはスロットが以前定義されていた場合にtrueを返します。そしておまけとしてフォールバックメカニズムを提供します。たとえば、リスト7-14で示されるように、レイアウトとデフォルトの内容のなかにある'sidebar'に対してプレースホルダーを定義してください。

リスト7-14 - 'sidebar'スロットをレイアウト内部でインクルードする

[php]
<div id="sidebar">
<?php if (has_slot('sidebar')): ?>
  <?php include_slot('sidebar') ?>
<?php else: ?>
  <!-- default sidebar code -->
  <h1>文脈上の領域</h1>
  <p>この領域はページのメインの内容と関連するリンクと情報を含みます。</p>
<?php endif; ?>
</div>

それぞれのテンプレートはスロットの内容を定義する機能を持ちます(実際には、部分テンプレートも可能です)。スロットはHTMLコードを保持することを目的としているので、symfonyはこれらを定義するために便利な方法を提供します: リスト7-15で示されるように、slot()end_slot()ヘルパーの間にスロットのコードを書いてください。

リスト7-15 - テンプレート内部の'sidebar'スロットをオーバーライドする

[php]
...
<?php slot('sidebar') ?>
  <!-- 現在のテンプレートに対するカスタムサイドバーのコード -->
  <h1>ユーザーの詳細</h1>
  <p>名前:  <?php echo $user->getName() ?></p>
  <p>Eメール: <?php echo $user->getEmail() ?></p>
<?php end_slot() ?>

スロットヘルパーに挟まれるコードはテンプレートの文脈内で実行されるので、このコードはアクション内部で定義されたすべての変数にアクセスできます。symfonyはこのコードの結果を自動的にレスポンスオブジェクトに設置します。これはテンプレート内部では表示されませんが、リスト7-14のような、将来のinclude_slot()呼び出しに対して利用できます。

スロットは文脈上の内容を表示することを目的とした領域を定義するためにとても便利です。これらは特定のアクションに対してHTMLコードをレイアウトに追加するために利用することも可能です。たとえば、最新のニュースのリストを表示するテンプレートがレイアウトの<head>部分のなかでリンクをRSSフィードに追加したい場合があります。これはfeedスロットをレイアウトに追加してテンプレートのリストのなかでこのスロットをオーバーライドすることで簡単に実現されます。

SIDEBAR テンプレートのフラグメントが見つかる場所

テンプレートにとり組む人達は通常はWebデザイナーで、symfonyについて詳しくないでしょうし、テンプレートのフラグメントはアプリケーション全体で拡散されているので、見つけることは困難でしょう。これらのいくつかのガイドラインによってsymfonyのテンプレートシステムにとり組む作業が快適になります。

最初に、symfonyのプロジェクトは多くのディレクトリを含みますが、すべてのレイアウト、テンプレート、テンプレートのフラグメントのファイルはtemplates/という名前のディレクトリのなかに存在します。Webデザイナーに関しては、プロジェクトの構造をつぎのように小さくすることができます:

myproject/
  apps/
    application1/
      templates/       # application 1のためのレイアウト
      modules/
        module1/
          templates/   # module 1のためのテンプレートと部分テンプレート
        module2/
          templates/   # module 2のためのテンプレートと部分テンプレート
        module3/
          templates/   # module 3のためのテンプレートと部分テンプレート

ほかのディレクトリはすべて無視されます。

include_partial()に遭遇するとき、Webデザイナーは最初の引数だけが重要であることを理解する必要があります。この引数のパターンはmodule_name/partical_nameで、これはプレゼンテーションのコードがmodules/module_name/templates/_partical_name.php内で見つかることを意味します。

include_component()ヘルパーに対して、モジュールと部分テンプレートの名前は最初の2つの引数です。残りに関しては、ヘルパーが何でありテンプレートでもっとも共通なヘルパーはどれかといった一般的な理解をしていればsymfonyのアプリケーションのためのテンプレートの設計を始めるには十分です。

ビューのコンフィギュレーション

symfonyにおいて、ビューは2つの相異なる部分で構成されます:

ビューのなかにおいて、HTMLではないすべてのものはビューの設定(コンフィギュレーション)と呼ばれ、symfonyはこれを操作するために2つの方法を提供します。通常の方法はview.yml設定ファイルです。このファイルは値がコンテキスト、もしくはデータベースのクエリに依存しないときはつねに使われます。動的な値を設定する必要があるとき、代わりの方法はsfResponseオブジェクトの属性を通してビューの設定をアクション内部で直接設定することです。

NOTE sfResponseオブジェクトとview.ymlファイルの両方を通してビューの設定パラメーターを設定した場合、sfResponseの定義が優先されます。

view.ymlファイル

それぞれのモジュールはビューの設定を定義するview.ymlファイルを1つ持つことができます。これによって単独のファイル内部でモジュール全体とビューごとにビューの設定できます。view.ymlファイルの最初のレベルのキーはモジュールのビューの名前です。リスト7-16はビューのコンフィギュレーションの例を示します。

リスト7-16 - モジュールレベルのview.ymlのサンプル

editSuccess:
  metas:
    title: プロファイルを編集する

editError:
  metas:
    title: プロファイルを編集している間に発生したエラー

all:
  stylesheets: [my_style]
  metas:
    title: 私のWebサイト

CAUTION view.ymlファイルのなかのメインキーはビューの名前で、アクションの名前ではないことに注意してください。復習として、ビューの名前はアクションの名前とアクションのサフィックスによって構成されます。たとえば、editアクションがsfView::SUCCESSを返す場合(もしくはデフォルトアクションの接尾辞なので、何も返さない場合)、ビューの名前はeditSuccessです。

モジュールのためのデフォルトの設定はモジュールのview.ymlall:キーのもとで定義されます。すべてのアプリケーションのビューに対するデフォルトの設定はアプリケーションのview.ymlのなかで定義されます。繰り返しますが、設定カスケードの原則を認識してください。

TIP モジュールレベルのview.ymlファイルはデフォルトでは存在しません。最初に、モジュールのためのビューの設定パラメーターを調整する必要があり、空のview.ymlconfig/ディレクトリのなかに作らなければなりません。

リスト7-5のデフォルトのテンプレートとリスト7-6の最後のレスポンスを見た後に、ヘッダーの値がどこから来るのか疑問に思うかもしれません。実際には、これらはビューのデフォルト設定であり、アプリケーションのview.ymlで定義され、リスト7-17で示されます。

リスト7-17 - アプリケーションレベルのビューのデフォルト設定(apps/myapp/config/view.yml)

default:
  http_metas:
    content-type: text/html

  metas:
    title:        symfony project
    robots:       index, follow
    description:  symfony project
    keywords:     symfony, project
    language:     en

  stylesheets:    [main]

  javascripts:    [ ]

  has_layout:     on
  layout:         layout

これらの設定はそれぞれ"ビューのコンフィギュレーション設定"のセクションで詳細に説明します。

レスポンスオブジェクト

ビューレイヤーの一部ではありますが、レスポンスオブジェクトはしばしアクションによって修正されます。アクションはgetResponse()メソッドを通してsfResponseと呼ばれるsymfonyのレスポンスオブジェクトにアクセスできます。リスト7-18は、アクションのなかでよく使われるsfResponseメソッドのいくつかのリストを示しています。

リスト7-18 アクションはsfResponseオブジェクトメソッドにアクセスできる

[php]
class mymoduleActions extends sfActions
{
  public function executeIndex()
  {
    $response = $this->getResponse();

    // HTTPヘッダー
    $response->setContentType('text/xml');
    $response->setHttpHeader('Content-Language', 'en');
    $response->setStatusCode(403);
    $response->addVaryHttpHeader('Accept-Language');
    $response->addCacheControlHttpHeader('no-cache');

    // Cookie
    $response->setCookie($name, $content, $expire, $path, $domain);

    // メタ情報とページのヘッダー
    $response->addMeta('robots', 'NONE');
    $response->addMeta('keywords', 'foo bar');
    $response->setTitle('My FooBar Page');
    $response->addStyleSheet('custom_style');
    $response->addJavaScript('custom_behavior');
  }
}

ここで示されるセッターメソッドに加えて、sfReponseクラスはレスポンスの属性の現在の値を返すゲッターを持ちます。

symfonyにおいてヘッダーのセッターはとても強力です。(sfRenderingFilter内で)ヘッダーは可能なかぎり遅く送信されるので、望む数だけ、そしてできるかぎり遅く変更できます。これらはとても便利なショートカットも提供します。たとえば、setContentType()を呼び出すときにcharsetを指定できない場合、symfonyはsettings.ymlファイルのなかで定義されたデフォルトのcharsetを自動的に追加します。

[php]
$response->setContentType('text/xml');
echo $response->getContentType();
 => 'text/xml; charset=utf-8'

symfonyのレスポンスのステータスコードはHTTPの仕様と互換性があります。例外の場合はステータス500を返し、ページが見つからない場合はステータス404を返し、通常のページの場合はステータス200を返し、修正されていないページの場合はステータス304と共にシンプルなヘッダーに減らすことができる(詳細は12章で)、などです。しかし、setStatusCode()レスポンスメソッドを持つアクションのなかで独自のステータスコードを設定することで、これらのデフォルトをオーバーライドできます。カスタムコードとメッセージ、もしくは単なるカスタムコードを指定できます。この場合、symfonyはこのコードに対してもっとも共通したメッセージを追加します。

[php]
$response->setStatusCode(404, 'このページは存在しません');

TIP ヘッダーを送るまえに、symfonyはこれらの名前を正規化します。setHttpHeader()の呼び出しのなかでContetn-Languageの代わりにcontent-languageを書くことに悩む必要はありません。syfmonyは前者を理解し、自動的に後者に変換するからです。

ビューのコンフィギュレーション設定

2種類のビューのコンフィギュレーション設定があることにお気づきかもしれません:

設定カスケードはユニークな値の設定を削除し複数の値の設定を集積することを覚えておいてください。この章を読み進めるとよりあきらかになります。

メタタグのコンフィギュレーション

レスポンスの<meta>タグに書かれた情報はブラウザーには表示されませんが、ロボットと検索エンジンには役立ちます。これはすべてのページのキャッシュ設定もコントロールします。リスト7-19のように、これらのタグはview.ymlのなかのhttp_metas:metas:キーの下で定義するか、リスト7-20のように、アクションのaddHttpMeta()addMeta()`のレスポンスメソッドでこれらのタグを定義します。

リスト7-19 - 「キー: 値」の組としてのメタの定義(view.yml)

http_metas:
  cache-control: public

metas:
  description:   Finance in France
  keywords:      finance, France

リスト7-20 - レスポンスアクションのレスポンス設定としてのメタの定義

[php]
$this->getResponse()->addHttpMeta('cache-control', 'public');
$this->getResponse()->addMeta('description', 'Finance in France');
$this->getResponse()->addMeta('keywords', 'finance, France');

既存のキーを追加すると現在の内容がデフォルトで置き換えられます。HTTPメタタグに対して、addHttpMeta()メソッド(setHttpHeader()も同様)が既存の値を置き換えるよりも、既存の値を追加するように、3番目のパラメーターを追加してfalseに設定できます。

[php]
$this->getResponse()->addHttpMeta('accept-language', 'en');
$this->getResponse()->addHttpMeta('accept-language', 'fr', false);
echo $this->getResponse()->getHttpHeader('accept-language');
 => 'en, fr'

これらのメタタグが最終的なドキュメントに表示されるように、include_http_metas()include_metas()ヘルパーは<head>セクションのなかで呼び出されなければなりません(これはデフォルトのレイアウトの場合; リスト7-5を参照)。適切な<meta>タグを出力するためにsymfonyはすべてのview.ymlファイル(リスト7-17で示されているデフォルトのものを含む)とレスポンスの属性から自動的に設定を集約します。リスト7-19の例はリスト7-21で示されているような状態で終わります。

リスト7-21 - 最終的なページ内のメタタグの出力

[php]
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta http-equiv="cache-control" content="public" />
<meta name="robots" content="index, follow" />
<meta name="description" content="Finance in France" />
<meta name="keywords" content="finance, France" />

おまけとして、レスポンスのHTTPヘッダーは、レイアウトのなかでinclude_http_metas()ヘルパーを持たないもしくはレイアウトをまったく持たない場合、http-metas:の定義にも影響されます。たとえば、ページをプレーンなテキストとして送る必要がある場合、つぎのようにview.ymlを定義します:

http_metas:
  content-type: text/plain

has_layout: false

タイトルのコンフィギュレーション

ページのタイトルは検索エンジンのインデックス作成のために重要な部分です。タブブラウジング機能を提供するモダンなブラウザーでも非常に便利です。HTMLにおいて、タイトルはページのタグとメタ情報の両方であるので、view.ymlファイルはtitle:キーをmetas:キーの子供として探します。リスト7-22はview.ymlのなかのタイトルの定義を示し、リスト7-23はアクションの定義を示します。

リスト7-22 - タイトルの定義(view.yml)

indexSuccess:
  metas:
    title: 3匹の子豚

リスト7-23 - アクションのなかのタイトルの定義 -- 動的なタイトルを可能にする

[php]
$this->getResponse()->setTitle(sprintf('%d匹の子豚', $number));

最終的なドキュメントの<head>セクションにおいて、<title>タグタイトルの定義は、include_metas()ヘルパーが存在する場合は<meta name="title">タグを、include_title()ヘルパーが存在する場合は<title>タグを設定します。両ほうが含まれる場合(リスト7-5のデフォルトのレイアウトなど)、タイトルはドキュメントのソース内で2回表示されますが(リスト7-6を参照)、これは無害です。

ファイルのインクルージョンのコンフィギュレーション

リスト7-24と7-25のお手本のように、特定のスタイルシートもしくはJavaScriptファイルをビューに追加することは簡単です。

リスト7-24 - ファイルをインクルードする(view.yml)

indexSuccess:
  stylesheets: [mystyle1, mystyle2]
  javascripts: [myscript]

リスト7-25 - ファイルをアクション内部でインクルードする

[php]
$this->getResponse()->addStylesheet('mystyle1');
$this->getResponse()->addStylesheet('mystyle2');
$this->getResponse()->addJavascript('myscript');

それぞれの場合、引数はファイルの名前です。ファイルが正しい拡張子を持つ場合(.cssはスタイルシート用、.jsはJavaScriptファイル用)、この拡張子を省略できます。ファイルが正しい位置(/css/はスタイルシート用で、/js/はJavaScriptファイル用)に存在する場合、同様に省略できます。symfonyは正しい拡張子、もしくは位置を理解するのに十分賢いです。

メタとタイトルの定義とは異なり、ファイルのインクルージョンの定義はインクルードされるテンプレートもしくはレイアウト内部のヘルパーを必要としません。このことはテンプレートとレイアウトの内容が何であれ以前の設定はリスト7-26のHTMLコードを出力することを意味します。

リスト7-26 - ファイルのインクルージョンの結果 -- レイアウトのなかではヘルパー呼び出しに対して必要ない

[php]
<head>
...
<link rel="stylesheet" type="text/css" media="screen" href="/css/mystyle1.css" />
<link rel="stylesheet" type="text/css" media="screen" href="/css/mystyle2.css" />
<script language="javascript" type="text/javascript" src="/js/myscript.js">
</script>
</head>

NOTE レスポンスのなかでのスタイルシートとJavaScriptのインクルージョンはsfCommonFilterという名前のフィルターによって実行されます。これはレスポンスの<head>タグを探し、</head>を丁度閉じるまえに<link>タグと<script>タグを追加します。レイアウトもしくはテンプレートのなかに<head>タグが存在しない場合、インクルージョンは行われないことを意味します。

設定カスケードの原則が適用されるので、アプリケーションのview.ymlのなかで定義されたどのファイルのインクルージョンはアプリケーションのすべてのページで現れます。リスト7-27、7-28、7-29はこの原則のお手本を示しています。

リスト7-27 - アプリケーションのview.ymlのサンプル

default:
  stylesheets: [main]

リスト7-28 - モジュールのview.ymlのサンプル

indexSuccess:
  stylesheets: [special]

all:
  stylesheets: [additional]

リスト7-29 - indexSuccessビューの結果

[php]
<link rel="stylesheet" type="text/css" media="screen" href="/css/main.css" />
<link rel="stylesheet" type="text/css" media="screen" href="/css/additional.css" />
<link rel="stylesheet" type="text/css" media="screen" href="/css/special.css" />

より高いレベルで定義されたファイルを除外することが必要がある場合、リスト7-30で示されるように、より低いレベルの定義で、マイナス記号(-)をファイルの名前のまえに追加します。

リスト7-30 - モジュールのview.ymlのサンプルはアプリケーションレベルで定義されたファイルを除外する

indexSuccess:
  stylesheets: [-main, special]

all:
  stylesheets: [additional]

すべてのスタイルシートもしくはJavaScriptのファイルを除外するには、つぎの構文を使います:

indexSuccess:
  stylesheets: [-*]
  javascripts: [-*]

ファイルを含む位置を強制するために追加のパラメーターをより厳密に定義できます(最初もしくは最後の位置):

// view.ymlにおいて
indexSuccess:
  stylesheets: [special: { position: first }]

[php]
// アクションにおいて
$this->getResponse()->addStylesheet('special', 'first');

スタイルシートのインクルージョンに対してメディアを指定するために、リスト7-31、7-32、7-33で示されるように、デフォルトのスタイルシートのタグオプションを変更できます。

リスト7-31 - view.ymlのなかでメディアを指定したスタイルシートのインクルージョン

indexSuccess:
  stylesheets: [main, paper: { media: print }]

リスト7-32 - アクション内部でメディアを指定したスタイルシートをインクルードする

[php]
$this->getResponse()->addStylesheet('paper', '', array('media' => 'print'));

リスト7-33 - ビューの結果

[php]
<link rel="stylesheet" type="text/css" media="print" href="/css/paper.css" />

レイアウトのコンフィギュレーション

Webサイトのグラフィカルな表にしたがって、いくつかのレイアウトを持ちます。古典的なWebサイトは少なくとも2つ持ちます: デフォルトのレイアウトとポップアップのレイアウトです。

デフォルトのレイアウトがmyproject/apps/myapp/templates/layout.phpであることはすでに見ました。追加のレイアウトは同じくグローバルなtemplates/ディレクトリに追加しなければなりません。myapp/templates/my_layout.phpファイルを使うビューが欲しい場合、リスト7-34のview.ymlもしくはリスト7-35のアクションの構文を使います。

リスト7-34 - レイアウトの定義(view.yml)

indexSuccess:
  layout: my_layout

リスト7-35 - アクション内部のレイアウトの定義

[php]
$this->setLayout('my_layout');

ビューのなかにはまったくレイアウトを持たないものがあります(たとえば、プレーンテキストページもしくはRSSフィード)。この場合、リスト7-36、リスト7-37で示されるように、has_layoutfalseに設定します。

リスト7-36 - view.ymlのなかでレイアウトの除外

indexSuccess:
  has_layout: false

リスト7-37 - アクションのなかでレイアウトの除外

[php]
$this->setLayout(false);

NOTE デフォルトではAjaxのアクションのビューはレイアウトを持ちません。

コンポーネントスロット

ビューのコンポーネントとビューの設定の力を組み合わせることでビューの開発に新しい観点がもたらされます: コンポーネントスロット(component slot)システムです。再利用性とレイヤーの分離に焦点を当てるスロットの代替物です。ですので、コンポーネントスロットはスロットよりも構造的ですが、実行は少し遅いです。

スロットのように、コンポーネントスロットはビューの要素内で宣言できる名前つきのプレースホルダーです。違いは入力コードを決定する方法にあります。スロットに対して、コードは別のビューの要素のなかに設定されます; コンポーネントスロットに対して、コードはコンポーネントの実行から由来し、このコンポーネントの名前はビューの設定から由来します。アクションのなかでコンポーネントスロットを見た後にこれらの理解がより深まります。

コンポーネントスロットのプレースホルダーを設定するには、include_component_slot()ヘルパーを使います。この関数はラベルをパラメーターとして必要とします。たとえば、アプリケーションのlayout.phpファイルが文脈上のサイドバーを含む場合を想像してください。リスト7-38はコンポーネントスロットのヘルパーがインクルードされる方法を示しています。

リスト7-38 - 'sidebar'という名前のコンポーネントをインクルードする

[php]
...
<div id="sidebar">
  <?php include_component_slot('sidebar') ?>
</div>

コンポーネントスロットのラベルとコンポーネントの名前の間の対応をビューの設定のなかで定義します。たとえば、アプリケーションのview.ymlのなかの、componentsヘッダーの下でsidebarコンポーネントに対してデフォルトのコンポーネントを設定してください。キーはコンポーネントスロットのラベルです; 値はモジュールとコンポーネントの名前を含む配列でなければなりません。リスト7-39は例を示しています。

リスト7-39 - デフォルトの'sidebar'スロットコンポーネントを定義する(myapp/config/view.yml)

default:
  components:
    sidebar:  [bar, default]

レイアウトが実行されたとき、sidebarコンポーネントスロットはbarモジュールに設置されたbarComponentsクラスのexecuteDefault()メソッドの結果で内容が満たされ、このメソッドはmodules/bar/templates/に設置された_default.php部分テンプレートを表示します。

設定カスケードによって任意のモジュールに対してこの設定をオーバーライドできます。たとえば、userモジュールにおいて、ユーザー名とユーザーが公開した記事の数を表示する文脈上のコンポーネントが欲しいことがあります。この場合、リスト7-40で示されるように、モジュールのview.ymlのなかでサイドバースロットの設定を特化します。

リスト7-40 - 'sidebar'スロットコンポーネントを特化する(myapp/modules/user/config/view.yml)

all:
  components:
    sidebar:  [bar, user]

このスロットを処理するコンポーネントの定義はリスト7-41のようなものになります。

リスト7-41 - 'sidebar'スロットによって使われるコンポーネント(modules/bar/actions/components.class.php)

[php]
class barComponents extends sfComponents
{
  public function executeDefault()
  {
  }

  public function executeUser()
  {
    $this->current_user = $this->getUser()->getCurrentUser();
    $c = new Criteria();
    $c->add(ArticlePeer::AUTHOR_ID, $this->current_user->getId());
    $this->nb_articles = ArticlePeer::doCount($c);
  }
}

リスト7-42はこれら2つのコンポーネントのためのビューを示しています。

リスト7-42 - 'sidebar'スロットコンポーネントによって使われる部分テンプレート(modules/bar/templates/)

[php]
// _default.php
<p>この領域はコンテキスト上の情報を含みます。</p>

// _user.php
<p>ユーザー名: <?php echo $current_user->getName() ?></p>
<p><?php echo $nb_articles ?> articles published</p>

コンポーネントスロットはパンくずリスト、コンテキスト上のナビゲーション、あらゆる種類の動的な挿入のために使われます。コンポーネントとして、これらはグローバルレイアウトと通常のテンプレート、もしくはほかのコンポーネントのなかで使用できます。スロットのコンポーネントを設定するコンフィギュレーションは最後に呼び出されたアクションのコンフィギュレーションからつねに除外されます。

任意のモジュールに対してコンポーネントスロットの利用を一時中止する必要がある場合、リスト7-43で示されるように、空のモジュール/コンポーネントを宣言します。

リスト7-43 - view.ymlのなかでコンポーネントスロットを無効にする

all:
  components:
    sidebar:  []

出力エスケーピング機能

動的なデータをテンプレートに挿入するとき、データの統合性について気をつけなければなりません。たとえば、データが匿名ユーザーから入力されたフォームから来た場合、クロスサイトスクリプティング(XSS - Cross-Site Scripting)攻撃を開始する悪意のあるスクリプトを含んでいるリスクが存在する可能性があります。出力データに含まれるHTMLタグが無害になるように、出力データをエスケープできることが必須です。

例として、ユーザーが入力フィールドをつぎのような値で満たすことを想像してください:

[php]
<script>alert(document.cookie)</script>

警告なしでこの値をechoする場合、JavaScriptはすべてのブラウザーで実行され、アラートを表示するよりはるかに危険な攻撃を許してしまいます。値をつぎのようにするために、表示するまえに値をエスケープしなければならない理由はそういうわけです:

[php]
&lt;script&gt;alert(document.cookie)&lt;/script&gt;

htmlentities()の呼び出しで信頼性のないすべての値をとり囲むことで出力を手動でエスケープできますが、このアプローチは繰り返し作業がとても多くエラーが起こりがちです。代わりに、symfonyは出力エスケーピング(output escaping)と呼ばれる特別なシステムを提供します。これは自動的にテンプレート内のすべての変数出力をエスケープします。これはアプリケーションのsettings.yml内の単純なパラメーターで有効になります。

出力エスケーピング機能を有効にする

出力エスケーピング機能はsettings.ymlのなかでアプリケーションに対してグローバルに設定されます。2つのパラメーターが出力エスケーピングの動作方法をコントロールします: 変数がビューに対して利用できるようにする方法を戦略が決定し、メソッドはデータに適用されるデフォルトのエスケーピング機能です。

つぎのセクションでこれらの設定を詳細に説明しますが、基本的に、出力エスケーピングを行うために必要なことは、リスト7-44で示されるように、escaping_strategyパラメーターをデフォルト値のbcの代わりにbothに設定することです。

リスト7-44 - 出力エスケーピング機能を有効にする(myapp/config/settings.yml)

all:
  .settings:
    escaping_strategy: both
    escaping_method:   ESC_ENTITIES

これはデフォルトでhtmlentities()をすべての変数の出力に追加します。たとえば、つぎのようにアクション内でtest変数を定義することを考えてみましょう:

[php]
$this->test = '<script>alert(document.cookie)</script>';

出力エスケーピング機能を有効にした場合、この変数をテンプレートのなかでechoするとエスケープされたデータが出力されます:

[php]
echo $test;
 => &gt;&lt;script&gt;alert(document.cookie)&lt;/script&gt;

出力エスケーピングを有効にすることですべてのテンプレート内で$sf_data変数にアクセスできるようになります。この変数はエスケープされたすべての変数を参照するコンテナオブジェクトです。 つぎのようにtest変数を出力することもできます:

[php]
echo $sf_data->get('test');
=> &gt;&lt;script&gt;alert(document.cookie)&lt;/script&gt;

TIP $sf_dataオブジェクトはArrayインターフェイスを実装するので、$sf_data->get('myvariable')を使う代わりに、$sf_data['myvariable']を呼び出すことでエスケープされた値をとり出すことができます。しかし、これは本当の配列ではないので、print_r()のような関数は期待どおりに動きません。

このオブジェクトによってエスケープされていない、もしくは生のデータにもアクセスできます。このことは、変数が信用できることを前提とすると、変数がブラウザーに解釈されることを目的としたHTMLコードを保存するときに便利です。生のデータを出力することが必要なとき、getRaw()メソッドを呼び出します。

[php]
echo $sf_data->getRaw('test');
 => <script>alert(document.cookie)</script>

実際にHTMLとして解釈されるHTMLを含む変数を必要とするときは毎回生のデータにアクセスしなければなりません。テンプレートをインクルードするためにデフォルトのレイアウトが、よりシンプルな$sf_contentの代わりに、$sf_data->getRaw('sf_content')を使う理由がこれで理解できます。出力エスケーピングが有効であるときに$sf_contentが壊れるからです。

エスケーピング戦略

escaping_strategyの設定は変数がデフォルトで出力される方法を決定します。 つぎのものが利用可能な値です:

エスケーピングヘルパー

エスケーピングヘルパー(escaping helper)はエスケープされたバージョンの入力を返す機能です。これらはsettings.ymlファイルのなかでデフォルトのescaping_methodとして、もしくはビューのなかで特定の値のためのエスケーピングメソッドを指定するために提供されます。つぎのエスケーピングヘルパーが利用できます:

配列とオブジェクトをエスケープする

出力エスケーピング機能は文字列だけでなく、配列とオブジェクトに対しても作用しますオブジェクトもしくは配列である値はこれらのエスケープされた状態をそれらの子供に渡します。戦略をbothに設定したと仮定すると、リスト7-45はエスケーピングカスケードのお手本を示しています。

リスト7-45 - エスケーピング機能は配列とオブジェクトに対しても作用する

[php]
// クラスの定義
class myClass
{
  public function testSpecialChars($value = '')
  {
    return '<'.$value.'>';
  }
}

// アクションのなか
$this->test_array = array('&', '<', '>');
$this->test_array_of_arrays = array(array('&'));
$this->test_object = new myClass();

// テンプレートのなか
<?php foreach($test_array as $value): ?>
  <?php echo $value ?>
<?php endforeach; ?>
 => &amp; &lt; &gt;
<?php echo $test_array_of_arrays[0][0] ?>
 => &amp;
<?php echo $test_object->testSpecialChars('&') ?>
 => &lt;&amp;&gt;

実際のところ、テンプレート内部の変数はあなたが期待した型ではありません。出力エスケーピングシステムはこれらを"デコレート"して、特別なオブジェクトに変換します:

[php]
<?php echo get_class($test_array) ?>
 => sfOutputEscaperArrayDecorator
<?php echo get_class($test_object) ?>
 => sfOutputEscaperObjectDecorator

これはいくつかの通常のPHP関数(array_shift()print_r()などの)がエスケープされた配列上ではもはや機能しない理由を説明しています。しかし、これらはまだ[]を使うことでアクセスすることが可能で、foreachを使うことで展開され、これらはcount()で正しい結果を返します(count()はPHP 5.2かそれ以降で動作します)。そしてテンプレートにおいて、ともかくデータはリードオンリーなので、たいていのアクセスは実際に動作するメソッドを通したものになります。

$sf_dataオブジェクトを通して生のデータをとり出す方法がまだあります。加えて、エスケープされたオブジェクトのメソッドは追加パラメーター: エスケーピングメソッドを受けとるために変更されます。テンプレートの変数を表示するたびに代わりのエスケーピングメソッド、もしくはエスケーピングを無効にするESC_RAWヘルパーを選ぶことができます。リスト7-46の例をご覧ください。

リスト7-46 - エスケープされたオブジェクトのメソッドは追加パラメーターを受けとる

[php]
<?php echo $test_object->testSpecialChars('&') ?>
=> &lt;&amp;&gt;
// つぎの3行は同じ値を返す
<?php echo $test_object->testSpecialChars('&', ESC_RAW) ?>
<?php echo $sf_data->getRaw('test_object')->testSpecialChars('&') ?>
<?php echo $sf_data->get('test_object', ESC_RAW)->testSpecialChars('&') ?>
 => <&>

テンプレート内のオブジェクトを処理する場合、追加パラメーターのトリックを多く利用することになります。これがメソッド呼び出し上で生のデータを得るための最速の方法だからです。

CAUTION symfonyの通常の変数は出力エスケーピングを有効にしたときにもエスケープすることができます。$sf_user$sf_request$sf_param$sf_contextはまだ機能しますが、ESC_RAWを最後の引数としてこれらのメソッド呼び出しに追加しないかぎり、これらのメソッドはエスケープされたデータを返すことを覚えておいてください。

まとめ

プレゼンテーションレイヤーを操作するためにあらゆる種類のツールを利用できます。ヘルパー(helper)のおかげで、テンプレート(template)が秒単位で作られます。レイアウト(layout)、部分テンプレート(partial - パーシャル)、コンポーネント(component)とコンポーネントスロット(component slot)によってモジュール性と再利用性の両ほうがもたらされます。ビュー(view)の設定においてたいていのページのヘッダーを扱うために速く書けるYAMLを利用します。設定カスケード(configuration cascade)によってビューごとにすべての設定を定義しないですみます。動的なデータに依存するプレゼンテーションのすべての修正のために、アクションはsfResponseオブジェクトにアクセスできます。そして出力エスケーピングシステム(output escaping system)のおかげで、ビューはクロスサイトスクリプティング(XSS - Cross-Site Scripting)攻撃から安全です。