CakePHP3,4 コーディングメモ

##はじめに
自分用のメモのため、読んでもよくわからないことが多いと思いますが、参考がてらにどうぞ。随時更新します。

###参照した画像がない場合、非同期で`no-image.jpg`を表示する方法

自分の作成したHelperクラスで画像を取得する際、以下のようにHtmlHelperを利用して`<img />`タグを生成できる。

```php5

    public function getProductImage()
    {
        $options = [
            'class' => 'css-class-name',
            'alt' => '',
        ];

        $imagePath = "product-image.jpg";

        return $this->Html->image($imagePath, $options);
    }

```

この時、もちろん`$imagePath`に画像がなければエラーになり何の画像も表示されない。そこで、以下のように`file_exists`を使用してファイルの存在チェックができる。

```php5

    public function getProductImage()
    {
        $options = [
            'class' => 'css-class-name',
            'alt' => '',
        ];

        $imagePath = "product-image.jpg";

        #ファイルの存在チェック
        if (file_exists($this->Url->image($imagePath))) {

           return $this->Html->image($imagePath, $options);
        }
        
        return $this->Html->image('no-image.jpg', $options);
    }

```

これでも動くが、わざわざサーバーサイドでこの処理を行う必要はない。`<img />`タグで使用できる、`onerror`オプションを使えば非同期にクライエントサイドで同じ処理ができる。よって最適なコードは以下。

```php5

    public function getProductImage()
    {
        $options = [
            'class' => 'css-class-name',
            'alt' => '',
            #onerrorオプションを導入
            'onerror' => 'this.onerror=null;this.src="' . $this->Url->image('no-image.jpg') . '"'
        ];

        $imagePath = "product-image.jpg";

        return $this->Html->image($imagePath, $options);
    }

```



###DBから引っ張ってきた値をFormのSelect Optionに設定

`find('list')`を使用する。ディフォルトでは[id]=>[name]のペアが返されるが、`keyField`,`valueField`を定義することで、`name`以外の項目をDBから取り出すことができる。

Goodコード:

```php3
$results = TableRegistry::getTableLocator()->get('Users')->find('list', [
            //key-valueを定義
            'keyField' => 'id',
            'valueField' => 'email'
        ])->where(['role =' => 'admin'])->toArray();
```

Badコード:

```php3
$results = TableRegistry::getTableLocator()->get('Users')->find('list', [
            //fieldsはCakePHP3では使われない
            'fields' => ['Users.id', 'Users.email']
        ])->where(['role =' => 'admin'])->toArray();
```

###単一レコードの取得

Cake\ORM\Table::find()を使用することで、SQLと同じようなロジックでDBからデータを取得できる。

```php
#find()を使った単一レコードの取得
$inquiry = $this->Users->find('all')
->where(['Users.id' => $id]) |
->firstOrFail();
```

が、単一レコードを取得する際は`Cake\ORM\Table::get()`を使用するべき。

```php
#get()を使った単一レコードの取得
$product = $this->Products->get($id)
```

###CustomFinderの使用
Finderは裏でSQLを生成しているわけだが、共通して使用されるクエリ部分はCustomFinderを作ることで使いまわせる。
公式にある通り、Tableレイヤーで定義する。

https://book.cakephp.org/3/en/orm/retrieving-data-and-resultsets.html


###App.phpの設定値を取得
https://sawatan.com/cakephp/cakephp3%E3%81%A7%E8%A8%AD%E5%AE%9A%E3%83%95%E3%82%A1%E3%82%A4%E3%83%ABapp-php%E3%81%AE%E8%A8%AD%E5%AE%9A%E5%80%A4%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B

##loadModel()とTableRegistry::getTableLocator()->get()の使い分け
あるMVCのモジュールで、他のモデルから値を引っ張りたい場合、`loadModel()`か`TableRegistry::getTableLocator()->get()`を使用できます。
この時、以下のようにして使い分けるといいです（動きとしては同じですが、そう推奨されているみたい）。

- Controller内であれば`loadModel()`

使用タイミング：
`UserController`の中で`BooksTable`から値を取得したいときなど。

サンプル：

```php5
// Load Logs model
$logsTable = $this->loadModel('Logs');
```

- Model内であれば`TableRegistry::getTableLocator()->get()`

使用タイミング：
例えば`Model/Entity/User.php`で`LogsTable`の値を取り出したい時など

サンプル：

```php5
// Load Logs model
$logsTable = TableRegistry::getTableLocator()->get('Logs');
```

参考：https://blog.sohelrana.me/loading-model-inside-another-model-and-controller-in-cakephp-3/

###Debug
以下のいずれかを使用。個人的には`dd()`か`print_r()`を使う。色々ルールや使い方の違いがあると思うが、データをみる目的であればなんでもいいのでは、と思う。

```php3
dd('Testing');
dump('Testing');
print_r('Testing');
debug('Testing');

```

###Pluginがインストールされてるかのチェック

```bash
# show all loaded plugins
bin/cake plugin loaded
# check for specific plugin (nix)
bin/cake plugin loaded | grep MyPlugin
```

### PluginとPackagistについて

`composer require [package name]`により、最新バージョンのPluginパッケージをインストールできる。

ただし、以下のような注意が必要。
1. ここでインストールできるパッケージは[Packagist](https://packagist.org/)にあるもののみである。
2. 最新バージョンで不具合がある場合があるのでできることならバージョンを指定した方がいい。

#####バージョンとは？どうやって指定するのか？
更新中。

###オブジェクトのインスタンスが何かをIf文に含める
PHPには予め`getType($data)`というメゾットがあり、変数のデータ型を取得できるが、Objectの場合、これだけでは何のインスタンスかわからない。
なので、
`var_dump($data);`でデータの型、インスタンスを調べた後、`$this instanceof [Instance Name]`をIf文に含める。

例：以下では、`var_dump($data);`で印字された内容を見るとErrorControllerのインスタンスであることがわかる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/464595/7593b764-3e85-d07b-43e4-8a57d7575ece.png)

```php5
if (!$this instanceof ErrorController) {
 //ErrorControllerのインスタンスでない場合、実行
}
```

###Cell, Element
`src/Template/Element/{element}.php`で定義されたHTML情報は、Templateで`<?= $this->element('sidemenu'); ?>`と記述すれば持ってくることができる。
Cellは、ElementにController機能を追加し、よりダイナミックかつ複雑な動きをできるようにしたもの。`src/View/Cell/{cellNameCell}.php`で定義できる。Templateで`<?= $this->cell('cellName::methodName', ['parameters']) ?>`でCellに定義したメゾットとそれに連携するTemplateを呼び出すことができる。

使用例：
新着メッセージ数を表示→Cellを使用し、メッセージ数をカウント　
共通のサイドメニューを表示→Elementを使用

###同一テーブルを複数箇所Associateする

例えば、以下のような出荷テーブル(shippings)、県テーブル(prefectures)というテーブルがあったとします。

*shippings*

|Column  |Associated table.column  |
|---|---|
|id  |-  |
|name|-|
|sender_pref_id  |prefectures.id|
|sender_address  |-
|receiver_pref_id  |prefectures.id|
|receiver_address  |-|
|quantity|-|
|shipping date|-|

*prefectures*

|Column  |Associated table.column  |
|---|---|
|id  |shippings.sender_pref_id, shippings.receiver_pref_id |
|name  |-|

ここで、`shippings`はsender_pref_id、receiver_pref_idを介して`prefectures`とAssociateの関係にあるのに注目してください。この時、ShippingsTableは以下のようになります。

```php5:ShippingsTable.php
class ShippingsTable extends AppTable
{
    /**
     * Initialize method
     *
     * @param array $config 
     * @return void
     */
    public function initialize(array $config): void
    {
        parent::initialize($config);

        $this->setTable('shippings');
        $this->setDisplayField('name');
        $this->setPrimaryKey('id');
        //ここで、Prefecturesに属していることを示します。
        $this->belongsTo('Prefectures', [
            'foreignKey' => 'sender_pref_id',
            'joinType' => 'INNER',
        ]);
        $this->belongsTo('ReceiverPrefs' //こうすることで$this->delivery_note_pref->nameのようにしてEntitityから県名を取得できます。, [
            'className' => 'Prefectures' //Associationの名前が実際のテーブル名と一致していないのでClassNameを明示します。
            'foreignKey' => 'receiver_pref_id',
            'joinType' => 'INNER',
        ]);
```

ここで注意してほしいのは、Controller等でAssociationの情報を取得する際、`contain`で使用するAssociationを指定してください。

```php5:ShippingsController.php
        $shipping = $this->Shippings->get($id, [
            'contain' => ['Prefectures', 'ReceiverPrefs']]);
```

##クエスチョンマーク
###??
PHP7から追加されたTernary Operatorでは表現できなかった便利な演算子。

```php5
// Fetches the value of $_GET['user'] and returns 'nobody'
// if it does not exist.
$username = $_GET['user'] ?? 'nobody';
// This is equivalent to:
$username = isset($_GET['user']) ? $_GET['user'] : 'nobody';
```

Reference: https://www.php.net/manual/en/migration70.new-features.php#migration70.new-features.null-coalesce-op

###Nullable types
パラメータや返却値の型宣言の前に？をつけることで、Nullでも許容できるようになります。

```php5

function testReturn(): ?string #これで返却値はNullかString型のみ許容されるようになる
{
    return null;
}

var_dump(testReturn());
```

Reference: https://www.php.net/manual/en/migration71.new-features.php

#Marshallingとは、beforeMarshalの使い方
CakePHPの公式サイトでは、beforeMarshalの機能について以下のように説明しています。

>If you need to modify request data before it is converted into entities, you can use the Model.beforeMarshal event. This event lets you manipulate the request data just before entities are created:

でも一体Marshalって何でしょう？
`StackOverflow`に良い説明がありました。
>Marshalling is the process of transforming the memory representation of an object to a data format that could be stored or transmitted. It's also called serialization (although it could be different in certain contexts). 

この説明から、beforeMarshalはデータを挿入する手前でEntityへ変換される際、データフォーマットを変えたい場合などに使えそうですね。

例えば、フロント側のフォームで`YYYY/MM/DD HH:MM`を入力する項目がある場合でテーブル挿入前にこのbeforeMarshalを使って`ss`部分を補ったりできます。

Reference: https://stackoverflow.com/questions/154185/what-is-object-marshalling

#Arrayから値をHashで取得する
Arrayから値を取得する際、CakePHPではHashを使用するように[推奨]( https://book.cakephp.org/4/en/core-libraries/hash.html#Cake%5CUtility%5CHash::get)されています。
Arrayの使い勝手をよくするこのHashはCakePHPの強みであり、統一して使われるべき。

```php5
// Define $users
$users = [
    ['id' => 1, 'name' => 'mark'],
    ['id' => 2, 'name' => 'jane'],
    ['id' => 3, 'name' => 'sally'],
    ['id' => 4, 'name' => 'jose'],
];

//HashでSallyを取得する
$results = Hash::get($users, 2.name)

//Hashを使わなずにSallyを取得する
$results = $users[2][name]

```
Reference: https://blog.howtelevision.co.jp/entry/2014/07/04/160309

###ドロップダウンのオプション項目をコントローラーからセットする

以下のような`templates/User/edit.php`があるとする。

```php5
               <div class="sample-class">
                    <?= $this->Form->control('user', [
                        'type' => 'select',
                        'class' => 'option-class',
                        //optionsを設定
                        'options' => $users,
                        'label' => false,
                        'empty' => __('指定なし'),
                        'default' => __('指定なし')
                    ]) ?>
                </div>
```

ルーティングを何もいじっていなければ、当然以下のようにして`UsersController`の`edit()`メゾットからこのテンプレートが呼び出される。

```php5
    public function edit($id = null)
    {
        $users = $this->User->find('list');
        $this->set(compact('users'));
        //テンプレートを明示的に指定する。なくてもよい。
        $this->render('edit');
    }
```

このような時、テンプレートで定義される項目名の複数形が既にセットされている場合オプションの設定を省略してもCakeが自動でオプション項目をセットしてくれる。上の場合、`user`というテンプレートの項目に対して`users`が既に`edit()`で定義されているため、`'options' => $users,`の明示的な設定は不要。


###Coding Standard
メゾット間は１行あける。
パラメータの前には必ずスペースを入れる。

https://book.cakephp.org/3/en/contributing/cakephp-coding-conventions.html#function-calls