#なぜ思いついたのか?
マーチン・ファウラー著リファクタリングの第3章**「コードの不吉な匂い」の一番最初に「重複したコード」**という項目があり冒頭に以下の記述があります。
同じようなコードが2箇所以上で見られたら1箇所にまとめると良いプログラムとなります。
CakePHPはとても良いフレームワークですがbakeすると同じようなコードがたくさん作成されてしまうので何とかまとめられないかな〜と考えたのです。
#最終的に作成したいController
基本的な機能は CrudController を継承すれば継承先は基本的に何も書かないで動作させたい。
<?php
namespace App\Controller;
use App\Controller\CrudController;
class GenresController extends CrudController
{
}
#各機能の抽象化
最初に genres テーブルのコントローラをbakeし変更を加えながらCrudControllerを作成してみます。
./bin/cake bake controller genres
##bake 後の index()
<?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
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()
...
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()
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
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()
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()
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で同じ処理をしてくれることになります。
$entity->set('user_id', $this->Auth->user('id'));
最後までお読み頂きありがとうございました。