[CakePHP3] addアクションとeditアクションをまとめてしまう

ちまちまとCakeでサービスを作っています。とっても楽しくて日々作り込んでいる画面が、結局のところ自分しか使わない管理画面だと気付いて愕然としました。フロントに力を入れよう、そうしよう。

気を取り直して。

bakeするとデフォルトでaddとeditというアクションが出来上がります。CRUDのC(作成)とU(更新)にあたる部分で、データベースにデータを登録したり、登録済みのデータを更新したりするアクションです。

でもこのaddとedit、中身がとっても似ている。新規か編集かだけの違いなら、ひとつにしちゃってよくない?というのがこの記事の主旨です。


スポンサーリンク

1.bake直後に出来上がっているaddとedit

命名規約に沿ったテーブルを作り、bakeコマンドを打った直後のaddとeditはこんな感じです。例として、idと色の名前を登録するColorsというテーブル(モデル)から作ったところです。

1-1.コントローラ

まずはColorsコントローラにあるaddアクションとeditアクションです。

//addアクション

    /**
  * Add method
  *
  * @return \Cake\Network\Response|void Redirects on successful add, renders view otherwise.
  */
  public function add()
  {
       $color = $this->Colors->newEntity();
             if ($this->request->is('post')) {
                 $color = $this->Colors->patchEntity($color, $this->request->data);
                 if ($this->Colors->save($color)) {
                     $this->Flash->success(__('The color has been saved.'));

                     return $this->redirect(['action' => 'index']);
                 } else {
                     $this->Flash->error(__('The color could not be saved. Please, try again.'));
                 }
             }
             $this->set(compact('color'));
             $this->set('_serialize', ['color']);
  }
// editアクション

    /**
  * Edit method
  *
  * @param string|null $id Color id.
  * @return \Cake\Network\Response|void Redirects on successful edit, renders view otherwise.
  * @throws \Cake\Network\Exception\NotFoundException When record not found.
  */
  public function edit($id = null)
  {
             $color = $this->Colors->get($id, [
                 'contain' => []
             ]);
             if ($this->request->is(['patch', 'post', 'put'])) {
                 $color = $this->Colors->patchEntity($color, $this->request->data);
                 if ($this->Colors->save($color)) {
                     $this->Flash->success(__('The color has been saved.'));

                     return $this->redirect(['action' => 'index']);
                 } else {
                     $this->Flash->error(__('The color could not be saved. Please, try again.'));
                 }
             }
             $this->set(compact('color'));
             $this->set('_serialize', ['color']);
  }

違いは後で見ます。

1-2.テンプレート

続いてテンプレートです。

//add.ctp

<nav class="large-3 medium-4 columns" id="actions-sidebar">
     <ul class="side-nav">
          <li class="heading"></li>
          <li> <?= $this->Html->link(__('List Colors'), ['action' => 'index']) ?></li>
     </ul>
</nav>
<div class="colors form large-9 medium-8 columns content">
     <?= $this->Form->create($color) ?>
     <fieldset>
          <legend><?= __('Add Color') ?></legend>
          <?php
          echo $this->Form->input('name');
          ?>
     </fieldset>
     <?= $this->Form->button(__('Submit')) ?>
     <?= $this->Form->end() ?>
< /div>
//edit.ctp

<nav class="large-3 medium-4 columns" id="actions-sidebar">
     <ul class="side-nav">
          <li class="heading"></li>
          <li><?= $this->Form->postLink(
                         __('Delete'),
                         ['action' => 'delete', $color->id],
                         ['confirm' => __('Are you sure you want to delete # {0}?', $color->id)]
                    )
               ?></li>
          <li>Html->link(__('List Colors'), ['action' => 'index']) ?></li>
     </ul>
</nav>
<div class="colors form large-9 medium-8 columns content">
     <?= $this->Form->create($color) ?>
     <fieldset>
          <legend><?= __('Edit Color') ?></legend>
          <?php
          echo $this->Form->input('name');
          ?>

     </fieldset>
     <?= $this->Form->button(__('Submit')) ?>
     <?= $this->Form->end() ?>
</div>

テンプレートに関しては、リンク部分と表のタイトル以外は全て同じです。フォームとしての機能部分が同じなので、ファイルひとつで十分ですね。

2.ひとつのアクションにまとめる

では実際にひとつにまとめていきましょう。addかeditのどちらかに寄せてもいいのですが、ここはひとつ「form」という新しいアクションにします。

先程記述したaddアクションとeditアクションの違いを見てみると、大きく分けて3つの違いがあることがわかります。

2-1.違いその1:引数を取るかどうか

addアクションは新規作成なので、引数を取りません。一方、editアクションは編集なので、どのレコードを編集するか?という情報が必要です。そのため、idを引数としています。idがnullであれば例外を投げます。

//addアクション

public function add(){
     …
}
//editアクション

public function edit($id = null){
     …
}

2-2.違いその2:エンティティの作り方

addアクションは新規作成なので、エンティティのインスタントを作る際、newEntity()で新しいエンティティを作ります。一方、editアクションは編集なので、引数のidを使ってget()でエンティティを取得します。

//addアクション

$color = $this->Colors->newEntity();
//editアクション

$color = $this->Colors->get($id, [
     'contain' => []
]);

2-3.違いその3:HTTPリクエストの場合分け

addアクションはPOSTリクエストのみを処理します。一方、editアクションはPATCH、POST、PUTの3ついずれかのリクエストであれば処理します。

//addアクション

if ($this->request->is('post')) {
     …
}
//editアクション

if ($this->request->is(['patch', 'post', 'put'])) {

     …
}

一般的に、POSTはレコードの作成、PUTはレコードの作成または置換、PATCHはレコードの部分更新に使われるものです。

FormHelperを見てみたところ、テンプレートファイルでForm->create()を使った場合、cakeはフォームデータの中身からPOSTかPUTかを判断してmethod属性に入れているみたいですね。

//FormHelper.php

public function create($model = null, array $options = [])
{
       $append = '';

     if (empty($options['context'])) {
          $options['context'] = [];
     }
     $options['context']['entity'] = $model;
     $context = $this->_getContext($options['context']);
     unset($options['context']);

     $isCreate = $context->isCreate();

     $options += [
          'type' => $isCreate ? 'post' : 'put',
          'action' => null,
          'url' => null,
          'encoding' => strtolower(Configure::read('App.encoding')),
          'templates' => null,
          'idPrefix' => null,
     ];

…中略…

$htmlAttributes = [];
switch (strtolower($options['type'])) {
     case 'get':
          $htmlAttributes['method'] = 'get';
          break;
     // Set enctype for form
     case 'file':
          $htmlAttributes['enctype'] = 'multipart/form-data';
          $options['type'] = ($isCreate) ? 'post' : 'put';
     // Move on
     case 'post':
     // Move on
     case 'put':
     // Move on
     case 'delete':
     // Set patch method
     case 'patch':
          $append .= $this->hidden('_method', [
          'name' => '_method',
          'value' => strtoupper($options['type']),
          'secure' => static::SECURE_SKIP
     ]);
     // Default to post method
     default:
          $htmlAttributes['method'] = 'post';
}
if (isset($options['method'])) {
     $htmlAttributes['method'] = strtolower($options['method']);
}

…以下略…

なので、Form->create()で(メソッドを指定せずに)フォームを作っている限りは、POSTかPUTならsaveするぜヒャッハー!という思考でよさそうです。間違っていたらご指摘ください…。

2-4.3つの違いを吸収してひとつのアクションにする

上記3つの違いを考えると、idがnullかどうかで新規か編集かを判断する方法がよさそうです。リクエストによる分岐はたぶんPOSTかPUTの受け入れでよさそうですが、まあeditのデフォルトにPATCHが入っているのでそのまま残しておきます。あずかり知らないところでPATCHが使われているかもしれないので…。

で、まとめたコードが下記の通り。

// formアクション

    /**
  * Form method
  *
  * @param string|null $id Color id.
  * @return \Cake\Network\Response|void Redirects on successful form, renders view otherwise.
  */
  public function form($id = null)
  {
             if (!is_null($id)){
                 //idが渡っていればエンティティをget(edit)
                 $color = $this->Colors->get($id, [
                 'contain' => []
                  ]);
             } else { //idがnullならば空エンティティを作成(add)
                 $color = $this->Colors->newEntity();
             }
             if ($this->request->is(['patch', 'post', 'put'])) {
                 $color = $this->Colors->patchEntity($color, $this->request->data);
                 if ($this->Colors->save($color)) {
                     $this->Flash->success(__('The color has been saved.'));

                     return $this->redirect(['action' => 'index']);
                 } else {
                     $this->Flash->error(__('The color could not be saved. Please, try again.'));
                 }
             }
             $this->set(compact('color'));
             $this->set('_serialize', ['color']);
  }

はい。まとまりました。


スポンサーリンク

3.テンプレートに最低限必要な記述

ビューとしてform.ctpを作るわけですが、ここは元からリンクや表示の違いだけだったので、最低限必要な記述は下記となります。

<?= $this->Form->create($color) ?>
<fieldset>
     <?php
     echo $this->Form->input('name');
     ?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>

あとは適当に肉付けする感じで。新規か編集かで変えたい表示がある場合は、$color[‘id’]がnullかどうかで判断すればいいのかな?ちょっとやってないのでわかりませんが、そんな感じです。

この投稿の投稿者は おさみ です。ブックマーク用 パーマリンク

スポンサーリンク