PHP
laravel
laravel5

Laravel5 チュートリアル ブログもどきを作る(4) ブログ記事を編集する

前回、ブログ記事を新規に作成するところまでできたので、今回は既存の記事を編集できるようにしていきます。

ルーティングの変更(ルートパラメータ)

まず、URL(ルーティング)をどうするかですが、下記のように設定して、ブログ記事の新規作成と編集を区別します。

新規/編集 URL 備考
新規作成 http://{ホスト名}/admin/form 新規作成のときは記事IDを付けない
編集 http://{ホスト名}/admin/form/{記事ID} 指定した記事IDの記事を編集できるようにする

それでは、routes/web.php を開いて、下記のように編集します。

routes/web.php
// URL中の値を取り出したいときは、ルートパラメータを利用する。{}で囲んだ部分を取り出すことができる
// パラメータ名末尾の `?` は、任意パラメータを表すもので、このパラメータはあっても無くても良い、ということになる
Route::get('admin/form/{article_id?}', 'AdminBlogController@form')->name('admin_form');
Route::post('admin/post', 'AdminBlogController@post')->name('admin_post');

ルートパラメータを使いました。詳細は日本語ドキュメントを参照してください。

モデルの編集

app/Models/Article.php を開き、下記2つのメソッドを追加します。

app/Models/Article.php
<?php

namespace App\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    ...中略...

    /**
     * post_date のアクセサ YYYY/MM/DD のフォーマットにする
     *
     * @return string
     */
    public function getPostDateTextAttribute()
    {
        // アクセサを定義しておくと $article->post_date_text という
        // プロパティにアクセスしたときに、このメソッドの返り値が返る
        // 'post_date' は $dates プロパティに設定してあるので、自動的に Carbon インスタンスとなる
        return $this->post_date->format('Y/m/d');
    }

    /**
     * post_date のミューテタ YYYY-MM-DD のフォーマットでセットする
     *
     * @param $value
     */
    public function setPostDateAttribute($value)
    {
        // ミューテタはプロパティに設定しようとする値を受け取って加工する
        // そして加工したものを Eloquent モデルの $attributes プロパティに設定する
        // 例えば $article->post_date = '2018/07/07' とすると、
        // このメソッドが自動的に呼び出され、引数 $value には '2018/07/07' が渡されます
        // 今回はDBに入れることのできる YYYY-MM-DD のフォーマットにする
        $post_date = new Carbon($value);
        $this->attributes['post_date'] = $post_date->format('Y-m-d');
    }
}

アクセサとミューテタは、モデルの属性値を取得するときに値を加工して渡したり(アクセサ)、モデルの属性値を設定するときに、受け取った値を加工して設定したり(ミューテタ)するときに利用します。

post_date は日付ミューテタとして設定したので、このまま利用すると自動的に Carbon インスタンスに変換され、時・分・秒まで表示されてしまうという不都合が生じてしまいます。そこで新たにpost_date_textというプロパティを追加して、ここにアクセスすれば post_date の値を YYYY/MM/DD のフォーマットで返すように、アクセサを使って定義しておきます。

これで、フォームで post_date の値が YYYY/MM/DD の形式で表示されるようになりましたが、この形式のままでは DB に入れようとするとエラーになってしまいます。そこでミューテタを定義して、YYYY-MM-DD というフォーマットに変換して、DB に入れられるようにしておきます。これで、日付として認識できる文字列であればDB(post_dateカラム)にデータを入れられるようになりました。

コントローラーの編集(フォームの表示)

コントローラの form メソッドを改修して、DBから記事データを読み出し、読み出したデータがフォームに表示されるようにします。app/Http/Controllers/AdminBlogController.php を開き、formメソッドを下記のように編集します。

app/Http/Controllers/AdminBlogController.php
    /**
     * ブログ記事入力フォーム
     *
     * @param  int $article_id 記事ID
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function form(int $article_id = null)
    {
        // メソッドの引数に指定すれば、ルートパラメータを取得できる

        // Eloquent モデルはクエリビルダとしても動作するので find メソッドで記事データを取得
        // 返り値は null か App\Models\Article Object
        $article = $this->article->find($article_id);

        // 記事データがあれば toArray メソッドで配列にしておき、フォーマットした post_date を入れる
        $input = [];
        if ($article) {
            $input = $article->toArray();
            $input['post_date'] = $article->post_date_text;
        } else {
            $article_id = null;
        }

        // old ヘルパーを使うと、直前のリクエストのフラッシュデータを取得できる
        // ここではバリデートエラーとなったときに、入力していた値を old ヘルパーで取得する
        // DBから取得した値よりも優先して表示するため、array_merge の第二引数に設定する
        $input = array_merge($input, old());

        // View テンプレートへ値を渡すときは、第二引数に連想配列を設定する
        // View テンプレートでは 連想配列のキー名で値を取り出せる
//        return view('admin_blog.form', ['input' => $input, 'article_id' => $article_id]);
        // compact 関数を使うと便利
        return view('admin_blog.form', compact('input', 'article_id'));
    }

ここで少し対応が必要なのは、既存記事編集時、通常の画面遷移と、バリデートエラーになってフォーム画面に遷移してくる2パターンが存在するので、どちらにも対応できるような作りにしておく必要があります。画面が表示されたときに、予めフォームに入っていて欲しい値は、通常遷移の場合、DBから取り出した値、バリデートエラーで遷移してきた場合は、元々フォームに入力していた値です。
これをどのように対応しているかというと、DBから既存記事の内容を読み出し、バリデートエラーになる直前に入力していた内容も old ヘルパーで読み出し、それをマージすることで対応しています。
Laravel には old ヘルパー以外にも便利なヘルパーがたくさんあります。詳しくは日本語ドキュメントを参照してください。
新規記事作成時、つまり $article_id = null のとき、エラーにならずに処理が完了することも、ソースを追って確認しておいてください。

View テンプレートの編集

resources/views/admin_blog/form.blade.php のフォーム部分を下記のように編集して、取得した記事データを表示します。

resources/views/admin_blog/form.blade.php
            <form method="POST" action="{{ route('admin_post') }}">
                <div class="form-group">
                    <label>日付</label>
                    {{--{{$variable or 'Default'}} は {{isset($variable) ? $variable : 'Default'}} と同じ意味で、変数があるかどうかわからないときに便利です--}}
                    <input class="form-control" name="post_date" size="20" value="{{ $input['post_date'] or null }}" placeholder="日付を入力して下さい。">
                </div>

                <div class="form-group">
                    <label>タイトル</label>
                    <input class="form-control" name="title" value="{{ $input['title'] or null }}" placeholder="タイトルを入力して下さい。">
                </div>

                <div class="form-group">
                    <label>本文</label>
                    <textarea class="form-control" rows="15" name="body" placeholder="本文を入力してください。">{{ $input['body'] or null }}</textarea>
                </div>

                <input type="submit" class="btn btn-primary btn-sm" value="送信">
                {{--article_id があるか無いかで新規作成か既存編集かを区別する--}}
                <input type="hidden" name="article_id" value="{{ $article_id }}">
                {{--CSRFトークンが生成される--}}
                {{ csrf_field() }}
            </form>

記事を新規作成する場合は、$input['title'] のようなデータは無く、$input の配列には title というキーすら存在しません。そこで、{{ $input['title'] or null }} と書くことによって、エラーとなることを防ぎ null を表示させています(nullを表示という表現もなんか変だな)。

ここまでできたら、http://{ホスト名}/admin/form/{任意の記事ID} にアクセスして、フォームに記事のデータが入力された状態で表示されることや、バリデートエラー時に、今まで入力していた値がフォームに残っていることを確認してください。

リクエストクラスの編集

hidden の値ですが article_id がパラメータとして渡ってくるようになったので、バリデーションを追加します。app/Http/Requests/AdminBlogRequest.php を開いて、rules()メソッドとmessages()メソッドを下記のように編集します。

app/Http/Requests/AdminBlogRequest.php
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        // バリデーションルールはここに追加する
        // 項目名 => ルールという形式で、ルールが複数ある場合は '|' で区切る
        return [
            'article_id' => 'integer|nullable',              // 整数・null でもOK
            'post_date'  => 'required|date',                 // 必須・日付
            'title'      => 'required|string|max:255',       // 必須・文字列・最大値(10000文字まで)
            'body'       => 'required|string|max:10000',     // 必須・文字列・最大値(10000文字まで)
        ];
    }

    public function messages()
    {
        // 表示されるバリデートエラーメッセージを編集したい場合は、ここに追加する
        // 項目名.ルール => メッセージという形式で書く
        // プレースホルダーを使うこともできる
        // 下記の例では :max の部分にそれぞれ設定した値(255, 10000)が入る
        return [
            'article_id.integer' => '記事IDは整数でなければなりません',
            'post_date.required' => '日付は必須です',
            'post_date.date'     => '日付は日付形式で入力してください',
            'title.required'     => 'タイトルは必須です',
            'title.string'       => 'タイトルは文字列を入力してください',
            'title.max'          => 'タイトルは:max文字以内で入力してください',
            'body.required'      => '本文は必須です',
            'body.string'        => '本文は文字列を入力してください',
            'body.max'           => '本文は:max文字以内で入力してください',
        ];
    }

追加したのは、rules() messages() 両メソッド共に、article_id の部分です。記事の新規作成のときは article_id を必要としないので、required は設定していません。ただし、このままだと、新規作成のとき article_id の値が null で渡り、null は整数ではないので、バリデーションに引っかかってしまいます。そこで nullable をつけて、null でも OK だよ、という設定にしています。
詳しくは日本語ドキュメントを参照してください。

コントローラーの編集(記事の保存)

再びコントローラークラスを開いて、ブログ記事保存処理の post メソッドを編集し、記事の新規作成・編集のどちらにも対応できるように改修します。app/Http/Controllers/AdminBlogController.php を開いて、post メソッドを下記のように編集します。

app/Http/Controllers/AdminBlogController.php
    /**
     * ブログ記事保存処理
     *
     * @param AdminBlogRequest $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function post(AdminBlogRequest $request)
    {
        // 入力値の取得
        $input = $request->input();

        // array_get ヘルパは配列から指定されたキーの値を取り出すメソッド
        // 指定したキーが存在しない場合のデフォルト値を第三引数に設定できる
        // 指定したキーが存在しなくても、エラーにならずデフォルト値が返るのが便利
        $article_id = array_get($input, 'article_id');

        // Eloquent モデルから利用できる updateOrCreate メソッドは、第一引数の値でDBを検索し
        // レコードが見つかったら第二引数の値でそのレコードを更新、見つからなかったら新規作成します
        // ここでは article_id でレコードを検索し、第二引数の入力値でレコードを更新、または新規作成しています
        $article = $this->article->updateOrCreate(compact('article_id'), $input);

        // フォーム画面にリダイレクト。その際、route メソッドの第二引数にパラメータを指定できる
        return redirect()
            ->route('admin_form', ['article_id' => $article->article_id])
            ->with('status', '記事を保存しました');
    }

今回はDBにデータを保存するために、updateOrCreate()を使いました。詳しくは日本語ドキュメントを参照してください。

ここまでできたら、実際に動作確認をして、既存記事が編集できること、新規記事が作成できることを確認してください。

次回は、ブログ記事を削除する処理を実装していきます。

参考資料

Laravel 日本語ドキュメント

プログラムソース(github)