Ikeda's Blog

Laravelでブログサイトを作る 15. 管理機能・記事一覧画面

はじめに

記事の一覧表示は、管理側とフロント側でほとんど同じものを用意することになります。
相違点は以下の2点ですので、それを踏まえて作成します。

  • 削除済みの内容を表示するかどうか
  • 使用するテンプレート

Model

app/Models/Article.php
一覧取得のためのメソッドをModelに記載します。
今回は「削除済みを含むか」だけですが、今後は「絞り込み(親カテゴリ、子カテゴリ)」「検索」についてもコチラを編集する予定です

<?php

// ...省略...

class Article extends Model
{
    // ...省略...

    /**
     * 一覧取得
     *
     * @param int    $num    件数
     * @param bool   $delete 削除済みを含めるか(true => 含める)
     * @return mixed
     */
    public function getList(int $num, bool $delete = false) {
        $article = $this;

        // 削除済みを含む場合は`withTrashed()`
        if ($delete !== true) {
            $article = $article->withTrashed();
        }

        // ソートは、新しい順に表示
        $article = $article->orderBy('open', 'desc');

        // 指定件数を取得
        return $article->paginate($num);
    }
}

Controller

app/Http/Controllers/ArticleController.php

「一覧取得する」「表示する」という工程に分けて考えます。後に、絞り込みや検索した際に、「表示する」アルゴリズムは共通なので、listView()を作成し、表示処理を行っています。

<?php

// ...省略...

class ArticleController extends Controller
{
    // ...省略...

    private $ADMIN_LIST_NUM = 30; // 管理画面での記事一覧表示件数

    // ...省略...

    /**
     * 管理画面・記事一覧
     *
     * @return mixed
     */
    public function adminList() {
        // ブログ記事一覧を取得
        $articleData = $this->article->getList($this->ADMIN_LIST_NUM, true);

        return $this->listView($articleData, 'admin');
    }

    /**
     * 記事の一覧表示
     *
     * @param object $articleData
     * @param string $type
     * @return mixed
     */
    private function listView(object $articleData, string $type = 'front') {
        // 親カテゴリを取得
        $parentCategoryList = $this->parentCategory->orderBy('number', 'asc')->orderBy('id', 'asc')->get();

        // 子カテゴリは、キーを親カテゴリIDにした連想配列にする
        $data = $this->childCategory->orderBy('number', 'asc')->orderBy('id', 'asc')->get();
        $childCategoryList = [];
        foreach ($data as $item) {
            $childCategoryList[$item->parent][] = $item;
        }

        return view($type . '.articleList', [
            'articleData' => $articleData,
            'parentCategoryList' => $parentCategoryList,
            'childCategoryList' => $childCategoryList,
        ]);
    }
}

テンプレート

resources/views/admin/articleList.blade.php
ページの移動には、独自に作成したテンプレートを使用しています。これについては、後述します。

@extends('admin.app')

@section('adminTitle', '記事一覧')

@section('paging')
    <nav>
        {{ $articleData->links('pagination') }}
    </nav>
@endsection

@section('adminMain')
    <article>
        <section class="article">
            <h2>記事一覧</h2>

            <table>
                <tr>
                    <th style="width:200px;">サムネイル</th>
                    <th>タイトル</th>
                </tr>
                @foreach ($articleData as $item)
                    {{-- 本文中の最初の画像をサムネイルとして扱う --}}
                    <?php preg_match('/<img.+src=[\'"](?P<src>.+?)[\'"].*>/i', $item->body, $image); ?>
                    <tr {!! ($item->private === 1) ? 'style="background-color: #cccccc;"' : '' !!}>
                        <td><img src="{{ $image['src'] ?? '/img/no_image.jpg' }}" /></td>
                        <td>
                            {{-- タイトル(クリックしたら編集画面へ) --}}
                            <a href="{{ route('admin_article_edit', ['id' => $item->id]) }}">{{ $item->title }}</a>
                            {{-- タイトルと本文を水平線で区切る --}}
                            <hr />
                            {{-- 本文は、タグを除いた先頭200文字だけを表示させる --}}
                            {!! mb_substr(strip_tags($item->body), 0, 200) !!}
                        </td>
                    </tr>
                @endforeach
            </table>
        </section>
    </article>
@endsection

独自定義のページネーション

resources/views/pagination.blade.php

先述のテンプレートに出てきた、{{ $articleData->links('pagination') }}で、ページネーションのHTMLを作成しています。

$articleDataは、Modelで検索条件を付けた後に$article->paginate($num);の戻り値が入っています。

このpaginate()の返り値に対して、->links()で、基本的なページネーションができます。テンプレート名を渡してやれば、対象のファイルを使ってくれます。

@if ($paginator->hasPages())
    <ul class="pagination">
        {{-- 現在が1ページ目でなければ、最初へ遷移するリンクを設置 --}}
        @if (!$paginator->onFirstPage())
            <li>
                <a href="{{ $paginator->url(1) }}">最初</a>
            </li>
        @endif

        {{-- ページ番号を使用したリンクを設置 --}}
        @foreach ($elements as $element)
            @if (is_array($element))
                @foreach ($element as $page => $url)
                    @if ($page === $paginator->currentPage())
                        {{-- 現在のページ番号には、リンクを設置しない --}}
                        <li class="active" aria-current="page"><span>{{ $page }}</span></li>
                    @elseif (($page - 2) <= $paginator->currentPage() && ($page + 2) >= $paginator->currentPage())
                        {{-- 2ページ前~2ページ次までのページ番号を表示 --}}
                        <li><a href="{{ $url }}">{{ $page }}</a></li>
                    @endif
                @endforeach
            @endif
        @endforeach

        {{-- Next Page Link --}}
        @if ($paginator->hasMorePages())
            <li>
                <a href="{{ $paginator->url($paginator->lastPage()) }}">最後</a>
            </li>
        @endif
    </ul>
@endif

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

Laravelでブログサイトを作る 13. 管理機能・子カテゴリの管理

はじめに

親カテゴリを基にして作成します。
一覧登録更新を作成した前提で進めますので、ご留意ください。

Controller

app/Http/Controllers/ChildCategoryController.phpの内容は、まずParentCategoryController.phpをコピーします。
その後、親カテゴリには無かった、カラム「parent」について追加します。
変更箇所はコード内にコメントを入れていますが、目的は以下の2点です。

  1. 画面表示する際に、セレクトメニューで使用するための「全親カテゴリ」データを取得しテンプレートへ渡す
  2. 登録更新する情報に、関連する親カテゴリのIDを追加
<?php

namespace App\Http\Controllers;

use App\Models\ChildCategory; // ※今回追加
use App\Models\ParentCategory;
use Illuminate\Http\Request;

class ChildCategoryController extends Controller
{
    /**
     * @var ChildCategory
     */
    protected $childCategory; // ※今回追加

    /**
     * @var ParentCategory
     */
    protected $parentCategory;

    /**
     * 新しいコントローラインスタンスの生成 ※ChildCategoryを追加
     *
     * @param ChildCategory $childCategory
     * @param ParentCategory $parentCategory
     * @return void
     */
    function __construct(ChildCategory $childCategory, ParentCategory $parentCategory) {
        $this->childCategory = $childCategory;
        $this->parentCategory = $parentCategory;
    }

    /**
     * 一覧画面表示
     *
     * @return mixed
     */
    public function list() {
        // 子カテゴリデータの一覧 ※今回追加
        $childCategoryData = $this->childCategory->withTrashed()->orderBy('id', 'asc')->get();

        // 親カテゴリデータの一覧 ※説明用のコメントを削除して一行にまとめ
        $parentCategoryData = $this->parentCategory->withTrashed()->orderBy('id', 'asc')->get();

        return view('admin.childCategory', [
            'childCategoryData' => $childCategoryData, // ※今回追加
            'parentCategory' => $parentCategoryData,
        ]);
    }

    /**
     * 子カテゴリ レコード総更新
     *
     * @param Request $request
     * @return mixed
     */
    public function update(Request $request) {
        $flg = true; // $flgがfalseになるまでループします
        for ($i = 1;$flg;$i++) {
            // ...省略...

            // 入力値を取得
            $id = $request['id_' . $i];
            $name = $request['name_' . $i];
            $number = $request['number_' . $i];
            $delete = $request['delete_' . $i];
            $parent = $request['parent_' . $i]; // ※今回追加

            // ...省略...

            // idがあるなら上書き、なければ新規作成
            $childCategoryData = $this->childCategory->withTrashed()->where('id', $id)->first();
            if ($childCategoryData === null) {
                $childCategoryData = new ChildCategory();
            }
            $childCategoryData->name = $name;
            $childCategoryData->number = $number;
            $childCategoryData->parent = $parent; // ※今回追加
            $childCategoryData->save();

            // ...省略...
        }

        return redirect()->route('admin_child');
    }

テンプレート

resources/views/admin/childCategory.blade.php
コチラも、ベースとしてparentCategory.blade.phpをコピーして使います。

  • テキストの「親」を「子」に変更
  • formのaction属性を、parentからchildに変更
  • 一覧表示でforeachに使用していた変数を$childCategoryData
  • 表に親カテゴリの項目を追加
@extends('admin.app')

@section('adminTitle', '子カテゴリ管理') {{-- 親→子 --}}

@section('adminMain')
    <article>
        <section class="article">
            <h2>子カテゴリ管理</h2> {{-- 親→子 --}}

            <form action="{{ route('admin_parent_update') }}" method="post"> {{-- parent→child --}}
                <table>
                    <tr>
                        <th>カテゴリ名</th>
                        <th>親カテゴリ</th> {{-- 追加 --}}
                        <th>表示順</th>
                        <th>削除</th>
                    </tr>
                    <tr id="model">
                        <td>
                            <input type="hidden" id="id" value="" />
                            <input type="text" id="name" value="" />
                        </td>
                        <td> {{-- 追加 --}}
                            <select id="parent">
                                @foreach ($parentCategory as $parentItem)
                                    <option value="{{ $parentItem->id }}">{{ $parentItem->name }}</option>
                                @endforeach
                            </select>
                        </td>
                        <td><input type="number" id="number" value="" /></td>
                        <td>
                            <label>
                                <input type="checkbox" id="delete" value="on" />削除
                            </label>
                        </td>
                    </tr>
                    <?php $cnt = 1; ?>
                    @foreach ($childCategoryData as $item) {{-- parent→child --}}
                        <tr>
                            <td>
                                <input type="hidden" name="id_{{ $cnt }}" value="{{ $item->id }}" />
                                <input type="text" name="name_{{ $cnt }}" value="{{ $item->name }}" />
                            </td>
                            <td> {{-- 追加 --}}
                                <select name="parent_{{ $cnt }}">
                                    @foreach ($parentCategory as $parentItem)
                                        <option value="{{ $parentItem->id }}"
                                            {{ ($item->parent === $parentItem->id) ? 'selected' : '' }}>
                                            {{ $parentItem->name }}
                                        </option>
                                    @endforeach
                                </select>
                            </td>
                            <td>
                                <input type="number" name="number_{{ $cnt }}"
                                    value="{{ $item->number }}" /></td>
                            <td>
                                <label>
                                    <input type="checkbox" name="delete_{{ $cnt }}" value="on"
                                        {{ ($item->deleted_at !== null) ? 'checked' : '' }} />削除
                                </label>
                            </td>
                        </tr>
                        <?php $cnt++; ?>
                    @endforeach
                    <tr>
                        <td>
                            <input type="hidden" name="id_{{ $cnt }}" value="" />
                            <input type="text" name="name_{{ $cnt }}" value="" />
                        </td>
                        <td> {{-- 追加 --}}
                            <select name="parent_{{ $cnt }}">
                                @foreach ($parentCategory as $parentItem)
                                    <option value="{{ $parentItem->id }}">{{ $parentItem->name }}</option>
                                @endforeach
                            </select>
                        </td>
                        <td><input type="number" name="number_{{ $cnt }}" value="{{ $cnt }}" /></td>
                        <td>
                            <label>
                                <input type="checkbox" name="delete_{{ $cnt }}" value="on" />削除
                            </label>
                        </td>
                    </tr>
                </table>

                <input type="hidden" id="max" value="{{ $cnt }}" />

                {{-- CSRFトークン --}}
                {{ csrf_field() }}

                <input type="button" value="+ 入力枠追加" id="addInput" />
                <input type="submit" value="更新" />
            </form>
        </section>
    </article>
@endsection

JavaScript

public/js/admin.js
入力枠追加の際に実行されるJavaScriptにも、親カテゴリについてを追記します。
lengthを使用して、「要素がある時」としているので、親子カテゴリの処理をできるだけ共通化しています。

$(document)
    // 親カテゴリの入力枠追加
    .on('click', '#addInput', function(){
        // 次の番号を算出
        const max = $('#max');
        const nextNum = parseInt(max.val()) + 1;
        max.val(nextNum);

        // id=modelになっているtr要素を、idを削除した上でcloneする
        const clone = $('#model').clone(true).removeAttr('id');

        // cloneしたtr内の各name属性を変更し、識別用のidを削除しておく
        $('#name', clone).attr('name', 'name_' + nextNum).removeAttr('id');
        $('#number', clone).attr('name', 'number_' + nextNum).val(nextNum).removeAttr('id');
        $('#id', clone).attr('name', 'id_' + nextNum).removeAttr('id');
        $('#delete', clone).attr('name', 'delete_' + nextNum).removeAttr('id');
        if ($('#parent', clone).length > 0) { // ※追加
            $('#parent', clone).attr('name', 'parent_' + nextNum).removeAttr('id');
        }

        // テーブルに追加
        clone.appendTo('table');
    })
;

Laravelでブログサイトを作る 12. 管理機能・親カテゴリの新規登録と更新

はじめに

前回、親カテゴリの一覧表示の続きです。

Controller

app/Http/Controllers/ParentCategoryController.php
前回で一覧表示を実装したControllerに、更新処理を追加します。
FORMのaction属性に{{ route('admin_parent_update') }}としておいたので、submit後はルーティングで指定したArticleController@update、つまり、上記Controller内のupdateメソッド(今回追加するもの)が実行されます。

    /**
     * 親カテゴリ レコード総更新
     *
     * @param Request $request
     * @return mixed
     */
    public function update(Request $request) {
        $flg = true; // $flgがfalseになるまでループします
        for ($i = 1;$flg;$i++) {
            // 入力欄が無くなったらループ終了
            if (!isset($request['id_' . $i])) {
                $flg = false;
                break;
            }

            // 入力値を取得
            $id = $request['id_' . $i];
            $name = $request['name_' . $i];
            $number = $request['number_' . $i];
            $delete = $request['delete_' . $i];

            // カテゴリ名、表示順が空白になっているレコードは登録・変更しない
            if ($name === null || $number === null) {
                continue;
            }

            // idがあるなら上書き、なければ新規作成
            $parentCategoryData = $this->parentCategory->withTrashed()->where('id', $id)->first();
            if ($parentCategoryData === null) {
                $parentCategoryData = new ParentCategory();
            }
            $parentCategoryData->name = $name;
            $parentCategoryData->number = $number;
            $parentCategoryData->save();

            if ($delete === 'on') {
                // 削除チェックが入っていれば削除
                $parentCategoryData->delete();
            } else if ($parentCategoryData->trashed()) {
                // 削除済みレコードを復帰させる
                $parentCategoryData->restore();
            }
        }

        return redirect()->route('admin_parent');
    }

パラメータは「id_連番」といった規則で送られてくるので、これを1件ずつ確認します。
カテゴリの名前と表示順が空欄であれば、登録も更新もしません。空欄で登録されると他で使う時に面倒なのでこのようにしています。

削除処理をsave()の後に実行しているのは、例えば「とりあえずこのカテゴリ作るけど、記事のネタがある程度できてから有効化しよう」のようなケースだと、新規登録のクセして削除チェックしている、なんてことが起きるためです。

Laravelでブログサイトを作る 11. 管理機能・親カテゴリの一覧表示

はじめに

今回は、親カテゴリの管理機能のうち、一覧表示画面を作成します。

Controller

app/Http/Controllers/ParentCategoryController.php
Controllerでは、親カテゴリのレコードを全て取得し、テンプレートに渡します。

<?php

namespace App\Http\Controllers;

use App\Models\ParentCategory;
use Illuminate\Http\Request;

class ParentCategoryController extends Controller
{
    /**
     * @var ParentCategory
     */
    protected $parentCategory;

    /**
     * 新しいコントローラインスタンスの生成
     *
     * @param ParentCategory $parentCategory
     * @return void
     */
    function __construct(ParentCategory $parentCategory) {
        $this->parentCategory = $parentCategory;
    }

    /**
     * 一覧画面表示
     *
     * @return mixed
     */
    public function index() {
        // 親カテゴリデータの取得
        $parentCategoryData = $this->parentCategory
            ->withTrashed() // 管理画面では、削除済みデータも取得対象とする
            ->orderBy('id', 'asc') // 管理画面の場合、IDでソートする
            ->get()
        ;

        return view('admin.parentCategory', [
            'parentCategoryData' => $parentCategoryData,
        ]);
    }
}

テンプレート

resources/views/admin/parentCategory.blade.php

@extends('admin.app')

@section('adminTitle', '親カテゴリ管理')

@section('adminMain')
    <article>
        <section class="article">
            <h2>親カテゴリ管理</h2>

            <form action="{{ route('admin_parent_update') }}" method="post">
                <table>
                    <tr>
                        <th>カテゴリ名</th>
                        <th>表示順</th>
                        <th>削除</th>
                    </tr>

                    {{-- 入力枠追加ボタンを押した時、下記trを複製の上で調整して追加する --}}
                    <tr id="model">
                        <td>
                            <input type="hidden" id="id" value="" />
                            <input type="text" id="name" value="" />
                        </td>
                        <td><input type="number" id="number" value="" /></td>
                        <td>
                            <label>
                                <input type="checkbox" id="delete" value="on" />削除
                            </label>
                        </td>
                    </tr>

                    {{-- 既存の親カテゴリを表示 --}}
                    <?php $cnt = 1; ?>
                    @foreach ($parentCategoryData as $item)
                        <tr>
                            <td>
                                <input type="hidden" name="id_{{ $cnt }}" value="{{ $item->id }}" />
                                <input type="text" name="name_{{ $cnt }}" value="{{ $item->name }}" />
                            </td>
                            <td>
                                <input type="number" name="number_{{ $cnt }}"
                                    value="{{ $item->number }}" /></td>
                            <td>
                                <label>
                                    <input type="checkbox" name="delete_{{ $cnt }}" value="on"
                                        {{ ($item->deleted_at !== null) ? 'checked' : '' }} />削除
                                </label>
                            </td>
                        </tr>
                        <?php $cnt++; ?>
                    @endforeach

                    {{-- 新規枠をひとつ表示させておく --}}
                    <tr>
                        <td>
                            <input type="hidden" name="id_{{ $cnt }}" value="" />
                            <input type="text" name="name_{{ $cnt }}" value="" />
                        </td>
                        <td><input type="number" name="number_{{ $cnt }}" value="{{ $cnt }}" /></td>
                        <td>
                            <label>
                                <input type="checkbox" name="delete_{{ $cnt }}" value="on" />削除
                            </label>
                        </td>
                    </tr>
                </table>

                {{-- 入力枠追加ボタンを押した時に使用する、通し番号の現在最大値--}}
                <input type="hidden" id="max" value="{{ $cnt }}" />

                {{-- CSRFトークン --}}
                {{ csrf_field() }}

                <input type="button" value="+ 入力枠追加" id="addInput" />
                <input type="submit" value="更新" />
            </form>
        </section>
    </article>
@endsection

JavaScript

public/js/admin.js
新規枠追加ボタンを押した時の動作を追加します。
テンプレートで用意しておいたid="model"のtrを複製し、調整した上でtableの最下列に追加しています。

$(document)
    // 親カテゴリの入力枠追加
    .on('click', '#addInput', function(){
        // 次の番号を算出
        const max = $('#max');
        const nextNum = parseInt(max.val()) + 1;
        max.val(nextNum);

        // id=modelになっているtr要素を、idを削除した上でcloneする
        const clone = $('#model').clone(true).removeAttr('id');

        // cloneしたtr内の各name属性を変更し、識別用のidを削除しておく
        $('#name', clone).attr('name', 'name_' + nextNum).removeAttr('id');
        $('#number', clone).attr('name', 'number_' + nextNum).val(nextNum).removeAttr('id');
        $('#id', clone).attr('name', 'id_' + nextNum).removeAttr('id');
        $('#delete', clone).attr('name', 'delete_' + nextNum).removeAttr('id');

        // テーブルに追加
        clone.appendTo('table');
    })
;