CakePHPで知ってると便利なコードまとめ

よく使うものをまとめています。随時更新。
※最新版では変更になっている場合があります。

CakePHP：ver3.7.6
PHP：7.2.3

Cookbook
https://book.cakephp.org/3/ja/
4.x版はこちら
https://qiita.com/egcmrsk/items/09417521c7f0ac5f210f

# Controller
## リクエスト
### 現在のルートを取得
```php
$this->request->getParam();

// controller
$this->request->getParam('controller');
// action
$this->request->getParam('action');
// prefix
$this->request->getParam('prefix');
```

### リクエストの状態をチェック
リクエストの種類は以下を参照
https://book.cakephp.org/3/ja/controllers/request-response.html#check-the-request

```php
$this->request->is(['post', 'put']);
```
### リクエストを取得

```php
// POST
$this->request->getData();
$this->request->getData('mode'); // $_POST['mode']

// GET
$this->request->getQuery();
$this->request->getQuery('mode'); // $_GET['mode']
```
### リクエストをセット

```php
// POST
$this->request = $this->request->withData('year', date('Y'));
$this->request = $this->request->withData('month', date('n'));

// GET
$this->request = $this->request->withQueryParams([
    'year' => date('Y'),
    'month' => date('n')
]);
```
## HTTPの例外を追加(Exception)
例外の種類は以下を参照
https://book.cakephp.org/3/ja/development/errors.html#http

```php
use Cake\Http\Exception\NotFoundException;

if (!$item) {
    throw new NotFoundException(__('アイテムが見つかりません'));
}
```

## フォームの値を変更可にする(Securityコンポーネント使用時)
JavaScriptでフォームの値を変更して送信したいとき、Securityコンポーネントを有効にしているとエラーが出てしまう。
そういうときは変更したいもののみ許可する。

```php
public function beforeFilter(Event $event) {
    parent::beforeFilter($event);

    // addメソッド内で、modeの値の変更を許可
    if ($this->request->getParam('action') == 'add') {
        $this->Security->setConfig('unlockedFields', ['mode']);
    }
}
```

## Sessionの読み書き
```php
$this->request->session()->read('calendar');
$this->request->session()->write('calendar', $calendar);
```

## Cacheの読み書き
```php
use Cake\Cache\Cache;

$calendar = Cache::read('calendar');
if ($calendar == false) {
    $calendar = $this->Calendars->find()->toArray();
    Cache::write('calendar', $calendar);
}
```

## リファラ
```php
$referer = $this->referer();
// リダイレクトにそのまま入れられます
return $this->redirect($referer);
```

## JSONを出力する
JSON用のControllerを用意し、出力はすべてJSONにする

```php:Controller/JsonController.php
class JsonController extends AppController
{
    public function initialize()
    {
        parent::initialize();
        $this->viewBuilder()->className('Json');
    }

    public function index()
    {
        $this->loadModel('Products');
        $products = $this->Products->find()
            ->where(['enabled' => 1])
            ->order(['id' => 'desc'])
            ->toArray();
        $this->set(['json' => $products, '_serialize' => 'json']);
    }
}
```

# Model
## 登録したレコードのIDを取得
save後にEntityにセットされる

```php
$entity = $this->Model->newEntity();
$entity = $this->Model->patchEntity($entity, $this->request->getData());
$this->Model->save($entity);

$id = $entity->id; // ID取得
```

## バリデーション
バリデーションのルールを追加したい場合
https://book.cakephp.org/3/ja/core-libraries/validation.html#id5

```php:Model/Table/UsersTable.php
public function validationDefault(Validator $validator)
{
    ...
    $validator
        ->scalar('username')
        ->maxLength('username', 255)
        ->allowEmptyString('username')
        // 文字数チェック
        ->add('username', 'length', [
            'rule' => ['minLength', 6],
            'message' => 'ユーザー名は6文字以上必要です'
        ])
        // 重複チェック
        // 動的なルールも設定できます
        ->add('username', 'unique', [
            'rule' => function ($value, $context) {
                $condition = [
                    'username' => $value
                ];
                if (isset($context['data']['id']) && $context['data']['id']>0) {
                    $condition['id !='] = $context['data']['id'];
                }
                $exist = $this->find()->where($condition)->first();
                return $exist ? false : true;
            },
            'message' => 'ユーザー名はすでに使用されています'
        ]);
}
```

## 仮想フィールド(virtualFields)
CakePHP2の仮想フィールドとは違い、フェッチした結果に対してセットする

```php:Model/Entity/User.php
class User extends Entity {
    protected $_virtual = ['name'];

    protected function _getName() {
        // last_name、first_nameをひとつにまとめてセット
        if (!isset($this->_properties['last_name']) || !isset($this->_properties['first_name'])) {
            return null;
        }
        return $this->_properties['last_name'] . ' ' . $this->_properties['first_name'];
    }
}
```
## カスタムファインダー
https://book.cakephp.org/3/ja/orm/retrieving-data-and-resultsets.html#custom-find-methods

```php:Model/Table/GroupsTable.php
public function findSelection($query, $options)
{
    $query->find('all')
        ->contain(['ParentGroups'])
        ->where([
          'Groups.enabled' => 1
        ])
        ->order([
          'Groups.lft' => 'asc',
          'Groups.id' => 'asc'
        ]);

    // Collectionを使って親グループ+子グループの名前を返す
    $collection = new Collection($query);
    $new = $collection->map(function ($entity) {
      $name = '';
      if ($entity->parent_group) {
        $name .= '[' . $entity->parent_group->name . '] ';
      }
      $name .= $entity->name;
      return [
        'id' => $entity->id,
        'name' => $name
      ];
    });
    return $new;
}
```
```php:Controller/UsersController.php
class UsersController extends AppController {
    public function initialize() {
        parent::initialize();
        $this->loadModel('Groups');
    }

    public function add() {
        ...
        $groups = $this->Groups->find('selection')->toArray();
        // [
        //    [id => 1, name => '[親グループ名] グループ名'],
        //    [id => 2, name => 'グループ名']
        // ]
    }
}
```

# Component / Helper
## Configのセット、取得
```php
// セット
$holidays = [
    '2020-01-01',
    '2020-01-13',
    '2020-02-11',
    '2020-02-24',
    ...
];
$this->setConfig('holidays', $holidays);

// 取得
$holidays = $this->getConfig('holidays');
```

##Modelを読み込み
```php:Controller/Component/HogeComponent.php
use Cake\Datasource\ModelAwareTrait;

class HogeComponent extends Component {
    use ModelAwareTrait;

    public function method() {
        $this->loadModel('Model');
    }
}

```

## Componentで他のComponentを使う

```php
public $components = ['Calendar'];
$calendar = $this->Calendar->build(2020);
```

## ControllerまたはComponent内でHelperを使う
自作したHelperの場合はパスに注意

```php
// Urlヘルパーを使う
use Cake\View\Helper\UrlHelper;

$Url = new UrlHelper(new \Cake\View\View());
$url = $Url->build('/', true); // http://hogehoge.com/

// 自作したヘルパーを使う
use App\View\Helper\UtilHelper;

$Util = new UtilHelper(new \Cake\View\View());
$password = $Util->createPassword(); // UtilヘルパーのcreatePasswordを実行
```

## ComponentからControllerへアクセス
```php
$controller = $this->_registry->getController();
```

## ComponentでSessionを扱う
Controllerを通して取得・編集できます

```php
// Controllerにアクセス
$controller = $this->_registry->getController();
// Controllerからセッションにアクセス
$session = $controller->getRequest()->getSession();

// あとはController内でセッションを扱うときと一緒
$session->read('hogehoge');
```

# View
## レイアウトを変更する

src/Template/Layout/front.phpをレイアウトに指定したい場合

```php
// Controllerのメソッドから
public function view()
{
    ...
    $this->viewBuilder()->setLayout('front');
}

// ビューファイルから
$this->layout = 'front';
```
# データの取得
## 任意のカラムで取得
いまいち使いどころがわからなかったんですが、存在チェックとかでさくっと取りたいときはいいかもしれません。

```php
// 「username」が一致するものを取得
$user = $this->Users->findByUsername('hogehoge');
// ORも使える
$user = $this->Users->findAllByUsernameOrEmail('hogehoge', 'hogehoge@hoge.com');

// 結果取得
$user->first();
```

## クエリが空か判定

```php
$query = $this->Users->find()->where(['enabled' => 1, 'email' => 'hogehoge@hoge.com']);
$query->isEmpty(); // trueまたはfalse
```

## データの整形
データ整形はとりあえずCollectionに突っ込んでいたのですが、そもそもクエリーはCollectionオブジェクトだそうで、直接extractとかcombineとかできるらしい（もっと早く知りたかった）
https://book.cakephp.org/3/ja/orm/query-builder.html#collection

```php
$query = $this->Users->find()->where(['enabled' => 1]);
// IDと名前の配列
$users = $query->combine('id', 'name')->toArray();
// [1 => 'ユーザー1', 2 => 'ユーザー2', 3 => 'ユーザー3']

// IDのみの配列
$users = $query->extract('id')->toArray();
// [1, 2, 3]
```

# データの保存
## アソシエーションを一緒に保存
| ユーザー | 中間テーブル | グループ |
|:-:|:-:|:-:|
| Users  | UsersGroups  | Groups  |
UsersとGroupsでbelongsToManyを設定しておく
https://book.cakephp.org/3/ja/orm/associations.html#belongstomany

```php:Controller/UsersController.php
// 取得
$id = 1;
$user = $this->Users->get($id, ['contain' => 'Groups']);

// 保存
$data = [
    'id' => 1,
    'name' => 'hogehoge',
    'groups' => [
        '_ids' => [1, 2, 3] // GroupsのIDを配列で指定
    ]
];
$user = $this->Users->patchEntity($user, $data, ['associated' => ['Groups']]);
$this->Users->save($user);
```

## 複数データを保存
複数レコードの変換
https://book.cakephp.org/3/ja/orm/saving-data.html#id9

```php
$data = [
    ['name' => '北海道', 'country' => 'JP'],
    ['name' => '東北', 'country' => 'JP'],
    ['name' => '関東', 'country' => 'JP'],
    ['name' => '中部', 'country' => 'JP'],
    ['name' => '近畿', 'country' => 'JP'],
    ['name' => '中国・四国', 'country' => 'JP'],
    ['name' => '九州・沖縄', 'country' => 'JP']
];

$entities = $this->Model->newEntities($data);

// 個別のトランザクションで処理
foreach ($entities as $entity) {
    $this->Model->save($entity);
}

// 単一トランザクションで処理
$this->Model->getConnection()->transactional(function () use ($entities) {
    foreach ($entities as $entity) {
        $this->Model->save($entity, ['atomic' => false]);
    }
});
```

## データを一気に更新
```php
// statusが「2」のものをすべて「1」にする
$this->Model->updateAll(['status' => 1], ['status' => 2]);
```

# エンティティ
## データの保存前にエラーをチェック
patchEntityを実行後、hasErrors、errorsで確認できる

```php
$entity = $this->Model->newEntity();
$entity = $this->Model->patchEntity($entity, $this->request->getData());

if ($entity->hasErrors()) {
    $this->Flash->error(__('エラーがあります'));
    // エラーの詳細を表示
    $errors = '';
    foreach ($entity->errors() as $key => $value) {
        if (is_array($value)) {
            foreach ($value as $k => $v) {
                $errors .= "[{$key}] {$v}\n";
            }
        } else {
            $errors .= "[{$key}] {$value}\n";
        }
    }
    $this->set(compact('errors'));
} else {
    $this->Model->save($entity);
}
```

## エラーをセット
バリデーションルール以外にチェックしたい項目があるときや、エラーがあった場合の動作チェックをしたいときに便利

```php
$entity = $this->Model->newEntity();
$entity = $this->Model->patchEntity($entity, $this->request->getData());

$entity->setError('required', ['必須項目です']);

if ($entity->hasErrors()) {
    // エラーがあった場合の動作
    ...
}
```

# DB関連
## SQL文を直接実行
```php
use Cake\Datasource\ConnectionManager;

$db = ConnectionManager::get('default');
$data = $db->execute('SELECT * FROM hoge Hoge WHERE enabled = ?', [1]);
```

## 実行したSQL文を確認

```php
$this->Model->getDataSource()->getLog();
```

# Command
## コマンドに引数を設定
対話形式にしたい場合など、引数を設定して条件分岐できます。
https://book.cakephp.org/3/ja/console-and-shells/commands.html

```php:Command/OrderCommand.php
protected function buildOptionParser(ConsoleOptionParser $parser)
{
    $parser->addArgument('id', [
        'help' => 'order id.',
        'required' => false
    ]);
    return $parser;
}

public function execute(Arguments $args, ConsoleIo $io)
{
    $id = $args->getArgument('id');
    if ($id) {
        // id があった場合の処理
    } else {
        // それ以外の処理
    }
}
```

```shell
$ bin/cake order 200
# '200'が渡される
```

# その他
## 定数設定
定数用ファイルを別途用意し、CakePHPに読み込む

```php:config/const.php
use Cake\Core\Configure;

return [
    define('YEAR_MIN', 1980),
    define('YEAR_MAX', date('Y', strtotime('+1 year'))),
    define('VERSION', Configure::version())
];
```
```php:config/bootstrap.php
try {
    Configure::config('default', new PhpConfig());
    Configure::load('app', 'default', false);
    Configure::load('const', 'default'); // 追加
} catch (\Exception $e) {
    exit($e->getMessage() . "\n");
}
```
## ドット(.)区切りで多次元配列を取得
Helperで使うと便利かもしれない

```php:View/Helper/HogeHelper.php
use Cake\Utility\Hash;

protected $_defaultConfig = [
    'users' => [
        'status' => [1 => '仮会員', 2 => '会員']
    ],
    'groups' => [
        'status' => [1 => '有効', 2 => '無効']
    ]
];

public function get($arg = null) {
    if (!$arg) {
        return null;
    }
    $config = $this->_defaultConfig;
    return Hash::get($config, $arg);
}
```
```php:View/Users/index.php
use Cake\Utility\Hash;

$this->Hoge->get('users.status'); // [1 => '仮会員', 2 => '会員']
$this->Hoge->get('groups.status'); // [1 => '有効', 2 => '無効']

$this->Hoge->get('users.status')[2]; // 会員
```
## キーと値の配列を作る

```php
use Cake\Utility\Hash;

$groups = $this->Groups->find()->toArray();
$groups = Hash::combine($groups, '{n}.id', '{n}.name');
```
値だけ欲しい場合はextract

```php
use Cake\Utility\Hash;

$groups = $this->Groups->find()->toArray();
$groups = Hash::extract($groups, '{n}.id');
```

## 日時を扱う
Timeクラスを使うとapp.phpで設定したタイムゾーンに合わせてくれます
https://book.cakephp.org/3/ja/core-libraries/time.html

```php
use Cake\I18n\Time;

// 現在時刻
$now = Time::now();

// インスタンスを作成して
$time = new Time();
// 足したり引いたり
$time->modify('-7 day');
// セットしたり
$time->year(2021)->month(1)->day(3);
$time->setDate(2021, 1, 3);

// 文字列から作成する場合は、YYYY-MM-DD HH:II:SSの形式で指定
$time = new Time('2021-04-01 12:30:30');
// タイムスタンプからも作成できます
$time = Time::createFromTimestamp(time());

// フォーマット
$time->format('Y/m/d');
```

## バージョン

```php
Configure::version();
```