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

[前回](https://qiita.com/yumgoo17/items/71c2feb5dd9d0138d74e)、ブログ記事を新規に作成するところまでできたので、今回は既存の記事を編集できるようにしていきます。



## ルーティングの変更（ルートパラメータ）
まず、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');
```

ルートパラメータを使いました。詳細は[日本語ドキュメント](https://readouble.com/laravel/5.5/ja/routing.html)を参照してください。



## モデルの編集
`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');
    }
}
```

[アクセサとミューテタ](https://readouble.com/laravel/5.5/ja/eloquent-mutators.html)は、モデルの属性値を取得するときに値を加工して渡したり（アクセサ）、モデルの属性値を設定するときに、受け取った値を加工して設定したり（ミューテタ）するときに利用します。

`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 ヘルパー以外にも便利なヘルパーがたくさんあります。詳しくは[日本語ドキュメント](https://readouble.com/laravel/5.5/ja/helpers.html)を参照してください。
新規記事作成時、つまり `$article_id = null` のとき、エラーにならずに処理が完了することも、ソースを追って確認しておいてください。



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

```html: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 だよ、という設定にしています。
詳しくは[日本語ドキュメント](https://readouble.com/laravel/5.5/ja/validation.html)を参照してください。



## コントローラーの編集（記事の保存）
再びコントローラークラスを開いて、ブログ記事保存処理の 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()`を使いました。詳しくは[日本語ドキュメント](https://readouble.com/laravel/5.5/ja/eloquent.html)を参照してください。

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

[次回は、ブログ記事を削除する処理を実装していきます。](https://qiita.com/yumgoo17/items/e51823f1f9f49db82867)




## 参考資料
### Laravel 日本語ドキュメント
* [ルーティング](https://readouble.com/laravel/5.5/ja/routing.html)
* [ミューテタ](https://readouble.com/laravel/5.5/ja/eloquent-mutators.html)
* [Eloquentの準備](https://readouble.com/laravel/5.5/ja/eloquent.html)
* [コントローラ](https://readouble.com/laravel/5.5/ja/controllers.html)
* [ヘルパ](https://readouble.com/laravel/5.5/ja/helpers.html)
* [Bladeテンプレート](https://readouble.com/laravel/5.5/ja/blade.html)
* [バリデーション](https://readouble.com/laravel/5.5/ja/validation.html)

### プログラムソース（github）
* [github yumgoo17/like_blog](https://github.com/yumgoo17/like_blog)