IBM®
本文へジャンプ
    Japan [変更]    ご利用条件
 
 
検索範囲検索:    
    ホーム    製品    サービス & ソリューション    サポート & ダウンロード    マイアカウント    
skip to main content

developerWorks Japan  >  Web development | Open source  >

「Zend Framework」で加速するPHP開発: 第3回 ブログツールで学ぶ実践的Zend Frameworkの使い方

developerWorks
ページオプション

JavaScript を要するドキュメントオプションは表示されません


レベル: 初級

杉田 直哉, Writer, ITmedia

2007年 3月 30日

今回は、Zend Frameworkで構築されたブログツールを題材にして、実際にZend Frameworkに備わる機能や使い方を解説します。題材とするブログツールは、Alexatnet.comで提供されている「Alex@Net Blog」です。

今回は、Zend Frameworkで構築されたブログツールを題材にして、実際にZend Frameworkに備わる機能や使い方を解説します。題材とするブログツールは、Alexatnet.comで提供されている「Alex@Net Blog」です。

Alex@Net Blogについて

Alex@Net Blogは、Zend Frameworkと同じNew BSD Licenseで公開されているシンプルなブログです(図1)。


図1Alex@Net Blogを実行したところ

図1をご覧になれば分かるとおりシンプルなデザインで、機能も「ブログの作成/編集/削除」、「ブログの閲覧」、「Atomフィードの配信」のみを備える非常に簡単なツールとなっています。このようにシンプルなツールは、実際に利用する上では物足りなく感じるかもしれませんが、逆にいえば、実装を理解してカスタマイズのベースとするにはかなり有用です。

ここでは、Alex@Net Blogを題材として、Zend Frameworkをどんなふうに使えば良いか解説しましょう。Alex@Net Blogは、次のようなコンセプトで実装されていて、Zend Frameworkの使い方が簡単に学べるようになっています。

  • Zend Frameworkコントローラの使い方の例示
  • Zend_Db_Tableを使用したデータベースデザインの例示
  • View/Model/Controllerパターンの例示
  • Atomフィードの使い方の例示

Alex@Net BlogのパッケージにはZend Frameworkが含まれており、インストールも容易です。データベースアクセスに使用するPDO_SQLiteの拡張モジュールもPHP 5では標準となっているので、追加インストールも必要ありません。





上に戻る


Alex@Net Blogの導入

まずは、Alex@Net Blogをインストールして、試しに動かしてみましょう。Alex@Net Blogをインストールするには、まずダウンロードしたzipファイルを展開し、「blog/htdocs」ディレクトリ以下にあるファイルをDocumentRootに配置してください。

次に、連載第1回の「httpd.confの設定」で説明したDirectoryディレクティブのAllowOverrideに、「Indexes」を追加する必要があります(リスト1)。これは、index.phpと同じディレクトリ階層にある.httaccessが、DirectoryIndexディレクティブを含むためです。逆にいえば、httpd.confを編集したくない場合には.httaccessを編集して、DirectoryIndexディレクティブを削除するだけでも構いません。以上で、インストールは終了です。


リスト1 httpd.confのDirectoryディレクティブに「Indexes」を追加する
                
<Directory>
    Options FollowSymLinks
    AllowOverride FileInfo Options Indexes
</Directory>

http://localhost/にアクセスすれば、先ほどの図1のようなページが表示されるでしょう。このサンプルブログには、すでに1件のブログエントリが登録されていますが、実際のデータは、「blog/htdocs/_/database」ディレクトリに存在するSQLiteデータベースのblogファイルが使われています。

うまくページが表示されない場合は、PHPランタイムかApacheの設定に問題があると考えられます。httpd.confの設定などを見直してください。また、「blog/htdocs/_/database/sqls」ディレクトリ以下に、データベースセットアップ用のSQLが用意されています(SQLはSQLite/MySQL用です)。データを初期化したい場合などは、このSQLを改造して使うことができます。





上に戻る


Alex@Net Blogの内部構造

Alex@Net Blogは、Routerとして「Zend_Controller_Router」を使用しています。そのため、「http://ホスト名/コントローラ名/アクション名」としてURIが解釈され、index.phpにrewriteされます(図2)。また、httpd.confで設定しているDocumentRoot直下にindex.phpを配置する必要があります。


図2Alex@Net BlogのアクションとURI

フロントコントローラ

フロントコントローラはリスト2のようになっています。処理の内容を見てみましょう。


リスト2 フロントコントローラのコード
                
set_include_path('./_/library/');
require_once 'config.php';
require_once 'Zend/Controller/Front.php';
Zend_Controller_Front::run('./_/application/controllers');

まず、set_include_pathには、Alex@Net Blogのディレクトリツリー内部にあるZend Frameworkのディレクトリが指定されています。アクセスのたびにindex.phpの処理が行われるので、Zend Frameworkは、必ずPHPのinclude_pathに入るようになります。

次に、require_onceで、アプリケーションの設定が記述されているconfig.phpを読み込みます。config.php内にConfigクラスが定義され、ディスパッチ先のコントローラでオブジェクトとして扱われます。また、その次で、フロントコントローラとなるFront.phpも読み込まれています。

最後に、Zend_Controller_Front::runでルーティングとディスパッチが行われます。このときのrunメソッドに指定されている引数は、アクションコントローラを置いてあるディレクトリです。ディスパッチの際に、該当するアクションが存在するかどうか調べるので、そのためにディレクトリを引数として指定するのです。

このルーティングは、Zend_Controller_RewriteRouterで表現するとリスト3のようになります。こちらの方がより冗長な記述になっていますが、複数のルーティングパターンを設定可能など、柔軟な制御が行えます。


リスト3 Zend_Controller_RewriteRouterで表現したリスト2と同等のコード
                
 $router = new Zend_Controller_RewriteRouter();
$router->addRoute('user', ':controller/:action', array('controller' => 'Index', 'action' 
=> 'Index'));
$ctrl = Zend_Controller_Front::getInstance();
$ctrl->setRouter($router);
$ctrl->setControllerDirectory('./application/controllers')->dispatch();

アクションコントローラ

Alex@Net Blogのアクションコントローラは、3つ用意されています。1つ目は、間違ったURIにアクセスされた場合など、イレギュラーな処理に対処するためのIndex Controller。2つ目は、ブログのトップページなどを表示するBlog Controller。3つ目は、ブログ管理者だけがアクセスできるAdmin Controllerです。それぞれ詳しく見ていきましょう。

Index Controller

前述したように、Alex@Net Blogへのアクセスは「http://ホスト名/コントローラ名/アクション名」というURIで行いますが、URIを「http://ホスト名/」などとして、アクションを指定せずにアクセスされる場合があります。また、不正なコントローラがURIへ指定される場合もあります。このような場合に、代わりにディスパッチされるコントローラがIndex Controllerです。

「http://ホスト名/」を指定された場合は、「http://ホスト名/Blog/Index」へリダイレクトされ、存在しないコントローラが指定された場合には、HTTP/1.1 404 Not foundのページが表示されます。ただし、「http://ホスト名/Index/route」と入力すると、Fatal ErrorのExceptionが表示されてしまいます。

存在しないアクションが指定された場合に、もっと気の利いたページを表示するには、例えばリスト4のように、Zend_Controller_Actionの__callメソッドをオーバーライドして、独自の実装を行う必要があります。


リスト4存在しないアクションが指定された場合の処理をカスタマイズ
                
require_once 'Zend/Controller/Action.php';
require_once 'Zend/View.php';
class IndexController extends Zend_Controller_Action
{
    public function indexAction()
    {
        $this->_redirect('/Blog/Index/');
    }
        public function noRouteAction()
    {
        header('HTTP/1.1 404 Not found');
        $view = new Zend_View();
        $view->setScriptPath('./_/application/views');
        $view->pageTitle = '404 error';
        echo $view->render('Pages/ServicePages/404.php');
    }

    public function __call($methodName, $args)
    {
        $this->_redirect('/Blog/Index/');
    }
}

Blog Controller

Blog Controllerは、Blogを表示するIndexアクションと、Atomフィード*を配信するAtomアクションで構成されます。

1:Indexアクション

Indexアクションのコードはリスト5のようになっています。ここではまず、「$this->_getAllParams()」によって、URIで指定されているキーとその値を取得しています。if文の最初の条件節であるpreg_matchは、「http://ホスト名/Blog/Index/2006-06-06/lorem-ipsum」のように、「Indexアクションのキーが日付であるかどうか」を判別しています。


リスト5 Indexアクションのコード
                
foreach ($this->_getAllParams() as $key => $value)
{
    if (preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $key))
    {
        $date = strtotime($key);
        $name = $value;
    }
    else if ('Page' == $key)
    {
        $page = (int)$value;
    }
    else
    {
        $this->_redirect('/Blog/Index/');
    }
}

このブログでは、Permalink*のURIを投稿日と簡略タイトルにすることで、各エントリを一意に判別するようにしているからです(同一日に同一タイトルの投稿がある場合は一意になりませんが、通常のブログ使用の範囲で問題は起こらないということでしょう)。

また、キーにPageが指定された場合も値が取得され、$page変数に代入されます。そのほか、想定外のキーが指定されていた場合は、Indexアクションに再びリダイレクトして受け付けないようになっています。

リスト6の処理では、Model層のオブジェクトを呼び出してデータベースへ問い合わせています。$dateがnullの場合はエントリの一覧表示、nullでない場合はPermalinkの問い合わせです。


リスト6 IndexアクションでModel層のオブジェクトを呼び出して、データベースへ問い合わせ
                
if (null == $date)
{
    $model = BlogModel::GetPosts(Config::getInstance()->DefaultBlog, $tags, $page);
    $model['page'] = $page;
    $p =& $model['posts'];
    $model['showPrevLink'] = (0 < count($p) & !$p[count($p) - 1]['is_last']);
    $model['showNextLink'] = $page > 1;
}
else
{
    $model = BlogModel::GetPost(Config::getInstance()->DefaultBlog, array($date, $name));
    $model['page'] = null;
}

リスト7のコードでは、Zend_Viewオブジェクトを生成してビュースクリプトの存在するディレクトリをsetScriptPathで指定、ヘルパーの存在するディレクトリをaddHelperPathで指定します。データベースから取得し、各変数値を整えた$model配列がまとめてassignされ、それとは別にpageTitleへ個別の変数が代入されています。最後は、renderによって指定されたビュースクリプトがレンダリングされます。


リスト7 Zend_Viewオブジェクトを生成からレンダリングまで
                
$view = new Zend_View();
$view->setScriptPath('./_/application/views');
$view->addHelperPath('./_/application/views/Helpers');
$view->assign($model);
if (null == $date)
{
    $view->pageTitle = $model['title'] . ' - Blog';
}
else
{
    $view->pageTitle = $model['title'] . ' - Blog - ' . $model['posts'][0]['title'];
}
echo $view->render('Pages/BlogView.php');

2:Atomアクション

Atomを生成するためのコードには、独自のライブラリが使用されています。現時点のZend_Feedでは、単純なエントリを作成する機能はあっても、複数のエントリを持つフィードを作成する機能を備えていないからです。

Zend_Feed_Atom が継承しているZend_Feed_Abstractのコンストラクタでは、URIまたは文字列でフィードを読み込むことを想定しており、空のフィードを生成するとFatal Errorが表示されます。PHPのDom関数などを使用して、Atom仕様に従ったXML文書を作成するか、Alex@Net Blogのように独自にライブラリを用意する必要があります。

このAtomアクションによって配信されるAtomフィードは、Zend_Feed_Atomクラスの引数に渡すことによって、リスト8のように読み取ることが可能です。Zend_Feed_Abstractは、読み取ったフィードをDomDocumentにするので、Atom仕様に従ったDOMアクセスでAtomエントリの各プロパティを参照できます。


リスト8 Atomフィードの読み取り
                
<?php
    $feed = new Zend_Feed_Atom('http://ホスト名/Blog/Atom');
    $entry = $feed->current();
    echo 'タイトル:'.$entry->title.'<br />';
    echo 'コンテンツ:'.$entry->content.'<br />';
?>

Admin Controller

Admin Controllerはリスト9のようなコードになっています。基本的にBlog Controllerと同じように考えれば良いのですが、postアクションではHTMLフォームの変数を受け取ってデータベースへ更新処理を行っています。


リスト9 Admin Controllerのコード
                
if ('POST' == $_SERVER['REQUEST_METHOD'])
{
    $post = array();
    $post['title'] = isset($_POST['title']) ? $this->_stripslashes($_POST['title']) : '';
    if ('' == $post['title'])
    {
        $data['errors'][] = 'Title is required.';
    }
    $post['content'] = isset($_POST['content']) 
? $this->_stripslashes($_POST['content']) : '';
    if ('' == $post['content'])
    {
        $data['errors'][] = 'Content is required.';
    }
    $post['tags'] = array();
    $tags = isset($_POST['tags']) ? $_POST['tags'] : '';
    foreach ($tags as $tag)
    {
        if ('' != $tag)
        {
            $post['tags'][] = $this->_stripslashes($tag);
        }
    }
    if (0 == count($data['errors']))
    {
        if (isset($_POST['delete']))
        {
            BlogData::DeletePost($id);
        }
        else if (null == $id)
        {
            BlogData::CreatePost(2, $post);
        }
        else
        {
            BlogData::UpdatePost($id, $post);
        }
        $this->_redirect('/Admin/Posts/');
    }
    $data['post'] = $post;

「if ('POST' == $_SERVER['REQUEST_METHOD'])」では、HTTPメソッドがPOSTの場合にForm変数が引き渡されたと解釈し、各POST変数に値が入っていない場合は「$data['errors']」にアラートを出力するようにエラーメッセージを代入しています。

1つでもPOST変数が不足していた場合は更新処理が行われず、再び同じビュースクリプトが読み込まれて、「$data['errors']」の内容も表示されます。変数の有無によってDELETE/UPDATE/INSERTの処理がデータベースに対して行われ、最後に「/Admin/Posts/」へリダイレクトされます。

ビュースクリプト

ビュースクリプトは、大きく分けてHTMLの全体構造を格納したBlogViewと、管理画面用のAdminViewの2つに分かれます。

ブログ閲覧用HTMLを出力するBlogView

前述したように、Blog ControllerではビュースクリプトとしてBlogView.phpが指定されていました。BlogView.phpは、ブログ全体のレイアウトを決定するもので、HTMLのdivタグで、ヘッダ/フッター/サイドバー/コンテンツといった4つの部分に分け、中身はそれぞれ個別のPHPスクリプトへ委譲しています(リスト10)。このように、ビュースクリプトの中でさらに別のビュースクリプトをrenderで読み込む場合でも、assignされた変数などはそのまま受け継がれます。


リスト10 HTMLの全体構造が格納されたBlogView.php
                
<div id="page">
  <div id="header">
    <?php echo $this->render('Pages/Blog/Header.php'); ?>
  </div>
  <div id="sidebar">
    <div id="sidebarInt">
      <?php echo $this->render('Pages/Blog/Sidebar.php'); ?>
    </div>
  </div>
  <div id="content">
    <div id="contentInt">
      <?php echo $this->render('Pages/Blog/Index.php'); ?>
    </div>
  </div>
  <div>
    <?php echo $this->render('Pages/Blog/Footer.php'); ?>
  </div>
</div>

コンテンツを表示する「Pages/Blog/Index.php」では、assignされた$model配列に含まれるposts変数に対して、foreach文の処理が行われています(リスト11)。posts変数にはブログの各エントリ(=post)が配列で代入されているので、各エントリの表示がプレゼンテーションロジックを通してこのスクリプト内で行われるわけです。


リスト11 コンテンツを表示する「Pages/Blog/Index.php」のコード
                
<?php
  foreach ($this->posts as $post) {
?>
<div class="entry"><h2><?php echo $post['title']; ?></h2>
<div class="info"><div class="date">Written on 
<?php echo date('F d, Y', $post['published']); ?>
                        :
                        :

管理画面を出力するAdminView

AdminView.phpでは、ログイン画面の表示しか行っていません(リスト12)。ログイン後の画面は「Pages/Admin/Posts.php」のビュースクリプトでレンダリングされます。「Pages/Admin/Post.php」は、Admin Controllerの項で説明したHTMLフォームからの受け渡しを行うビュースクリプトとなります。


リスト12AdminViewのコード
                
<form method="post">
<?php
  foreach ($this->errors as $error)
  {
    echo '<span style="color: red;">' . $error . '</span><br />';
  }
?>
Title:
<?php
  echo $this->formText('title', $this->post['title'], array('size' => 60));
?>
<br />
Content:<br />
<?php
  echo $this->formTextarea('content', $this->post['content'], array('cols' => 60, 'rows' 
=> 20));
?>
<br />
Tags:
<?php
  for($i = 0; $i < 5; $i++)
  {
    echo $this->formText('tags[]', $this->post['tags'][$i]);
  }
?>
<br />
<input type="submit" id="save" name="save" value="save" />
<input type="submit" id="delete" name="delete" value="delete" 
onClick="return confirm('Are you sure?');" />
</form>

このHTMLフォームは、コントローラの中で指定したヘルパーを利用しています。「$this->formText」のように参照して、idとなる名称と編集の場合に表示されるvalue、フォームのサイズをarrayで渡しています。

モデル

コントローラやビュースクリプトはHTMLのページごとに1つずつ用意されるのが一般的です。しかし、モデルの場合は規約も少なく、「こう記述すべきだ」という点は少ないです。Alex@Net Blogでは、データベースへ接続し、照会や更新を行う処理はDatabase.phpに記述され、最終的にビュースクリプトへassignされる$model配列を生成する処理が、BlogModel.php内に記述されています。

データベースへの接続/照会/更新を行うDatabase

Database.phpには、Zend_Db_Tableを継承するクラスとしてDbObjectが定義され、データベースにはこれに対応するobject表が定義されています(リスト13)。ブログデータのほとんどはこの表の1レコードとなり、ドメインがusersのデータはusers表、blogsのデータはblogs表といったように区別されます。


リスト13 Zend_Db_Tableを継承するクラスとしてDbObjectを定義
                
class DbObject extends Zend_Db_Table
{
    protected static function _getDomainID($domain)
    {
        assert('"string" == gettype($domain)');
        switch ($domain)
        {
            case 'users': return 1;
            case 'blogs': return 2;
            case 'posts': return 3;
        }
        throw new Exception('Undefined domain');
    }

各ドメインのクラスは、DbObjectクラスを継承して定義されます。ベースとしてZend_Db_Tableの機能を使用し、共通に使用される拡張メソッドがDbObjectクラス内でオーバーライドされています。各ドメインのデータがinsertされる際、objectと各ドメインのidの値が同じになるように、まずobject表へinsertしてid列の値を自動生成します。その後、Zend_Db_AdapterのlastInsertIdメソッドでid列の値を取得して、それを各ドメイン表のid列値としてinsertしています。

class DbUsers extends DbObject
{
    protected $_name = 'users';
    function _getName($params)
    {
        return $params['login'];
    }
}

このように更新処理は主にZend_Db_Tableの機能を利用して行っていますが、照会処理の方はBlogDataクラス内でZend_Db_Adapterを直接利用しています(リスト14)。まず、引数で渡された変数が想定されたデータ型であるかを、assertでチェックしています。その後、Zend_Db_Selectオブジェクトを生成します(引数の有無によって実行するSELECT文を変更するように作成しています)。最後にfetchAllで全行フェッチした後、変数を整えて返しています。


リスト14照会処理のコード
                
public static function findPosts($id, $tags = null, $limit = null, $offset = null)
{
    assert('"integer" == gettype($id)');
    assert('null == $tags || "array" == gettype($tags)');
    assert('null == $limit || "integer" == gettype($limit)');
    assert('null == $offset || "integer" == gettype($offset)');

    $select = self::$db->select();
    $select->from('posts p', 'p.id, p.title, p.content, p.published')
        ->join('objects o', 'p.id = o.id', 'o.name')
        ->join('users u', 'o.owner = u.id', 'u.login')
        ->order('p.published DESC');



    if (null == $tags)
    {
        $select->where('p.draft = 0 AND p.blog = ?');
    }
    else
    {
        $select->distinct()
            ->join('object_tags ot', 'o.id = ot.object')
            ->join('tags t', 't.id = ot.tag')
            ->where('p.draft = 0 AND p.blog=? AND ' . self::$db
            ->quoteInto('t.name IN (?)', $tags));
    }

    if (null != $limit)
    {
        if (null != $offset)
        {
            $select->limit((int)$limit + 1, (int)$offset);
        }
        else
        {
            $select->limit((int)$limit + 1);
        }
    }
}

$rowset = self::$db->query($select, $id);
$rows = $rowset->fetchAll();

ブログデータの配列を生成するBlogModel

リスト14のfindPostsメソッドを呼び出しているのは、BlogModel.phpに定義されたGetPostsメソッドです(リスト15)。このメソッドはBlog Controller内で呼ばれ、最終的に返している$blog変数はビュースクリプトへassignされる$model配列です。Database.phpと同様に、ここでもassertによってデータ型のチェックを行っています。

BlogDataクラスのfindBlogWithNameメソッドによって、対象のブログが存在するか確認した後、findPostsメソッドでタグ*とエントリの表示範囲(現在のページ$pageに対して7件表示する)を引き渡します。返ってきた配列を$blog['posts']に代入して、ブログのエントリ(=post)ごとに変数の値を整えます。


リスト15 BlogModelのGetPostsメソッド
                
public static function GetPosts($blog, $tags, $page)
{
    assert('"string" == gettype($blog)');
    assert('"array" == gettype($tags)');
    assert('"integer" == gettype($page)');

    $blog = BlogData::findBlogWithName($blog);
    if (null == $blog)
    {
        return null;
    }
    $blog['posts'] = BlogData::findPosts((int)$blog['id'], $tags, 7, ($page - 1) * 7);
    foreach ($blog['posts'] as & $post)
    {
        $post['tags'] = BlogData::findTags((int)$post['id']);
        $post['url'] = '/Blog/Index/' . date('Y-m-d', 
								$post['published']) . '/' . $post['name'];
    }
    return $blog;
}




上に戻る


まとめ

今回は、ブログツールを例に、Zend Framework独特の書き方を紹介しました。ここでは特に、どんなアプリケーションでも使用するような機能を取り上げたつもりです。

必ずしもこれがベストというわけではありませんが、やはりフレームワークの使い方を覚えるには、使用例を見ていくことが近道といえるでしょう。本稿で取り上げたアプリケーションが単純すぎると思われた方は、例えば「データベースアクセスを減らすためにZend_Cacheを利用して$modelをキャッシュする」といったように、Zend Frameworkのほかの機能をつけ足したりして自分なりに書き換えてみてください。特にモデル層は、より効率良く書き換える余地がありそうです。




上に戻る


このページで出てきた専門用語

Atomフィード
ほとんどのブログツールやサイトでは、サイトの更新情報を「RSS」や「Atom」といったXMLで配信します。Atomは、更新情報を配信するためのフォーマットで、「Atomフィード」は更新情報が格納されたファイルを指します。Atomの仕様はこちらで参照できます。
Permalink
「パーマリンク」と読み、ブログに書かれたエントリのそれぞれを固有に指し示すURIのことです。「永続リンク」などとも呼びます。
タグ
findPostsメソッド内のSELECT文で、tags表に該当するタグが存在しないか条件節を追加する「self::$db->quoteInto('t.name IN (?)', $tags)」の部分に適用されます。


参考文献



著者について

杉田直哉,ITmedia




記事の評価


サイト改善のため、ご意見をお寄せください。こちらのフォームからお願いいたします。



はいいいえわからない
 


 


54321
大変素晴らしい不充分・不完全である
 


上に戻る


    日本IBMについて プライバシー お問い合わせ