この記事はCakePHP Advent Calendar 2017の24日目の記事です。
昨日は@yuzoiwasakiさんの「後からアソシエーションを制御する方法いろいろ」でした。
みなさん、突然ですがフォームをグリグリ動かしたくないですか?
CakePHPで簡単にフォームをグリグリ動かしたくないですか??
今回はそんな思いから、CakePHP3にjquery-uiのSortableフォームを導入した記録を残しておきます。
jquery-uiの導入
jquery-uiのSortableについてはこちらにサンプルがあります。
実装に必要なJSファイルはCDNでも提供されているので今回はそちらを利用しましょう。
$this->Html->script('https://code.jquery.com/jquery-2.2.4.min.js'); $this->Html->script('https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js');
ViewかTemplateあたりで読み込んでおきましょう。
-
+
-
+
$(".todo-list").sortable({ placeholder: "sort-highlight", handle: ".handle", forcePlaceholderSize: true, zIndex: 999999 });
あとはこんな感じでHTMLとJSを書けばフロント側は完成です。
ここからCSSを整える必要はありますが、とりあえずグリグリ動かせるフォームが出来上がります。
ちなみにbootstrapベースの管理画面用CSSプラグイン AdminLTEを導入していると、このコードだけでもそこそこ綺麗な可動式フォームができるようです。
システムを組み込む
それでは実際にjquery-uiのSortableを使用したフォームにシステムを組み込んでみましょう。
ここでは仮にブログシステムのようなものを想定し、記事を管理するPostモデルとそれに対して一対多で紐づくCategoryモデル、そして多対多で紐づくTagモデルを作成しておきます。
作成したテーブルの略図を載せておきますね。
posts
id | integer | ID |
title | varchar | 記事タイトル |
body | text | 本文 |
categories
id | integer | ID |
name | varchar | カテゴリ名 |
post_id | integer | 記事ID |
order_num | integer | 表示順 |
tags
id | integer | ID |
name | varchar | タグ名 |
posts_tags
id | integer | ID |
post_id | integer | 記事ID |
tag_id | integer | タグID |
order_num | integer | 表示順 |
bakeコマンドで勝手にいろいろ作ってもらうため、テーブル名やカラム名はできる限りCakePHPの命名規則に沿ったものを作るといいと思います。
そのほかSortableフォームを作成する際のポイントですが、表示順を保持しておくためのカラムを用意しておくことでしょうか。一対多であればbelongsTo側のテーブルに用意しますが、多対多の場合は中間テーブルに保持しないといけません。
一対多のSortableフォーム
まずは一対多で紐づくPostsとCategoriesのフォームを実装してみましょう。
Template
= $this->Form->create($post) ?> ...
-
categories) : ?>
categories as $category) : ?>
-
+
= $this->Form->control('categories..name', ['value' => $category->name]) ?>= $this->Form->button(__('削除'), ['type' => 'button', 'class' => 'category-delete-btn']) ?>
Templateに記述するのはこんな感じです。
addでもeditでも使いまわせるように、$post
で渡されたEntityにcategoriesが紐づいていた場合はそれをフォームとして表示させておき、それぞれに削除ボタンと、新しくカテゴリを追加できるボタンを設置します。
ポイントは下記の通り。
FormHelper::control()の第一引数の書き方
一対多のリレーションを登録するフォームであれば基本的に
$this->Form->control('<リレーション名>.<順番>.<カラム名>');
のようにすることで登録ができますが、Sortableフォームでグリグリ好き勝手に動かされちゃうと順番の管理が面倒なので、順番のところを空欄にしておいて勝手に入るようにしておきます。
(このやり方だと一度に複数カラムを登録するときに面倒なんですが、ここでは見ないふりをします)
登録済みカテゴリのvalue値設定
上記で順番を指定しないようにしてしまったため、editしたときなどに自動でvalue値を取ってこれなくなります。ちょっとダサいですが手動で引っ張ってきましょう。
なお、フォーム追加ボタンのJSとフォーム削除ボタンのJSはそれぞれこのようになります。
$("#categoryAddBtn").on("click", function () { var categoryForm = $("#newCategoryForm").clone(true); $(categoryForm).attr("id", ""); $(categoryForm).show(); $("#categoryForm").append(categoryForm); });
$(".category-delete-btn").on("click", function () { $(this).parent().remove(); });
Controller
次はControllerでの処理を見ていきましょう。
基本的にはbakeコマンドで生成されるadd()
の処理を流用しますが、少しだけ処理を加えます。
public function add() { $post = $this->Posts->newEntity(); if ($this->request->is('post')) { $postData = $this->request->getData(); $categories = []; $i = 1; foreach ($postData['categories'] as $category) { if (!empty($category['name'])) { $categories[] = ['name' => $category['name'], 'order_num' => $i]; $i++; } } $postData['categories'] = $categories; $post = $this->Posts->patchEntity($post, $postData); if ($this->Posts->save($post)) { ...
追加した処理でやっていることとしては、空欄で追加されたカテゴリの削除と表示順制御カラムorder_num
の追加ぐらいです。
Model
あとはModelの設定もちょこちょこ変更します。PostsTableのinitialize()
を見ていきます。
$this->hasMany('Categories', [ 'foreignKey' => 'post_id', 'sort' => ['order_num' => 'asc'], 'saveStrategy' => 'replace', ]);
'sort' => ['order_num' => 'asc'],
リレーションでデータを取得した際の表示順をorder_num
の昇順に設定しておきます
'saveStrategy' => 'replace',
これがないとフォームを編集するたびにカテゴリがどんどん増えていきます。
なお、categoriesテーブルのpost_id
カラムにNOT NULL制約を付けていないと、フォームを編集するたびに既存のレコードは削除されずにpost_id
がNULLになってレコード自体は残り続けます。これもなんだか気持ち悪いので気を付けましょう。
これで、グリグリ動かせるカテゴリ追加フォームができたかと思います!
次はこのままタグ追加フォームに行ってみましょう!!
多対多のSortableフォーム
ここでPostsとTagsの関係はbelongsToMany、つまり多対多を想定しています。
CategoriesのようにPostsを追加・編集する際に都度レコードを作成していく形式ではなく、すでに作成されたTagsのレコードの中からセレクトボックスなりで選び取っていく形です。
カテゴリと同様にこちらも自由に追加、削除ができるフォームを作成していきましょう。
Template
-
tags) : ?>
tags as $tag) : ?>
-
+
= $this->Form->control('tags..id', ['type' => 'select', 'options' => $tags, 'empty' => '-', 'value' => $tag->id]) ?>= $this->Form->button(__('削除'), ['type' => 'button', 'class' => 'tag-delete-btn']) ?>
まずは、Templateです。
だいたいは先ほどのCategoriesフォームを踏襲していますが、ところどころ変えています。
表示順を挿入する関係で、多対多リレーションに便利なtags._idsは使っていません。
Controller
次にControllerの処理はこんな感じです。
public function add() { $post = $this->Posts->newEntity(); if ($this->request->is('post')) { $postData = $this->request->getData(); $tags = []; $j = 1; foreach ($postData['tags'] as $tag) { if (!empty($tag['id'])) { $tags[] = ['id' => $tag['id'], '_joinData' => ['order_num' => $j]]; $j++; } } $postData['tags'] = $tags; $post = $this->Posts->patchEntity($post, $postData); if ($this->Posts->save($post)) { ...
ここでポイントになるのは
$tags[] = ['id' => $tag['id'], '_joinData' => ['order_num' => $j]];
この処理です。
カテゴリと同様に表示順を登録しますが、タグの場合は中間テーブルposts_tagsに登録しなければなりません。
その場合、_joinData
というキーを使用すると、登録した中間テーブルのレコードに対してデータを挿入することができます。
Model
Modelの設定もしてやります。
PostsTableのinitialize()
内で、下記のリレーション設定を定義してあげてください。
$this->belongsToMany('Tags', [ 'foreignKey' => 'post_id', 'targetForeignKey' => 'tag_id', 'joinTable' => 'posts_tags', 'sort' => ['order_num' => 'asc'], ]);
(ここでのsort設定は特に何も考えずに書いてもposts_tagsテーブルのデータを見てくれるようです。便利ですね。)
いかがでしたでしょうか??
これにてSortableなカテゴリ追加/タグ追加フォームができたかと思います。
この記事を見たフォーム作成に苦しむCakePHPerの皆様が少しでも救われますように!
明日はいよいよCakePHP Advent Calendar 2017最終日、@chinpei215さんの担当です!
ここまでお読みいただきありがとうございました!
よい聖夜をお過ごしください。