FuelPHP での「管理パネル」チュートリアル

この記事は FuelPHP 1.1 を前提にしています。また、Build an Admin Panel with the Fuel PHP Framework を基にしています。

このチュートリアルでは、ブログの管理ページを作成し管理者が記事を投稿できるようにし、フロントエンドの記事一覧ページと個別の記事表示ページを作成し、読者が記事にコメントを投稿できるようにします。

FuelPHP の oil コマンドでコードを生成し、ORM を使います。

FuelPHP のインストール

最初に oil コマンドをインストールします。

$ curl get.fuelphp.com/oil | sh

上記のコマンドは、/usr/bin/ に oil コマンドをインストールします。

続いて、FuelPHP をインストールします。

$ oil create blog

上記のコマンドは、カレントフォルダに blog フォルダを作成し、その中に FuelPHPリポジトリGitHub から clone します。

Note: コマンドラインを使わずに 手動でインストールする方法 もあります。

Apache の設定

blog/public/ を http://localhost/blog/ でアクセスできるように Apache を設定します。

http://localhost/blog/ に Web ブラウザでアクセスすると以下のように表示されるはずです。


データベースの設定

MySQL にデータベースを作成します。

CREATE DATABASE `blog_example` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

fuel/app/config/development/db.php を以下のように変更します。

<?php
/**
 * The development database settings.
 */

return array(
	'default' => array(
		'type'			=> 'mysql',
		'connection'	=> array(
			'hostname'		=> 'localhost',
			'port'			=> '3306',
			'database'		=> 'blog_example',
			'username'		=> 'root',
			'password'		=> '',
		),
		'table_prefix' => 'ap_',
	),
);

FuelPHP のデフォルトのデータベースドライバは pdo ですが、ここでは mysql に変更しています。

FuelPHP の設定

FuelPHP の設定ファイルである config.php を変更します。

oil コマンドで生成するコードが、ビューに Validation オブジェクトを渡すため、Fuel\Core\Validation クラスを security の whitelisted_classes に追加します。

Auth、Orm の 2つのパッケージを使うため、always_load の packages に設定します。

--- a/fuel/app/config/config.php
+++ b/fuel/app/config/config.php
@@ -136,6 +136,7 @@ return array(
            'Fuel\\Core\\Response',
            'Fuel\\Core\\View',
            'Fuel\\Core\\ViewModel',
+           'Fuel\\Core\\Validation',
            'Closure',
        )
    ),
@@ -185,7 +185,8 @@ return array(
         * );
         */
        'packages'  => array(
-           //'orm',
+           'auth',
+           'orm',
        ),
 
        /**

Note: 安易に whitelisted_classes に追加し、ビューの中でオブジェクトを使うと XSS 脆弱性ができる可能性がありますので、注意してください。

SimpleAuth の設定

Auth パッケージには、SimpleAuth というドライバが含まれています。その設定をします。

fuel/packages/auth/config/simpleauth.php を fuel/app/config/simpleauth.php にコピーします。

そして、fuel/app/config/simpleauth.php の groups を以下のように変更します。この設定は、コメントアウトされている Example そのままです。これは、グループの定義になります。

	'groups' => array(
		-1   => array('name' => 'Banned', 'roles' => array('banned')),
		0    => array('name' => 'Guests', 'roles' => array()),
		1    => array('name' => 'Users', 'roles' => array('user')),
		50   => array('name' => 'Moderators', 'roles' => array('user', 'moderator')),
		100  => array('name' => 'Administrators', 'roles' => array('user', 'moderator', 'admin')),
	),

User モデルの作成

ここから、oil コマンドを使って、コードを生成していきます。

まず、User モデルと users テーブルを作成します。引数は users とします。

$ oil generate model users username:varchar[50] password:string group:int email:string last_login:int login_hash:string profile_fields:text

以下のファイルが生成されます。

  • fuel/app/classes/model/user.php
  • fuel/app/migrations/001_create_users.php

fuel/app/classes/model/user.php:

<?php

class Model_User extends \Orm\Model
{
	protected static $_properties = array(
		'id',
		'username',
		'password',
		'group',
		'email',
		'last_login',
		'login_hash',
		'profile_fields',
		'created_at',
		'updated_at'
	);

	protected static $_observers = array(
		'Orm\Observer_CreatedAt' => array(
			'events' => array('before_insert'),
			'mysql_timestamp' => false,
		),
		'Orm\Observer_UpdatedAt' => array(
			'events' => array('before_save'),
			'mysql_timestamp' => false,
		),
	);
}

fuel/app/migrations/001_create_users.php:

<?php

namespace Fuel\Migrations;

class Create_users
{
	public function up()
	{
		\DBUtil::create_table('users', array(
			'id' => array('constraint' => 11, 'type' => 'int', 'auto_increment' => true),
			'username' => array('constraint' => 50, 'type' => 'varchar'),
			'password' => array('constraint' => 255, 'type' => 'varchar'),
			'group' => array('constraint' => 11, 'type' => 'int'),
			'email' => array('constraint' => 255, 'type' => 'varchar'),
			'last_login' => array('constraint' => 11, 'type' => 'int'),
			'login_hash' => array('constraint' => 255, 'type' => 'varchar'),
			'profile_fields' => array('type' => 'text'),
			'created_at' => array('constraint' => 11, 'type' => 'int'),
			'updated_at' => array('constraint' => 11, 'type' => 'int'),
		), array('id'));
	}

	public function down()
	{
		\DBUtil::drop_table('users');
	}
}

マイグレーションを実行し、実際にテーブルを作成します。

$ oil refine migrate

これで、データベースにテーブルが作成されました。


管理者の登録

oil console コマンドを使うと、コンソールから FuelPHP へコードを渡し実行することができます。この機能を使い、管理者を登録しておきます。

コンソールから Auth パッケージの create_user() メソッドを実行し、ユーザを作成します。

$ oil console
Fuel 1.1 - PHP 5.3.8 (cli) (Sep 19 2011 13:26:12) [Linux]
>>> Auth::create_user('admin', 'password', 'admin@example.jp', 100);
Warning - Cannot modify header information - headers already sent by (output started at .../blog/fuel/core/classes/cli.php:127) in COREPATH/classes/cookie.php on line 95
1
>>> ^C

コンソールからは Ctrl + C で抜けます。

管理ページの生成

FuelPHP 1.1 では、管理ページを生成する oil generate admin コマンドが追加されました。

このコマンドは、scaffold にユーザ認証を追加したコードを生成します。

これで、Post モデルと posts テーブル、関連するコントローラとビューを生成します。

$ oil generate admin posts title:string slug:string summary:text body:text user_id:int

生成されるファイルは以下の通りです。

Creating controller: .../blog/fuel/app/classes/controller/base.php
Creating controller: .../blog/fuel/app/classes/controller/admin.php
Creating views: .../blog/fuel/app/views/admin/template.php
Creating views: .../blog/fuel/app/views/admin/dashboard.php
Creating views: .../blog/fuel/app/views/admin/login.php
Creating migration: .../blog/fuel/app/migrations/002_create_posts.php
Creating model: .../blog/fuel/app/classes/model/post.php
Creating controller: .../blog/fuel/app/classes/controller/admin/posts.php
Creating view: .../blog/fuel/app/views/admin/posts/index.php
Creating view: .../blog/fuel/app/views/admin/posts/view.php
Creating view: .../blog/fuel/app/views/admin/posts/create.php
Creating view: .../blog/fuel/app/views/admin/posts/edit.php
Creating view: .../blog/fuel/app/views/admin/posts/_form.php
Creating view: .../blog/fuel/app/views/template.php

マイグレーションを実行し、実際にテーブルを作成します。

$ oil refine migrate

作成された管理ページを見てみる

http://localhost/blog/index.php/admin/posts にアクセスします。


http://localhost/blog/index.php/admin/login へリダイレクトされました。

先ほど作成した管理者の ID とパスワードでログインします。

Dashboard が表示されました。


Posts の一覧ページです。


Post を作成するページです。

記事投稿ページ (Post) の CRUD フォームを改良する

CRUD フォームを改良します。

slug フィールドを title から自動的に生成するようにし、user_id フィールドはユーザのリストをセレクトメニューから選択できるようにします。

Controller_Admin_Posts クラスの action_create() メソッドを変更します。

--- a/fuel/app/classes/controller/admin/posts.php
+++ b/fuel/app/classes/controller/admin/posts.php
@@ -29,7 +29,7 @@ class Controller_Admin_Posts extends Controller_Admin
            {
                $post = Model_Post::forge(array(
                    'title' => Input::post('title'),
-                   'slug' => Input::post('slug'),
+                   'slug' => Inflector::friendly_title(Input::post('title'), '-', true),
                    'summary' => Input::post('summary'),
                    'body' => Input::post('body'),
                    'user_id' => Input::post('user_id'),
@@ -53,9 +53,13 @@ class Controller_Admin_Posts extends Controller_Admin
            }
        }
 
-       $this->template->title = "Posts";
-       $this->template->content = View::forge('admin/posts/create');
+       $this->template->title = "Create Post";
+       $view = View::forge('admin/posts/create');
+
+       // Set some data
+       $view->set_global('users', Arr::assoc_to_keyval(Model_User::find('all'), 'id', 'username'));
 
+       $this->template->content = $view;
    }
 
    public function action_edit($id = null)

Note: Inflector::friendly_title() メソッドは日本語では使えません。

同じように action_edit() メソッドを変更します。

@@ -66,7 +70,7 @@ class Controller_Admin_Posts extends Controller_Admin
        if ($val->run())
        {
            $post->title = Input::post('title');
-           $post->slug = Input::post('slug');
+           $post->slug = Inflector::friendly_title(Input::post('title'), '-', true);
            $post->summary = Input::post('summary');
            $post->body = Input::post('body');
            $post->user_id = Input::post('user_id');
@@ -100,9 +104,13 @@ class Controller_Admin_Posts extends Controller_Admin
            $this->template->set_global('post', $post, false);
        }
 
-       $this->template->title = "Posts";
-       $this->template->content = View::forge('admin/posts/edit');
+       $this->template->title = "Edit Post";
+       $view = View::forge('admin/posts/edit');
+
+       // Set some data
+       $view->set_global('users', Arr::assoc_to_keyval(Model_User::find('all'), 'id', 'username'));
 
+       $this->template->content = $view;
    }
 
    public function action_delete($id = null)

ビューファイルを変更します。

--- a/fuel/app/views/admin/posts/_form.php
+++ b/fuel/app/views/admin/posts/_form.php
@@ -10,14 +10,6 @@
            </div>
        </div>
        <div class="clearfix">
-           <?php echo Form::label('Slug', 'slug'); ?>
-
-           <div class="input">
-               <?php echo Form::input('slug', Input::post('slug', isset($post) ? $post->slug : ''), array('class' => 'span6')); ?>
-
-           </div>
-       </div>
-       <div class="clearfix">
            <?php echo Form::label('Summary', 'summary'); ?>
 
            <div class="input">
@@ -37,7 +29,7 @@
            <?php echo Form::label('User id', 'user_id'); ?>
 
            <div class="input">
-               <?php echo Form::input('user_id', Input::post('user_id', isset($post) ? $post->user_id : ''), array('class' => 'span6')); ?>
+               <?php echo Form::select('user_id', Input::post('user_id', isset($post) ? $post->user_id : $current_user->id), $users, array('class' => 'span6')); ?>
 
            </div>
        </div>

最後に、Model_Post クラスの validate() メソッドで定義されているバリデーションのルールを変更します。slug はフォームからは投稿されませんのでコメントアウトしておきます。

--- a/fuel/app/classes/model/post.php
+++ b/fuel/app/classes/model/post.php
@@ -27,7 +27,7 @@ class Model_Post extends \Orm\Model
    {
        $val = Validation::forge($factory);
        $val->add_field('title', 'Title', 'required|max_length[255]');
-       $val->add_field('slug', 'Slug', 'required|max_length[255]');
+       //$val->add_field('slug', 'Slug', 'required|max_length[255]');
        $val->add_field('summary', 'Summary', 'required');
        $val->add_field('body', 'Body', 'required');
        $val->add_field('user_id', 'User Id', 'required|valid_string[numeric]');

わかりやすいように、ユーザを 1人追加しておきましょう。

$ oil console
Fuel 1.1 - PHP 5.3.8 (cli) (Sep 19 2011 13:26:12) [Linux]
>>> Auth::create_user('user1', 'password', 'user1@example.jp', 1);

http://localhost/blog/index.php/admin/posts/create にアクセスします。

User id がきちんとプルダウンになっています。

フロントエンドの作成

フロントエンドの記事一覧ページを作成します。

fuel/app/classes/controller/blog.php:

<?php

class Controller_Blog extends Controller_Base
{
	public function action_index()
	{
		$view = View::forge('blog/index');

		$view->posts = Model_Post::find('all');

		$this->template->title = 'My Awesome Blog';
		$this->template->content = $view;
	}
}

fuel/app/views/blog/index.php:

<h2>Recent Posts</h2>

<?php foreach ($posts as $post): ?>

	<h3><?php echo Html::anchor('blog/view/'.$post->slug, $post->title) ?></h3>

	<p><?php echo $post->summary ?></p>

<?php endforeach; ?>

http://localhost/blog/index.php/blog にアクセスします。


表示されました。

続いて、記事を表示するページも作成します。

--- a/fuel/app/classes/controller/blog.php
+++ b/fuel/app/classes/controller/blog.php
@@ -11,4 +11,14 @@ class Controller_Blog extends Controller_Base
        $this->template->title = 'My Awesome Blog';
        $this->template->content = $view;
    }
+
+   public function action_view($slug)
+   {
+       $post = Model_Post::find_by_slug($slug);
+
+       $this->template->title = $post->title;
+       $this->template->content = View::forge('blog/view', array(
+           'post' => $post,
+       ));
+   }
 }

fuel/app/views/blog/view.php:

<h2><?php echo $post->title ?></h2>

<p><strong>Posted: </strong><?php echo date('jS F, Y', $post->created_at) ?> (<?php echo Date::time_ago($post->created_at) ?>)</p>

<p><?php echo nl2br($post->body) ?></p>

記事にアクセスしてみます。


ORM を使ってみる

ORM にリレーションを追加します。

記事 (Post) は 1人のユーザ (User) により作成されますので、関係は、

a post belongs to a user

となります。

Model_Post モデルに $_belongs_to プロパティを追加します。

--- a/fuel/app/classes/model/post.php
+++ b/fuel/app/classes/model/post.php
@@ -1,6 +1,8 @@
 <?php
 class Model_Post extends \Orm\Model
 {
+   protected static $_belongs_to = array('user');
+
    protected static $_properties = array(
        'id',
        'title',

一方、ユーザ (User) は複数の記事 (Post) を投稿できますので、関係は、

a user has many posts

となります。

Model_User モデルに $_has_many プロパティを追加します。

--- a/fuel/app/classes/model/user.php
+++ b/fuel/app/classes/model/user.php
@@ -2,6 +2,8 @@
 
 class Model_User extends \Orm\Model
 {
+   protected static $_has_many = array('posts');
+
    protected static $_properties = array(
        'id',
        'username',

oil console から確認してみます。

$ oil console
Fuel 1.1 - PHP 5.3.8 (cli) (Sep 19 2011 13:26:12) [Linux]
>>> $post = Model_Post::find('first');
>>> $post->user->username;
admin

うまくユーザ名が取得できました。

view.php を変更し、ユーザ名を表示するようにします。

--- a/fuel/app/views/blog/view.php
+++ b/fuel/app/views/blog/view.php
@@ -1,5 +1,9 @@
 <h2><?php echo $post->title ?></h2>
 
-<p><strong>Posted: </strong><?php echo date('jS F, Y', $post->created_at) ?> (<?php echo Date::time_ago($post->created_at) ?>)</p>
+<p>
+   <strong>Posted: </strong><?php echo date('jS F, Y', $post->created_at) ?>
+   (<?php echo Date::time_ago($post->created_at) ?>)
+   by <?php echo $post->user->username ?>
+</p>
 
 <p><?php echo nl2br($post->body) ?></p>

これでユーザ名も表示されるようになりました。

データベースクエリのプロファイル

データベースのクエリをプロファイルに表示させてみましょう。

設定ファイルを変更します。

--- a/fuel/app/config/config.php
+++ b/fuel/app/config/config.php
@@ -43,7 +43,7 @@ return array(
     */
    'index_file'  => 'index.php',
 
-   'profiling'  => false,
+   'profiling'  => true,
 
    /**
     * Settings for Cache class
--- a/fuel/app/config/development/db.php
+++ b/fuel/app/config/development/db.php
@@ -14,5 +14,6 @@ return array(
            'password'      => '',
        ),
        'table_prefix' => 'ap_',
+       'profiling'    => true,
    ),
 );

プロファイラが表示されるようになりました。

以下のクエリが実行されたことがわかります。

SELECT * FROM `ap_users` WHERE `username` = 'admin'
Type: ALL ·Rows: 2 ·Speed: 0.430 ms

SELECT `ap_t0`.`id` AS `t0_c0`, `ap_t0`.`username` AS `t0_c1`, `ap_t0`.`password` AS `t0_c2`, `ap_t0`.`group` AS `t0_c3`, `ap_t0`.`email` AS `t0_c4`, `ap_t0`.`last_login` AS `t0_c5`, `ap_t0`.`login_hash` AS `t0_c6`, `ap_t0`.`profile_fields` AS `t0_c7`, `ap_t0`.`created_at` AS `t0_c8`, `ap_t0`.`updated_at` AS `t0_c9` FROM `ap_users` AS `ap_t0` WHERE (`ap_t0`.`username` = 'admin') ORDER BY `ap_t0`.`id` ASC, `ap_t0`.`id` ASC LIMIT 1
Type: ALL ·Rows: 2 ·Speed: 0.535 ms

SELECT `ap_t0`.`id` AS `t0_c0`, `ap_t0`.`title` AS `t0_c1`, `ap_t0`.`slug` AS `t0_c2`, `ap_t0`.`summary` AS `t0_c3`, `ap_t0`.`body` AS `t0_c4`, `ap_t0`.`user_id` AS `t0_c5`, `ap_t0`.`created_at` AS `t0_c6`, `ap_t0`.`updated_at` AS `t0_c7` FROM `ap_posts` AS `ap_t0` WHERE (`ap_t0`.`slug` = 'fuelphp-is-awesome') ORDER BY `ap_t0`.`id` ASC, `ap_t0`.`id` ASC LIMIT 1
Type: ALL ·Rows: 2 ·Speed: 0.391 ms

SELECT `ap_t0`.`id` AS `t0_c0`, `ap_t0`.`username` AS `t0_c1`, `ap_t0`.`password` AS `t0_c2`, `ap_t0`.`group` AS `t0_c3`, `ap_t0`.`email` AS `t0_c4`, `ap_t0`.`last_login` AS `t0_c5`, `ap_t0`.`login_hash` AS `t0_c6`, `ap_t0`.`profile_fields` AS `t0_c7`, `ap_t0`.`created_at` AS `t0_c8`, `ap_t0`.`updated_at` AS `t0_c9` FROM `ap_users` AS `ap_t0` WHERE `ap_t0`.`id` = '1' LIMIT 1
Possible keys: PRIMARY ·Key Used: PRIMARY ·Type: const ·Rows: 1 ·Speed: 0.475 ms

Eager Loading を使ってみる

先ほどのデータの取得方法は Lazy Loading であり、Eager Loading に変更することで SQL クエリの回数を減らしパフォーマンスを改善できると、Build an Admin Panel with the Fuel PHP Framework に記載されています。

そして、以下のように変更すると、Eager Loading になると記述されていますが、

--- a/fuel/app/classes/controller/blog.php
+++ b/fuel/app/classes/controller/blog.php
@@ -14,7 +14,7 @@ class Controller_Blog extends Controller_Base
 
    public function action_view($slug)
    {
-       $post = Model_Post::find_by_slug($slug);
+       $post = Model_Post::find_by_slug($slug, array('related' => array('user')));
 
        $this->template->title = $post->title;
        $this->template->content = View::forge('blog/view', array(

この結果は、以下のエラーになってしまいました。

Notice!

ErrorException [ Notice ]: Trying to get property of non-object

APPPATH/classes/controller/blog.php @ line 20:

19:	
20:	$this->template->title = $post->title;
21:	$this->template->content = View::forge('blog/view', array(

仕方がないので、Build an Admin Panel with the Fuel PHP Framework のコメント欄にあった別の方法に変更します。

--- a/fuel/app/classes/controller/blog.php
+++ b/fuel/app/classes/controller/blog.php
@@ -14,7 +14,8 @@ class Controller_Blog extends Controller_Base
 
    public function action_view($slug)
    {
-       $post = Model_Post::find_by_slug($slug);
+       //$post = Model_Post::find_by_slug($slug, array('related' => array('user')));
+       $post = Model_Post::find()->where('slug', $slug)->related('user')->get_one();
 
        $this->template->title = $post->title;
        $this->template->content = View::forge('blog/view', array(

これだとうまく動作しました。また、クエリが 1つ減りました。

SELECT * FROM `ap_users` WHERE `username` = 'admin'
Type: ALL ·Rows: 2 ·Speed: 0.444 ms

SELECT `ap_t0`.`id` AS `t0_c0`, `ap_t0`.`username` AS `t0_c1`, `ap_t0`.`password` AS `t0_c2`, `ap_t0`.`group` AS `t0_c3`, `ap_t0`.`email` AS `t0_c4`, `ap_t0`.`last_login` AS `t0_c5`, `ap_t0`.`login_hash` AS `t0_c6`, `ap_t0`.`profile_fields` AS `t0_c7`, `ap_t0`.`created_at` AS `t0_c8`, `ap_t0`.`updated_at` AS `t0_c9` FROM `ap_users` AS `ap_t0` WHERE (`ap_t0`.`username` = 'admin') ORDER BY `ap_t0`.`id` ASC, `ap_t0`.`id` ASC LIMIT 1
Type: ALL ·Rows: 2 ·Speed: 0.574 ms

SELECT `ap_t0`.`id` AS `t0_c0`, `ap_t0`.`title` AS `t0_c1`, `ap_t0`.`slug` AS `t0_c2`, `ap_t0`.`summary` AS `t0_c3`, `ap_t0`.`body` AS `t0_c4`, `ap_t0`.`user_id` AS `t0_c5`, `ap_t0`.`created_at` AS `t0_c6`, `ap_t0`.`updated_at` AS `t0_c7`, `ap_t1`.`id` AS `t1_c0`, `ap_t1`.`username` AS `t1_c1`, `ap_t1`.`password` AS `t1_c2`, `ap_t1`.`group` AS `t1_c3`, `ap_t1`.`email` AS `t1_c4`, `ap_t1`.`last_login` AS `t1_c5`, `ap_t1`.`login_hash` AS `t1_c6`, `ap_t1`.`profile_fields` AS `t1_c7`, `ap_t1`.`created_at` AS `t1_c8`, `ap_t1`.`updated_at` AS `t1_c9` FROM (SELECT `ap_t0`.`id`, `ap_t0`.`title`, `ap_t0`.`slug`, `ap_t0`.`summary`, `ap_t0`.`body`, `ap_t0`.`user_id`, `ap_t0`.`created_at`, `ap_t0`.`updated_at` FROM `ap_posts` AS `ap_t0` WHERE `ap_t0`.`slug` = 'fuelphp-is-awesome' LIMIT 1) AS `ap_t0` LEFT JOIN `ap_users` AS `ap_t1` ON (`ap_t0`.`user_id` = `ap_t1`.`id`)
Type: system ·Rows: 1 ·Speed: 0.958 ms

コメント欄を追加する

oil generate admin コマンドでコメント欄に関するコードを生成します。

$ oil generate admin comments name:string email:string website:string message:text post_id:int

Creating migration: .../blog/fuel/app/migrations/003_create_comments.php
Creating model: .../blog/fuel/app/classes/model/comment.php
Creating controller: .../blog/fuel/app/classes/controller/admin/comments.php
Creating view: .../blog/fuel/app/views/admin/comments/index.php
Creating view: .../blog/fuel/app/views/admin/comments/view.php
Creating view: .../blog/fuel/app/views/admin/comments/create.php
Creating view: .../blog/fuel/app/views/admin/comments/edit.php
Creating view: .../blog/fuel/app/views/admin/comments/_form.php

$ oil refine migrate

リレーションを設定します。

--- a/fuel/app/classes/model/comment.php
+++ b/fuel/app/classes/model/comment.php
@@ -1,6 +1,8 @@
 <?php
 class Model_Comment extends \Orm\Model
 {
+   protected static $_belongs_to = array('post', 'user');
+
    protected static $_properties = array(
        'id',
        'name',
--- a/fuel/app/classes/model/post.php
+++ b/fuel/app/classes/model/post.php
@@ -2,6 +2,7 @@
 class Model_Post extends \Orm\Model
 {
    protected static $_belongs_to = array('user');
+   protected static $_has_many = array('comments');
 
    protected static $_properties = array(
        'id',
--- a/fuel/app/classes/model/user.php
+++ b/fuel/app/classes/model/user.php
@@ -2,7 +2,7 @@
 
 class Model_User extends \Orm\Model
 {
-   protected static $_has_many = array('posts');
+   protected static $_has_many = array('posts', 'comments');
 
    protected static $_properties = array(
        'id',

http://localhost/blog/index.php/admin/comments/create にアクセスしてコメントを追加します。Post id は 1 にします。

oil console で確認します。

$ oil console
Fuel 1.1 - PHP 5.3.8 (cli) (Sep 19 2011 13:26:12) [Linux]
>>> Model_Post::find(1)->comments;
array (
  1 => 
  Model_Comment::__set_state(array(
     '_is_new' => false,
     '_frozen' => false,
     '_data' => 
    array (
      'id' => '1',
      'name' => 'Who am I',
      'email' => 'test@example.jp',
      'website' => 'http://example.jp/',
      'message' => 'This is a comment, even if you believe or not.

<s>test</s>
',
      'post_id' => '1',
      'created_at' => '1328005079',
      'updated_at' => '1328005079',
    ),
     '_original' => 
    array (
      'id' => '1',
      'name' => 'Who am I',
      'email' => 'test@example.jp',
      'website' => 'http://example.jp/',
      'message' => 'This is a comment, even if you believe or not.

<s>test</s>
',
      'post_id' => '1',
      'created_at' => '1328005079',
      'updated_at' => '1328005079',
    ),
     '_data_relations' => 
    array (
    ),
     '_original_relations' => 
    array (
    ),
     '_view' => NULL,
     '_iterable' => 
    array (
    ),
  )),
)

Controller_Blog クラスのクエリに comments へのリレーションを追加します。

--- a/fuel/app/classes/controller/blog.php
+++ b/fuel/app/classes/controller/blog.php
@@ -15,7 +15,7 @@ class Controller_Blog extends Controller_Base
    public function action_view($slug)
    {
        //$post = Model_Post::find_by_slug($slug, array('related' => array('user')));
-       $post = Model_Post::find()->where('slug', $slug)->related('user')->get_one();
+       $post = Model_Post::find()->where('slug', $slug)->related('user')->related('comments')->get_one();
 
        $this->template->title = $post->title;
        $this->template->content = View::forge('blog/view', array(

ビューファイルにコメントの表示とコメントを投稿するフォームを追加します。

--- a/fuel/app/views/blog/view.php
+++ b/fuel/app/views/blog/view.php
@@ -7,3 +7,43 @@
 </p>
 
 <p><?php echo nl2br($post->body) ?></p>
+
+<hr />
+
+<h3 id="comments">Comments</h3>
+
+<?php foreach ($post->comments as $comment): ?>
+
+   <p><?php echo Html::anchor($comment->website, $comment->name) ?> said "<?php echo $comment->message ?>"</p>
+
+<?php endforeach; ?>
+
+<h3>Write a comment</h3>
+
+<?php echo Form::open('blog/comment/'.$post->slug) ?>
+
+<div class="row">
+   <label for="name">Name:</label>
+   <div class="input"><?php echo Form::input('name'); ?></div>
+</div>
+
+<div class="row">
+   <label for="website">Website:</label>
+   <div class="input"><?php echo Form::input('website'); ?></div>
+</div>
+
+<div class="row">
+   <label for="email">Email:</label>
+   <div class="input"><?php echo Form::input('email'); ?></div>
+</div>
+
+<div class="row">
+   <label for="message">Comment:</label>
+   <div class="input"><?php echo Form::textarea('message'); ?></div>
+</div>
+
+<div class="row">
+   <div class="input"><?php echo Form::submit('submit'); ?></div>
+</div>
+
+<?php echo Form::close() ?>


コメントが表示されています。

投稿されたコメントを処理する action_comment() メソッドをコントローラに追加します。

--- a/fuel/app/classes/controller/blog.php
+++ b/fuel/app/classes/controller/blog.php
@@ -22,4 +22,42 @@ class Controller_Blog extends Controller_Base
            'post' => $post,
        ));
    }
+
+   public function action_comment($slug)
+   {
+       $post = Model_Post::find_by_slug($slug);
+
+       // Lazy validation
+       if (Input::post('name') AND Input::post('email') AND Input::post('message'))
+       {
+           // Create a new comment
+           $post->comments[] = new Model_Comment(array(
+               'name'    => Input::post('name'),
+               'website' => Input::post('website'),
+               'email'   => Input::post('email'),
+               'message' => Input::post('message'),
+               'user_id' => $this->current_user->id,
+           ));
+
+           // Save the post and the comment will save too
+           if ($post->save())
+           {
+               $comment = end($post->comments);
+               Session::set_flash('success', 'Added comment #'.$comment->id.'.');
+           }
+           else
+           {
+               Session::set_flash('error', 'Could not save comment.');
+           }
+
+           Response::redirect('blog/view/'.$slug);
+       }
+
+       // Did not have all the fields
+       else
+       {
+           // Just show the view again until they get it right
+           $this->action_view($slug);
+       }
+   }
 }

このメソッドはバリデーションをきちんとしてませんし、バリデーションにパスしなかった場合、入力値が消えてしまいますが、とりあえず、これで、コメントを投稿できるようになりました。


ORM の生成する SQLデバッグ

先ほどのエラーが出た場合の動作を、確認しておきます。

実は、最初の記事ではエラーは出ずに記事が正常に表示されましたが、2つ目の記事ではエラーが表示されました。

Debug::dump() メソッドで変数を表示してみると、エラーが出た場合は null になっていました。

それでは、最終的なコメントのリレーションも含まれた以下のコードを確認してみます。

$post = Model_Post::find_by_slug($slug, array('related' => array('user', 'comments')));

上記の場合の SQL をプロファイラで取得し、整形すると、

SELECT
 `ap_t0`.`id` AS `t0_c0`,
 `ap_t0`.`title` AS `t0_c1`,
 `ap_t0`.`slug` AS `t0_c2`,
 `ap_t0`.`summary` AS `t0_c3`,
 `ap_t0`.`body` AS `t0_c4`,
 `ap_t0`.`user_id` AS `t0_c5`,
 `ap_t0`.`created_at` AS `t0_c6`,
 `ap_t0`.`updated_at` AS `t0_c7`,
 `ap_t1`.`id` AS `t1_c0`,
 `ap_t1`.`username` AS `t1_c1`,
 `ap_t1`.`password` AS `t1_c2`,
 `ap_t1`.`group` AS `t1_c3`,
 `ap_t1`.`email` AS `t1_c4`,
 `ap_t1`.`last_login` AS `t1_c5`,
 `ap_t1`.`login_hash` AS `t1_c6`,
 `ap_t1`.`profile_fields` AS `t1_c7`,
 `ap_t1`.`created_at` AS `t1_c8`,
 `ap_t1`.`updated_at` AS `t1_c9`,
 `ap_t2`.`id` AS `t2_c0`,
 `ap_t2`.`name` AS `t2_c1`,
 `ap_t2`.`email` AS `t2_c2`,
 `ap_t2`.`website` AS `t2_c3`,
 `ap_t2`.`message` AS `t2_c4`,
 `ap_t2`.`post_id` AS `t2_c5`,
 `ap_t2`.`created_at` AS `t2_c6`,
 `ap_t2`.`updated_at` AS `t2_c7`
FROM 
	(SELECT
	 `ap_t0`.`id`,
	 `ap_t0`.`title`,
	 `ap_t0`.`slug`,
	 `ap_t0`.`summary`,
	 `ap_t0`.`body`,
	 `ap_t0`.`user_id`,
	 `ap_t0`.`created_at`,
	 `ap_t0`.`updated_at`
	FROM
	 `ap_posts` AS `ap_t0`
	ORDER BY
	 `ap_t0`.`id` ASC 
	LIMIT 1)
	AS `ap_t0` 
LEFT JOIN
 `ap_users` AS `ap_t1` ON (`ap_t0`.`user_id` = `ap_t1`.`id`) 
LEFT JOIN
 `ap_comments` AS `ap_t2` ON (`ap_t0`.`id` = `ap_t2`.`post_id`)
WHERE
 (`ap_t0`.`slug` = 'first-post') 
ORDER BY
 `ap_t0`.`id` ASC

となりました。

FROM 句の SQL が明らかにおかしいですね。ということで、このクエリはやはりバグってました。

うまく動作した場合の以下のコードでは、

$post = Model_Post::find()->where('slug', $slug)->related('user')->related('comments')->get_one();

生成される SQL は以下のようになりました。

SELECT
 `ap_t0`.`id` AS `t0_c0`,
 `ap_t0`.`title` AS `t0_c1`,
 `ap_t0`.`slug` AS `t0_c2`,
 `ap_t0`.`summary` AS `t0_c3`,
 `ap_t0`.`body` AS `t0_c4`,
 `ap_t0`.`user_id` AS `t0_c5`,
 `ap_t0`.`created_at` AS `t0_c6`,
 `ap_t0`.`updated_at` AS `t0_c7`,
 `ap_t1`.`id` AS `t1_c0`,
 `ap_t1`.`username` AS `t1_c1`,
 `ap_t1`.`password` AS `t1_c2`,
 `ap_t1`.`group` AS `t1_c3`,
 `ap_t1`.`email` AS `t1_c4`,
 `ap_t1`.`last_login` AS `t1_c5`,
 `ap_t1`.`login_hash` AS `t1_c6`,
 `ap_t1`.`profile_fields` AS `t1_c7`,
 `ap_t1`.`created_at` AS `t1_c8`,
 `ap_t1`.`updated_at` AS `t1_c9`,
 `ap_t2`.`id` AS `t2_c0`,
 `ap_t2`.`name` AS `t2_c1`,
 `ap_t2`.`email` AS `t2_c2`,
 `ap_t2`.`website` AS `t2_c3`,
 `ap_t2`.`message` AS `t2_c4`,
 `ap_t2`.`post_id` AS `t2_c5`,
 `ap_t2`.`created_at` AS `t2_c6`,
 `ap_t2`.`updated_at` AS `t2_c7` 
FROM 
	(SELECT
	 `ap_t0`.`id`,
	 `ap_t0`.`title`,
	 `ap_t0`.`slug`,
	 `ap_t0`.`summary`,
	 `ap_t0`.`body`,
	 `ap_t0`.`user_id`,
	 `ap_t0`.`created_at`,
	 `ap_t0`.`updated_at` 
	FROM
	 `ap_posts` AS `ap_t0` 
	WHERE
	 `ap_t0`.`slug` = 'fuelphp-is-awesome' 
	LIMIT 1)
	AS `ap_t0` 
LEFT JOIN
 `ap_users` AS `ap_t1` ON (`ap_t0`.`user_id` = `ap_t1`.`id`) 
LEFT JOIN
 `ap_comments` AS `ap_t2` ON (`ap_t0`.`id` = `ap_t2`.`post_id`)

本家フォーラムを検索したところ、

FuelPHP » Forums にて、__callStatic() メソッドのバグだとのコメントがありました。そして、以下ならうまく動作すると。

$post = Model_Post::query()->where('slug', $slug')->related('user')->get_one(); 
// or 
$post = Model_Post::find('first', array('where' => array('slug' => $slug), 'related' => array('user')));