Laravelでブログサイトを作る 14. 管理機能・記事編集画面から新規登録まで

今回のゴール
記事を作成し、保存するところまで。
編集することも考慮した作りにすること。
Controller
インスタンスの生成等
useに記載した、Illuminate\Support\Facades\DBは、トランザクション処理のためです。
(親子カテゴリでは実装漏れです。後で対応しました)
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use App\Models\ArticleChild;
use App\Models\ChildCategory;
use App\Models\ParentCategory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ArticleController extends Controller
{
protected $article;
protected $articleChild;
protected $childCategory;
protected $parentCategory;
/**
* 新しいコントローラインスタンスの生成
*
* @param Article $article
* @param ArticleChild $articleChild
* @param ChildCategory $childCategory
* @param ParentCategory $parentCategory
* @return void
*/
function __construct(
Article $article,
ArticleChild $articleChild,
ChildCategory $childCategory,
ParentCategory $parentCategory)
{
$this->article = $article;
$this->articleChild = $articleChild;
$this->childCategory = $childCategory;
$this->parentCategory = $parentCategory;
}
入力画面
editメソッドの最後、画面表示はeditView()を作成して行っています。
これは、"更新に失敗した時"の場合に、変更した内容をそのままに再表示させたいので、ArticleとArticleChildだけが違う入力画面表示として用意しました。
- 通常:DBのArticleとArticleChildを取得して、画面表示
- 更新失敗:入力内容をそのままに、画面表示
としたいので、「画面表示」の部分だけ共通化した形です。
/**
* 記事内容入力画面
*
* @param string|null $id
* @return mixed
*/
public function edit(string $id = null) {
// 記事情報を取得(存在しない場合はnull)
$articleData = $this->article->withTrashed()->where('id', $id)->first();
// 記事と子カテゴリの連携データは、テンプレートで使いやすいように子カテゴリIDをキーにしておく
$list = $this->articleChild->where('article', $id)->get();
$articleChildData = [];
foreach ($list as $item) {
$articleChildData[$item->children] = 'on';
}
// 編集画面表示
return $this->editView($articleData, $articleChildData);
}
/**
* 記事内容入力画面の生成
*
* @param $articleData
* @param $articleChildData
* @param $message
* @return mixed
*/
private function editView($articleData, $articleChildData, $message = '') {
// 親子カテゴリを取得(削除状態のカテゴリを設定しないように、withTrashed()はつけない)
$parentCategoryData = $this->parentCategory->orderBy('number', 'asc')->orderBy('id', 'asc')->get();
$childCategoryData = $this->childCategory->orderBy('number', 'asc')->orderBy('id', 'asc')->get();
return view('admin.articleEdit', [
'articleData' => $articleData,
'articleChildData' => $articleChildData,
'parentCategoryData' => $parentCategoryData,
'childCategoryData' => $childCategoryData,
'message' => $message,
]);
}
更新処理
Articleの登録更新は、カテゴリと同様に対応できます。
ArticleChildについては、一旦すべての子カテゴリを取得し、それぞれにチェックが入っているかを確認しています。
チェックボックスで子カテゴリを選択する予定なので、チェックしない(関連付けない)場合、情報がPOSTされません。なので、全部の子カテゴリのうち、チェックされているものを有効に、チェック情報が存在しないものを無効に、という考えです。
/**
* 記事情報更新
*
* @param Request $request
* @return mixed
*/
public function update(Request $request)
{
// 入力値取得
$id = $request['id'];
$tiitle = $request['title'];
$body = $request['body'];
$parent = $request['parent'];
$childList = $request['child'];
$open = $request['open'];
$private = $request['private'];
// トランザクション開始
DB::beginTransaction();
try {
// 記事データ保存
$articleData = $this->article->withTrashed()->where('id', $id)->first();
if ($articleData === null) {
$articleData = new Article();
}
$articleData->title = $tiitle;
$articleData->body = $body;
$articleData->parent = $parent;
$articleData->open = $open;
$articleData->private = $private;
$articleData->save();
// 記事-子カテゴリの連携(削除検出のため、子カテゴリ全件取得)
$childCategoryData = $this->childCategory->withTrashed()->get();
foreach ($childCategoryData as $item) {
// 連携データを取得
$articleChildData = $this->articleChild->withTrashed()
->where('article', $articleData->id)->where('children', $item->id)->first();
if (isset($childList[$item->id]) && $childList[$item->id] === 'on') {
// チェックが入っている => insert or restore
if ($articleChildData === null) {
$articleChildData = new ArticleChild();
$articleChildData->article = $articleData->id;
$articleChildData->children = $item->id;
$articleChildData->save();
} else {
$articleChildData->restore();
}
} else if ($articleChildData !== null) {
// チェックが入っていない => 以前に作られていたら削除
$articleChildData->delete();
}
}
// コミット
DB::commit();
// 完了表示
//return redirect()->route('admin_article_list');
return redirect()->route('admin_article_edit', ['id' => $articleData->id]);
} catch (\Exception $e) {
// 処理失敗時はロールバック
DB::rollback();
// 入力内容をそのままに、編集画面再表示
return $this->editView(
$articleData,
$childList,
'DBの書き込みに失敗しました。[' . $e->getMessage() . ']'
);
}
}
}
テンプレート
resources/views/admin/articleEdit.blade.php
{{ $hoge ?? 'fuga' }}は、「変数$hogeが存在するなら$hogeの内容を表示し、なければfugaを表示」します。
子カテゴリのlabelタグに記載しているdata-parentは、JavaScript側での制御に利用します。属する親カテゴリIDを持たせ、表示/非表示を切り替えます。
@extends('admin.app')
@section('adminTitle', '記事作成編集')
@section('adminMain')
<article>
@if ($message !== '')
<section class="article">
<p>{{ $message }}</p>
</section>
@endif
<section class="article">
<h2>記事作成編集</h2>
<form method="post">
<input type="hidden" name="id" value="{{ $articleData->id ?? '' }}" />
<h3>記事タイトル</h3>
<input type="text" name="title" value="{{ $articleData->title ?? '' }}" />
<h3>親カテゴリ</h3>
<select name="parent" id="articleEditParent">
@foreach ($parentCategoryData as $parent)
<option value="{{ $parent->id }}" {{ ($articleData !== null && $articleData->parent == $parent->id) ? 'selected' : '' }}>
{{ $parent->name }}
</option>
@endforeach
</select>
<h3>子カテゴリ</h3>
<div id="childArea">
@foreach ($childCategoryData as $child)
<label class="articleEditChild" data-parent="{{ $child->parent }}">
<input type="checkbox" name="child[{{ $child->id }}]" value="on"
{{ isset($articleChildData[$child->id]) ? 'checked' : '' }} />{{ $child->name }}
</label>
@endforeach
</div>
<h3>記事本文</h3>
<textarea name="body">{{ $articleData->body ?? '' }}</textarea>
<h3>公開日時</h3>
<input type="text" name="open" value="{{ $articleData->open ?? date('Y/m/d H:i:s') }}" />
<h3>状態</h3>
<div style="margin-bottom: 30px;">
<label><input type="radio" name="private" value="0" {{ ($articleData === null || $articleData->private !== 1) ? 'checked' : '' }} />公開/公開待ち</label>
<label><input type="radio" name="private" value="1" {{ ($articleData !== null && $articleData->private === 1) ? 'checked' : '' }} />非公開</label>
</div>
{{-- CSRFトークン --}}
{{ csrf_field() }}
<input type="button" id="update" value="{{ isset($articleData->id) ? '更新' : '登録' }}" />
</form>
</section>
</article>
<script>
$(document)
.on('click', '#update', function(){
$(this).parents('form').attr('action', '{{ route('admin_article_update') }}').attr('target', '');
$(this).parents('form').submit();
})
;
</script>
@endsection
JavaScript
public/js/admin.js
ページ読み込み時と、親カテゴリを変更した時に、changeArticleEditSubArea()を実行します。
この関数では、子カテゴリ全てを確認し、data-parentの値が親カテゴリIDと一致したら表示するようにしています。
また、非表示にする時にはチェックを外しています。「見えてないけどチェックされている」を防ぐためです。
// ページ読み込み時の処理
$(document).ready(function(){
// 親カテゴリの初期表示にあわせて、子カテゴリの表示を切り替える(記事作成画面のみ)
if ($('#articleEditParent').length > 0) {
changeArticleEditSubArea($('#articleEditParent').val());
}
});
$(document)
.on('change', '#articleEditParent', function() {
// 親カテゴリが変更された際、子カテゴリの表示を切り替える
changeArticleEditSubArea($(this).val());
})
;
/**
* 記事作成画面の時、子カテゴリの表示を親カテゴリにあわせたものにする
*
* @param parentID
*/
function changeArticleEditSubArea(parentID) {
$('.articleEditChild').each(function(){
if ($(this).attr('data-parent') === parentID) {
$(this).css('display', 'inline-block');
} else {
$(this).css('display', 'none');
$('input', this).prop('checked', false).change(); // 非表示にするチェックボックスはチェックを外す
}
});
}
2022-05-09 09:00:00
