Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

10分でFuelPHPの基礎を理解する

FuelPHP入門

FuelPHP入門

様々な技術を求められる開発現場

じゃあ、いつRails始めるの?... 今でしょ! - Yuta.Kikuchiの日記 はてなブックマーク - じゃあ、いつRails始めるの?... 今でしょ! - Yuta.Kikuchiの日記
様々な開発現場でそこに必要な技術を求められていて四苦八苦中の@yutakikucです。ついこの前Railsのエントリーを書いたばかりなのに今度はFuelPHPについて調べなければいけなかったり...僕の体はいつ休まるんだろうか。FuelPHPRailsと似ているところが多いのでRailsを学習した記憶が新しいうちにFuelPHPについて学んだことをどんどん書いていこうと思います。Railsに比べると少し設定が面倒なんで、ハマった方の少しでも参考になればと思います。

Index

  1. FuelPHP Setting
    1. Nginx WebServer
    2. oil Install / oil create
    3. Error - date_default_timezone_get()
    4. DB設定
    5. Scaffoldで雛形を作る
    6. Routingの設定
    7. Crypt Key Error
    8. FuelPHPに接続
  2. FuelPHP
    1. Coreのライブラリのautoload
    2. Controller
    3. Model
    4. View
    5. Security/htmlentities

FuelPHP Setting

Nginx WebServer

CentOSでNginxのログをFluentdを使ってMongodbにリアルタイムで格納する - Yuta.Kikuchiの日記 はてなブックマーク - CentOSでNginxのログをFluentdを使ってMongodbにリアルタイムで格納する - Yuta.Kikuchiの日記
FuelPHPを動かす環境をNginxを利用します。CentOSでのNginxの設定は上のエントリーを確認しながらやってみてください。ここではProcessが立ち上がっていることの確認とphp-fpmのCGI設定について記述します。プロセスの確認はpsコマンドで、php-fpmはWebでPHPを利用するためのCGIパッケージでyumにて最初にinstallしておくと良いでしょう。
先にエントリポイントについて少し説明をしていおくとnginxの設定でrootを/usr/share/nginx/html/fuel_sample/publicとし、fuelのエントリポイントとなるパスを指定します。fuel_sampleは後で定義するプロジェクト名です。/usr/share/nginx/html/fuel_sample/public直下に.htaccessファイルが生成されそちらでもaccessの設定が編集できますが、今回はnginx側で全て対応させます。
fastcgi_param FUEL_ENV development;の箇所でFuelの環境に合わせた設定変更ができます。FUEL_ENVをアプリケーション側が自動で読み込んで、fuel/app/config//config.phpのような設定ファイルを切り替えます。設定を変更したらnginx/php-fpmの両方を再起動します。またnginx/php-fpm/mysqldを全て自動起動設定を入れておきます。

$ /usr/share/nginx# ps auxw | grep nginx
root      1879  0.0  0.0  44688   848 ?        Ss   01:05   0:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx     1881  0.0  0.1  45096  1548 ?        S    01:05   0:00 nginx: worker process     
$ sudo yum install php-fpm -y
$ sudo vim /etc/nginx/conf.d/default.conf
server {
    listen       80;
    server_name  localhost;

    root   /usr/share/nginx/html/fuel_sample/public;
    index index.php;
    location / {
        try_files $uri /index.php?$uri&$args;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    location ~ \.php$ {
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        fastcgi_param  FUEL_ENV  development;
        include        fastcgi_params;
    }
}
$ sudo vim /etc/php-fpm.d/www.conf
; user,groupをapacheからnginxに変更
; RPM: apache Choosed to be able to access some dir as httpd
user = nginx 
; RPM: Keep a group allowed to write in log dir.
group = nginx
$ sudo /etc/init.d/nginx restart
nginx を停止中:                                            [  OK  ]
nginx を起動中:                                            [  OK  ]

$ sudo /etc/init.d/php-fpm restart 
php-fpm を停止中:                                          [  OK  ]
php-fpm を起動中:                                          [  OK  ]

$ sudo /sbin/chkconfig nginx on
$ sudo /sbin/chkconfig php-fpm on
$ sudo /sbin/chkconfig mysqld on
oil Install / oil create

gitをinstallしておきます。Centosのgit-install時にPackageの依存関係のエラーがでるので--disablerepo=epelのオプションを指定してinstallします。その次にoilコマンドをダウンロードします。oilはRailsで言うrakeコマンドのようなものです。Projectを作成したりModel/View/Controllerを生成したり、Scaffoldができたり、DBMigrationができます。oilのダウンロードが完了したらoil create でfuel_sampleというProjectを作成します。NginxのDocumentRootはDefaultでは/usr/share/nginx/html

$ sudo yum install git --disablerepo=epel -y
$ curl get.fuelphp.com/oil | sh
$ cd /usr/share/nginx/html
$ sudo oil create fuel_sample
Error - date_default_timezone_get()

/etc/php.iniでdefaultのtimezoneが指定されていない場合は修正します。

[Date]
; Defines the default timezone used by the date functions
; http://www.php.net/manual/en/datetime.configuration.php#ini.date.timezone
;date.timezone =
date.timezone = "Asia/Tokyo"
DB設定

fuel_sample/fuel/app/config//db.phpとして生成されているdbの設定ファイルを修正します。接続username、passwordを自分の環境に合わせて適宜設定をする必要があります。FuelはORMというDB接続クラスを用意しているのでORMを使えるようにcoreのconfig.phpをormをコメントインします。またoil refineコマンドでmigrationをしようとするとerrorが出てしまうので、先に環境に合わせたdbをcreateしておきます。ここでは開発環境のdevelopmentの説明を前提に記述します。
fuel_sample/fuel/app/config/development/db.php

<?php
/**
 * The development database settings. These get merged with the global settings.
 */

return array(
        'default' => array(
                'connection'  => array(
                        'dsn'        => 'mysql:host=localhost;dbname=fuel_dev',
                        'username'   => 'root',
                        'password'   => '',
                ),
        ),
);

fuel_sample/fuel/core/config/config.php

<?php

        /**************************************************************************/
        /* Always Load                                                            */
        /**************************************************************************/
        'always_load'  => array(

                /**
                 * These packages are loaded on Fuel's startup.
                 * You can specify them in the following manner:
                 *
                 * array('auth'); // This will assume the packages are in PKGPATH
                 *
                 * // Use this format to specify the path to the package explicitly
                 * array(
                 *     array('auth'     => PKGPATH.'auth/')
                 * );
                 */
                'packages'  => array(
                        'orm',
                ),
mysql> create database fuel_dev;
Query OK, 1 row affected (0.00 sec)
Scaffoldで雛形を作る

今回はnoteを作成/編集/詳細表示/一覧表示することを考えます。Scaffoldでは特定のURL/Directoryのルールに従ってコードを自動生成してくれます。noteで扱うデータはtitle/descriptionの2種類を定義します。oil generate sfaffold module : のような文法です。生成される名前のルールとしてDBはnotesのような名詞の複数形になります。

$ sudo oil generate scaffold note title:varchar[255] description:text
	Creating migration: /usr/share/nginx/html/fuel_sample/fuel/app/migrations/001_create_notes.php
	Creating model: /usr/share/nginx/html/fuel_sample/fuel/app/classes/model/note.php
	Creating controller: /usr/share/nginx/html/fuel_sample/fuel/app/classes/controller/note.php
	Creating view: /usr/share/nginx/html/fuel_sample/fuel/app/views/note/index.php
	Creating view: /usr/share/nginx/html/fuel_sample/fuel/app/views/note/view.php
	Creating view: /usr/share/nginx/html/fuel_sample/fuel/app/views/note/create.php
	Creating view: /usr/share/nginx/html/fuel_sample/fuel/app/views/note/edit.php
	Creating view: /usr/share/nginx/html/fuel_sample/fuel/app/views/note/_form.php
	Creating view: /usr/share/nginx/html/fuel_sample/fuel/app/views/template.php

migraionファイルはDBの初期設定を行うためのものです。oil refine migrateでDBの設定を行います。念のためmigraionファイルがどのようなものかを確認します。またoil refine migrateを行った後はDBのtableが作成されたかどうかを見ます。
fuel_sample/fuel/app/migrations/001_create_notes.php

<?php

namespace Fuel\Migrations;

class Create_notes
{
    public function up()
    {
        \DBUtil::create_table('notes', array(
            'id' => array('constraint' => 11, 'type' => 'int', 'auto_increment' => true, 'unsigned' => true),
            'title' => array('constraint' => 255, 'type' => 'varchar'),
            'description' => array('type' => 'text'),
            'created_at' => array('constraint' => 11, 'type' => 'int', 'null' => true),
            'updated_at' => array('constraint' => 11, 'type' => 'int', 'null' => true),

        ), array('id'));
    }

    public function down()
    {
        \DBUtil::drop_table('notes');
    }
}
$ sudo oil refine migrate
Performed migrations for app:default:
001_create_notes

$ mysql -u root -p
mysql> use fuel_dev;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+--------------------+
| Tables_in_fuel_dev |
+--------------------+
| migration          |
| notes              |
+--------------------+
2 rows in set (0.00 sec)

mysql> show create table notes;
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                                                |
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| notes | CREATE TABLE `notes` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `description` text NOT NULL,
  `created_at` int(11) DEFAULT NULL,
  `updated_at` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 |
+-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

Mysqlの設定にもよりますがmigrationを実行すると残念ながらDefaultではENGINE = MyISAMとして設定されてしまうようです。migraionファイルを修正することによってENGINEやCHASETを自由に書き換えることができます。下はmigrationファイルから呼び出しているDBUtilクラスのcreate_tableメソッドです。5/6番目の引数でENGINEとCHARSETが指定出来ます。
fuel_sample/fuel/core/classes/dbutil.php

<?php

    /**     * Creates a table.
     *
     * @throws   \Database_Exception
     * @param    string    $table          the table name
     * @param    array     $fields         the fields array
     * @param    array     $primary_keys   an array of primary keys
     * @param    boolean   $if_not_exists  whether to add an IF NOT EXISTS statement.     * @param    string    $engine         storage engine overwrite
     * @param    string    $charset        default charset overwrite
     * @param    array     $foreign_keys   an array of foreign keys
     * @return   int       number of affected rows.
     */
    public static function create_table($table, $fields, $primary_keys = array(), $if_not_exists = true, $engine = false, $c
harset = null, $foreign_keys = array(), $db = null) 
Routingの設定

URIの規則で重要な役割を果たしているのがroutes.phpです。ここでアクセスしたURIと呼び出したいControllerとメソッドを指定することができます。_root_はBaseURIになります。下の例でいうとhttp://localhost/helloにアクセスすると内部的にはhttp://localhost/welcome/helloを呼び出していることになります。

<?php
return array(
    '_root_'  => 'welcome/index',  // The default route
    '_404_'   => 'welcome/404',    // The main 404 route
    
    'hello(/:name)?' => array('welcome/hello', 'name' => 'hello'),
);
Crypt Key Error


ここまで設定できたらScaffoldで作成されたnoteにアクセスしてみます。http://localhost/note/createにアクセスすると、Crypt Key Errorという画面が出てしまいます。暗証Keyをどうやら手動で設定する必要があるようです。画面に出力されたphpコードをconfig直下に配置します。
fuel_sample/fuel/app/config/crypt.php

<?php
/**
 * Part of the Fuel framework.
 *
 * @package     Fuel
 * @version     1.6
 * @author      Fuel Development Team
 * @license     MIT License
 * @copyright   2010 - 2013 Fuel Development Team
 * @link        http://fuelphp.com
 */

return array (
    'crypto_key' => 'Leg2Qg73448k594xBob-s-wc',
    'crypto_iv' => 'j2w-MchSEx7IZUES3QBfkKAQ',
    'crypto_hmac' => 'KUQ9hEL6wLsMw1MMKIqDAJ_U',
);
FuelPHPに接続

http://localhost/note/createに再度アクセスをすると以下の様な画面が出力されると思います。URIの規則は呼び出したいController名/メソッド名という感じになります。ここではnote/createとするとnoteを入力する画面が表示されます。これでNginx上でのFuel設定は完了です。

FuelPHP

以下ではscaffoldされたnoteのMVCを例にFuelPHPの内部構造を少し説明しようと思います。

Coreのライブラリのautoload

fuel/core/classes以下に様々なライブラリがoilでcreateされます。createされたclassesはautoloadされます。autoloadしているファイルはfuel/core/bootstrap.phpのsetup_autoloader()メソッドになります。このautoload機能により様々なファイルで利用されているSession、Input、Viewクラスなどが利用できる事になります。

/usr/share/nginx/html/fuel_sample/fuel/core/classes# tree
├── agent.php
├── arr.php
├── asset
│&#160;&#160; └── instance.php
├── asset.php
├── autoloader.php
├── cache
│&#160;&#160; ├── handler
│&#160;&#160; │&#160;&#160; ├── driver.php
│&#160;&#160; │&#160;&#160; ├── json.php
│&#160;&#160; │&#160;&#160; ├── serialized.php
│&#160;&#160; │&#160;&#160; └── string.php
│&#160;&#160; ├── notfound.php
│&#160;&#160; └── storage
│&#160;&#160;     ├── apc.php
│&#160;&#160;     ├── driver.php
│&#160;&#160;     ├── file.php
│&#160;&#160;     ├── memcached.php
│&#160;&#160;     └── redis.php
├── cache.php
├── cli.php
├── config
│&#160;&#160; ├── file.php
│&#160;&#160; ├── ini.php
│&#160;&#160; ├── interface.php
│&#160;&#160; ├── json.php
│&#160;&#160; ├── php.php
│&#160;&#160; └── yml.php
├── config.php
├── controller
│&#160;&#160; ├── hybrid.php
│&#160;&#160; ├── rest.php
│&#160;&#160; └── template.php
├── controller.php
├── cookie.php
├── crypt.php

fuel/core/bootstrap.php

<?php

function setup_autoloader()
{
    Autoloader::add_namespace('Fuel\\Core', COREPATH.'classes/');

    Autoloader::add_namespace('PHPSecLib', COREPATH.'vendor'.DS.'phpseclib'.DS, true);

    Autoloader::add_classes(array(
        'Fuel\\Core\\Agent'           => COREPATH.'classes/agent.php',

        'Fuel\\Core\\Arr'             => COREPATH.'classes/arr.php',

        'Fuel\\Core\\Asset'           => COREPATH.'classes/asset.php',
        'Fuel\\Core\\Asset_Instance'  => COREPATH.'classes/asset/instance.php',

        'Fuel\\Core\\Cache'                     => COREPATH.'classes/cache.php',
        'Fuel\\Core\\CacheNotFoundException'    => COREPATH.'classes/cache/notfound.php',
        'Fuel\\Core\\CacheExpiredException'     => COREPATH.'classes/cache.php',
Controller

FuelのビジネスロジックはControllerがメインです。Controllerのメソッド名はaction_<メソッド名>として定義されます。
Controllerの役割は当然Modelとのデータのやり取りとViewへの流しこみを担います。データを保存する流れとしてはModelクラスで定義したValidateを呼び出し、Validate結果が問題なければModelのsaveメソッドを使ってデータを保存します。
Controller/Viewのルールとして覚えておきたいことは$this->template->content = View::forge('note/index', $data);の箇所でViewに渡すデータの設定を行います。$data[key]は連想配列として渡すことによりView側でkey名の変数で呼び出す事が可能です。例えばaction_indexで$data['notes'] = Model_Note::find('all');$this->template->content = View::forge('note/index', $data);されたデータはnote/indexのView側で$notesとして呼び出すことができます。
fuel/app/classes/controller/note.php

<?php
class Controller_Note extends Controller_Template{

	public function action_index()
	{
		$data['notes'] = Model_Note::find('all');
		$this->template->title = "Notes";
		$this->template->content = View::forge('note/index', $data);

	}

	public function action_view($id = null)
	{
		is_null($id) and Response::redirect('note');

		if ( ! $data['note'] = Model_Note::find($id))
		{
			Session::set_flash('error', 'Could not find note #'.$id);
			Response::redirect('note');
		}

		$this->template->title = "Note";
		$this->template->content = View::forge('note/view', $data);

	}

	public function action_create()
	{
		if (Input::method() == 'POST')
		{
			$val = Model_Note::validate('create');
			
			if ($val->run())
			{
				$note = Model_Note::forge(array(
					'title' => Input::post('title'),
					'description' => Input::post('description'),
				));

				if ($note and $note->save())
				{
					Session::set_flash('success', 'Added note #'.$note->id.'.');

					Response::redirect('note');
				}

				else
				{
					Session::set_flash('error', 'Could not save note.');
				}
			}
			else
			{
				Session::set_flash('error', $val->error());
			}
		}

		$this->template->title = "Notes";
		$this->template->content = View::forge('note/create');

	}
Model

ModelはDataのやり取りをするための仕組みです。ここではDataSourceをDBとしているのでDBModelになります。
Modelの仕組みはとてもシンプルで固有のValidateメソッドを定義するぐらいでDBとのやり取りが出来てしまいます。Modelのルールとしてはprotected static $_propertiesにてDBのカラムを定義、public static function validateとしてどの種別のValidateを掛けるかを定義します。Validationの細かいルールはValidation - Classes - FuelPHP Documentation はてなブックマーク - Validation - Classes - FuelPHP Documentationこちらを参照して下さい。Validationファイルはcore以下のfuel/core/classes/validation.phpになります。noteのModelでは必須とMaxLengthだけをチェックしています。複数の条件してはrequired|max_length[255]のようにパイプでつなぎます。$val->add_fieldの引数は第一引数がValidationしたいカラム名、第二引数がValidationのLabel名、第三引数がRuleになります。

fuel/app/classes/model/note.php

<?php
use Orm\Model;

class Model_Note extends Model
{
    protected static $_properties = array(
        'id',
        'title',
        'description',
        '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,
        ),
    );

    public static function validate($factory)
    {
        $val = Validation::forge($factory);
        $val->add_field('title', 'Title', 'required|max_length[255]');
        $val->add_field('description', 'Description', 'required');

        return $val;
    }

}
View

Viewはとてもシンプルです。覚えておくべきことはcoreライブラリのViewクラスで定義されたメソッドを呼び出している箇所がありますが、ViewHelperとしての機能だと思ってもらえれば問題ありません。その他for/foreach/ifなどの制御コードをif ($notes): 〜 endif; やforeach ($notes as $note): 〜endforeach; のように定義するところだけを覚えておけばいいと思います。

<h2>Listing <span class='muted'>Notes</span></h2>
<br>
<?php if ($notes): ?>
<table class="table table-striped">
	<thead>
		<tr>
			<th>Title</th>
			<th>Description</th>
			<th>&nbsp;</th>
		</tr>
	</thead>
	<tbody>
<?php foreach ($notes as $note): ?>		<tr>

			<td><?php echo $note->title; ?></td>
			<td><?php echo $note->description; ?></td>
			<td>
				<?php echo Html::anchor('note/view/'.$note->id, '<i class="icon-eye-open" title="View"></i>'); ?> |
				<?php echo Html::anchor('note/edit/'.$note->id, '<i class="icon-wrench" title="Edit"></i>'); ?> |
				<?php echo Html::anchor('note/delete/'.$note->id, '<i class="icon-trash" title="Delete"></i>', array('onclick' => "return confirm('Are you sure?')")); ?>

			</td>
		</tr>
<?php endforeach; ?>	</tbody>
</table>

<?php else: ?>
<p>No Notes.</p>

<?php endif; ?><p>
	<?php echo Html::anchor('note/create', 'Add new Note', array('class' => 'btn btn-success')); ?>

</p>
Security/htmlentities

FuelPHPでは出力時にhtmlentitiesの関数を使ってサニタイジングを行なっています。これはControllerでView::forgeにてデータをセットした時に自動的にサニタイジング処理が走ります。呼び出しの定義はfuel/app/config/config.phpで定義されています。サニタイジングせずにsafedataとして出力したい場合はController側で$this->template->set_safeメソッドにより格納することができます。
fuel/app/config/config.php

<?php
        /**
         * This output filter can be any normal PHP function as well as 'xss_clean'
         *
         * WARNING: Using xss_clean will cause a performance hit.
         * How much is dependant on how much input data there is.
         */
        'output_filter'  => array('Security::htmlentities'),