yano3nora
11/14/2017 - 5:53 AM

[cakephp: CakePHP3 note] CakePHP3 basic knowledge note. #cakephp

[cakephp: CakePHP3 note] CakePHP3 basic knowledge note. #cakephp #php

OVERVIEW

CakePHP は PHP 製の MVC フレームワーク。フルスタック/設定より規約な Ruby On Rails の PHP 版といった位置づけ。日本市場では PHP の MVC フレームワークとして CakePHP 1 ~ 2 系ユーザがとても多いため、リファレンスは充実している。

2 → 3 系でその他モダン MVC フレームワーク ( Laravel 等 ) と同等の機能を備えることになったが、その大きな仕様変更により「モダンな FW なら Cake 以外でもいい」「レガシーなプロダクトなら 2.x でいい」といった潮流がでてきているため今後ユーザは緩やかに減少すると思われる。

とりあえず以下を読めば一般的な WEB アプリケーションに必要な一通りの操作を覚えられるはず。

初心者のためのCakePHP3 プログラミング入門

References

Merits

  • PHP 要件が上がり機能・速度の向上
    • 5.6 以上 ( extension=intl,mbstring,openssl ) MySQL 5.1 以上
    • namespace や use や trait など純粋 PHP で提供されて OOP しやすい
  • composer でバージョン管理やビルドがらくちん
    • 必然的にその他 PHP ライブラリとの共生がしやすくなる
    • autoload ( PSR-4 ) 対応でファイル名・ディレクトリ構造・namespace を認識
  • モデル周りが大幅変更になりできることがかなり増えた
    • ORM にやっとこ対応し配列地獄から抜け出せる
    • QueryBuilder で SQL 生成がらくちんに
      • Model > Table にこれまでのモデル処理 ( DB アクセスロジック )
      • Model > Entity で Table で取得した各レコードがアクティブレコードっぽくなる

Demerits

  • 2.x 系のコードの多く ( 特に Model 周り ) は書き直し
  • 2 系プラグインは全滅 3 系プラグインはまだ少ない
  • 名前空間 や ORM など OOP 知識が必須

INSTALLATION

Process

  1. Composer の導入
  2. php.ini を設定
    • ;extension=php_intl.dll ←コメントアウト解除
  3. composer から CakePHP プロジェクト生成
    • $ composer create-project --prefer-dist cakephp/app {PROJECT_NAME}
  4. app/config/app.php でDB設定 ( app.default.php も Git 管理用に変えておく)
  5. bake コマンドでファイル生成したり、ごりごり書いたり...

composer create-project / composer install

$ composer create-project はまだ アプリルートディレクトリが存在しないとき に行うことで CakePHP 本体 + スケルトンを内部的に git clone してから composer install している。

既存のアプリルート ( app ディレクトリ ) が存在する場合は composer install でよい。

このとき app\src\Console\InstallerWRITABLE_DIRS/tmp などのアプリケーション動作に必要かつ書き込みをするディレクトリ を足しておき自動生成されるようにできる。便利。参考

CakePHP2 は基本 Download 運用だったため tmp ディクトリとかの設定が面倒だったが全部やってくれて助かる。


INTRODUCTION

Bake

スケルトンファイルを自動生成するコマンド。現在 DB にアクセスして各 MVC ファイルを「最低限の CRUD が出来るレベル」で自動生成してくれる。各種プラグインの導入も Bake で行うみたい。

CakePHP3 Manual - Bake でコード生成
Bake の拡張
bakeによるコードの自動挿入

$ app/bin/cake bake {all/model/controller/template} {table名}
$ app/bin/cake bake plugin admin

Controller / View の Bake 時にプレフィクスをつける

$ bin/cake bake controller Users --prefix Admin

Association

CakePHPはDBの連携(アソシエーションと呼ぶ)を前提に作られていて、リレーショナルなDBの利用を簡単に開発できる。

1:1 - hasOne / belongsTo

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'
      ]);
   }
}
普段は hasMany なんだけど hasOne でとりたい場面もあるやつ
$this->hasMany('Comments', [
  'foreignKey' => 'user_id'
]);

$this->hasOne('LatestComments', [
  'className'  => 'Comments',
  'foreignKey' => 'user_id',
  'strategy'   => 'select',
  'sort'       => ['created' => 'desc']
]);

1:n - hasMany / belongsTo

一番よくあるやつ。save() 時の関連エンティティについての挙動を saveStrategy で定義可能、デフォルトは append

// ArticlesTable.php
// タグを沢山持っているのでhasMany
$this->hasMany('Tags', [
    'foreignKey' => 'tag_id',
]);

// TagsTable.php
// 記事に所属するものなのでbelongsTo
$this->belongsTo('Articles', [
    'foreignKey' => 'article_id',
]);
dependent で依存モデルをまとめて削除

CakePHP3のアソシエーション機能を使い関連レコードをまとめて削除

User が消えたらその User が書いた Article は消えるべき、みたいな依存性を持たせる場合。

// UsersTable.php
$this->hasMany('Articles', [
    'foreignKey' => 'user_id'
    'dependent'  => true,
]);

n:n - belongsToMany

交差テーブル時推奨。 純粋な交差の場合は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']) とかしなくて済む
belongsToMany なんだけど常に 1:1 のケース

中間テーブルで外部キー連携を管理しているんだけど、片方のエンティティからみたら常に 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
外部キー名テーブル名単数形_idsalary_id
フィールド名スネーク単数形first_name, given_name, last_name
モデル > テーブルクラス名アッパキャメル複数形+Tableshift_patterns → ShiftPatternTable
モデル > エンティティクラス名アッパキャメル単数形ShiftPattern
コントローラクラス名アッパキャメル複数形+ControllerShiftPatternsController
アクション名キャメルケースregisterAll()
ビュー > テンプレート名 (DIR)アッパキャメル複数形ShiftPatterns
ビュー > テンプレート名 (FILE)アクション名のスネークregister_all.ctp

モデル層(Table:データアクセス・ロジック / Entity:レコードロジック)

がっつり分けようと思ったら、データアクセス系を Tables へ、ドメインロジックを Behavior へ、エンティティ単体の振る舞いを Entity へ入れるのかな。


TIPS & REFERENCES

ルーティングの確認

Routes Shell

Rails の $ rake routes と同様に現在のルーティングを確認可能。

$ bin/cake routes
$ bin/cake routes check /bookmarks/edit/1

DI 依存性注入について

少し 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.
  }
}

Robots.txt

CakePHPはRobots.txtを置いておかないとエラーログがいっぱいでちゃいます

$ 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,    // 分単位で指定
],

CLASSES

基本の MVC クラスや利用頻度の高いクラスについては別メモ参照。

Router

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 でコントローラや認証設定を分けたいとき。

TableRegistry

use Cake\ORM\TableRegistry;
$this->Articles = TableRegistry::get('Articles');  // 関連してないやつはこっち
$this->Articles->find();

// 但し関連モデルならチェーンでいけるのでこっちを利用すべき
$this->Users->UserTokens->find();  

// コントローラで関連もない場合はこっちを使うべき
$this->loadModel('Articles');
$articles = $this->Articles->find();

ConnectionManager

トランザクションの実装

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());
}

生 SQL の実行

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 のデバッグ

// 実行前の 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のログ出力を無効化

CsrfComponent

トークンを利用した二重送信の防止

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');
}

SecurityComponent

画像 Submit ボタンが弾かれる

http://www.aipacommander.com/entry/2015/04/29/150000