CakePHP3 bake無しで動くControllerの作りかた

#なぜ思いついたのか？

マーチン・ファウラー著リファクタリングの第3章**「コードの不吉な匂い」**の一番最初に**「重複したコード」**という項目があり冒頭に以下の記述があります。

>同じようなコードが2箇所以上で見られたら1箇所にまとめると良いプログラムとなります。

CakePHPはとても良いフレームワークですが*bakeすると同じようなコードがたくさん作成されてしまう*ので何とかまとめられないかな〜と考えたのです。

#最終的に作成したいController
基本的な機能は CrudController を継承すれば継承先は基本的に何も書かないで動作させたい。

```php:GenresController.php
<?php
namespace App\Controller;
use App\Controller\CrudController;
class GenresController extends CrudController
{

}
```

#各機能の抽象化

最初に genres テーブルのコントローラをbakeし変更を加えながらCrudControllerを作成してみます。

```sh:bake
./bin/cake bake controller genres
```

##bake 後の index()

```php:GenresController.php
<?php
namespace App\Controller;
use App\Controller\AppController;
class GenresController extends AppController
{
    public function index()
    {
        $this->paginate = [
            'contain' => ['Users']
        ];
        $genres = $this->paginate($this->Genres);
        $this->set(compact('genres'));
    }
```

他の機能もほぼ同じ様にモデル名、エンティティー名が独自のものになっています。その他には contain を制御する必要があります。

## CrudController の 定義 およびindex()

```php:CrudController.php
<?php
namespace App\Controller;

class CrudController extends Controller
{
     public function index(array $contains = [])
    {
        $this->paginate['contain'] = $contains;
        $entities = $this->paginate($this->{$this->modelClass});
        $this->set(compact('entities'));
    }

```

* モデル名は {$this->modelClass}で置き換えます。
* ビューに引き渡す変数名は entities (複数形)で固定します。（Template側の変更が必要になります。）
* contain は引数で継承先から受け取る仕様にします。

## CrudController の view()
```php:CrudController.php
...
public function view($id = null, array $contains = [])
    {
        $options = [];
        if (count($contains) > 0) {
            $options = ['contain' => $contains];
        }
        $entity = $this->{$this->modelClass}->get($id, $options);
        $this->set(compact('entity'));
    }

```
index()とほぼ同じです。

* モデル名は {$this->modelClass}で置き換えます。
* ビューに引き渡す変数名は entity （単数）で固定します。（Template側の変更が必要になります。）
* contain は引数で継承先から受け取る仕様にします。

## CrudController の add()
```php:CrudController.php
    public function add(array $redirect = [])
    {
        $entity = $this->{$this->modelClass}->newEntity();
        if ($this->request->is('post')) {
            $entity = $this->{$this->modelClass}->patchEntity($entity, $this->request->getData());
            if ($this->{$this->modelClass}->save($entity)) {
                $this->Flash->success(__('The ' . $this->entity_name . ' has been saved.'));
                if ($redirect) {
                    return $this->redirect($redirect);
                }
                return $this->redirect(['action' => 'view' . DS . $entity->id]);
            }
            $this->Flash->error(__('The ' . $this->entity_name . ' could not be saved. Please, try again.'));
        }
        $this->set(compact('entity'));
    }

```
* モデル名は {$this->modelClass}で置き換えます。
* ビューに引き渡す変数名は entity （単数）で固定します。（Template側の変更が必要になります。）
* 追加後のリダイレクト先を指定可能にしました。
* フラッシュメッセージがちょっと問題なのですがエンティティー名部分を$this->entity_nameに置き換えました。

## エンティティー名を定義する。

```php:CrudController.php
<?php
namespace App\Controller;
use Cake\Event\Event;
use Cake\Utility\Inflector;

class CrudController extends AppController
{
    protected $entity_name;

    function beforeFilter(Event $event)
    {
        $this->entity_name = Inflector::singularize(Inflector::humanize($this->modelClass));
        return parent::beforeFilter($event);
    }
...
```
beforeFilter（）でInflectorを利用し$this->entity_nameを定義しました。
これでadd()のフラッシュメッセージ問題も解決しました。
エンティティー名に左右されない固定のメッセージにしても問題ないとも思います。

## CrudController の edit()
```php:CrudController.php
 public function edit($id = null, array $contains = [], array $redirect = [], $entity = null)
    {
        if (!$entity) {
            $options = [];
            if (count($contains) > 0) {
                $options = ['contain' => $contains];
            }
            $entity = $this->{$this->modelClass}->get($id, $options);
        }
        if ($this->request->is(['patch', 'post', 'put'])) {
            $entity = $this->{$this->modelClass}->patchEntity($entity, $this->request->getData());
            if ($this->{$this->modelClass}->save($entity)) {
                $this->Flash->success(__('The ' . $this->entity_name . ' has been saved.'));
                if ($redirect) {
                    return $this->redirect($redirect);
                }
                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('The ' . $this->entity_name . ' could not be saved. Please, try again.'));
        }
        $this->set(compact('entity'));
    }
```

* モデル名は {$this->modelClass}で置き換え。
* ビューに引き渡す変数名は entity （単数）で固定します。（Template側の変更が必要になります。）
* フラッシュメッセージのエンティティー名部分を$this->entity_nameに置き換え。
* contain は引数で継承先から受け取る仕様。
* 継承先で エンティティーを作成した場合に対応して最後の引数にエンティティーを渡すことも可能にしました。

## CrudController の delete()
```php:CrudController.php
public function delete($id = null, array $redirect = [], $entity = null)
    {
        $this->request->allowMethod(['post', 'delete']);
        if (!$entity) {
            $entity = $this->{$this->modelClass}->get($id);
        }
        if ($this->{$this->modelClass}->delete($entity)) {
            $this->Flash->success(__('The ' . $this->entity_name . ' has been deleted.'));
        } else {
            $this->Flash->error(__('The ' . $this->entity_name . ' could not be deleted. Please, try again.'));
        }
        if ($redirect) {
            return $this->redirect($redirect);
        }
        return $this->redirect(['action' => 'index']);
    }
```
* モデル名は {$this->modelClass}で置き換え。
* フラッシュメッセージのエンティティー名部分を$this->entity_nameに置き換え。
* 継承先での エンティティー 設定可能
* 削除後のリダイレクト先を指定可能にしました。

# Templateについて

bakeされたTemplateは各々のエンティティー名が利用されていますので全て ＄entitiesや$entity に置き換える必要があります。

$this->entity_nameを利用すれば置き換え不要にすることも可能かも知れませんがTempalte にも extend（） 機能がありますので重複コード取り除くことがある程度可能です。
そのために敢えてエンティティー名は抽象的な名前で作成しています。
Tempalteのリファクタリングは次の機会があれば発表したいと思います。

#最後に

この発表では単純な置き換えだけをしましたが独自ルールを反映させる場合にメンテナンスが簡単になります。

例えば DB保存時に誰が操作したのか user_id を保存したい場合はsaveする前に
次の様な行を追加すれば全てのControllerで同じ処理をしてくれることになります。

```php:CrudController.php
$entity->set('user_id', $this->Auth->user('id'));
```

最後までお読み頂きありがとうございました。