[cakephp: CakePHP3 note] CakePHP3 basic knowledge note. #cakephp #php
CakePHP は PHP 製の MVC フレームワーク。フルスタック/設定より規約な Ruby On Rails の PHP 版といった位置づけ。日本市場では PHP の MVC フレームワークとして CakePHP 1 ~ 2 系ユーザがとても多いため、リファレンスは充実している。
2 → 3 系でその他モダン MVC フレームワーク ( Laravel 等 ) と同等の機能を備えることになったが、その大きな仕様変更により「モダンな FW なら Cake 以外でもいい」「レガシーなプロダクトなら 2.x でいい」といった潮流がでてきているため今後ユーザは緩やかに減少すると思われる。
とりあえず以下を読めば一般的な WEB アプリケーションに必要な一通りの操作を覚えられるはず。
;extension=php_intl.dll
←コメントアウト解除$ composer create-project --prefer-dist cakephp/app {PROJECT_NAME}
app/config/app.php
でDB設定 ( app.default.php
も Git 管理用に変えておく)$ composer create-project
はまだ アプリルートディレクトリが存在しないとき に行うことで CakePHP 本体 + スケルトンを内部的に git clone
してから composer install
している。
既存のアプリルート ( app ディレクトリ ) が存在する場合は composer install
でよい。
このとき app\src\Console\Installer
の WRITABLE_DIRS
に /tmp
などのアプリケーション動作に必要かつ書き込みをするディレクトリ を足しておき自動生成されるようにできる。便利。参考
CakePHP2 は基本 Download 運用だったため tmp ディクトリとかの設定が面倒だったが全部やってくれて助かる。
スケルトンファイルを自動生成するコマンド。現在 DB にアクセスして各 MVC ファイルを「最低限の CRUD が出来るレベル」で自動生成してくれる。各種プラグインの導入も Bake で行うみたい。
$ app/bin/cake bake {all/model/controller/template} {table名}
$ app/bin/cake bake plugin admin
$ bin/cake bake controller Users --prefix Admin
CakePHPはDBの連携(アソシエーションと呼ぶ)を前提に作られていて、リレーショナルなDBの利用を簡単に開発できる。
hasOne にしておくと find()
contain()
などでイーガロードする際に常に1件だけ取得するようになり first()
を呼ぶ必要がなくなる。Bake すると大体全部 hasMany 扱いになるので プロジェクト開始時に以下コードで hasOne なものは厳密に定義してから始めること 。
// UsersTable.php
// ユーザー詳細を1件持っているのでhasOne
$this->hasOne('UserDetails', [
'foreignKey' => 'user_id',
]);
// UserDetailsTable.php
// ユーザーに所属するものなのでbelongsTo
$this->belongsTo('Users', [
'foreignKey' => 'user_id',
]);
また 1:0-n のような外部キーに NULL がくるようなケース では contain()
で結合する際に 'joinType' => 'INNER'
だと 関連モデルが NULL 時に主モデルも検索にひっかからなくなる。 関連モデルが NULL でも主モデルをひっぱりたい場合は INNER JOIN
ではなく LEFT JOIN
にすることで回避する。 (そもそも NULL を利用しないのが理想ではある)
// @link https://goo.gl/TLtgWT
class UserinfosTable extends Table
{
public function initialize(array $config)
{
$this->belongsTo('Userinfos', [
'foreignKey' => 'office_id',
'joinType' => 'LEFT'
]);
}
}
$this->hasMany('Comments', [
'foreignKey' => 'user_id'
]);
$this->hasOne('LatestComments', [
'className' => 'Comments',
'foreignKey' => 'user_id',
'strategy' => 'select',
'sort' => ['created' => 'desc']
]);
一番よくあるやつ。save()
時の関連エンティティについての挙動を saveStrategy
で定義可能、デフォルトは append
。
// ArticlesTable.php
// タグを沢山持っているのでhasMany
$this->hasMany('Tags', [
'foreignKey' => 'tag_id',
]);
// TagsTable.php
// 記事に所属するものなのでbelongsTo
$this->belongsTo('Articles', [
'foreignKey' => 'article_id',
]);
User が消えたらその User が書いた Article は消えるべき、みたいな依存性を持たせる場合。
// UsersTable.php
$this->hasMany('Articles', [
'foreignKey' => 'user_id'
'dependent' => true,
]);
交差テーブル時推奨。 純粋な交差の場合はbelongsToMany設定にしておくと contain()
で取得時に、中間テーブルのレコードが省かれてうれしい。多対多テーブル (n:n) の命名規則は規約では「articles_tags」と両方複数形。 この規約通りの命名なら特に設定は不要( Bake 時に自動的に belongsToMany になる)が、命名ミスったら以下コードで設定する。
save()
時の関連エンティティについての挙動を saveStrategy
で定義可能、デフォルトは replace
。
// ArticlesTable.php
// 中間テーブルを joinTable に設定して belongsToMany に
$this->belongsToMany('Tags', [
'joinTable' => 'ArticlesTags', // articles_tags でも ok
]);
// TagsTable.php
// 交差先も同じく中間テーブルを設定し belongsToMany に
$this->belongsToMany('Articles', [
'joinTable' => 'ArticlesTags',
]);
// contain
$this->Articles->find()->contain(['Tags']);
// ->contain(['Articles.ArticlesTags.Tags']) とかしなくて済む
中間テーブルで外部キー連携を管理しているんだけど、片方のエンティティからみたら常に 1 件しかない制約をアプリケーションレベルで設けている場合、 contain
で持ってくると必ず複数前提になって少し面倒... $user->companies[0]->hoge; //ださい
...この場合 エンティティのゲッターに first()
でとってくる制約を設けてあげればよい 。
// Tables
$this->belongsToMany('Companies', [
'joinTable' => 'CompanyUsers'
]);
// Entity
/**
* Get my first company when user belongs to many companies.
* @link https://laracasts.com/discuss/channels/general-discussion/belongstomany-first
*/
public function _getCompany() {
if (empty($this->companies)) return null;
if (is_array($this->companies)) return $this->companies[0];
return $this->companies->first();
}
CakePHPはディレクトリ名/ファイル名/テーブル名/クラス名など多くの命名規則をもち、これら規約に準拠することでさまざまな手続きを省略できる設計になっている。
スコープ | 命名規則 | 例 |
---|---|---|
テーブル名 | スネーク複数形 | oranges, salaries, shift_patterns |
外部キー名 | テーブル名単数形_id | salary_id |
フィールド名 | スネーク単数形 | first_name, given_name, last_name |
モデル > テーブルクラス名 | アッパキャメル複数形+Table | shift_patterns → ShiftPatternTable |
モデル > エンティティクラス名 | アッパキャメル単数形 | ShiftPattern |
コントローラクラス名 | アッパキャメル複数形+Controller | ShiftPatternsController |
アクション名 | キャメルケース | registerAll() |
ビュー > テンプレート名 (DIR) | アッパキャメル複数形 | ShiftPatterns |
ビュー > テンプレート名 (FILE) | アクション名のスネーク | register_all.ctp |
がっつり分けようと思ったら、データアクセス系を Tables へ、ドメインロジックを Behavior へ、エンティティ単体の振る舞いを Entity へ入れるのかな。
Rails の $ rake routes
と同様に現在のルーティングを確認可能。
$ bin/cake routes
$ bin/cake routes check /bookmarks/edit/1
少し Cake の規約と外れるが、心掛けたいポイント。
CakePHP は TableRegistry::get()
や $this->loadModel()
などモデルの コンストラクタインジェクション により、各クラス ( Entity / Table / Controller など) のメソッドで 引数に id などを受け取り内部でインスタンス生成してもクラス同士が直接依存しない建付けになっている 。ただメモリや DB アクセスなど多少はリソースを食うので、可能な場面では 引数にインスタンスを受け渡すインタフェースインジェクション を行う方が低コストかつ保守性に優れ、テストもしやすいので積極的に利用したい。
依存性注入
これを行わないとあるクラスのメソッドが別クラスに依存し、互いの変更に引っ張られ合う & 結合状態でしかテストできなくなる。インタフェースインジェクションにより、インスタンスを引数に受ける際は必ず use 宣言 & タイプヒンティング を行うようにして (\App\Hoge
みたいにトップレベルからとってこないで... ) こうすることで このクラスが何に依存しているのか? が一目でわかるようになる。
use App\Model\Entity\Article; // 途中まで書けばエディタが補完してくれるようにしとく
class UsersTable extends Table {
public function hoge(Article $Article) {
// Do something by using $Article.
}
}
$ vim webroot/robots.txt
> User-agent: *
> Allow: /
WEB ルート移動してる & 全部 NG にする場合以下。
$ vim public/robots.txt
> User-agent: *
> Disallow: /
// refs: https://book.cakephp.org/3.0/ja/development/sessions.html#session-configuration
// in app.php
'Session' => [
'defaults' => 'php',
'timeout' => 60, // 分単位で指定
],
基本の MVC クラスや利用頻度の高いクラスについては別メモ参照。
use Cake\Routing\Router;
Router::url(); // /bookmarks
Router::reverse($this->request, false); // /bookmarks?hoge=fuga
Router::reverse($this->request, true); // http://example.com/bookmarks?hoge=fuga
User と Admin でコントローラや認証設定を分けたいとき。
use Cake\ORM\TableRegistry;
$this->Articles = TableRegistry::get('Articles'); // 関連してないやつはこっち
$this->Articles->find();
// 但し関連モデルならチェーンでいけるのでこっちを利用すべき
$this->Users->UserTokens->find();
// コントローラで関連もない場合はこっちを使うべき
$this->loadModel('Articles');
$articles = $this->Articles->find();
use Cake\Datasource\ConnectionManager;
// e.g.) In Controller.
$connection = ConnectionManager::get('default');
$connection->begin();
try {
if (!$newUser = $this->Users->save($user)) {
throw new Exception('Failed to save user.');
}
$post->id = $newUser->id;
if (!$this->Posts->save($post)) {
throw new Exception('Failed to save post.');
}
$connection->commit();
} catch (Exception $e) {
$connection->rollback();
$this->Flash->error($e->getMessage());
}
Cakeのモジュール(ConnectionManager)を利用しつつ生クエリ書く例。
// 基本形
use Cake\Datasource\ConnectionManager;
$cnct = ConnectionManager::get('default');
$results = $cnct->execute(' sql文 ')->fetchAll('assoc');
// 読込(select)
$results = $cnct->execute(
'select * from articles where created >= :created',
['created' => DataTime('1 day ago')],
['created' => 'datetime']
)
->fetchAll('assoc');
// 追加(insert)
$connection->insert('articles', [
'title' => 'A New Article',
'created' => new DateTime('now')
], ['created' => 'datetime']);
// 更新(update)
$connection->update('articles', ['title' => 'New title'], ['id' => 10]);
// 削除(delete)
$connection->delete('articles', ['id' => 10]);
// 実行前の SQL チェック
$query = $this->Model->find();
debug($query->sql());
// 実行されたやつチェック → ログが cake/logs/debug.log に吐かれる
$connection = \Cake\Datasource\ConnectionManager::get('default'); // DB接続を取得
$connection->logQueries(true); // SQL Queryのログ出力を有効化
$this->Model->find()->all(); // SQL文を確認したいSQLを実行
$connection->logQueries(false); // SQL Queryのログ出力を無効化
Csrf トークンはデフォルトでは Session 発行と同じタイミングで更新され、クッキーに保存される。これについて リクエスト毎に Cookie の Csrf トークン情報の存在をチェックし、存在するときこれを削除し Csrf トークンをリジェネレートする 状態に変更する。
これにより、リクエスト毎にトークンが再発行され、正常なレンダリングを伴わない POST リクエストについて「トークンのミスマッチ」エラーを発生させることができる。
但し、Csrf 対策としては トークンを都度変更するより固定する方が適切 であり、弊害もある気がするので運用時は「特に二重送信を防止したい」箇所のみに絞った方が無難。
public function initialize() {
parent::initialize();
// Regenerate csrf token per request for multiple submit.
$this->loadComponent('Csrf');
$this->loadComponent('Cookie');
if ($this->request->cookie('csrfToken')) $this->Cookie->delete('csrfToken');
}