Ikeda's Blog

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(); // 非表示にするチェックボックスはチェックを外す
        }
    });
}