markdown [cakephp:CakePHP3注意事项] CakePHP3基本知识说明。 #cakephp

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了markdown [cakephp:CakePHP3注意事项] CakePHP3基本知识说明。 #cakephp相关的知识,希望对你有一定的参考价值。

## なにか
CakePHP3 の CSRFコンポーネント使っているときに Ajax で POST したいよ。

### 参考
- https://book.cakephp.org/3.0/ja/controllers/components/csrf.html#csrfajax
- http://shota-natuta.hatenablog.com/entry/2017/02/01/205401

### コード
> Riot.js と SuperAgent の組み合わせ前提

```php
// in Controller
$this->set('token', $this->request->getParam('_csrfToken'));

// in View
<my-tag data-token="<?php echo $token ?>"></my-tag>
```

#### パラメータないパターン
```js
// in 'my-tag.tag' method
this.token = this.opts.dataToken

this.post = () => {
  request.post(`/path/to/api`)
    .set('X-Requested-With', 'XMLHttpRequest')
    .set('X-CSRF-Token', token)  // passing token 
    .type('form')
    .end((err, res) => {
      if (err) console.error(err)
      if (res.body.response != null) {
        // do something
        // this.update()
      } else console.warn('API Error Occurred.')  
  })
}
</script>
```

#### パラメータあるパターン
POST で `send()` でパラメータ送ると FormHelper 使わずにフォーム送信したとみなされて `SecurityComponent` に飲み込まれる。回避策わかんない。Ajaxなら `query()` でごまかせ。

```js
// in 'my-tag.tag' method
this.token = this.opts.dataToken
this.hoge  = this.opts.dataHoge
this.fuga  = this.opts.dataFuga

this.post = () => {
  request.post(`/path/to/api`)
    .set('X-Requested-With', 'XMLHttpRequest')
    .set('X-CSRF-Token', token)  // passing token 
    .type('form')
    .query({
      hoge:  this.hoge,
      fuga:  this.fuga,
    })
    .end((err, res) => {
      if (err) console.error(err)
      if (res.body.response != null) {
        // do something
        // this.update()
      } else console.warn('API Error Occurred.')  
  })
}
</script>
```
## Overview ##
CakePHP は PHP製のMVCフレームワーク。フルスタック/設定より規約な RubyOnRails のPHP版といった位置づけ。日本市場ではPHPのMVCフレームワークとしてCakePHP1~2系ユーザがとても多いため、リファレンスは充実している。[公式マニュアル](https://book.cakephp.org/3.0/ja/index.html)もなかなか見やすい。

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

### References
- [CakePHP3 - Manual](https://book.cakephp.org/3.0/ja/index.html)
- [CakePHP3 - API](https://api.cakephp.org/3.4/)
- [3系チュートリアル*](http://amaraimusi.sakura.ne.jp/note_prg/cakephp3/)
- [3系チートシート](http://cake3.codaxis.com/)
- [2系との比較](https://speakerdeck.com/sakuragawa/cakephp2to3falsewei-i)
- [2系から変わったこと](http://qiita.com/s-kiriki/items/636ec1109f48fb977347)

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

### Demerits
- 2.x系のコードの多く(特にModel周り)は全書き直し
- 2系プラグインは全滅。3系プラグインはまだ少ない
- 名前空間 `namespace` `new \DateTime()` や ORM によるデータ層のモデリング/オブジェクト設計の知識が必須


-------


## Installation ##
### Process
1. Composerの導入 
    - [オフィシャル](https://getcomposer.org/doc/articles/custom-installers.md)
    - [翻訳](https://kohkimakimoto.github.io/getcomposer.org_doc_jp/doc/00-intro.html)
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コマンドでファイル生成したり、ごりごり書いたり...

### Bake Command
DBにアクセスして各MVCファイルを自動生成するコマンド。DBのテーブルを参照して基本的なCRUDが出来る状態で各ファイルを自動生成生成されるので、そこに追記してアプリを拡張していくのが開発の基本。各種プラグインの取得もBakeで行う。DBを参照してファイルを作るのでbake前にDBの内容を確定させとく必要有り。

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

# app/bin/cake bake model all -f で 全モデル ( テーブル/エンティティ ) 上書き
```


-------------------------------------------------------


## CakePHPの規約について ##

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

- http://qiita.com/chiyoyo/items/3008d3a0212ca3148bdd
- http://book.cakephp.org/3.0/ja/plugins.html#plugin-models
- http://book.cakephp.org/3.0/ja/controllers/components/pagination.html#join

#### 1:1 - hasOne / belongsTo
hasOne にしておくと `find()` `contain()` などでイーガロードする際に常に1件だけ取得するようになり `first()` を呼ぶ必要がなくなる。Bake すると大体全部 hasMany 扱いになるので **プロジェクト開始時に以下コードで hasOne なものは厳密に定義してから始めること** 。

```php
// 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 を利用しないのが理想ではある)

```php
// @link https://goo.gl/TLtgWT

class UserinfosTable extends Table
{

   public function initialize(array $config)
    {

      $this->belongsTo('Userinfos', [
            'foreignKey' => 'office_id',
            'joinType' => 'LEFT'
      ]);
   }
}
```

##### 普段は hasMany なんだけど hasOne でとりたい場面もあるやつ
```php
$this->hasMany('Comments', [
  'foreignKey' => 'user_id'
]);

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

// In controller.
$users = $this->Users->find()
  ->where(/* something conditions */)
  ->contain(['LatestComments']);
$this->set(compact('users'));

// In view.
<?php foreach ($users ?? [] as $user) : ?>
  <?php echo $user->name ?>
  <?php echo $user->lastet_comment->comment ?? '' ?>
<?php endforeach ?>
```

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

// TagsTable.php
// 記事に所属するものなのでbelongsTo
$this->belongsTo('Articles', [
    'foreignKey' => 'article_id',
]);
```

#### n:n - belongsToMany
**交差テーブル時推奨。** 純粋な交差の場合はbelongsToMany設定にしておくと `contain()` で取得時に、中間テーブルのレコードが省かれてうれしい。**多対多テーブル (n:n) の命名規則は規約では「articles_tags」と両方複数形。** この規約通りの命名なら特に設定は不要( Bake 時に自動的に belongsToMany になる)が、命名ミスったら以下コードで設定する。

```php
// 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 |
| 外部キー名   | テーブル名単数形_id | salary_id |
| フィールド名 | スネーク単数形 | first_name, given_name, last_name |
| モデル > テーブルクラス名 | アッパキャメル複数形+Table | shift_patterns → ShiftPatternTable | 
| モデル > エンティティクラス名 | アッパキャメル単数形 | ShiftPattern | 
| コントローラクラス名 | アッパキャメル複数形+Controller | ShiftPatternsController | 
| アクション名 | キャメルケース | registerAll() |
| ビュー > テンプレート名 (DIR) | アッパキャメル複数形 | ShiftPatterns |
| ビュー > テンプレート名 (FILE) | アクション名のスネーク | register_all.ctp |

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

- マニュアル http://book.cakephp.org/3.0/ja/orm/retrieving-data-and-resultsets.html  
- 基本コード http://easyramble.com/associated-table-with-cakephp.html  
- モデル層ざっくり https://norm-nois.com/blog/archives/3310
- モデル層もっとくわしく http://tech.connehito.com/entry/cakephp3-orm-entity


------------------------------------------------


## Sample Codes ##

### View
#### HtmlHelper
```php
// link
$this->Html->link(__'リンク',['action'=>'add','?'=>['id'=>'110']]);

// disuse escape for fontawesome
$this->Html->link("<i class='fa fa-edit'></i>", ['action'=>'edit',$user->id],['escape'=>false]);
```

#### FormHelper - basic
- [基本インプット](http://libro.tuyano.com/index3?id=8586003&page=3)
- [セレクト・ラジオ出力](http://libro.tuyano.com/index3?id=8586003&page=4)

> `$this->Form->input()` について 3.4 から `Form::input()` から `Form::control()` に名称変更されてます。今のところどっちも同じ挙動ぽいけどさっさと変えたほうがいいのかしら。  
> また `control()` `input()` は常に `<div>` で WRAP され `date()` や `text()` は WRAP されない。もっと細かいカスタマイズをする場合は **ウィジェットテンプレート** なるものをカスタマイズする必要がある。だるい。[参考 - CakePHP3 のFormHelperが生成するHTMLをカスタマイズしたい](https://qiita.com/alpha_350/items/9c28c200eeb5b12b79e5)

```php
$this->Form->create($user /* ← ここに Entity オブジェクトぶちこむ、なければ null を */ ,[
  'type'=>'post',
  'url'=>['controller'=>'Users', 'action'=>'index'],
  'onsubmit' => 'return confirm(\'実行しますか?\');'
]);

$this->Form->hidden('id',['default'=>$id]);
$this->Form->input('name'); //スキーマからインプットタイプをよしなに
$this->Form->input('password',['label'=>'パスワード','type'='password');

// テンプレート編集しないでできるのはこんくらい
$this->Form->input('birthday', [
    'label'      => '誕生日',
    'type'       => 'date',
    'dateFormat' => 'YMD',
    'monthNames' => false,
    'maxYear'    => date('Y')+1,
    'minYear'    => date('Y')-100,
    'empty'      => ['year' => '年', 'month' => '月', 'day' => '日'],
    // 'default'    => date('Y-m-d'),
]);

// Date 日本対応
$this->Form->templates([
    'dateWidget' => '<ul class="list-inline"><li class="year">{{year}} 年</li><li class="month"> {{month}} 月 </li><li class="day"> {{day}} 日 </li><li class="hour"> {{hour}} 時 </li><li class="minute"> {{minute}} 分 </li></ul>'
]);

echo $this->Form->input('start_datetime', array(
                                     'type' => 'datetime',
                                     'label' => '開始日時',
                                     'dateFormat' => 'YMD',
                                     'monthNames' => false,
                                     'separator' => '/',
                                     'minYear' => date('Y'),
                                     'maxYear' => date('Y')+1,
                                     'default' => date('Y-m-d')
                                ));

$this->Form->input('open', [
    'type'=>'time', //日付なし
    'format' => 24, //24h表記
    'interval'=>15 //15分刻み
]);
$this->Form->text('class',[
    'default'=>$default,
    'readonly'=>'readonly'
]);

```

#### FormHelper - association
FormHelper でうまいこと POST データ練り上げて `newEntity()` `patchEntity()` なりに `$this->request->getData()` を渡してあげると **エンティティの関連データ (アソシエーション) を一括で `save()` できる** のでとっても便利。

```php
/**
 * Example Associations.
 *
 * - articles
 *     - belongsTo: authors
 *         - hasOne: profiles
 *     - hasMany: comments
 *     - belongsToMany: tags
 */
 
$this->Form->create($article);
 
// Article
echo $this->Form->hidden('id', ['value' => $article->id])
echo $this->Form->control('title');
 
// Article > Author (belongsTo)
echo $this->Form->control('author.id');
echo $this->Form->control('author.first_name');
echo $this->Form->control('author.last_name');
 
// Article > Author > Profile (belongsTo > hasOne)
echo $this->Form->control('author.profile.id');
echo $this->Form->control('author.profile.username');
 
// Article > Comments (hasMany)
echo $this->Form->control('comments.0.id');
echo $this->Form->control('comments.0.comment');
echo $this->Form->control('comments.1.id');
echo $this->Form->control('comments.1.comment');


// Article > Tags (belongsToMany) -- input-pattern
echo $this->Form->control('tags.0.id');
echo $this->Form->control('tags.0.name');
echo $this->Form->control('tags.1.id');
echo $this->Form->control('tags.1.name');
 
// Article > Tags (belongsToMany) -- select-pattern
echo $this->Form->control('tags._ids', [
    'type' => 'select',
    'multiple' => true,
    'options' => $tagList,
]);

// Article > ArticleTags (belongsToMany, ['joinTable' => 'articles_tags'])
echo $this->Form->control('tags.0._joinData.starred');
echo $this->Form->control('tags.1._joinData.starred');
```

#### FormHelper - postLink()
```php
// in view
<?php echo $this->Form->postLink(
  '<i class="tiny material-icons md-18">delete</i> 削除',
  [
    'action' => 'delete',
    $event->id,
  ],
  [
    'confirm' => '削除しますか?',
    'method' => 'delete',
    'escape' => false,
    'class' => 'btn waves-effect waves-light red accent-4',
    'tabindex' => -1 // タブで選択しないようにするやつ
  ]
) ?>

// in controller
public function delete(int $id=null) {
  $this->request->allowMethod(['delete']);
  $event = $this->Events->get($id);
  if ($this->Events->delete($event)) {
    $this->Flash->success(MSG_DELETE_SUCCESS);
  } else {
    $this->Flash->error(MSG_DELETE_ERROR);
  }
  return $this->redirect(['action' => 'index']);
}
```

#### FormHelper - image/file
- http://book.cakephp.org/3.0/en/views/helpers/form.html#creating-file-inputs   
- http://stackoverflow.com/questions/29648335/cakephp-3-0-uploading-images  

```php
// In View.
echo $this->Form->create($Users, [
  'controller'=>'Users',
  'action'=>'edit',
  'type' => 'file'
]);
echo $this->Form->input('img_data',['type'=>'file']);
echo $this->Form->hidden('img_user',['value'=>$user['id']]);
echo $this->Form->button('登録', ['class' => 'c-btn']);
echo $this->Form->end();


// In Controller
$this->request->data('img_data');
[
    'name' => 'conference_schedule.pdf',
    'type' => 'application/pdf',
    'tmp_name' => '/var/www/tmp/php1EE.tmp',
    'error' => 0, // On Windows this can be a string.
    'size' => 41737,
];

// Using File Class of CakePHP API.
// @link https://book.cakephp.org/3.0/ja/core-libraries/file-folder.html
try {
  $fileName = $this->request->data('file.name');
  $fileTmp  = $this->request->data('file.tmp_name');
  $FileApi  = new File($fileTmp);
  $fileExt  = pathinfo($fileName, PATHINFO_EXTENSION);
  $filePath = $fileName.'.'.$fileExt;
  $fileDest = FILE_DEST.'/'.$filePath;
  // Verify it that allowed extension.
  if (!in_array($fileExt, ALLOW_EXTENSIONS)) throw new \Exception('Denied file extensions.');
  $FileApi->safe();
  $FileApi->copy($fileDest);
  $FileApi->close();
  $this->Flash->success('Upload was success.');
} catch (\Exception $e) {
  if (!empty($FileApi)) $FileApi->close();
  $this->Flash->error($e->getMessage());
}
return $this->redirect($this->referer());
```

### Controller
#### バリデーションエラー確認
```php
public function add() {
  $event = $this->Events->newEntity();
  if ($this->request->is('post')) {
    $event = $this->Events->patchEntity($event, $this->request->getData());
    if ($this->Events->save($event)) {
      $this->Flash->success(MSG_CREATE_SUCCESS);
      return $this->redirect(['action' => 'index']);
    }
    die(debug($event->errors($data))); //エンティティのerrors()に入っている
    $this->Flash->error(MSG_CREATE_ERROR);
  }
  $this->set(compact('event'));
  $this->set('_serialize', ['event']);
}
```

#### 現在URL取得
```php
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
```

#### リクエスト取得
```php
// get - /books/edit/1 や method=getのフォームなど
public function edit($id=null, $foo=null){
    $var = $this->request->param('foo');
}

// querystring - /book?hoge=2&fuga=3 など
$q = $this->request->query;
var_dump($q);

// post
$this->request->data('User.name');
```

#### リクエストのメソッド is ?
```php
if ($this->request->is('post')) {
    $this->Flash->success('posted values!');
    var_dump($this->request->data('input_name'));
}
```

#### ファイルアップロード受け処理
```php
if (isset($this->request->data['img_data'])) {
    if ($this->request->data('img_del')==="1") {
        $del_dir = WWW_ROOT."img/".$this->request->data('img_user')."/";
        $del_file = $del_dir."user_img.jpg";
        if (file_exists($del_file)) {
            $this->Flash->success('delete done!');
        } else {
            $this->Flash->success('delete error.');
        }
    } elseif($this->request->data('img_data.tmp_name')) {
        $up_dir = WWW_ROOT."img/".$this->request->data('img_user')."/";
        $up_file = $up_dir."user_img.jpg";
        if (move_uploaded_file($this->request->data('img_data.tmp_name'),$up_file)) {
            $this->Flash->success('upload done!');
        } else {
            $this->Flash->success('upload error.');
        }
    }
}
```


#### レンダリングTemplateを変えたい
```
// 初期処理メソッド( initialize , beforeFilter等 )で...
$this->viewBuilder()->layout("admin");
```


#### アクションを内部で切り替え
```
$this->setAction('action_name',$arg);
(引数は省略OK、後のコードも全て実行して移動する仕様)
```


#### コントローラからビューへ値渡し
```
$foo = 1;
$bar = 'aaa';
$this->set(compact('foo', 'bar');
// ↑各ビューで$foo,$barが使える
// $this->set('foo', $foo);でもOK
```


#### 別コントローラやビューの値を参照
`$this->viewVars['custcode'];`


#### コンポーネントの読み込み
```
// in AppController
public function initialize()
{
  parent::initialize();
  $this->loadComponent('RequestHandler');
  $this->loadComponent('Auth');
  $this->loadComponent('Flash');
  $this->Session = $this->request->session();
//  Be sure to use FormHelper
//  $this->loadComponent('Security');
//  $this->loadComponent('Csrf');
}

// in AppView
public function initialize() {
  $this->loadHelper('Html');
  $this->loadHelper('Form');
  $this->loadHelper('Flash');
  $this->loadHelper('Paginator');
}
```


#### リダイレクト
```
// return 仕様になってます
if ($this->Users->save($user)) { //DB保存できたら
    $this->Flash->success(__('saved');
    return $this->redirect(['action'=>'index']);
}

// リダイレクトついでにget値を渡すとか
return $this->redirect(['action'=>'edit', $id]);
```


#### フラッシュ
```
$this->Flash->success(__('failed'));
```


#### Session/セッション
```
// Prepare in AppController
public function initialize() {
  parent::initialize();
  $this->Session = $this->request->session();  // アクセスのらくちん化
}

// Read
$hoge = $this->Session->read('Hoge.hoge');

// Read with Delete
echo $this->Session->consume('User.token');

// Write
$this->Session->write('Hoge.hoge', 'hoge');
$this->Session->write([
  'Member.name' => 'taro',
  'Member.mail' => 'taro@test.com',
]);

// Check ( Return bool )
if ($this->Session->check('User.loggedIn')) { /* ... */ }

// Delete
$this->Session->delete('Hoge');
$this->Session->delete('User.password');

// Destroy ( Delete all )
$this->Session->destroy();
```


### Model 

### exists()
CakePHP2 系では id を渡してたけど、配列で条件を渡す仕様に挙動が変わってる。

```php
$fancyTable = TableRegistry::get('FancyTable');
$exists = $fancyTable->exists(['name' => 'fancy', 'active' => false]);
```

### save()
- [デバッグに使える saveOrFail()](https://book.cakephp.org/3.0/ja/orm/saving-data.html#id21)
- [複数保存](https://book.cakephp.org/3.0/ja/orm/saving-data.html#id22)
- [条件つきの一括更新](https://book.cakephp.org/3.0/ja/orm/saving-data.html#id23)
- 複数の関連モデルを同時に更新したい
    - [CakePHP3で連携するテーブルのデータを保存する方法](https://hacknote.jp/archives/26902/)
    - [BelongsToManyの変換](https://book.cakephp.org/3.0/ja/orm/saving-data.html#belongstomany)

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

#### Transaction with ConnectionManager
```php
// use class
use Cake\Datasource\ConnectionManager; 

// implementation
$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) {
    $this->Flash->error($e->getMessage());
    $connection->rollback();
}
```

#### TableRegistory / QueryBuilder
- [マニュアル](http://book.cakephp.org/3.0/ja/orm/query-builder.html)
- [QueryBuilderまとめ](http://qiita.com/kozo/items/87dc9f725e71dd742468)

```php
use Cake\ORM\TableRegistry;

public function index($id){
    $today = new Time();
    $today = $today->format('Y-m-d');
    $schedules = TableRegistry::get('Schedules');
    $query = $users->find()
        ->select(['date','start','end','last','user_id','Users.name'])
        ->where(['date' => $today])
        ->order(['start' => 'ASC', 'end' => 'ASC']); // SORT
    foreach ($query as $schedule) {
        $end = $schedule->last ? "LAST" : $schedule->end->format('H:i') ;
        $schedule->end = $end;
        $results = $schedule;
    }
    $this->set(compact('results'));

    /**
     * 注意!!
     * foreach前のクエリ($members)は絞り込まれたまま実行されていない遅延評価状態
     * TableRegistryのメソッドにはselect()やwhere()等のsql的なやつの他にも ...
     * [ ->first(); ] で一行取得 [ ->extract('name'); ] で値のみ取得とか
     * [ ->find('list')->select(['id','name']); ] [ foreach ($members as $id => $name){ ... } ]
     * ↑みたいにリスト取得してkey-valueリスト作ったりできる
     * 遅延評価前に上1行のラストで [ ->all(); ] [ ->toArray(); ]  で〆てさっさと実行する手段も
     */
}
```

##### 関連データフィルタリングをしつつ OR する
```php
$tagName = 'PHP';
$categoryName = 'IT';

$postsTable = TableRegistry::get('Posts');

$conditions = [];
$conditions[] = ['Posts.id IN' =>
                 $postsTable->find()
                 ->select(['Posts.id'])
                 ->matching('Tags', function (Query $q) use ($tagName) {
                   return $q->where(['Tags.name' => $tagName]);
                 })
                ];
$conditions[] = ['Posts.id IN' =>
                 $postsTable->find()
                 ->select(['Posts.id'])
                 ->matching('Categories', function (Query $q) use ($categoryName) {
                   return $q->where(['Categories.name' => $categoryName]);
                 })
                ];

$query = $postsTable->find();
$query->where(['OR' => $conditions]);
```

##### Update / Delete + Where
```php
// https://book.cakephp.org/3.0/ja/orm/query-builder.html#delete

$query = $articles->query(); //findじゃないよ
$query->delete()
    ->where(['id' => $id])
    ->execute();

$query = $articles->query();
$query->update()
    ->set(['published' => true])
    ->where(['id' => $id])
    ->execute();
```

#### find() メソッド
##### 基本
```php
// すべての article を検索、この時点ではクエリーは走らない。
$query = $articles->find('all');

// イテレーションはクエリーを実行する
foreach ($query as $row) {
}

// all() の呼び出しはクエリーを実行し、結果セットを返す
$results = $query->all();

// 結果セットがあれば すべての行を取得できる
$data = $results->toArray();

// クエリーから配列への変換はクエリーを実行する
$data = $query->toArray();
```

##### 発展
```php
// クエリオブジェクトから結果をフェッチする
$query = $articles->find('all', [
    'order' => ['Articles.created' => 'DESC']
]);
$row = $query->first();  //吐かれるクエリは LIMIT 1 となる
$number = $query->count();  //ここでは結果セットのカウントが返る

// キー/値のペアを検索する
$query = $articles->find('list');
$data = $query->toArray();
// $data = [
//    1 => '最初の投稿',
//    2 => '私が書いた2つ目の記事',
// ];
```

##### Func
```php
// usersテーブルのidカラムの最大値は?
$query = $this->Users->find();
$ret = $query->select(['max_id' => $query->func()->max('id')])->first();
echo $ret->max_id;
```

```php
$comments = $this->ArticlesComments->find()
  ->where([
    'ArticlesComments.article_id' => $articleId,
  ])
  ->contain([
    'CommentVote' => function ($q){
      return $q->select(['vote']);
    },
    'Users'       => function ($q){
      return $q->select(['username']);
    },
  ]);

// Left Join with ArticlesComments
$comments
  ->leftJoin(
    ['ArticlesCommentsVotes' => 'articles_comments_votes'],
    ['ArticlesComments.id = ArticlesCommentsVotes.comment_id']
  );


// Case
$ratingCases = $comments->newExpr()->addCase([
  $comments->newExpr()->add(['ArticlesCommentsVotes.vote' => '1']),
  $comments->newExpr()->add(['ArticlesCommentsVotes.vote' => '0'])
], [1, - 1, 0], ['integer','integer','integer']);

// Calculate rating and sort
$comments
  ->select([
    'rating' => $comments->func()->sum($ratingCases)
  ])
  ->group('ArticlesCommentsVotes.comment_id')
  ->order($sortBy . ' ' . $sortOrder)->autoFields(true);
```

##### Distinct
```php
// in Table (Users hasMany Terminals)
  public function getUsersByEventId(int $event_id=null) {
    if (empty($event_id)) return [];
    $users = [];
    $this->Terminals = TableRegistry::get('Terminals');
    $terminals = $this->Terminals->find()
      ->distinct(['user_id'])
      ->where([
        'event_id' => $event_id,
        'NOT' => ['user_id' => false],
      ])
      ->contain('Users')
      ->all();
    foreach ($terminals ?? $terminals ?? [] as $terminal) {
      $users[] = $terminal->user;
    }
    return $users;
  }
```

##### Expression を用いた条件付け
マニュアル [高度な条件](https://book.cakephp.org/3.0/ja/orm/query-builder.html#advanced-query-conditions) を参照。

```php
$query = $articles->find()
    ->where(function ($exp) {
        $orConditions = $exp->or_(['author_id' => 2])
            ->eq('author_id', 5);
        return $exp
            ->add($orConditions)
            ->eq('published', true)
            ->gte('view_count', 10);
    });

// SQL
SELECT *
	FROM articles
	WHERE (
		(author_id = 2 OR author_id = 5)
		AND published = 1
		AND view_count >= 10)
```

##### NOT / IS / OR / IN

###### NOT
```php
// in controller
$terminals = $this->Terminals->find()
  ->where([
    'event_id' => $event_id,
    'NOT' => ['user_id IS' => null],  // null は IS 必須
  ])
  ->contain('Users')
  ->all();
$users = [];
```

###### OR
```php
// http://www.chitana.me/entry/2016/12/03/164348

$conditions = ['Trees.is_initial'  => false];

foreach ($trees as $tree) {
  $conditions['OR'][] = [
    'Trees.path LIKE' => $tree->path.'_.',
  ];
}

$results = $this->find()->where($conditions);
```

###### Where IN / Not IN
```php
// IN
$users = TableRegistry::get('User')->find()
    ->where(['User.id IN' => [1, 2, 3]])
    ->all();

// NOT IN
$users = TableRegistry::get('User')->find()
    ->where([
        'NOT' => ['User.id IN' => [1, 2, 3]]
    ])
    ->all();
```


##### Containで関連データ結合
```php
// find() のオプションとして
$query = $articles->find('all', ['contain' => ['Authors', 'Comments']]);

// クエリーオブジェクトのメソッドとして
$query = $articles->find('all');
$query->contain(['Authors', 'Comments']);

// フィルタリングのクロージャ
$query->contain(['Schedules' => function($q) use ($id) {
    return $q->where(['Schedules.date >=' => new time('today')]);
}]);

// contain() 後なら order() なりで関連モデルを指定できる (未検証)
$query = $articles->find();
  ->contain(['Authors', 'Comments'])
  ->order(['Authors.rate' => 'desc']);   // Article を Author の rate (人気度) 降順で
```

##### matching で関連データフィルタリング
[マニュアル](https://book.cakephp.org/3.0/ja/orm/retrieving-data-and-resultsets.html#matching-joins) にある通り、 `Articles belongsToMany Tags` のとき「タグ Tag を持つ記事 Article を探索」するような **特定の関連データを持つデータのフィルタリング** をしたいときは **matching()** メソッドを利用する。

```
// コントローラーやテーブルのメソッド内で
$tagName = 'CakePHP';
$query = $articles->find();
$query->matching('Tags', function ($q) use ($tagName) {
    return $q->where(['Tags.name' => $tagName]);
});

// notMatching() ってのもあるよ
$tagName = '退屈';
$query = $articlesTable
    ->find()
    ->notMatching('Tags', function ($q) use ($tagName) {
        return $q->where(['Tags.name' => $tagName]);
    });

// matching の 連結
$tagNames = ['CakePHP', 'PHP'];
$categoryNames = ['IT', 'コンピュータ'];

$query = TableRegistry::get('Posts')->find();
$query->matching('Tags', function (Query $q) use ($tagNames) {
  return $q->where(['Tags.name IN' => $tagNames]);
})->matching('Categories', function (Query $q) use ($categoryNames) {
  return $q->where(['Categories.name IN' => $categoryNames]);
});

// 深い関連の matching 
$query = $products->find()->matching(
    'Shops.Cities.Countries', function ($q) {
        return $q->where(['Countries.name' => 'Japan']);
    }
);

// 深い関連の matching の closure に 変数を渡す
$username = 'markstory';
$query = $articles->find()->matching('Comments.Users', function ($q) use ($username) {
    return $q->where(['username' => $username]);
});
```

#### 動的な Finder メソッド
```php
// コントローラーの中
$query = $this->Users->findAllByUsername('joebob');

// テーブルメソッドの中
$users = TableRegistry::get('Users');
$query = $users->findByUsername('joebob');
$query = $users->findAllByUsername('joebob');
```

#### Table - Validation / BuildRules
CakePHP3 には2つのバリデーションクラスがあり、管理したい項目によってこれらを使い分ける。大体のケースにおいて **フロントやコントローラ内で項目1つ1つについて頑張る必要がなくなる** ので是非利用していきたい。

- `Cake\ORM\RulesChecker` クラス
    - [データの検証 - buildRules](https://book.cakephp.org/3.0/ja/orm/validation.html#application-rules)
    - Entity の状態をチェックするため **フォーム提供せずシステム側で担保するべき項目のバリデーションで利用する**
- `Cake\Validation\Validator` クラス
    - [カスタムバリデーション](https://book.cakephp.org/3.0/ja/core-libraries/validation.html#id8)
    - [条件付きバリデーション](https://book.cakephp.org/3.0/ja/core-libraries/validation.html#id9)
    - Form から POST で渡る `$this->request->getData()` から `newEntity() / patchEntity()` 時に動作
    - `$entity->errors()` で「なにがなぜ弾かれているのか」分かり `$this->save()` が false を返す
    - 上記エラー持ちエンティティをフォームに突っ込む `$this->Form->create($entity)` と `$this->Form->control()` がメッセージ表示
    - フォームによる操作をチェックするため **フォーム提供しユーザに入力させる項目のバリデーションで利用する**

```
// in Table

  // データを保存する前の検査 (DBつながない)
  public function validationDefault(Validator $validator)
  {
    $validator
      ->allowEmpty('id', 'create');
    $validator
      ->requirePresence('first_name', 'create')
      ->notEmpty('first_name');
      
    // 固定・携帯電話のどちらかを入力させたいケース
    // - View 側で $this->Form->input('telephone, ['required'=>false]); する
    //   - サーバでジャッジする前にフロントで弾かれフォームサブミット自体できなくなる
    // - Controller 側で if ($entity->errors()) $this->redirect($this->referer()); で戻してやる
    // - ちなみに $this->Form->text(); などでフォーム出力するとエラーメッセージ表示 div が自動挿入されない
    //   - 上記理由で手動挿入せんとあかん場合は $this->Form->error() を添えてあげる
    $validator  
      ->notEmpty('telephone', '固定・携帯電話番号どちらかを入力してください。')
      ->allowEmpty('telephone', function ($context) {
        return !empty($context['data']['cellular_phone']);
      });

    return $validator;
  }

  // 保存される/されているデータの整合性を保つ検査 (DBつなぐ)
  public function buildRules(RulesChecker $rules)
  {
    // email のシステム全体での重複を許さない
    $rules->add($rules->isUnique(['email']), [
      'errorField' => 'email',
      'message' => MSG_VALIDATE_DUPLICATE,
    ]);
    // validateEmptyPhone
    $validateEmptyPhone = function ($owner) {
      return !(empty($owner->telephone) && empty($owner->cellular_phone));
    };
    $rules->add($validateEmptyPhone, [
      'errorField' => 'cellular_phone',
      'message'    => '携帯電話/固定電話番号のどちらかは必須入力です。',
    ]);
    return $rules;
  }
```


-------------


## MIGRATION ##

### 概要
- http://easyramble.com/cakephp-develop-tutorial.html#toc_5
- http://qiita.com/ozawan/items/8144e02ca70519f3dcaf

CakePHPサイドからDBのテーブルを管理する為の機能。bakeを使って操作する。DBの初期状態から何かひとつでも変更する場合は、この機能を使ってDBの拡張をアプリ管理するほうがよろしい。(MySqlをコマンドやGUIでごりごり修正すると履歴が残らず移行が大変)ある程度固まったらinitialで残しとけば、あとでそれだけmigrateすれば良いのでおすすめ。

まっさらなDBにmigrateすればこれまでの変更履歴含め最新状態にしてくれるのでDBの拡張をバージョン管理システム=gitで管理できるようになって良い感じ。逆にmigrateするときはDBをまっさらにしとかないといけないけどね。ちなみに CakePHP3 の Migration 機能は Phinx というマイグレーションツールのラッパー。[Phinx - マニュアル](http://docs.phinx.org/en/latest/)


```
// DBを参照し現在の内容をmigrationファイルとして保存
$ bin/cake bake migration_snapshot Initial

// テーブルの拡張・修正情報を記述する空のmigrationファイルを生成
$ bin/cake bake migration {file_name}

// マイグレーションの実行
$ bin/cake migrations migrate

// マイグレーションのステータス確認
// ( up: 適用済み / down: 未適用 )
$ bin/cake migrations status

// マイグレーションのロールバック(一つ前取り消し)
$ bin/cake migrations rollback

// 現行DBと現行のマイグレーションファイル群を参照し差分から新規 migration ファイル作成
$ bin/cake bake migration_diff {NEW_MIGRATION_FILE_NAME}
```

### migrationファイル記述
基本的に1ファイルに1テーブルの変更をまとめる。1つのファイルであれこれテーブルを書き換えるとエラーする気がする。

```
// テーブルの追加
$table = $this->table('table_name');
$table->addColumn('name', 'string')
      ->addColumn('description', 'text')
      ->addColumn('created', 'datetime')
      ->addColumn('modified', 'datetime')
      ->create();
```

#### テーブル照合順序の明示
DB側を `utf8mb4_general_ci` にしていても、テーブル照合順序は `utf8_general_ci` になってしまうみたい。多分 `my.ini` とかでちゃんと設定せんとあかんけど面倒い。しかも自動的に作った Migration ダンプファイルだと照合順序まで明示してくれない FXXK!! 以下コードで明示的にテーブル照合順序を定義してやらんと Android とか iOS とかの絵文字化ける。

> refs: https://github.com/cakephp/migrations/issues/63#issuecomment-104805039

```
$this->table('users', [
    'collation' => 'utf8mb4_general_ci'  // ここで明示
  ])
  ->addColumn('id', 'biginteger', [
    'autoIncrement' => true,
    'comment' => 'ユーザID',
    'default' => null,
    'null' => false,
    'signed' => false,
  ])
  ->addPrimaryKey(['id'])
  // ...
```

#### その他テーブル操作
```
// テーブル名の変更
$table = $this->table('old_table_name');
$table->rename('new_table_name');

// テーブルの削除
$this->dropTable('table_name');

// カラムの追加
$table = $this->table('table_name');
$table->addColumn('col_name', 'integer', [
    'default' => 0,
    'limit' => 4,
    'null' => false,
    'after' => 'prev_col_name',
]);
$table->update();

// カラム名の変更
$table = $this->table('table_name');
$table->renameColumn('old_col_name', 'new_col_name');

// カラム定義の変更
$table = $this->table('table_name');
$table->changeColumn('col_name', 'boolean', [
    'default' => null,
    'null' => false,
]);

// カラムの削除
$table = $this->table('table_name');
$table->removeColumn('col_name');
```

### Seed の利用
Migration はDBスキーマの管理なので、実際に中に流し込むテストデータ/初期データの管理は Seed を使う。[参考](http://koltatt.net/programing/cakephp_seed/)

#### 流れ
```
# マイグレーションファイルの作成
$ bin/cake bake migration CreateBlog title:string body:text created: datetime

# マイグレーションの実行
$ bin/cake migrations migrate

# シーダーの作成
$ bin/cake bake seed Blogs
$ vi App/config/Seeds/BlogsSeed.php #シーダー編集

# 現在DBからシーダー作る
$ bin/cake bake seed --data Articles

# シードの実行
$ bin/cake migrations seed

# 特定モデルのシード実行
$ bin/cake migrations seed --seed ArticlesSeed
```

#### シードのサンプル
```php
use Migrations\AbstractSeed;
 
class BlogsSeed extends AbstractSeed {
    public function run() {
        $data = [
            [
                'title' => 'ポスト1',
                'body' => 'テスト ポスト', 
                'created' => date('Y-m-d H:i:s')
            ],
            [
                'title' => 'ポスト2',
                'body' => 'ふたつ目のテスト', 
                'created' => date('Y-m-d H:i:s')
            ],
            [
                'title' => 'ポスト3',
                'body' => 'これもテスト', 
                'created' => date('Y-m-d H:i:s')
            ]
        ];
        $table = $this->table('blogs');
        $table->insert($data)->save();
    }
}
```

#### Seeding 時に外部キーで弾かれて面倒なんだけど
シード実行時は順番を指定できない(一件一件やるか全部アルファベット順で流すかの2択という謎仕様)。よって外部キー設定しているテーブルだと外部キー制約で弾かれるケースがある。この場合(シードファイルの記述が正しい前提で)以下のように **外部キー制約を一時的に除外する** ことで一括の `migrations seed` ができるようになる。

```php
public function run() {
  $this->execute('SET FOREIGN_KEY_CHECKS = 0');
  $data = [
    /* [], [], [], ... 略 */
  ];
  $table = $this->table('users');
  $table->insert($data)->save();
  $this->execute('SET FOREIGN_KEY_CHECKS = 1');
}
```



-----------------------------------------------

## TEST & FIXTURE ##
- [Testing - CakeBook](https://book.cakephp.org/3.0/ja/development/testing.html)
- [CakePHP3 x PHPUnitでテストの自動化](http://insight.hiliberate.biz/?p=2313)
- [CakePHP3 Unitテスト モデル編](https://donow.jp/skillup/?p=2295)

### Fixture

#### Bake
```sh
# 既存 default DB より Fixture 生成 ( 100 件分 )
bin/cake bake fixture -r -n 100 -f -s Users

# オプション
--conditions      The SQL snippet to use when importing records.
                  (default: 1=1)
--connection, -c  The datasource connection to get data from.
                  (default: default)
--count, -n       When using generated data, the number of records to
                  include in the fixture(s). (default:1)
--force, -f       Force overwriting existing files without prompting.
--help, -h        Display this help.
--plugin, -p      Plugin to bake into.
--quiet, -q       Enable quiet output.
--records, -r     Generate a fixture with records from the non-test
                  database. Used with --count and --conditions to limit
                  which records are added to the fixture.
                  ( -n でレコード数は指定しないとあかん )
--schema, -s      Create a fixture that imports schema, instead of
                  dumping a schema snapshot into the fixture.
                  ( これつけると public $import = ['table' => 'hoge']; で焼いてくれる )
--table           The table name if it does not follow conventions.
--theme, -t       The theme to use when baking code. (choices:Bake|Migrations)
--verbose, -v     Enable verbose output.
```

#### Settings
```php
// 現在の default 接続 DB の当該テーブルから fixture レコードをインポート
public $import = ['table' => 'users'];
public $import = ['model' => 'Users']; // 上と同義

// 異なる接続先 DB の当該テーブルからインポート
public $import = ['table' => 'articles', 'connection' => 'other'];

// スキーマを既存 DB から取得しつつレコードは独自に
public $import = ['table' => 'articles'];
public $records = [
  [
    'title' => 'First Article',
    'body' => 'First Article Body',
    'published' => '1',
    'created' => '2007-03-18 10:39:23',
    'modified' => '2007-03-18 10:41:31'
  ],
  //略
];
```

### Test

#### Code
```php
namespace App\Test\TestCase\Model\Table;

use App\Model\Table\ArticlesTable;
use Cake\ORM\TableRegistry;
use Cake\TestSuite\TestCase;
use Cake\Log\Log;              // デバッグログ出力用に追記

class ArticlesTableTest extends TestCase
{
    // Fixture のロード
    public $fixtures = ['app.articles'];

    // テストメソッドの前処理
    public function setUp()
    {
        parent::setUp();
        $this->Articles = TableRegistry::get('Articles');
    }

    // テストメソッド
    public function testFindPublished()
    {
        // Table で既に実装済みのカスタム Finder メソッド `findPublished()` をテストする例
        // `$this` はこのときテストクラスなので `$this->find()` でない点に注意
        $query = $this->Articles->find('published'); 
        $this->assertInstanceOf('Cake\ORM\Query', $query);
        $result = $query->hydrate(false)->toArray();
        $expected = [
            ['id' => 1, 'title' => 'First Article'],
            ['id' => 2, 'title' => 'Second Article'],
            ['id' => 3, 'title' => 'Third Article']
        ];
        $this->assertEquals($expected, $result);  // アサーション
        Log::debug($result);                      // app/tmp/logs/cli-debug.log へ吐き出す
    }
    
    // テストメソッド後処理 (テストDB初期化)
    public function tearDown()
    {
    	unset($this->Teams);
        parent::tearDown();
    }
}
```

#### Shell
```sh
# composer スクリプト経由でテストメソッドを絞って phpUnit 実行する例
composer test -- --filter=testMethod tests/TestCase/Model/Table/UsersTableTest

# composer.json に phpunit パスを書いとく
# {"scripts": {"test": "vendor/bin/phpunit --colors=always"}}
```


#### Assertions
```php
/* CakePHP Assertions */

// Check for a 2xx response code
$this->assertResponseOk();

// Check for a 2xx/3xx response code
$this->assertResponseSuccess();

// Check for a 4xx response code
$this->assertResponseError();

// Check for a 5xx response code
$this->assertResponseFailure();

// Check for a specific response code, e.g. 200
$this->assertResponseCode(200);

// Check the Location header
$this->assertRedirect(['controller' => 'Articles', 'action' => 'index']);

// Check that no Location header has been set
$this->assertNoRedirect();

// Check a part of the Location header
$this->assertRedirectContains('/articles/edit/');

// Assert not empty response content
$this->assertResponseNotEmpty();

// Assert empty response content
$this->assertResponseEmpty();

// Assert response content
$this->assertResponseEquals('Yeah!');

// Assert partial response content
$this->assertResponseContains('You won!');
$this->assertResponseNotContains('You lost!');

// Assert layout
$this->assertLayout('default');

// Assert which template was rendered (if any)
$this->assertTemplate('index');

// Assert data in the session
$this->assertSession(1, 'Auth.User.id');

// Assert response header.
$this->assertHeader('Content-Type', 'application/json');

// Assert view variables
$user =  $this->viewVariable('user');
$this->assertEquals('jose', $user->username);

// Assert cookies in the response
$this->assertCookie('1', 'thingid');

// Check the content type
$this->assertContentType('application/json');


/* PHPUnit Assertions */

// mixed is ...
$this->assertNull($var);                // $varがNULLである
$this->assertEquals($val1, $val2);      // $val1が$val2と等しい
$this->assertSame($val1, $val2);        // $val1と$val2が型も含めて等しい
$this->assertInternalType($type, $val); // $valの型名が$typeである - @link https://goo.gl/X71MJ2

// number is ...
$this->assertGreaterThan($expect, $var);         // $expect < $var が成立する
$this->assertGreaterThanOrEqual($expect, $var);  // $expect <= $var が成立する
$this->assertLessThan($expect, $var);            // $expect > $var が成立する
$this->assertLessThanOrEqual($expect, $var);     // $expect >= $var が成立する

// string is ...
$this->assertJsonStringEqualsJsonString($str1, $str2);  // $str1と$str2がjsonとして等しい
$this->assertRegExp($ptn, $str);                        // $strが正規表現$ptnにマッチする

// boolean is ...
$this->assertTrue($var);  // $varがTRUEである
$this->assertFalse($var); // $varがFALSEである

// array is ...
$this->assertArrayHasKey($key, $array);   // 配列$arrayにキー$keyが存在する
$this->assertContains($val, $array);      // 配列$arrayに値$valが存在する
$this->assertContainsOnly($type, $array); // 配列$arrayの値の型がすべて$typeである
$this->assertCount($count, $array);       // 配列$arrayの値の数が$countである
$this->assertEmpty($array);               // 配列$arrayが空である

// object/class is ...
$this->assertObjectHasAttribute($attr, $object);      // オブジェクト$objectにプロパティ変数$attrが存在する
$this->assertClassHasAttribute($attr, $class);        // クラス名$classにプロパティ変数$attrが存在する
$this->assertClassHasStaticAttribute($attr, $class);  // クラス名$classに静的プロパティ変数$attrが存在する
$this->assertInstanceOf($class, $instance);           // $instanceがクラス名$classのインスタンスである

// file is ...
$this->assertFileExists($file);                        // $fileが存在する
$this->assertFileEquals($file1, $file2);               // $file1と$file2の内容が等しい
$this->assertJsonFileEqualsJsonFile($file1, $file2);   // $file1と$file2の内容がjsonとして等しい
$this->assertJsonStringEqualsJsonFile($file1, $json);  // $file1の内容と$jsonがjsonとして等しい
```


-----------------------------------------------


## TIPS & REFERENCES ##

### UUID の生成
> https://book.cakephp.org/3.0/ja/core-libraries/text.html#uuid

```php
// use Cake\Utility\Text;
Text::uuid(); // 485fc381-e790-47a3-9794-1337c0a8fe68
```

### 別 sessionKey の Auth に代理ログイン
```php
<?php
class UsersController extends AppController
{

  /**
   * Assign as proxy login
   * @param int $user_id
   */
  public function assign(int $user_id) {

    $this->Roles = TableRegistry::get('Roles');
    $assignUser = $this->Users->get($user_id, ['contain' => ['Roles']]);

    $this->Auth->__set('sessionKey', 'Auth.'.ucfirst($assignUser->role->name));
    $this->Auth->setUser($assignUser);
    
    return $this->redirect(TOPPAGES[$assignUser->role->id]);
  }

}
```

### Validator でエンティティのプロパティに入力制限
http://tsuralabo.hatenablog.com/entry/2016/07/11/014859

### セッションの有効時間を設定
```php
// refs: https://book.cakephp.org/3.0/ja/development/sessions.html#session-configuration
// in app.php

'Session' => [
    'defaults' => 'php',
    'timeout'  => 60,    // 分単位で指定
],

```

### ロギング
```php
// in Controller || Component || View
$this->log('Error has occurred!');  // app/tmp/logs/error.log にロギング
$this->log($array, 'debug');        // app/tmp/logs/debug.log にロギング

// in Model
Log::write();            // Require `use Cake\Log\Log;`
\Cake\Log\Log::write();  // Direct call
```

### DefaultPasswordHasher のハッシュ値毎回違うんだけど?
```php
/**
 * 毎度ハッシュ値が違うのだがどうも check() を使って入力値とハッシュ値の比較をするようです
 * refs: https://norm-nois.com/blog/archives/3237
 */
$hasher = new DefaultPasswordHasher();
$bool = $hasher->check($this->request->data['password'], $user->password);

if($bool) {
  echo '一致しました';
}
```

### ConnectionManager で 生SQLクエリ
Cakeのモジュール(ConnectionManager)を利用しつつ生クエリ書く例。

```php
// 基本形
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]);
```

### タイムゾーン timezone 設定
`php.ini` でなんと設定しようが `bootstrap.php` の `date_default_timezone_set();` で上書きされるのでここを変えること。

### 日付・時刻を扱うTimeクラス
CakePHP3では時間や日付を扱う Timeクラスが用意されていて、Controller内ではうかつに [ new DateTime() ] とかできない。(Viewでは逆にTimeクラスが使えないのでDateTimeが使える)

```
use Cake\I18n\Time;

class UsersController extends AppController {
    public function afterLogin() {
        $time = new Time($this->Auth->user('date_of_birth'));
        if ($time->isToday()) {
            $this->Flash->success(__('Happy birthday to you...'));
        }
    }
}
```

```php
// app.php で時刻ロケールを日本時へ変更する例
    'App' => [
    //    'defaultLocale' => env('APP_DEFAULT_LOCALE', 'en_US'),
        'defaultLocale' => env('APP_DEFAULT_LOCALE', 'ja-JP'),
    ],

// モデルの created ( timestamp ビヘイビア ) から日本っぽい時刻表記に変換する例
  use Cake\I18n\Time;
 
  public function hoge() {
    $createDate = $hogeee->created->i18nFormat('YYYY年MM月dd日 HH時mm分ss秒');
  }
```


### Helper自作
http://mobb-inc.jp/blog/archives/8777  

### ざっくり導入→ログイン認証まで
- [導入](http://gomocool.net/gomokulog/?p=640)
- [PWを暗号化](http://gomocool.net/gomokulog/?p=657)
- [ログイン認証](http://gomocool.net/gomokulog/?p=664)

### WordPress のっける
webrootにいれて.htaccessに例外書けばいいかな → [参考](http://www.omnioo.com/record/phpframework/cakephp3にwordpressをのせる/)

### 問合せフォーム作りたい
http://pk-brothers.com/1756/

### RSS生成
http://book.cakephp.org/3.0/en/views/helpers/rss.html

### 文字列の複数形やケース変換 Inflector クラス
- [Inflector - CakeBook](https://book.cakephp.org/3.0/ja/core-libraries/inflector.html)
- [Inflector のテスト](https://inflector.cakephp.org/)

```php
<?php
use Cake\Utility\Inflector;

class UsersController extends AppController
{
  public function inflectorTest() {
    echo Inflector::pluralize('MyChild');       // MyChildren
    echo Inflector::singularize('big_apples');  // big_apple
    echo Inflector::camelize('user_articles');  // UserArticles
    echo Inflector::underscore('UserArticles'); // user_articles
    echo Inflector::humanize('big_apples');     // Big Apples
    echo Inflector::classify('status store');   // StatusStore
    echo Inflector::tableize('userToken');      // user_tokens
    // いやあんべんり...!
  }
}
```

### ファイル/ディレクトリ操作したいな
> `exec()` よりも [Folder/File クラス](https://book.cakephp.org/3.0/ja/core-libraries/file-folder.html)

### Email 送りたいな
> [Email クラス](https://book.cakephp.org/3.0/ja/core-libraries/email.html)

```php
// In Controller with `use Cake\Mailer\Email;`
try {
  $email = new Email('default');
  $email
    ->template('my_template', 'default')
    ->emailFormat('text')
    ->subject('【'.APP_NAME.'】 '.MAIL_SUBJECTS[$subject])
    ->setTo('mail.to@example.com')
    ->setFrom($User->email)
    ->setBcc($bccAddresses)  // Passable Array or String
    ->setReplyTo($replyToAddresses)
    ->viewVars([
      'name'   => $User->name,
      'body'   => $body,
      'footer' => $User->mail_footer,
    ])
    ->send();
  $this->Flash->success('Sent your mail.');
} catch (Exception $e) {
  $this->Flash->error($e->getMessage());
}
$this->redirect($this->referer());
```

### バッチ処理仕込みたいな
> [Shell クラス](https://book.cakephp.org/3.0/ja/console-and-shells.html)

```php
<?php 
namespace App\Shell;

use Cake\Console\Shell;
use Cake\ORM\TableRegistry;
use Cake\Auth\DefaultPasswordHasher;
use Cake\Datasource\ConnectionManager;
use Cake\Filesystem\Folder;
use Cake\Filesystem\File;

/**
 * ExampleShell Class.
 *
 * @access public
 * @author hyano@ampware.jp
 * @package Shell
 * @link https://book.cakephp.org/3.0/ja/console-and-shells.html#namespace-Cake\Console
 */
class ExampleShell extends Shell
{


  /** 
   * Initialize instance for all methods.
   * @param  void
   * @return void
   */
  public function initialize() {
    parent::initialize();
    // Loading utilities.
    $this->Hasher     = new DefaultPasswordHasher();
    $this->Connection = ConnectionManager::get('default');
    $this->loadModel('Users');
  }

  
  /**
   * Printing welcome messages.
   * @param  void
   * @return void
   */
  public function startup() {
    // If you want to output welcome messages, override parent method like this.
    // parent::initialize();
  }


  /** 
   * Main command for `$ bin/cake example`.
   * Method runs when executed without sub-command or args.
   * @param  void
   * @return void
   */
  public function main() {
    $this->out('Hello world.');   // Print stdout.
    $this->err('Error sample.');  // Print stderr.
    $this->out($this->nl(1));     // Print EOL.
    $this->hr();                  // Print horizon border like '----'.

    // To interact with user inputs.
    $isContinue = $this->in('Want to continue the process ?', ['y', 'N'], 'N');
    if ($isContinue !== 'y') {
      $this->abort('Process was canceled.');  // Print stderr with throw StopException.
    }
    
    // Case of without $this->loadModel(): $this->Users = TableRegistry::get('Users');
    $users = $this->Users->find('all')
      ->where(['Users.email' => DEV_EMAIL]);
    if (empty($users)) $this->abort('Missing users.');
    $this->out($users->first());  // Can to pass object/array.
    
    // Create file.
    $json = json_encode($users->toArray());
    $this->createFile('user.json', $json);  // Put to called directory.
    
    // Execute another command.
    $this->dispatchShell([
      'command' => 'example hey_there john --verbose',
      'extra'   => ['foo' => 'bar'],  // Passing to Shell::param('foo')
    ]);
  }
  
  
  /**
   * Sub command for `$ bin/cake example hey_there {name}`.
   * @param  string $name
   * @return string $response
   */
  public function heyThere(string $name = 'Anonymous') {
    $this->out('Hey there, '.$name.' !');                     // Hey there, john !
    $this->out('Passed param is '.Shell::param('foo').' !');  // Passed param is bar !
    $this->verbose('This message is verbose output to print only using --verbose option.');  
    $this->quiet('This message is quiet output to print always.');  
  }


}
```

以上是关于markdown [cakephp:CakePHP3注意事项] CakePHP3基本知识说明。 #cakephp的主要内容,如果未能解决你的问题,请参考以下文章

php [cakephp:Paginator示例] CakePHP上PaginatorComponent的示例代码。 #cakephp

php [cakephp:mysqldump] mysqldumpのcakephp実装サンプル。#php #cakephp #mysql

ORM如何在CakePHP3中运行

使用 Cakephp-jwt-auth [CakePHP 3] 过期后颁发新令牌

从 cakephp 3.x 迁移到 cakephp 4.x [关闭]

CakePHP 2.2.1 - 在表单上显示 CakePHP 错误 - 自定义验证