今回は、CakePHP3について書きたいと思います。サンプルのアプリケーションをつくって、bakeされたController、Model、View、ViewCellを見てみました。
CakePHP3について
CakePHPは、Ruby on Rails流の「設定より規約優先」なパラダイムによる高速開発PHPフレームワークです。現在の安定版は2系で、nanapiやnanapiワークス、アンサーのサーバサイドで使っています。
次世代のCakePHPであるバージョン3は現在開発中で、約2ヶ月前に CakePHP 3.0.0 dev preview 2 がでました。CakePHP3でどんな機能が実装されていくかはロードマップhttps://github.com/cakephp/cakephp/wiki/3.0-Roadmapに書かれています。PHP5.4以降のみでの動作とし、各クラス間を疎結合に、よりシンプルでより小さいCakePHPを目指しているようです。より良いプラグインとして実装されている機能については本体から機能を削除されていくようです。Scaffolding機能は削除されています。そしてなにより、ORMの部分が一新され、モデルのfind結果が配列からオブジェクトになったのが大きい変更点かなあと思います。
CakePHP3を動かす
PHPの環境を作ってCakePHP3を動かすまでやってみます。MacにHomebrewを使って、PHPとMySQLをインストールします。PHPのビルトインサーバが使えるのでApacheやnginx等httpサーバは不要です。また、Rubyでいうところのbundler、パッケージ管理をするComposerもHomebrewでインストールします。
PHPとMySQLの準備
ホームディレクトリにBrewfileを作って brew bundle を実行します。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
tap homebrew/dupes tap homebrew/versions tap homebrew/homebrew-php install mysql install php55 install php55-mcrypt install php55-intl install composer cleanup |
HomebrewでインストールしたPHPを使う設定をします。bash_profileなどにパスの情報を書いたら、 source ~/.bash_profile などして再読込します。
|
1 2 3 |
cat << EOF >> ~/.bash_profile export PATH="\$(brew --prefix homebrew/php/php55)/bin:\$PATH" EOF |
次に、 mysql.server start でMySQLを起動したら、MySQLの初期設定するために mysql_secure_installation を実行します。
これで終わり。かなり楽ですね。
アプリケーションの作成
CakePHP3のプロジェクトを作成します。
|
1 2 3 |
mkdir ~/works/cakephp3sample cd ~/works/cakephp3sample composer create-project -s dev cakephp/app |
composer create-project は、すでにあるComposerパッケージから新しいプロジェクトをつくるコマンドです。https://github.com/cakephp/appをgit cloneしてプラスアルファでいくつかの自動処理するイメージです。デフォルトで作成されるアプリケーションディレクトリはappです。違う名前にしたい場合は、 composer create-project -s dev cakephp/app myproject のように指定します。
設定ファイルの作成、tmpディレクトリのパーミションの設定、Security.salt値の更新 もやってくれます。最後にcakephp/appリポジトリのバージョン情報を削除するかと聞かれるので、消しておきます。
ビルトインサーバで起動
php -S localhost:8000 -t app/webroot/ でビルトインサーバを起動したら、http://localhost:8000 にアクセスするといつものCakePHPの画面が現れます。ビルトインサーバを止めるときは、Ctrl-cです。
サンプルアプリケーション
アプリケーションを作ってみます。ユーザが複数の記事を持つ、Users hasMany Articlesの構造です。
SQL
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
CREATE DATABASE cakephp3sample DEFAULT CHARACTER SET=utf8; use cakephp3sample; CREATE TABLE `users` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `nickname` varchar(50) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `articles` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(10) unsigned DEFAULT NULL, `title` varchar(50) DEFAULT NULL, `body` text, `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; GRANT ALL PRIVILEGES ON `cakephp3sample`.* TO 'sampleuser'@'localhost' IDENTIFIED BY 'samplepassword'; FLUSH PRIVILEGES; INSERT INTO users (nickname, email, created) VALUES ('リュウ', 'ryu@nanapi.co.jp', NOW()); INSERT INTO users (nickname, email, created) VALUES ('ケン', 'ken@nanapi.co.jp', NOW()); INSERT INTO articles (user_id, title, body, created) VALUES (1, 'できる!波動拳','しゃがんだら前に一歩立つように、両掌をひらき突き出してみましょう。', NOW()); INSERT INTO articles (user_id, title, body, created) VALUES (1, '俺より強い奴に会う方法', '意外と近くにいます。墨田区東駒形の銭湯に行きましょう。', NOW()); INSERT INTO articles (user_id, title, body, created) VALUES (2, '投げ技について', '巴投げより地獄車が好きです。', NOW()); |
上のSQLを実行してデータを投入したら、次にMySQLへの接続情報を設定ファイル(app/App/Config/app.php)に書きます。CakePHP2の頃と違い、database.phpではなくなっています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
'Datasources' => [ 'default' => [ 'className' => 'Cake\Database\Connection', 'driver' => 'Cake\Database\Driver\Mysql', 'persistent' => false, 'host' => 'localhost', 'login' => 'sampleuser', 'password' => 'samplepassword', 'database' => 'cakephp3sample', 'prefix' => false, 'encoding' => 'utf8', // 'timezone' => 'UTC' 'timezone' => 'SYSTEM' ], |
timezoneはDB接続ごとのタイムゾーンの設定です。
bake
データを投入したら、Model、Controller、Viewの各ファイルを生成してみます。コンソールからbakeコマンドを実行します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# Models ./app/App/Console/cake bake model Users ./app/App/Console/cake bake model Articles # Controllers # テストを焼かない場合には --no-test # CRUDアクションを焼かない場合には --no-actions ./app/App/Console/cake bake controller Users ./app/App/Console/cake bake controller Articles # Views ./app/App/Console/cake bake view Users ./app/App/Console/cake bake view Articles |
http://localhost:8000/users にアクセスすると、アプリケーションが動きます。
CakePHP3のソース
bakeして焼かれたソースを見てみます。
Model
モデルはCakePHP2とは大きく違っています。EntityクラスとTableクラスに分かれるようになりました。設定はTableクラスに、ロジックはEntityクラスに、といった分担でしょうか。テーブル名やプライマリキー、デフォルトで有効にするビヘイビアなどの設定はTableクラスのinitialize()メソッド内に書かれています。バリデーションはvalidationDefault()メソッド内に書かれています。そして各設定はメンバ変数ではなくメソッドを使っての指定となりました。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# ModelのEntityクラス <?php namespace App\Model\Entity; use Cake\ORM\Entity; /** * User Entity. */ class User extends Entity { /** * Fields that can be mass assigned using newEntity() or patchEntity(). * * @var array */ protected $_accessible = [ 'nickname' => true, 'email' => true, ]; } # ModelのTableクラス <?php namespace App\Model\Table; use Cake\ORM\Table; use Cake\Validation\Validator; /** * Users Model */ class UsersTable extends Table { /** * Initialize method * * @param array $config The configuration for the Table. * @return void */ public function initialize(array $config) { $this->table('users'); $this->displayField('id'); $this->primaryKey(['id']); $this->addBehavior('Timestamp'); $this->hasMany('Articles', [ 'foreignKey' => 'user_id', ]); } /** * Default validation rules. * * @param \Cake\Validation\Validator $validator * @return \Cake\Validation\Validator */ public function validationDefault(Validator $validator) { $validator ->add('id', 'valid', ['rule' => 'numeric']) ->allowEmpty('id', 'create') ->allowEmpty('nickname') ->add('email', 'valid', ['rule' => 'email']) ->allowEmpty('email'); return $validator; } } |
View
ビュー用のファイルは、App/View/ではなくApp/Template/に作るようになっています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php foreach ($articles as $article): ?> <tr> <td><?= h($article->id); ?> </td> <td> <?= $this->Html->link($article->user->id, ['controller' => 'users', 'action' => 'view', $article->id]); ?> </td> <td><?= h($article->title); ?> </td> <td><?= h($article->body); ?> </td> <td><?= h($article->created); ?> </td> <td><?= h($article->modified); ?> </td> <td class="actions"> <?= $this->Html->link(__('View'), ['action' => 'view', $article->id]); ?> <?= $this->Html->link(__('Edit'), ['action' => 'edit', $article->id]); ?> <?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $article->id], [], __('Are you sure you want to delete # %s?', $article->id)); ?> </td> </tr> <?php endforeach; ?> |
コントローラでfind()して得られた$articlesをループしているTemplate/Articles/index.ctpをみてみると、配列ではなくオブジェクトのメンバ変数を参照して表示していることが分かります。findの返値である$articlesはCake\ORM\Query、$articleはさっきbakeしたApp\Model\Entity\Articleクラスのオブジェクトとなっています。
また、日時のcreatedやmodifiedはオブジェクト(Cake\Utility\Time)になっています。Utility/Timeは内部的にDateTimeの拡張であるCarbonを使っています(https://github.com/briannesbitt/Carbon)。いままではビューで配列内のcreatedの文字列をいったんDateTimeクラスのオブジェクトのしてからformatとかしてたのですが、卒業できそうです。
Controller
|
1 2 3 4 5 |
$article = $this->Articles->newEntity($this->request->data); $this->Articles->save($article); article = $this->Articles->patchEntity($article, $this->request->data); $this->Articles->save($article); |
コントローラです。フォームから送信されたデータを保存する部分を見てみます。連想配列のデータをsave()する今までとは違い、TableクラスのnewEntity()やpatchEntity()メソッドで生成したEntityクラスをsave()の引数に渡すようになっています。
ViewCells
CakePHP3からViewCellが追加されています。これは既存のCakePHPの概念を使って簡単に表現すると、小さいController付きelementのような機能で、RailsのコンポーネントのCellからアイデアを得ています。
|
1 |
./app/App/Console/cake bake cell Article |
bakeすると、App/Template/Cell/Article/display.ctpとApp/View/Cell/ArticleCell.phpが生成されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<?php namespace App\View\Cell; use Cake\View\Cell; /** * Article cell */ class ArticleCell extends Cell { /** * List of valid options that can be passed into this * cell's constructor. * * @var array */ protected $_validCellOptions = []; /** * Default display method. * * @return void */ public function display() { $this->loadModel('Articles'); $articles_count = $this->Articles->find('all')->count(); $this->set(compact('articles_count')); } } |
|
1 2 3 |
<div class="articles count"> You have <?= $articles_count ?> articles. </div> |
上記のようにCellクラスとデフォルトのdisplayを実装したら、他のビューからは次用のように呼ぶことができます。
|
1 |
<?= $this->cell('Article') ?> |
http://book.cakephp.org/3.0/en/views/cells.html のマニュアルを見てみると、オーバーヘッドの少ないリクエストコンテナとして、既存のrequestAction()を置き換える機能になるだろうとされています。
いままで、Viewの共通部分はelementやViewBlockにまとめることができましたが、より疎結合にしようとするとそのなかでClassRegistoryするロジックをPHPタグを開始して冒頭に書いてしまったりしたので、cellを使うとすっきりまとまりそうです。
また、CakePHPのCoreDeveloperであるJoseさんのエントリを見ると、うまくやるとプレゼンテーションロジックの実装用途でCellが使えそうです。最近RailsをやっていてDraper(https://github.com/drapergem/draper)がとても便利で感動したので、この流れはいいなぁと思います。
まとめ
CakePHP3はとても素晴らしい進化をしていると思います。ウェブアプリケーションフレームワークとして非常に敷居が低く初心者にはとても優しいままで、シンプルかつフルスタックで、プラグインによる拡張性も高く、中級者以上も使いやすいと思います。今回やったように、環境構築も簡単です。
Railsはとても良いですが、CakePHPもかなり良いですよ!