ZendFramework2ガイド

入門編

ビューの基本

ビューの位置付け

MVCにおけるビューは、ユーザーインターフェイスに関わる処理を担当します。つまりWEBにおいてはレスポンスするHTMLの出力がビューの役割ということになります。
Zend2では、普通に使っている限り、ビューはHTMLテンプレートです。テンプレートには最低限のPHPコードが埋め込まれ、そこへコントローラーからデータが割り当てられることでHTMLとして完成します。

MVCに限らず、PHPロジックとHTMLタグはファイルレベルで分離するというのが今のPHPの主流です。
入門書に出てくるような、HTMLのあちこちにPHPコードが長々と埋め込まれているような作りだと、少し複雑なシステムでも可読性が極端に落ちる結果になってしまいます。
Zend2はテンプレート方式をとっているため、そのようにならないようにはなっていますが、テンプレートの中身を見れば分かる通り、単にPHPタグが埋め込まれたHTMLです。
そのため、いくらでも複雑なロジックを埋め込むことが可能なわけです。そうなるとMVCの意味がなくなるため、気をつける必要はあります。

基本的にはechoによる変数の出力、配列データを元にリスト系タグ(ULやTABLE等)を組み立てるためのforeachなどの制御構造、
ごく単純なif文(データの状態により、一部のHTMLの表示・非表示を切り分ける等)程度に止めておくべきです。

ビューモデルという考え方

コントローラーのアクションメソッドを見ると、ViewModelというクラスのインスタンスを返しています。
アーキテクチャ的な観点ではビューモデルとは何かというと、ビューに関するロジック、またはデータ保持を担当する概念とでもいいましょうか。

少しZend2という世界の話からはそれますが、MVCと似たアーキテクチャとしてMVVMなるものがあります。
これは一部のWindowアプリケーション用言語で取り入れられているもので、モデル・ビュー・ビューモデルという3つの概念からなります。
ビューはUIに当たるため、ユーザーへのデータ表示の意味での出力と、ユーザー入力を受け付けるという意味での入力を担いますが、そのようなビュー用のデータを保持しているのがビューモデルで、ユーザー入力をリアルタイムに監視し、それによるデータ変化をモデルに伝える役目などをするというのが本来のMVVMです。

一方、Webアプリにおいてはビューとモデルの間にリアルタイムな結びつきはありません。ブラウザからのリクエストを受け、Webサーバーがレスポンスを返したところで処理プロセスが完結するため、入力→出力の一方通行です。なので本来的な意味でのMVVMはWebにはマッチしないアーキテクチャです。

しかしZend2ではViewModelというクラスが存在しています。これはビューモデルの概念を一部取り入れていると考えられます。
本来で言うモデルとビューの間のリアルタイムな橋渡し的な役割ではなく、ビュー表示用にデータを保持するとか、加工するためのロジックを担当するという位置づけです。
普通にZend2を利用する限り、ビューモデルについてはあまり意識する必要はありませんが、フレームワーク内部ではそのような概念のものが存在しているということは知っておいてもいいと思います。

と言うのは、このViewModelを拡張してコントローラーより渡されたデータを加工するクラスをコントローラーとビューテンプレートの間に挟ませるという事が考えられるからです。
普通であれば、module/{モジュール名}/src/{名前空間}/の下にはControllerとModelの2つが並ぶことになり、
ビューに関してはmodule/{モジュール名}/view/以下ということになるわけですが、
例えばmodule/{モジュール名}/src/{名前空間}/Viewというディレクトリを作り、ここにビューモデルにあたる概念を作り上げる事が良いと考えられます。

モデルは単純にデータを保持しており、それを要求通りに組み合わせて返す、と考えることができますが、このデータを表示用に加工するのはビューの役割です。
なぜなら、モデルで加工してしまうとユーザーに対しての見た目に関わるロジックがモデルの中に流れ込んでいることになるからです。
そのためモデルではそのようなことは行なわず、ビューにその役割をさせるわけですが、
少し複雑なロジックが必要になる場合、今度はテンプレートに複雑なPHPロジックが混ざりこんでしまうことになり、PHPロジックとHTMLの分離という観点から逸脱してしまうことになります。
これを防ぐため、ビューに関するロジックを受け持つクラスを作り上げるというわけです。

ここでは具体的にその手法について取り上げることはしませんが、一つの方法論として考えられるということは知っておいても損はないと思います。

module.config.phpでのビュー関連の設定

スケルトンアプリケーションのデフォルトで存在するmodule.config.phpの中でビューに関連する設定は以下の部分です。

module/Ec/config/module.config.php
    'view_manager' => array(
        'display_not_found_reason' => true,
        'display_exceptions'       => true,
        'doctype'                  => 'HTML5',
        'not_found_template'       => 'error/404',
        'exception_template'       => 'error/index',
        'template_map' => array(
            'layout/layout'  => __DIR__ . '/../view/layout/layout.phtml',
            'ec/index/index' => __DIR__ . '/../view/ec/index/index.phtml',
            'error/404'      => __DIR__ . '/../view/error/404.phtml',
            'error/index'    => __DIR__ . '/../view/error/index.phtml',
        ),
        'template_path_stack' => array(
            __DIR__ . '/../view',
        ),
    ),

display_not_found_reason

display_not_found_reasonは、その訳の通り、404 not found時に、その理由を表示するかどうかを設定します。
以下にtrueの場合とfalseの場合の違いを示します。
trueの方は「The requested URL cound not be matched routing」との表示があります。
これはリクエストURLに対してルーティングの設定が無いことを言っていますが、
これは言ってみればプログラムの作り上の不備が分かってしまう内容です。
開発中であれば必要な情報ですが、公開された状態でこれが表示されているのはあまり嬉しくありませんので、開発中のみtrueにするなどします。

trueの場合

falseの場合

display_exceptions

display_exceptionsは、PHPの例外が発生した場合に表示されるページに例外の詳細情報を表示するか否かを設定します。
これも開発時は表示された方がわかりやすいですが、公開中はバグをさらけ出すようなものなので、falseにしましょう。

trueの場合

falseの場合

doctype

HTMLの冒頭のDOCTYPEを設定します。
スケルトンアプリケーションのデフォルトでは'HTML5'になっています。
ここで指定できるものは以下の通りです。

上記のうち、いくつか例を示します。

HTML5
<!DOCTYPE html>
HTML4_STRICT
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

template_path_stack

template_path_stackは、ビューテンプレートのルートディレクトリを設定します。
デフォルトではmodule.config.phpからの相対パスで設定されており、
module/{モジュール名}/view
に設定されています。

template_map

template_mapは、リクエストに対して表示するテンプレートを指定します。
通常、テンプレートは、ルーティング先のコントローラーに応じて、
{モジュール名}/{コントローラー名}/{アクション名}.phtml
という感じのテンプレートを検索してそれを表示します。
この場合、ルーティング先のコントローラー・アクションとテンプレートのパスが名前規則で結びついた形になります。
そこで、template_mapで、任意のテンプレートを個別に指定することが可能です。

template_mapの設定は配列になっており、キーに{モジュール名}/{コントローラー名}/{アクション名}を指定し、
それに対して表示するテンプレートのパスをその値に設定します。
リクエストに対するルーティング先のモジュール・コントローラー・アクションの名前に一致する設定が存在する場合、その設定を採用し、
一致する設定が存在しない場合は名前規則によりテンプレートが決定されます。

また、layout/layoutというキーに対する設定はレイアウトテンプレートとして扱われます。
error/404は404 not foundエラー発生時に表示するテンプレート、
error/indexは例外エラー発生に表示するテンプレートを設定します。

not_found_template

not_found_templateは、404 not foundエラー発生時に表示するテンプレートのキーを設定します。
キーとは、template_mapの設定のキーを表し、template_mapの設定中に存在するキーを指定することにより、
そのキーに対して設定したテンプレートが404 not found時に表示されるテンプレートということになります。
なので、404 not found時のテンプレートは、not_found_templateの設定と、template_mapの設定の組み合わせで決まると言えます。

exception_template

exception_templateは、例外エラー発生時に表示するテンプレートのキーを設定します。
キーとは、template_mapの設定のキーを表し、template_mapの設定中に存在するキーを指定することにより、
そのキーに対して設定したテンプレートが例外エラー時に表示されるテンプレートということになります。
なので、例外エラー時のテンプレートは、exception_templateの設定と、template_mapの設定の組み合わせで決まると言えます。

レイアウト

大抵のサイトでは、サイト内の全ページで共通のデザイン、レイアウトになっています。
サイトタイトル、ロゴ、グローバルメニュー、フッターなどがそうです。

旧来の手法としては、共通部分を記述したHTMLを、ヘッダー、フッター、メニューというように個別に別ファイルとして部品化し、
各ページのHTMLではPHPを埋め込み、これらの部品を適切な箇所に読み込むというような事が考えられます。

Zend2では、同じような事を実現する仕組みがレイアウトです。
上記の手法とは少し考え方が異なりますが、レイアウトは大枠の共通部分のテンプレートです。
そして各ページのコンテンツ部分をレイアウト内部に埋め込む様なイメージになります。
なので各ページのテンプレートはレイアウト部分を全く意識すること無く、コンテンツの部分だけを記述するファイルになります。

modules/Application/view/layout/layout.phtml
<?php echo $this->doctype(); ?>

<html lang="en">
    <head>

~~~(中略)~~~

    </head>
    <body>
        <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
            <div class="container">
                <div class="navbar-header">
                    <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                        <span class="icon-bar"></span>
                    </button>
                    <a class="navbar-brand" href="<?php echo $this->url('home') ?>"><img src="<?php echo $this->basePath('img/zf2-logo.png') ?>" alt="Zend Framework 2"/> <?php echo $this->translate('Skeleton Application') ?></a>
                </div>
                <div class="collapse navbar-collapse">
                    <ul class="nav navbar-nav">
                        <li class="active"><a href="<?php echo $this->url('home') ?>"><?php echo $this->translate('Home') ?></a></li>
                    </ul>
                </div><!--/.nav-collapse -->
            </div>
        </nav>
        <div class="container">
            <?php echo $this->content; ?>
            <hr>
            <footer>
                <p>© 2005 - <?php echo date('Y') ?> by Zend Technologies Ltd. <?php echo $this->translate('All rights reserved.') ?></p>
            </footer>
        </div> <!-- /container -->
        <?php echo $this->inlineScript() ?>
    </body>
</html>

これはスケルトンアプリケーションのデフォルトのレイアウトテンプレートです。この中に、

<?php echo $this->content; ?>

という箇所がありますが、ここに各ページのコンテンツが埋め込まれます。

例えば、トップページのテンプレートを見てみましょう。

modules/Ec/view/ec/index/index.phtml
<div class="jumbotron">
    <h1><?php echo sprintf($this->translate('Welcome to %sZend Framework 2%s'), '<span class="zf-green">', '</span>') ?></h1>
    <p><?php echo sprintf($this->translate('Congratulations! You have successfully installed the %sZF2 Skeleton Application%s. You are currently running Zend Framework version %s. This skeleton can serve as a simple starting point for you to begin building your application on ZF2.'), '<a href="https://github.com/zendframework/ZendSkeletonApplication" target="_blank">', '</a>', \Zend\Version\Version::VERSION) ?></p>
    <p><a class="btn btn-success btn-lg" href="https://github.com/zendframework/zf2" target="_blank"><?php echo $this->translate('Fork Zend Framework 2 on GitHub') ?> »</a></p>
</div>

<div class="row">

    <div class="col-md-4">
        <div class="panel panel-default">
            <div class="panel-heading">
                <h3 class="panel-title"><?php echo $this->translate('Follow Development') ?></h3>
            </div>
            <div class="panel-body">
                <p><?php echo sprintf($this->translate('Zend Framework 2 is under active development. If you are interested in following the development of ZF2, there is a special ZF2 portal on the official Zend Framework website which provides links to the ZF2 %swiki%s, %sdev blog%s, %sissue tracker%s, and much more. This is a great resource for staying up to date with the latest developments!'), '<a href="http://framework.zend.com/wiki/display/ZFDEV2/Home">', '</a>', '<a href="http://framework.zend.com/zf2/blog">', '</a>', '<a href="https://github.com/zendframework/zf2/issues">', '</a>') ?></p>
                <p><a class="btn btn-success pull-right" href="http://framework.zend.com/zf2" target="_blank"><?php echo $this->translate('ZF2 Development Portal') ?> »</a></p>
            </div>
        </div>
    </div>

    <div class="col-md-4">
        <div class="panel panel-default">
            <div class="panel-heading">
                <h3 class="panel-title"><?php echo $this->translate('Discover Modules') ?></h3>
            </div>
            <div class="panel-body">
                <p><?php echo sprintf($this->translate('The community is working on developing a community site to serve as a repository and gallery for ZF2 modules. The project is available %son GitHub%s. The site is currently live and currently contains a list of some of the modules already available for ZF2.'), '<a href="https://github.com/zendframework/modules.zendframework.com">', '</a>') ?></p>
                <p><a class="btn btn-success pull-right" href="http://modules.zendframework.com/" target="_blank"><?php echo $this->translate('Explore ZF2 Modules') ?> »</a></p>
            </div>
        </div>
    </div>

    <div class="col-md-4">
        <div class="panel panel-default">
            <div class="panel-heading">
                <h3 class="panel-title"><?php echo $this->translate('Help & Support') ?></h3>
            </div>
            <div class="panel-body">
                <p><?php echo sprintf($this->translate('If you need any help or support while developing with ZF2, you may reach us via IRC: %s#zftalk on Freenode%s. We\'d love to hear any questions or feedback you may have regarding the beta releases. Alternatively, you may subscribe and post questions to the %smailing lists%s.'), '<a href="irc://irc.freenode.net/zftalk">', '</a>', '<a href="http://framework.zend.com/wiki/display/ZFDEV/Mailing+Lists">', '</a>') ?></p>
                <p><a class="btn btn-success pull-right" href="http://webchat.freenode.net?channels=zftalk" target="_blank"><?php echo $this->translate('Ping us on IRC') ?> »</a></p>
            </div>
        </div>
    </div>
</div>

見て分かる通り、コンテンツのテンプレートとしては、共通部分は記述されておらず、まさにレイアウトのコンテンツ部分のみが記述されています。
なので、実際にはレイアウトのテンプレートを好みに書き換えることから始めるとよいでしょう。

レイアウトの設定

スケルトンアプリケーションでは
modules/{モジュール名}/view/layout/layout.phtml
がレイアウトテンプレートになっています。

この設定については上で説明している通り、module.config.phpにあります。
これを書き換えることでテンプレートのファイルのパスは自由に設定することが出来るというわけです。

レイアウトの切り替え

復数のレイアウトを用意し、ページによって切り替えたい場合などがあると思います。
例えばログインページ専用のテンプレートだとか、管理者用画面のテンプレートだとか。

この場合、コントローラーで指定することで、module.config.phpに設定されているテンプレートとは異なるものをレイアウトとすることが可能です。

まずは、module.config.phpにtemplate_mapの設定を追加する事によって、新しいレイアウトテンプレートの存在をフレームワークに伝える必要があります。

module/Ec/config/module.config.php
    'view_manager' => array(

        'template_map' => array(
            'layout/layout'           => __DIR__ . '/../view/layout/layout.phtml',
            'layout/layout2'         => __DIR__ . '/../view/layout/layout2.phtml',      // ←追加
            'ec/index/index' => __DIR__ . '/../view/ec/index/index.phtml',
            'error/404'               => __DIR__ . '/../view/error/404.phtml',
            'error/index'             => __DIR__ . '/../view/error/index.phtml',
        ),

    ),

そしてコントローラーで、追加したレイアウトのキーを指定します。

class ListController extends AbstractActionController
{
    public function indexAction()
    {
        // レイアウトの指定
        $this->layout('layout/layout2');
        // または
        $this->layout()->setTemplate('layout/layout2');

        return new ViewModel();
    }	
}

こうすれば、アクションごとに異なるレイアウトを使用することが可能になります。

レイアウトの無効化

場合によっては、特定のページではレイアウトを使用したくない場合もあるかもしれません。
この場合は以下のようにします。

class ListController extends AbstractActionController
{
    public function indexAction()
    {
        $view = new ViewModel();

        // レイアウトの無効化
        $view->setTerminal(true);

        return $view;
    }	
}

テンプレートの指定

上のように、ルーティング先のアクションとの名前規則またはmodule.config.phpでの設定により、コントローラーではレイアウトのファイルの指定など無しにテンプレートが読み込まれるわけですが、どうしてもコントローラーからテンプレートを指定したい場合などは発生すると思います。
例えば、処理結果によって表示するテンプレートを分けたい、という場合など。

それにはコントローラーで以下のようにします。

class ListController extends AbstractActionController
{
    public function indexAction()
    {
        $view = new ViewModel();

        // レイアウトの無効化
        $view->setTemplate('index/hoge');

        return $view;
    }	
}

setTermplateに指定するのは、
{モジュール名}/{コントローラー名}/{アクション名}
か、module.config.phpのtemplate_maps内に存在するキーかどちらかです。
指定されたキーがtemplate_mapsに存在しない場合は、
前者に合致するファイルがviewディレクトリ内に存在すればそれを表示します。