Curry
PHP Framework

ビュー

ビューの概念

ビューはMVCの中でHTML出力の役割を受け持ちます。
MVCに限らず最近のPHP実装においては、ロジックとビューを分離させる手法が一般的です。従来のようにPHPからHTMLソースを直接出力するのではなく、HTMLテンプレートを使って処理のメインロジックではHTMLを一切意識しないような構造にします。

有名なテンプレートエンジンとしてSmartyがありますが、CurryではこのSmartyを利用したビュークラスを推奨しています。標準でもテンプレート形式の仕組みを持っていますが、Smartyとの連携が簡単に行えるように作られています。

標準のビュークラス

まずはテンプレートファイルの取り扱いから見ていきます。テンプレートはapp/views/templatesの中にコントローラーと同名のディレクトリを作成し、その中にアクションと同名のテンプレートファイルを配置することで、リクエストのコントローラー・アクションに応じたテンプレートが自動的に出力される仕組みになっています。

∟site/
    ∟app/
        ∟controllers/
            ∟cartcontroller.php
        ∟views/
            ∟templates/  ← テンプレート格納ディレクトリ
                ∟cart/      ←コントローラーと同名ディレクトリ
                    ∟products.php   ←アクションと同名テンプレートファイル
app/controllers/cart_controller.php
class CartController extends Controller
{
    public function products()
    {

    }
}
app/views/templates/cart/products.php
<p>Too hot!</p>

上記のような状態で、

http://www.xxxx.com/cart/products

へアクセスすると以下のようにブラウザに表示される結果となります。

Too hot!

コントローラーサブディレクトリへの対応

コントローラーにサブディレクトリを利用している場合、それに応じてビューテンプレートのディレクトリもコントローラーと同じ階層にする必要があります。

∟site/
    ∟app/
        ∟controllers/
            ∟admin/
                ∟product_controller.php
            ∟cart_controller.php
        ∟views/
            ∟templates/
                ∟admin/
                    ∟product/
                        ∟index.php
                ∟cart/
                    ∟products.php

この例では、controllers/admin/product_controller.phpが存在するため、それに対応するビューディレクトリはviews/templates/admin/product/になります。

変数の割り当て

テンプレートに変数を持たせ、コントローラーからテンプレートの変数へ値を割り当てる例です。

app/controllers/cart_controller.php
class CartController extends Controller
{
    public function products()
    {
        $this->view->message = 'Too hot!';
    }
}
app/views/templates/cart/products.php
<p><?php echo $message; ?></p>

コントローラーはビュークラスのインスタンスを保持しています。単純に$this->viewで利用可能です。このビュークラスのインスタンスに対して適当な名前のフィールドに直接値をセットすると、テンプレートで同名の変数に値が割り当てられ、echo等によってその値が出力できます。

Too hot!

配列の利用

テンプレート変数に配列を割り当てると、テンプレート側で制御構造を利用して例えば表の行を動的に出力することなどが可能です。

class CartController extends Controller
{
    public function products()
    {
        $products = array(
            'magazine'   => array('price' => 200, 'num' => 1),
            'dictionary' => array('price' => 1800, 'num' => 1),
            'comic'      => array('price' => 300, 'num' => 3)
        );
        $this->view->products = $products;
    }
}
<table border=1>
  <tr><th>商品</th><th>価格</th><th>数量</th></tr>
<?php foreach ($products as $name => $product) { ?>
  <tr>
    <td><?php echo $name; ?></td>
    <td><?php echo $product['price']; ?></td>
    <td><?php echo $product['num']; ?></td>
  </tr>
<?php } ?>
</table>
<table border=1>
  <tr><th>商品</th><th>価格</th><th>数量</th></tr>
  <tr>
    <td>magazine</td>
    <td>200</td>
    <td>1</td>
  </tr>
  <tr>
    <td>dictionary</td>
    <td>1800</td>
    <td>1</td>
  </tr>
  <tr>
    <td>comic</td>
    <td>300</td>
    <td>3</td>
  </tr>
</table>

任意のテンプレートの指定

デフォルトではリクエストのコントローラーとアクションに応じて自動的にテンプレートが決まりますが、任意に指定したい場合もあるでしょう。
ビュークラスには使用するテンプレートを任意に指定できるメソッドが用意されています。

∟site/
    ∟app/
        ∟controllers/
            ∟cartcontroller.php
        ∟views/
            ∟templates/
                ∟cart/
                     ∟index.php

class CartController extends Controller
{
    public function products()
    {
        $this->view->message = 'Too hot!';
        $this->view->setTemplate('index');
    }
}

アクションは"products"ですが、テンプレートは"index.php"を使って出力したい場合はこのようになります。
基本的にテンプレートのパスはコントローラーとアクションの組み合わせなので、setTemplateの引数は下記のようにコントローラーとアクションで指定します。

$this->view->setTemplate(アクション名[, コントローラー名]):

第2引数がコントローラー名ですが、これは省略可能で、省略した場合は同じコントローラー内の、第1引数で指定したアクションのテンプレートということになります。別のコントローラーのテンプレートを利用したい場合は第2引数でし指定します。別のコントローラーとは言っても、テンプレートのディレクトリ内にその名前のディレクトリがあればよいだけで、それに対応するコントローラークラスが存在しなければならないわけではありません。setTemplateで指定するのはあくまで単純にテンプレートファイル名、ディレクトリ名ということです。

エラーテンプレート

例外テンプレート

何らかの例外発生時、既定のエラー表示用テンプレートが表示されます。デフォルトではtemplates/error/error.phpがエラーテンプレートです。

∟site/
    ∟app/
        ∟views/
            ∟templates/
                ∟error/
                     ∟error.php   ← エラー表示用テンプレート
                     ∟not_found.php

既定のエラーテンプレートが存在しないと更に例外が発生しますので、エラーテンプレートは必ず配置してください。

エラーテンプレートには例外情報をもとにしたいくつかの変数がデフォルトで割り当てられます。

変数名内容
file 例外が発生したPHPファイルのパス
line PHPファイルの例外発生行番号
message 例外メッセージ
trace 例外のトレース情報

これらの情報はサイト閲覧者に見せるべきものではありません。サイト運用時はエラーテンプレートにはお決まりのエラーメッセージ、例えば「システムエラーが発生しました」等を出力するだけの固定の内容にするのがよいでしょう。
開発中のデバッグなどには出力された例外情報が見えるようにすると便利です。

NotFoundテンプレート

URLをもとにしたコントローラー・アクションのルーティングが失敗した場合、error/not_found.phpが出力されます。
デフォルトでは「Not Found」という文字が表示されるだけのものですので、カスタマイズしたい場合はこのテンプレートを直接編集すれば可能です。

レイアウト

大抵のWebサイトでは、サイトの基本となるデザインやヘッダ、メニューなど、サイト全体で共通となる部分が存在します。全てのページのテンプレートで共通部分をいちいち書いていては、共通部分に何か変更があったときに全てのテンプレートを修正しなければなりません。
Curryのビューでは共通部分を抜き出して別テンプレートとし、これを共通の枠組みとして利用するレイアウトの仕組みが利用できます。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="/curryfw/htdocs/css/layout.css" />
<script type="text/javascript" src="/curryfw/htdocs/js/common.js"></script>
<title>通販サイト</title>
</head>
<body>

<div id="frame">
  <div id="header">
    <!-- ヘッダソース -->
  </div>
  <div id="side_menu">
    <!-- サイドメニュー -->
  </div>
  <div id="main">
    <!-- メインコンテンツ↓ -->
    <h1>カート</h1>
    <table>
      <tr><th>商品名</th><th>数量</th><th>価格</th></tr>
      <tr><td>Spices</td><td>3</td><td>900</td></tr>
      <tr><td>Meets</td><td>1</td><td>800</td></tr>
      <tr><td>Vegetables</td><td>1</td><td>500</td></tr>
      <tr><td colspan="2">Total</td><td>2,200</td></tr>
    </table>
    <!-- メインコンテンツ↑ -->
  </div>
</div>

</body>
</html>

たとえばこのHTMLでは「メインコンテンツ」の部分が各ページごとに異なる内容の部分です。そのほかの部分はサイト共通の部分なので共通化が出来ると考えられます。なのでこのHTMLを共通部分とメインコンテンツ部分に分けてみます。

views/layouts/default.php
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="/curryfw/htdocs/css/layout.css" />
<script type="text/javascript" src="/curryfw/htdocs/js/common.js"></script>
<title>通販サイト</title>
</head>
<body>

<div id="frame">
  <div id="header">
    <!-- ヘッダソース -->
  </div>
  <div id="side_menu">
    <!-- サイドメニュー -->
  </div>
  <div id="main">
    <!-- メインコンテンツ↓ -->
    <?php echo $inner_contents; ?>
    <!-- メインコンテンツ↑ -->
  </div>
</div>

</body>
</html>
views/templates/cart/index.php
    <table>
      <tr><th>商品名</th><th>数量</th><th>価格</th></tr>
      <tr><td>Spices</td><td>3</td><td>900</td></tr>
      <tr><td>Meets</td><td>1</td><td>800</td></tr>
      <tr><td>Vegetables</td><td>1</td><td>500</td></tr>
      <tr><td colspan="2">Total</td><td>2,200</td></tr>
    </table>

上が共通部分である「レイアウト」です。
レイアウトの中では、"$inner_contents"という変数にメインコンテンツのソースが割り当てられます。レイアウトの中で適当な位置にこれを出力するわけです。

レイアウトは専用のディレクトリに配置します。配置先ディレクトリは、views/layoutsです。

∟site/
    ∟app/
        ∟controllers/
            ∟cartcontroller.php
        ∟views/
            ∟layouts/          ← レイアウト格納ディレクトリ
                ∟default.php
            ∟templates/
                ∟cart/
                     ∟products.php

任意のレイアウトの指定

レイアウトのファイル名は"default.php"がデフォルトになっており、何も指定しなければこの名前でlayoutsフォルダに配置するだけです。ただ、ページによって複数のレイアウトを使い分けたいと言う場合はあると思います。例えば通販サイトでユーザーのマイページのようなものがある場合、そこに関しては少し違ったレイアウトにしたいと思うかもしれません。

∟site/
    ∟app/
        ∟controllers/
            ∟cartcontroller.php
        ∟views/
            ∟layouts/
                ∟default.php
                ∟my.php
            ∟templates/
                ∟cart/
                     ∟products.php
                ∟my/
                     ∟index.php
                     ∟history.php

class MyController extends Controller
{
    public function preProcess()
    {
        $this->view->setLayout('my');
    }

    public function index()
    {

    }

    public function history()
    {

    }
}

ビュークラスのsetLayoutメソッドでこれが実現できます。
レイアウトディレクトリに任意の名前でレイアウトのテンプレートを配置し、コントローラーでビューのsetLayoutを実行します。引数には使いたいレイアウトテンプレートのファイル名を拡張子なしで指定します。この例ではpreProcessで指定しているため、Myコントローラーの全アクションでこの設定が適用されることにります。もし、特定のアクションでのみ違うレイアウトを利用したい場合は、そのアクションメソッド内でsetLayoutします。

レイアウトの無効化

レイアウトを無効にすることも可能です。

class MyController extends Controller
{
    public function preProcess()
    {
    }

    public function index()
    {
        $this->view->enabledLayout(false);
    }

    public function history()
    {

    }
}

例えばこの例ではMyControllerのindexアクション時のみレイアウトを無効化しています。
preProcessメソッドに記述すればMyController全体でレイアウトが無効になります。
サイト全体でレイアウトを一切使用したくない場合には、Pluginの:preProcessに記述します。
もしくは、ViewAbstractクラスの静的メソッド、setDefaultLayoutEnabledでfalseを指定することにより、レイアウトの仕組みが一切無効になるので、このメソッドをindex.phpで実行します。

index.php
class Initializer extends InitializerStandard
{
    public function initialize()
    {
        ViewAbstract::setDefaultLayoutEnabled(false);
    }
}

またはcurry.iniでの初期設定でも対応可能です。

curry.ini
[view]
;--If you want to disable view layout, set this flag "0".
layout_enabled = 0

Smartyの利用

CurryではビュークラスにSmartyを利用することを推奨しています。なぜなら標準のビュークラスの仕組みでは、テンプレートとは言えやはりPHPファイルであり、書こうと思えばPHPのロジックをいくらでも書けてしまいます。これはメリットも取れますが、ビューとロジックの分離の観点からは好ましくありません。
Smartyのテンプレートの変数はよりシンプルです。直接のPHPファイルではないし、ロジックも基本的には書けません。その意味ではより純粋なHTMLに近いといえます。

app/views/templates/cart/products.tpl
<p>{$message}</p>

Smartyの場合、変数記述はこのように単純です。

<table>
  <tr><th>商品</th><th>価格</th><th>数量</th></tr>
{foreach from=$products item=product}
  <tr>
    <td>{$product.name}</td>
    <td>{$product.price}</td>
    <td>{$product.num}</td>
  </tr>
{/foreach}
</table>

配列の出力でもこのように分かりやすい記述になります。

Smartyとの連携設定

CurryではSmartyとの連携が非常に簡単に行えます。
まずはSmartyをダウンロードする必要があります。Smartyのダウンロードサイトからダウンロードしてください。

ダウンロードしたものを展開して出来た「Smarty-x.x.x」ディレクトリを「Smarty」という名前にリネームし、任意のディレクトリに置きます。例ではCurryのフレームワークディレクトリ直下に置いています。
(※バージョン1.3.0以降はパッケージに同梱しています。)

curry
    ∟db/
    ∟exception/
            ・
            ・
            ・
    ∟Smarty/

次に、Smartyと連携するためにcurry.iniで設定、またはInitializerで設定を行います。

configs/curry.ini
[view]
;--If you want to specify view class, set this.
class_name = "ViewSmarty"
class Initializer extends InitializerStandard
{
    public function initialize()
    {
        $this->dispatcher->setViewClass('ViewSmarty');
    }
}

設定はこれだけです。

あと、Smartyは処理高速化のために、キャッシュやコンパイルという仕組みが存在します。サイトへのアクセス時、Smartyはテンプレートを一旦、PHPで解釈しやすい形に変えた物を特定ディレクトリに保管します。これがコンパイルです。更に変数を値に置き換え、完全に静的なHTMLになった状態のものを保管します。これがキャッシュです。Smartyのデフォルトではコンパイルは有効、キャッシュは無効になっています。

このキャッシュやコンパイルの保管先ディレクトリが必要になりますが、Curryではそれぞれviewsの中のcompileとcacheというディレクトリに出力するように設定されているので、これらを用意しておく必要があります。

        ∟views/
            ∟cache/       ← Smartyテンプレートキャッシュディレクトリ
            ∟compile/     ← Smartyテンプレートコンパイルディレクトリ
            ∟layouts/
                ∟default.tpl
                ∟my.tpl
            ∟templates/
                ∟cart/
                     ∟products.tpl
                ∟my/
                     ∟index.tpl
                     ∟history.tpl


views以下のディレクトリ構成はこんな感じになります。
compileやcacheはシステムから書き込みが行われるのでそれが可能なようにパーミッションを設定しておく必要があります。777にしておけば間違いがないでしょう。

Smarty利用の場合の違い

Smartyを利用する場合、基本的にはテンプレートの拡張子は"tpl"になります。
また当然テンプレートの書き方は変わります。Smartyに関しては本家のマニュアル他、解説サイトが多数存在するのでそちらを参照してください。

それ以外の違いは一切ありません。標準のビュークラスはViewStandardというクラスで、Smarty連携の場合はViewSmartyというクラスです。これらのクラスは共通の抽象クラス、ViewAbstractを継承しているため、基本的に利用できるメソッドやメソッドの働きも同じになるように設計されています。なので、Smarty連携だからといって何も意識する必要はなく、例えば既にViewStandardクラスで運用中のサイトのビューをViewSmartyに変えることも簡単にできます。

スタイルシート・Javascript

スタイルシート

スタイルシートは基本的に外部スタイルシートとしてファイルを作成し、既定のディレクトリに配置したうえでテンプレートに読み込みのタグを記述します。レイアウト利用時はレイアウトのテンプレートにこれを記述することになるでしょう。サイト全体の共通のスタイルシートなら問題ありませんが、ページごとに違うものを読み込みたい場合はレイアウト利用時は対応が難しくなります。

そこでCurryではコントローラー名.cssという名前でcssファイルを既定のcssディレクトリに配置すれば、そのコントローラーのリクエスト時に自動的に読み込まれる仕組みを取っています。
Curryのパッケージに付属されているサンプルディレクトリ構成に、デフォルトのビューレイアウトファイルが存在します。そのヘッダ部分に以下のような箇所があります。

<?php foreach ($stylesheets as $css) { ?>
<link rel="stylesheet" type="text/css" href="<?php echo $request['base_path']; ?>/css/<?php echo $css; ?>" />
<?php } ?>

$stylesheetsという変数には配列でcssファイル名が格納されています。
この配列には、common.cssが存在する場合はcommon.cssが格納されています。
更に(リクエストのコントローラー名).cssの名前のファイルが存在する場合はそれも格納されています。
つまり、common.cssと(コントローラー名).cssの2つのCSSファイルについては、htdocs/cssに配置するだけで読み込まれて適用されるわけです。

Javascript

Javascriptもスタイルシートと全く同じ考え方です。
common.jsと(コントローラー名).jsが存在すれば読み込まれる仕組みです。

<?php foreach ($javascripts as $js) { ?>
<script type="text/javascript" src="<?php echo $request['base_path']; ?>/js/<?php echo $js; ?>"></script>
<?php } ?>

任意のcss、jsの読み込み

"common"やコントローラー名でcssやjsファイルを配置すると自動的に読み込まれるのは前述のとおりですが、ユーザーが任意のcssやjsを読み込みたい場合ですが、
ひとつの方法として、レイアウトに直接記述する方法が考えられます。しかしレイアウト自体がサイト共通の仕組みなので、レイアウトに記述してしまうとサイト全体に対して適用されてしまうことになり、ある特定のページだけで読み込みたいスタイルシートなどには対応できません。

これを実現するにはビュークラスのaddCssメソッドやaddJsメソッドを利用します。
ViewStandardやViewSmartyクラスに存在するaddCssやaddJsは、引数にファイル名を指定することで、上記で説明しているテンプレート変数、$stylesheetsや$javascriptsという配列変数に任意のファイル名を追加することができます。

controllers/cart_controller.php
class CartController extends Controller
{
    public function preProcess()
    {
        // 任意のstylesheet、javascriptを追加読み込み
        $this->view->addJs('thickbox.js');
        $this->view->addCss('thickbox.css');
    }

    public function products()
    {
        $this->view->message = 'Too hot!';
        $this->view->setTemplate('index');
    }
}
テンプレート出力結果
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="/css/common.css" />
<link rel="stylesheet" type="text/css" href="/css/cart.css" />
<link rel="stylesheet" type="text/css" href="/css/thickbox.css" />
<script type="text/javascript" src="/js/common.js"></script>
<script type="text/javascript" src="/js/cart.js"></script>
<script type="text/javascript" src="/js/thickbox.js"></script>
<title>通販サイト</title>
</head>
<body>
                  ・
                  ・
                  ・

この例ではpreProcessでaddJsやaddCssを実行しているため、cartコントローラーへのリクエストは全てに対して有効になりますが、アクションメソッドの中で実行すれば、特定のアクションでのみ適用することが可能です。

出力文字コードの指定(ver.1.4.7以降)

通常、レスポンスとして出力するHTMLの文字コードはテンプレートファイルの文字コードで決まり、それに合わせてHTMLヘッダで文字コードを宣言します。しかし、様々なクライアント環境に対応するため、ユーザーエージェントなどによって出力の文字コードを切り分けたい場合があるかもしれません。
このような場合、ビュークラスのsetOutputEncodingメソッドによって文字コードを指定することで対応が可能です。

注意が必要なのは、php.iniでdefault_charsetが設定されているとHTMLヘッダの文字コード宣言が無視されることです。
基本的にはphp.iniのdefault_charsetはコメントアウトして指定しないことをおすすめします。

以下にユーザーエージェントにより、文字コードを切り分ける例を示します。

app/controllers/plugin.php
class Plugin extends PluginAbstract
{
    public function preProcess()
    {
        $ua = $this->request->getServer('HTTP_USER_AGENT');
        if (preg_match('/example/', $ua)) {
            $this->view->header_encoding = 'euc-jp';
            $this->view->setOutputEncoding('EUC-JP');
        }        
    }
}
app/views/layoutd/default.php
     ・
     ・
<meta http-equiv="Content-Type" content="text/html; charset=<?php echo $header_encoding; ?>">
     ・
     ・