Ikeda's Blog

Laravelでブログサイトを作る 20. DBに後からカラムを追加する

目的

記事に、Meta用の項目"Description"を設定できるようにしたい。
(設計する時に考慮しきれなかった部分に対応する)

migration

migrationファイルの作成

サーバに入り、コマンドを実行します。

  • meta情報を追加するので、ファイルの名前は"add_meta_to_article"にします。
  • --table=で、追加するテーブルを指定します。
php artisan make:migration add_meta_to_article --table=articles

migrationファイルの内容を記述

database/migrations/XXXX_XX_XX_XXXXXX_add_meta_to_article.php
テーブル作成の時と同様に、up()に追加する項目を記述します。

// ...省略...
    public function up()
    {
        Schema::table('articles', function (Blueprint $table) {
            $table->text('description');  // 追加:ディスクリプション
        });
    }
// ...省略...

migration実行

php artisan migrate

これで、テーブルにカラムが追加されました。

テンプレートの修正

記事編集画面

resources/views/admin/articleEdit.blade.php
編集画面に、それぞれの入力欄を設置します。

// ...省略...
<form action="{{ route('admin_article_update') }}" method="post">
    // ...省略...
    <h3>Description</h3>
    <input type="text" name="description" value="{{ $articleData->description ?? '' }}" />
    // ...省略...
</form>
// ...省略...

共通テンプレート

resources/views/app.blade.php
headタグ内に、@yield('meta')だけ記載することで、各テンプレートでMetaタグを自在に設定できるようになります。
存在しない場合は、何も表示されませんので、既存のテンプレートを修正しなくてもエラーが出たりはしないので、必要な箇所にのみ記述するだけで良いのです。

<head>
    // ...省略...

    @yield('meta')

    // ...省略...
</head>

記事の表示画面

resources/views/front/article.blade.php

// ...省略...

@section('meta')
    <meta name="description" content="{{ $articleData->description }}" />
@endsection

// ...省略...

Controller

記事編集

app/Http/Controllers/ArticleController.php
入力されたDescriptionを取得して、テーブルに保存します。
なお、未入力であれば、記事の冒頭100文字を抽出して保存するようにします。


    /**
     * 記事情報更新
     *
     * @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'];
        $description = $request['description'] ?? mb_substr(strip_tags($body), 0, 100);

        // ...省略...

        // トランザクション開始
        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->description = $description;
            $articleData->save();

            // ...省略...
        } catch (\Exception $e) {
            // ...省略...
        }
    }

Laravelでブログサイトを作る 19. 管理画面・記事編集の時に、画像をアップロードできるように(TinyMCE)

はじめに

前回にて実装したTinyMCEの機能を利用して、記事の編集中に画像をアップロードする仕組みを用意します。

サーバー側での対応

Laravelのディレクトリ構造から考えまして、画像は、storageディレクトリ(storage/app/public)に保存するべきと考えました。
しかし、このディレクトリを参照することはできません。
そこで、以下のコマンドを実行しておきます。

$ php artisan storage:link

これで、public/storageにシンボリックリンクが作成されますので、storage/***.jpgのようにして画像を表示できます。

$ ls -l public/
(省略) storage -> /home/vagrant/blog/storage/app/public

TinyMCEでアップロードできるようにする

resources/views/admin/articleEdit.blade.php
前回実装したTinyMCSの記述を修正します。

<script src="{{ asset('/js/tinymce/tinymce.min.js') }}"></script>
<script>
    tinymce.init({
        selector:'textarea',
        language: "ja", // 日本語対応(https://qiita.com/nissuk/items/e31bdfa858d6c5c018c2)

        // ▼画像アップロード対応(https://www.tiny.cloud/docs/configure/file-image-upload/)
        plugins: 'image code',
        toolbar: 'undo redo | link image | code',
        image_title: true, // イメージダイアログのタイトルフィールドを有効にする
        automatic_uploads: true, // ブロブやデータのURIで表現された画像の自動アップロードを可能にする
        images_upload_url: '{{ route('admin_article_image_upload') }}?_token={{ csrf_token() }}',
        file_picker_types: 'image',
        relative_urls : false,  // 画像やリンクのパスが相対パスに変換されるのを防ぐ
        convert_urls: false,

        /* カスタムイメージピッカー */
        file_picker_callback: function (cb, value, meta) {
            var input = document.createElement('input');
            input.setAttribute('type', 'file');
            input.setAttribute('accept', 'image/*');

            input.onchange = function () {
                var file = this.files[0];

                var reader = new FileReader();
                reader.onload = function () {
                    var id = 'blobid' + (new Date()).getTime();
                    var blobCache =  tinymce.activeEditor.editorUpload.blobCache;
                    var base64 = reader.result.split(',')[1];
                    var blobInfo = blobCache.create(id, file, base64);
                    blobCache.add(blobInfo);
                    cb(blobInfo.blobUri(), { title: file.name });
                };
                reader.readAsDataURL(file);
            };

            input.click();
        },
        content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }'
    });
</script>

アップロードされた画像を保存する

ルーティング

routes/web.php
アップロードするルーティングを追加します。

Route::group(['prefix' => 'admin', 'middleware' => 'auth', 'namespace' => 'App\\Http\\Controllers\\'], function() {
    // ...省略...

    Route::post('articleImageUpload', 'ArticleController@image')->name('admin_article_image_upload'); // 画像アップロード

    // ...省略...
});

Controller

app/Http/Controllers/ArticleController.php

アップロードされた画像を保存し、パスを返却します。

/**
 * 画像アップロード
 *
 * @param Request $request
 * @return false|string
 */
public function image(Request $request) {
    // ファイルの保存(戻り値はファイル名)
    $imgpath = request()->file('file')->store('', 'public');

    // 画像パスを返却
    return json_encode(['location' => '/storage/' . $imgpath]);
}

参考文献

https://www.tiny.cloud/docs/configure/file-image-upload/
https://qiita.com/kawa_matsu/items/50306befb7a5f879e38e
https://readouble.com/laravel/7.x/ja/structure.html

Laravelでブログサイトを作る 18. 管理画面・記事編集の際に入力補助を(TinyMCE)

はじめに

記事を編集する際に、HTMLの入力補助を行えるようにします。
様々なライブラリが存在しますが、TinyMCEを使用します。
画像のアップロード機能を組み込むことができるので(次回解説予定)、という理由で

選定しました。

導入方法

  1. JSファイルをダウンロードし、public/jsに配置する
    • CDNを使うと、日本語化で手こずる懸念と、バージョンアップに引きずられて問題が発生する可能性があるため、ファイルをダウンロードします。
  2. テンプレートファイルに、JSの読み込みと初期化構文を記述する

1. JSファイルのダウンロード

ダウンロードは、コチラから可能です。
ページ中程にある「Download TinyMCE Community」から本体を、「Download Language Pack」から日本語パックをダウンロードし、解凍します。

本体は、public/jsに配置します。
その後、langディレクトリに日本語パックのja.jsを移動します。
最終的には、以下のような形です。

public/
└ js/
  └ tinymce/
    ├ lang
    |  └ ja.js ←日本語パックから移動
    └ その他、jquery.tinymce.min.js など

テンプレート

resources/views/admin/articleEdit.blade.php
JSファイルの読み込みと、初期化についての記述を追加するだけで、便利なTinyMCEが使えるようになります。
日本語化せずにCDNを使う場合は、前述のダウンロード云々もスキップできるので、非常に簡単です。


@section('adminMain')
    {{-- 省略 --}}

    <h3>記事本文</h3>
    <textarea name="body">{{ $articleData->body ?? '' }}</textarea>

    {{-- 省略 --}}
    
    <script src="{{ asset('/js/tinymce/tinymce.min.js') }}"></script>
    <script>
        tinymce.init({
            selector:'textarea',
            language: "ja"
        });
    </script>
@endsection

参考文献

TinyMCEを設置する
https://qiita.com/nissuk/items/e31bdfa858d6c5c018c2

Laravelでブログサイトを作る 17. フロントページ

はじめに

今回の、フロント側作成によって、ブログは一応の完成となります。
カテゴリを管理し、記事を作成編集でき、それを表示する。ごくごくシンプルな構成ですが、ブログとしての最低限が備わった状態になります。

そしてその作業ですが、実はほとんどありません。

Controller

app/Http/Controllers/ArticleController.php

一覧

ルーティングの際に設定した、一覧表示画面を用意します。
新着順、親子カテゴリの絞り込み、検索。前々回前回で、管理画面で用意したものとほとんど同じです。
違う点は、以下の2つ。

  • 削除済み記事を出さない→getList()の第2引数をfalseにしてあげればOKです
  • フロント用のテンプレートを使う→listView()の第2引数をfrontに変更
    /**
     * 記事一覧
     *
     * @return mixed
     */
    public function list() {
        // ブログ記事一覧を取得
        $articleData = $this->article->getList($this->ADMIN_LIST_NUM, false);

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

    /**
     * 親カテゴリ別記事一覧
     *
     * @param string $parent
     * @return mixed
     */
    public function parentList(string $parent) {
        // ブログ記事一覧を取得
        $articleData = $this->article->getList($this->ADMIN_LIST_NUM, false, (int)$parent);

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

    /**
     * 子カテゴリ別記事一覧
     *
     * @param string $child
     * @return mixed
     */
    public function childList(string $child) {
        // ブログ記事一覧を取得
        $articleData = $this->article->getList($this->ADMIN_LIST_NUM, false, 0, $child);

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

    /**
     * 記事一覧(ワード検索)
     *
     * @param Request $request
     * @return mixed
     */
    public function searchList(Request $request) {
        // ブログ記事一覧を取得
        $articleData = $this->article->getList($this->ADMIN_LIST_NUM, false, 0, 0, $request['search']);

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

記事詳細

① 記事取得
記事テーブルと親カテゴリテーブルのJoinは、カテゴリ名を取得するための連結です。後述の子カテゴリ同様に、記事の終わりにカテゴリ別一覧へのリンク生成に使います。
一覧のようにget()を使うのではなく、first()を使っているのは、主キーで検索しているからです。

② 記事と関連付けられている子カテゴリの情報を取得
記事のIDが固定なので、そこに関連する子カテゴリを取得します。関連付けテーブルと子カテゴリテーブルをJoinして、カテゴリ名も取得します。

    /**
     * 記事詳細
     *
     * @param string $id
     * @return mixed
     */
    public function content(string $id) {
        // ① 記事取得
        $articleData = $this->article
            // 親カテゴリについても取得
            ->leftJoin(
                'parent_categories',
                function ($join) {
				    $join->on('articles.parent', '=', 'parent_categories.id');
                }
            )
            // ID指定して1件分のデータを取得
            ->where('articles.id', $id)->first();

        if ($articleData === null) {
            // 記事データが見つからない場合はエラー出力する
            return $this->error('warning', '記事が見つかりません。');
        }

        // ② 記事と関連付けられている子カテゴリの情報を取得
        $articleChildrenData = $this->articleChild
            ->leftJoin('child_categories', function ($join) {
                $join->on('article_children.children', '=', 'child_categories.id');
            })->where('article_children.article', $id)->get();

        // 親カテゴリを取得
        $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('front.article', [
            'articleData' => $articleData,
            'articleChildrenData' => $articleChildrenData,
            'parentCategoryList' => $parentCategoryList,
            'childCategoryList' => $childCategoryList,
        ]);
    }

    /**
     * エラー出力
     *
     * @param string $type
     * @param string $message
     * @return mixed
     */
    public function error(string $type, string $message) {
        // $typeに特定文字列以外を指定させない
        if (!in_array($type, ['information', 'caution', 'warning'])) {
            $type = 'information';
        }

        return view('front.error', [
            'type' => $type,
            'message' => $message,
        ]);
    }

テンプレート

記事詳細

resources/views/front/article.blade.php
あまり、特筆することはありません。記事のタイトルと本文を表示し、属するカテゴリの表示と、それぞれの絞り込み一覧へのリンクを生成しています。

@extends('front.app')

@section('title')
    {{ $articleData->title }}
@endsection

@section('main')
    <article>
        <section class="article">
            {{-- 記事タイトル --}}
            <h2>{{ $articleData->title }}</h2>

            {{-- 本文 --}}
            {!! $articleData->body !!}

            {{-- 親子カテゴリ --}}
            <div class="articleCategory">
                <ul>
                    <li>
                        <a href="{{ route('list_parent', ['id' => $articleData->parent]) }}">
                            {{ $articleData->name }}
                        </a>
                    </li>
                    @foreach ($articleChildrenData as $articleChildren)
                        <li>
                            <a href="{{ route('list_child', ['id' => $articleChildren->children]) }}">
                                {{ $articleChildren->name }}
                            </a>
                        </li>
                    @endforeach
                </ul>
            </div>

            {{-- 公開日時 --}}
            <p class="articleDate">{{ $articleData->created_at }}</p>
        </section>
    </article>
@endsection

エラー

resources/views/front/error.blade.php
$typeはエラーレベルを指定するイメージです。ひとまず「通知(information)」「注意(caution)」「警告(warning)」を想定しています。

@section('title', 'エラー')

@section('main')
    <article>
        <section class="article">
            <div class="{{ $type }}"><p>{{ $message }}</p></div>
        </section>
    </article>
@endsection

Laravelでブログサイトを作る 16. 管理機能・記事一覧の絞り込みと検索

はじめに

前回で記事一覧を作成した続きになります。

Model

app/Models/Article.php
前回作成した一覧取得メソッドを拡張します。
引数に、今回追加分が未指定の場合の初期値を設定しているので、既存の呼び出し箇所に変更は不要となります。

// ...省略...

    /**
     * 一覧取得
     *
     * @param int    $num      件数
     * @param bool   $delete   削除済みを含めるか(true => 含める)
     * @param int    $parent   親カテゴリID 【今回追加】
     * @param int    $children 子カテゴリID 【今回追加】
     * @param string $search   検索ワード   【今回追加】
     * @return mixed
     */
    public function getList(
        int $num,
        bool $delete = false,
        int $parent = 0,
        int $children = 0,
        string $search = '')
    {
        $article = $this;

        // 【今回追加】親カテゴリが指定されている場合、記事テーブルのカラムと一致するものを抽出する
        if ($parent !== 0) {
            $article = $article->where('articles.parent', '=', $parent);
        }

        // 【今回追加】子カテゴリが指定されている場合、テーブルをJOINする
        if ($children !== 0) {
            $article = $article
                // 記事テーブルと、記事-子カテゴリ関連付けテーブルをJOIN
                ->leftJoin('article_children', function ($join) {
                    $join->on('articles.id', '=', 'article_children.article')
                        ->where('article_children.deleted_at', '=', NULL);
                })
                // 子カテゴリテーブルと、記事-子カテゴリ関連付けテーブルをJOIN
                ->leftJoin('child_categories', function ($join) {
                    $join->on('child_categories.id', '=', 'article_children.children');
                })
                // 子カテゴリの一致するものを抽出する
                ->where('child_categories.id', '=', $children);
        }

        // 【今回追加】検索ワードが指定されている場合
        if ($search !== '') {
            $article = $article
                // タイトルか本文のどこかに検索ワードが含まれていれる記事を抽出する
                ->where(function($query) use ($search) {
                    $query->where('articles.title', 'like', '%'.$search.'%')
                        ->orWhere('articles.body', 'like', '%'.$search.'%')
                    ;
                });
        }

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

        // ソートは、新しい記事を上位表示
        $article = $article->orderBy('open', 'desc');

        // 取得
        return $article->paginate($num);
    }

// ...省略...

Controller

ルーティングで設定したメソッドを用意し、それぞれのパラメータを変更して一覧取得します。
その後は、表示用メソッドlistView()を実行するだけです。

// ...省略...


    /**
     * 管理画面・親カテゴリ別記事一覧
     *
     * @param string $parent
     * @return mixed
     */
    public function adminParentList(string $parent) {
        // ブログ記事一覧を取得
        $articleData = $this->article->getList($this->ADMIN_LIST_NUM, true, (int)$parent);

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

    /**
     * 管理画面・子カテゴリ別記事一覧
     *
     * @param string $child
     * @return mixed
     */
    public function adminChildList(string $child) {
        // ブログ記事一覧を取得
        $articleData = $this->article->getList($this->ADMIN_LIST_NUM, true, 0, $child);

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

    /**
     * 管理画面・記事一覧(ワード検索)
     *
     * @param Request $request
     * @return mixed
     */
    public function adminSearchList(Request $request) {
        // ブログ記事一覧を取得
        $articleData = $this->article->getList($this->ADMIN_LIST_NUM, true, 0, 0, $request['search']);

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

// ...省略...

テンプレート

resources/views/admin/app.blade.php
サイドメニューに、検索ボックスとカテゴリの絞り込みリンクを設置します。

// ...省略...

    <section class="aside">
        <h3>検索</h3>
        <div id="searchWrapper">
            <form action="{{ route('admin_article_list_search') }}" method="post" name="searchForm">
                {{ csrf_field() }}
                <input type="text" id="searchTxt" name="search" placeholder="サイト内検索" />
                    <a href="javascript:searchForm.submit()" id="searchBtn">
                        <i class="fas fa-search"></i>
                    </a>
            </form>
        </div>
    </section>

    @if (isset($parentCategoryList))
    <section class="aside">
        <h3>カテゴリ</h3>
        <ul>
            @foreach ($parentCategoryList as $parent)
                <li>
                    <a href="{{ route('admin_article_list_parent', ['parent' => $parent->id]) }}">
                        {{ $parent->name }}
                    </a>
                    @if (isset($childCategoryList[$parent->id]))
                        <ul>
                            @foreach ($childCategoryList[$parent->id] as $child)
                                <li>
                                    <a href="{{ route('admin_article_list_child', ['child' => $child->id]) }}">
                                        {{ $child->name }}
                                    </a>
                                </li>
                            @endforeach
                        </ul>
                    @endif
                </li>
            @endforeach
        </ul>
    </section>
    @endif

// ...省略...