67
82

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Laravelチュートリアル - 汎用業務Webアプリを作る (2/4) テーブルとCRUD画面

Last updated at Posted at 2019-05-04

はじめに

Django経験者が、既存DjangoアプリをLaravelで再現するチュートリアル形式の記事を書くことでLaravelを学びます。

題材とさせていただいたDjangoアプリはこちらです。

Djangoチュートリアル - 汎用業務Webアプリを最速で作る - Qiita

チュートリアル全体の構成

  1. Laradockで環境を構築する

  2. テーブルとCRUD画面を作る(本記事

  3. 検索画面とページネーションを作る

  4. 認証機能を作る(作成予定)

CRUD画面の構成

機能 HTTP動詞 URI アクション ビューファイル
一覧画面 GET / index index.blade.php
登録画面 GET /items/create create create.blade.php
登録処理 POST /items/ store -
詳細画面 GET /items/{id} show create.blade.php
編集画面 GET /items/{id}/edit edit edit.blade.php
編集処理 PATCH /items/{id} update -
削除確認画面 GET /items/{item}/delete delete delete.blade.php
削除処理 DELETE /items/{item} destroy -

一覧画面はこのような内容です。

laravel_index0.png

ソースコード

環境

  • macOS High Sierra 10.13.6
  • php 7.2.16
  • Laravel 5.5.45
  • PostgreSQL 9.6.2

2.1. テーブルの作成

前回の記事から引き続き、コンテナの中で作業を行います。
もし、コンテナから出てしまっていたら、再び入り直してください。

laradock $ docker-compose exec workspace bash

2.1.1. マイグレーションファイルの作成

今回作成するテーブル名は、itemsとします。

まず、Laravelプロジェクトのディレクトリsrcにて、itemsテーブルを作成するためのマイグレーションファイル作成コマンドを実行します。

/var/www# $ cd src
/var/www/src# $ php artisan make:migration create_items_table --create=items
  • create_items_tableの部分が、マイグレーションファイルのファイル名の一部となります。

  • --create=itemsの部分にて、itemsという名前のテーブルを新規作成することを意味します。

実行すると以下の結果が表示されます。

Created Migration: Created Migration:yyyy_mm_dd_hhmiss_create_items_table

2.1.2. マイグレーションファイルの編集

作成されたマイグレーションファイルは、database/migrations/に存在します。

続いて、このファイルに以下のようにテーブルとして持たせる項目を追記します。

src/databese/migrations/yyyy_mm_dd_hhmiss_create_items_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateItemsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name'); // 追加
            $table->integer('age')->nullable(); // 追加
            $table->integer('sex')->nullable(); // 追加(1:男性, 2:女性, 3:指定なし)
            $table->text('memo')->nullable(); // 追加
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('items');
    }
}

2.1.3. マイグレーションの実行

マイグレーションファイルの編集が終わったら、マイグレーションを実行し、テーブルを作成します。

/var/www/src# php artisan migrate

実行に成功すると、以下が表示されます。

Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
Migrating: yyyy_mm_dd_hhmiss_create_items_table
Migrated:  yyyy_mm_dd_hhmiss_create_items_table

今回初めてのマイグレーション実行であったため、Laravel標準のusersテーブルやpassword_resetsテーブルも作成されます。

2.1.4. 作成されたテーブルの確認

データベース(PostgreSQL)に接続し、テーブルが作成されていることを確認します。

まず、PostgreSQLに接続します。

/var/www/src# psql -h postgres -d sample -U default
Password for user default: 

次に、itemsテーブルの構成を確認します。

sample=# \d items

以下の通り、itemsテーブルが作成されていることを確認できました。

                                       Table "public.items"
   Column   |              Type              |                     Modifiers                      
------------+--------------------------------+----------------------------------------------------
 id         | integer                        | not null default nextval('items_id_seq'::regclass)
 name       | character varying(255)         | not null
 age        | integer                        | 
 sex        | integer                        | 
 memo       | text                           | 
 created_at | timestamp(0) without time zone | 
 updated_at | timestamp(0) without time zone | 
Indexes:
    "items_pkey" PRIMARY KEY, btree (id)
  • created_atおよびupdated_atの2つのカラムは、マイグレーションファイル内の$table->timestamps();によって生成されたものとなります。

itemsテーブルの確認を終えたので、PostgreSQLへの接続を終了します。

quickstart=# \q

続いて、モデルの作成を行います。

2.2. モデル

LaravelではEloquentモデルを通して、テーブル中のデータを操作します。

Eloquent ORMはLaravelに含まれている、美しくシンプルなアクティブレコードによるデーター操作の実装です。それぞれのデータベーステーブルは関連する「モデル」と結びついています。モデルによりテーブル中のデータをクエリできますし、さらに新しいレコードを追加することもできます。
利用を開始するには、まずEloquentモデルを作成しましょう。

2.2.1. モデルの作成

以下コマンドでitemsテーブルに対応するItemモデルが作成されます。

/var/www/src# php artisan make:model Item

2.2.2. 作成されたモデルの確認

作成されたモデルは、app/に存在します。

/src/app/Item.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Item extends Model
{
    //
}
  • $fillable(詳細後述)の指定は後ほど行います。

テーブルとモデルの作成まで完了したので、CRUD画面を順次作成していきます。

2.3. 一覧画面の作成

最初に作る画面は、itemsテーブルのデータを一覧表示する画面にします。

画面のイメージは以下になります。

laravel_index0.png

2.3.1. ルート定義

まず、routes/web.phpを編集し、一覧画面へのルートを定義します。

src/routes/web.php
<?php

Route::get('/', 'ItemController@index');
  • 一覧画面にはGETでリクエストするので、getとします。
  • 第一引数にURLを指定します。ここにアクセスすると第二引数の関数が実行されます。

2.3.2. コントローラの新規作成

次に、先ほどのルート定義で登場したItemControllerを以下コマンドで新規作成します。

/var/www/src# php artisan make:controller ItemController

2.3.3. コントローラの編集

続いて、/app/Http/Controllers/に新規作成されたItemController.phpを以下の通り編集します。

src/app/Http/Controllers/ItemController.php
<?php

namespace App\Http\Controllers;

use App\Item;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class ItemController extends Controller
{
    /**
     * 一覧表示
     *
     * @param Request $request
     * @return Response
     */
    public function index(Request $request){
      $items = Item::orderBy('created_at', 'desc')->get();

      return view('items.index', [
        'items' => $items
      ]);
    }
}
  • view()について
    • 第一引数にビューを指定しています。'items.index'と指定することで、後ほど作成するresources/views/items/index.blade.phpがビューとして使用されます。
    • 第二引数にビューに渡すデータを連想配列で指定します。ここではItemモデルから登録日の降順で取り出したデータを渡しています。

2.3.4. ビューの作成

ビューは/resources/views/に配置します。

2.3.4.1. レイアウト(親ビュー)の新規作成 〜 @yieldの利用 〜

一覧画面本体のビューを作成する前に、全画面で共通する部分を持った、レイアウトとなるビューを新規作成します。

レイアウトのビューファイルはbase.blade.phpという名前にすることにします。

src/resources/views/base.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <title>アプリケーション名</title>
  <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
        crossorigin="anonymous">
</head>

<body>
  <nav class="navbar navbar-expand-lg navbar-light bg-light">
    <a class="navbar-brand" href="#">アプリケーション名</a>
    <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#Navber" aria-controls="Navber" aria-expanded="false"
            aria-label="ナビゲーションの切替">
      <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="Navber">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item">
          <!-- TODO:リンク先追加 -->
          <a class="nav-link" href="">管理サイト</a>
        </li>
        <li class="nav-item">
          <!-- TODO:リンク先追加 -->
          <a class="nav-link" href="">ログアウト</a>
        </li>
      </ul>
    </div>
  </nav>

@yield('content')

  <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
          crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
</body>

</html>
  • 管理サイトログアウトのリンク先は別記事で指定することとし、いったんのところhref=""としておきます。

  • @yield('content')の部分に、本記事内で今後作成する一覧画面や登録画面といった子画面のビューが展開されます。

2.3.4.2. 一覧画面(子ビュー)の新規作成 〜 @extendsの利用 〜

続いて、一覧画面のビューとして、index.blade.phpを新規作成します。
こちらは、先ほど作成した共通レイアウトから見て、子のビューとなります。

なお、今回の記事では一覧画面やその他の子ビューの保存場所として、itemsディレクトリを新規に作成することにしました。

src/resources/views/
├── base.blade.php
└── items
    └── index.blade.php 

一覧画面のビューファイルは以下になります。

src/resources/views/items/index.blade.php
@extends('base')
@section('content')
  <div class="container">
    <div id="myModal" class="modal fade" tabindex="-1" role="dialog">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">検索条件</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="閉じる">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <form id="filter" method="get">
            <div class="modal-body">
              <!-- TODO:検索フォーム -->
            </div>
          </form>
          <div class="modal-footer">
            <a class="btn btn-outline-secondary" data-dismiss="modal">戻る</a>
            <button type="submit" class="btn btn-outline-secondary" form="filter">検索</button>
          </div>
        </div>
      </div>
    </div>
    <div class="row">
      <div class="col-12">
        <a class="btn btn-secondary filtered" style="visibility:hidden" href="/?page=1">検索を解除</a>
        <div class="float-right">
          <!-- TODO リンク先追加 -->
          <a class="btn btn-outline-secondary" href="">新規</a>
          <a class="btn btn-outline-secondary" data-toggle="modal" data-target="#myModal" href="#">検索</a>
        </div>
      </div>
    </div>

    <div class="row" >
      <div class="col-12">
        <!-- TODO:ページネーション -->
      </div>
    </div>

    <div class="row">
      <div class="col-12">
        <ul class="list-group">
          @empty(count($items))
            <li class="list-group-item">
              対象のデータがありません
            </li>
          @endempty
          @foreach($items as $item)
            <li class="list-group-item">
              <div class="row">
                <div class="col-3">
                  <p>名前</p>
                </div>
                <div class="col-9">
                  <p>{{ $item->name }}</p>
                </div>
              </div>
              <div class="row">
                <div class="col-3">
                  <p>登録日</p>
                </div>
                <div class="col-9">
                  <p>{{ $item->created_at }}</p>
                </div>
              </div>
              <div class="row">
                <div class="col-12">
                  <div class="float-right">
                    <!-- TODO:リンク先追加 -->
                    <a class="btn btn-outline-secondary " href="">詳細</a>
                    <!-- TODO:リンク先追加 -->
                    <a class="btn btn-outline-secondary " href="">編集</a>
                    <!-- TODO:リンク先追加 -->
                    <a class="btn btn-outline-secondary " href="">削除</a>
                  </div>
                </div>
              </div>
            </li>
          @endforeach
        </ul>
      </div>
    </div>
    <div class="row" >
      <div class="col-12">
        <div class="float-right">
          <!-- TODO:リンク先追加 -->
          <a class="btn btn-outline-secondary" href="">新規</a>
          <a class="btn btn-outline-secondary" data-toggle="modal" data-target="#myModal" href="#">検索</a>
        </div>
      </div>
    </div>
  </div>
@endsection
  • @extends('base')と指定することで、共通レイアウトとして作成したbase.blade.phpがベースとして呼び出されます。

  • @section('content')@endsectionの部分が、base.blade.phpから見た@yield('content')の部分に対応します。

  • @empty(count($items))@endemptyで、コントローラから渡された$itemsが0件であるときの表示を行っています。

  • @foreach($items as $item)@endforで、コントローラから渡された$itemsからデータを取り出し、明細として順次表示しています。

  • {{ $item->name }}のように{{}}で囲われた部分で、コントローラから渡されたデータを出力できます。

ブラウザでlocalhostにアクセスすると、以下画面が表示されます。

laravel_index1.png

itemsテーブルにはまだデータが1件も登録されていないので、@empty(count($items))@endemptyの部分が表示され、@foreach($items as $item)@endforの部分は何も表示されません。

続いて、登録画面を作成します。

2.4. 登録画面の作成

itemsテーブルにデータを新規登録するための画面を作成します。

2.4.1. ルート定義

まず、登録画面へのルートを定義します。

URLは、/items/createとしました。

src/routes/web.php
Route::get('/', 'ItemController@index');
Route::get('/items/create', 'ItemController@create'); // 追加

2.4.2. コントローラーの編集

次に、ItemControllerに、登録画面を表示するためのcreateメソッドを追加します。

src/app/Http/Controllers/ItemController.php
//略
class ItemController extends Controller
{
//略
    /**
     * 登録画面の表示
     *
     * @param  Request $request
     * @return Response
     */
    public function create(Request $request){
        return view('items.create');
    }
}
  • view()の第一引数に'items.create'と指定することで、後ほど作成するresources/views/items/create.blade.phpがビューとして使用されます。

2.4.3. ビューの新規作成 〜 csrf_field()の利用 〜

続いて、登録画面のビューとして、create.blade.phpを新規作成します。

src/resources/views/items/create.blade.php
@extends('base')
@section('content')
  <div class="container">
    <div class="row">
      <div class="col-12">
        <h2 class="text-center">データ入力</h2>
      </div>
    </div>
    <div class="row">
      <div class="col-12">
        <div class="float-right">
          <!-- TODO: route()を使う -->
          <a class="btn btn-outline-secondary" href="/">戻る</a>
          <button class="btn btn-outline-secondary save" type="submit" form="myform">保存</button>
        </div>
      </div>
    </div>
    <div class="row">
      <div class="col-12">
        <!-- TODO: actionの指定 -->
        <form method="POST" action="" id="myform">
          @if ($errors->any())
            <div class="alert alert-danger">
              <ul>
                @foreach( $errors->all() as $error)
                  <li>{{ $error }}</li>
                @endforeach
              </ul>
            </div>
          @endif
          {{ csrf_field() }}
          <div class="form-group">
            <label>名前</label>
            <input type="text" name="name" placeholder="記入例:山田 太郎" class="form-control">
          </div>
          <div class="form-group">
            <label>年齢</label>
            <input type="number" name="age" class="form-control">
          </div>
          <div class="form-group">
            <label>性別</label>
            <div class="form-check">
              <input type="radio" name="sex" id="sex-male" value="1" class="form-check-input">
              <label class="form-check-label" for="sex-male">男性</label>
            </div>
            <div class="form-check">
              <input type="radio" name="sex" id="sex-female" value="2" class="form-check-input">
              <label class="form-check-label" for="sex-female">女性</label>
            </div>
            <div class="form-check">
              <input type="radio" name="sex" id="sex-unspecified" value="3" class="form-check-input">
              <label class="form-check-label" for="sex-unspecified">指定なし</label>
            </div>
          </div>
          <div class="form-group">
            <label>備考</label>
            <textarea name="memo" class="form-control"></textarea>
          </div>
        </form>
      </div>
    </div>
    <div class="row">
      <div class="col-12">
        <div class="float-right">
          <!-- TODO: route()を使う -->
          <a class="btn btn-outline-secondary" href="/">戻る</a>
          <button class="btn btn-outline-secondary save" type="submit" form="myform">保存</button>
        </div>
      </div>
    </div>
  </div>
@endsection
  • @if ($errors->any())@endifの部分で、登録画面からのリクエストでバリデーションエラーがあった場合のエラーメッセージを表示します。

  • <form>タグ内に{{ csrf_field() }}を指定することでCSRF対策となるトークン(下記)が埋め込まれます。

<input type="hidden" name="_token" value="IIEeVyeOLWFLHpkNaOO59GeVKD5CTG7nrrEkwMdZ">

ブラウザでlocalhost/items/createにアクセスすると、以下画面が表示されます。

laravel_create1.png

2.4.4. 一覧画面から登録画面へのリンクを追加する 〜 action()の利用 〜

一覧画面の新規をクリックしたら、登録画面へ遷移するようにします。

/src/resources/views/items/index.blade.php
//略
<a class="btn btn-outline-secondary" href="{{ action('ItemController@create') }}">新規</a>
//略
  • action()コントローラ名@アクション名を指定することで、ルート定義(下記)に沿って、紐付くURLを導出できます。
/src/routes/web.php
//略
Route::get('items/create', 'ItemController@create');
  • 他にはroute()を使う方法もあります(後述)。

続いて、登録画面で保存をクリックした後の処理を作成します。

2.5. 登録処理の作成

ここからは、登録画面で保存をクリックした後のPOSTリクエストに対する処理を作成します。

2.5.1. ルート定義

まず、登録処理へのルートを定義します。

/src/routes/web.php
<?php

Route::get('/', 'ItemController@index');
Route::get('/items/create', 'ItemController@create');
Route::post('/items', 'ItemController@store'); //追加

2.5.2. フォームリクエストによるバリデーション 〜 rules()の利用 〜

次に、登録画面に入力された各項目のバリデーションを行うための、フォームリクエストを作成します。

下記コマンドで、ItemRequestを新規作成します。

/var/www/src# php artisan make:request ItemRequest

app/Http/Requests/に新規作成されたItemRequest.phpを以下の通り編集します。

src/app/Http/Requests/ItemRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class ItemRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|max:30',
            'age' => '',
            'sex' => 'required|integer',
            'memo' => 'max:100'
        ];
    }
}
  • authorize()メソッドは、認証されたユーザーがitemsテーブルにデータを登録することが認可されているかどうか判定するために使用します。現時点ではこのWebアプリに認証や認可の機能は持たせていないので、必ずtrueを返すようにしています。

  • rules()メソッドに、バリデーションのルールを記述します。

    • maxで最大文字数を指定できます。数値の場合は最大桁数となります(最大値ではありません)。
    • integerでは、マイナス値も入力可能です。

なお、agesexに記述したバリデーションは厳密ではなく改善の余地があるかと思いますが、いったんこのままチュートリアルを進めます。

2.5.3. コントローラーの編集

続いて、ItemControllerに、登録処理を行うためのstoreメソッドを追加します。

src/app/Http/Controllers/ItemController.php
<?php

namespace App\Http\Controllers;

use App\Item;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Http\Requests\ItemRequest; //忘れずに追加すること

class ItemController extends Controller
{
// 略
    /**
     * 登録処理
     *
     * @param  ItemRequest  $request
     * @return Response
    */
    public function store(ItemRequest $request){
      $item = new Item();
      $item->name = $request->name;
      $item->age = $request->age;
      $item->sex = $request->sex;
      $item->memo = $request->memo;
      $item->save();
      return redirect()->action('ItemController@index');
    }
}
  • return redirect()->action('ItemController@index');とすることで、登録処理が完了したら、ItemControlle@indexに紐付くURL、つまり/にリダイレクトされます。

2.5.4. 登録画面から登録処理へのリンクを名前付きルートで指定する 〜 name()route()

登録画面で保存をクリックした後の遷移先を指定します。

src/resources/views/items/create.blade.php
//略
<form method="POST" action="{{ action('ItemController@store') }}" id="myform">
//略

上記のように、これまで通りaction()関数を使っても良いのですが、ルーティングに名前を付けて、その名前を指定する方法もあります。

まず、下記のようにname()メソッドで、ルーティングに名前を付けます。

/src/routes/web.php
//略
Route::post('/items', 'ItemController@store')->name('store');

その上で、route()関数に先ほど付けた名前を渡すと、そのルーティングに紐付くURL(ここでは/items/)が展開されます。

src/resources/views/items/create.blade.php
//略
<form method="POST" action="{{ route('store') }}" id="myform">
//略

これで、登録画面からの保存が可能となり、データ保存後は一覧画面へ遷移するようになりました。

2.5.5. 画面サンプル

ここまで作成した登録画面と一覧画面は以下のようになります。

2.5.5.1. 登録画面サンプル

laravel_create2.png

2.5.5.2. 登録処理完了後の一覧画面サンプル

laravel_index2.png

2.5.6. バリデーションエラーメッセージの表示

登録画面の入力内容に対してバリデーションエラーが発生すると、登録画面へリダイレクトされ、登録画面のビューの下記処理により、エラーメッセージが表示されます。

/src/resources/views/items/create.blade.php
//略
@if ($errors->any())
  <div class="alert alert-danger">
    <ul>
      @foreach( $errors->all() as $error)
        <li>{{ $error }}</li>
          @endforeach
    </ul>
  </div>
@endif
//略

例えば、登録画面で項目に何も入力せずに、保存をクリックすると下記の表示になります。

laravel_create3.png

デフォルトでは以下の通り英語ですが、次はこれを日本語化します。

  • The name field is required.
  • The sex field is required.

2.5.7. バリデーションエラーメッセージの日本語化

2.5.7.1. 項目名の日本語化

まず、登録画面からのPOSTリクエストの各項目に対して、日本語の名前を付けます。

それには、ItemRequestattrebutes()メソッドで下記のように指定します。

src/app/Http/Requests/ItemRequest.php
//略
class ItemRequest extends FormRequest
{
//略
    /**
     * 項目名
     *
     * @return array
     */
    public function attributes()
    {
        return [
            'name' => '名前',
            'age' => '年齢',
            'sex' => '性別',
            'memo' => '備考'
        ];
    }
}

これにより、先ほどのバリデーションエラーメッセージは以下のように変化します。

  • The 氏名 field is required.
  • The 性別 field is required.

項目名は日本語化されたものの、定型文は英語なので次はこれも日本語化します。

2.5.7.2. 定型文の日本語化

次に定型文を英語化していきます。

まず、/config/app.phpを以下の通り編集します。

src/config/app.php
<php?

return [
//略
    'locale' => 'ja',
//略
];

その上で、/resources/lang配下にjaディレクトリを作成し、validation.phpを作成します。

/resources/
├── en
│   ├── auth.php
│   ├── pagination.php
│   ├── passwords.php
│   └── validation.php
└── ja
    └── validation.php # 新規作成

これにより、バリデーションエラーメッセージの定型文は、ja/validation.phpが使用されるようになります。

ja/validation.phpの中身についてですが、
Laravel公式の日本語サイトには、Laravel標準の/en/validation.php
を日本語化したものが掲載されていますので、その内容の通りとします。

validation.php言語ファイル 5.5 Laravel

/src/resources/lang/ja/validation.php
<?php

return [

  /*
  |--------------------------------------------------------------------------
  | バリデーション言語行
  |--------------------------------------------------------------------------
  |
  | 以下の言語行はバリデタークラスにより使用されるデフォルトのエラー
  | メッセージです。サイズルールのようにいくつかのバリデーションを
  | 持っているものもあります。メッセージはご自由に調整してください。
  |
  */

  'accepted'             => ':attributeを承認してください。',
  'active_url'           => ':attributeが有効なURLではありません。',
  'after'                => ':attributeには、:dateより後の日付を指定してください。',
//略

以上により、バリデーションエラーメッセージは定型文も含め、日本語化されました。

laravel_create4.png

2.6. 詳細画面

ここからは、itemsテーブルのデータ1件を詳細表示するための画面を作成します。

laravel_show1.png

2.6.1. 詳細画面のルート定義と一覧画面・登録画面のルート名追加 〜 where()の利用 〜

まず、詳細画面へのルートを定義します。

併せて、今後各ビューからのリンクをroute()でも指定できるよう、既存の一覧画面と登録画面のルートにも、name()にてルート名を追加することにします。

src/routes/web.php
//略
<?php

Route::get('/', 'ItemController@index')->name('index'); 
 //ルート名追加
Route::get('/items/create', 'ItemController@create')->name('create'); //ルート名追加
Route::post('/items', 'ItemController@store')->name('store');
Route::get('/items/{id}', 'ItemController@show')->name('show')->where('id', '[0-9]+'); //新規追加
  • 詳細画面のURLは、/items/{id}としました。

  • {id}の部分で、itemsテーブルのレコードのidを指定します。

  • メソッド名やルート名はshowとしました。

  • whereメソッドで、idの値を正規表現にて制約をかけることができます。

2.6.2. コントローラの編集 〜 findOrFail()の利用 〜

次に、ItemControllerに、詳細画面を表示するためのshowメソッドを追加します。

src/app/Http/Controllers/ItemController.php
//略
class ItemController extends Controller
{
//略
    /**
     * 詳細画面の表示
     *
     * @param  string  $id
     * @return Response
     *
     */
    public function show(string $id){
      $item = Item::findOrFail($id);

      return view('items.show')->with('item', $item);
    }
//略
}
  • findOrFailメソッドに、取得したいレコードのidカラムの値を渡すと、そのレコードがテーブルから取得されます。

  • view()の第一引数に'items.show'と指定することで、後ほど作成するresources/views/items/show.blade.phpがビューとして使用されます。

  • ビューにデータを渡す方法としては、view()の第二引数に連想配列で渡す方法(下記)以外に、上記のようにwith()を使う方法もあります。

return view('items.show', ['item' => $item]);

2.6.3. ビューの新規作成

続いて、詳細画面のビューとして、show.blade.phpを新規作成します。

src/resources/views/items/show.blade.php
@extends('base')
@section('content')
  <div class="container">
    <h2 class="text-center">詳細表示</h2>
    <div class="row">
      <div class="col-12">
        <a class="btn btn-outline-secondary float-right" href="{{ route('index') }}">戻る</a>
      </div>
    </div>
    <div class="row">
      <div class="col-3">
        <p>名前</p>
      </div>
      <div class="col-9">
        <p>{{ $item->name }}</p>
      </div>
    </div>
    <div class="row">
      <div class="col-3">
        <p>年齢</p>
      </div>
      <div class="col-9">
        <p>{{ $item->age }}</p>
      </div>
    </div>
    <div class="row">
      <div class="col-3">
        <p>性別</p>
      </div>
      <div class="col-9">
        @if($item->sex === 1)
          <p>男性</p>
        @elseif($item->sex === 2)
          <p>女性</p>
        @elseif($item->sex === 3)
          <p>指定なし</p>
        @else
          <p></p>
        @endif
      </div>
    </div>
    <div class="row">
      <div class="col-3">
        <p>備考</p>
      </div>
      <div class="col-9">
        <p>{{ $item->memo }}</p>
      </div>
    </div>
    <div class="row">
      <div class="col-3">
        <p>登録日</p>
      </div>
      <div class="col-9">
        <p>{{ $item->created_at }}</p>
      </div>
    </div>
    <div class="row">
      <div class="col-12">
        <a class="btn btn-outline-secondary float-right" href="{{ route('index') }}">戻る</a>
      </div>
    </div>
  </div>
@endsection
  • @if($item->sex === 1)@endifの部分で、sexカラムの値に応じて'男性'などの文字列を表示するようにしています。今後、sexカラムを扱う画面が増えた場合に、それらのビューに毎回これと同等のロジックを埋め込むのは保守性が悪いので改善の余地があるかと思いますが、いったんこの内容のままチュートリアルを進めます。

2.6.4. 一覧画面から詳細画面へのリンクを追加する 〜 パラメータありのroute()

一覧画面の詳細をクリックしたら、詳細画面へ遷移するようにします。

//略
<a class="btn btn-outline-secondary " href="{{ route('show', ['id' => $item->id]) }}">詳細</a>
//略
  • 一覧画面のURLはitems/{id}なので、{id}部分を指定する必要があります。route()の第二引数で、そうしたパラメータ部分を連想配列で指定します。

2.7. 編集画面と編集処理

ここからは、itemsテーブルのデータを更新するための編集画面と編集処理を作成します。

laravel_edit1.png

2.7.1. ルート定義

まず、編集画面へのルートと、編集画面からのリクエストを処理する編集処理へのルートを定義します。

src/routes/web.php
<?php

Route::get('/', 'ItemController@index')->name('index');
Route::get('/items/create', 'ItemController@create')->name('create');
Route::post('/items', 'ItemController@store')->name('store');
Route::get('/items/{id}', 'ItemController@show')->name('show')->where('id', '[0-9]+');
Route::get('/items/{id}/edit', 'ItemController@edit')->name('edit')->where('id', '[0-9]+'); //追加
Route::patch('/items/{id}', 'ItemController@update')->name('update')->where('id', '[0-9]+'); //追加
  • 今回の編集処理では、編集画面で変更された項目についてのみ、itemsテーブルのレコードのカラムを更新するので、HTTPのメソッドとして'部分更新'を表すpatchを使用するようにしています。

2.7.2. コントローラの編集とItemモデルへの$fillableの追加

次に、ItemControllerに、編集画面を表示するためのeditメソッドと、編集処理を行うためのupdateメソッドを追加します。

src/app/Http/Controllers/ItemController.php
//略
class ItemController extends Controller
{
//略
    /**
     * 編集画面の表示
     *
     * @param  String $id
     * @return Response
     *
     */
    public function edit(String $id){
        return view('items.edit')->with('item', Item::findOrFail($id));
    }
    /**
     * 編集処理
     *
     * @param ItemRequest $request
     * @param string $id
     * @return Response
     *
     */
    public function update(ItemRequest $request, string $id){
        $item = Item::findOrFail($id);
        $item->fill($request->all())->save();
        return redirect()->route('index');
    }
}
  • updateメソッド内
    • fill($request->all())とすることで、更新するカラム名をひとつひとつ指定しなくて済んでいます。
    • ただし、fill()を使う場合は、更新しても良いカラム名をモデルの$fillableで指定しておく必要があります。

Itemモデルに以下の通り$fillableを追加します。

/src/app/Item.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Item extends Model
{
    protected $fillable = [
        'name',
        'age',
        'sex',
        'memo'
    ];
}

2.7.3. ビューの新規作成 〜 method_field('PATCH')の利用 〜

続いて、編集画面のビューとして、edit.blade.phpを新規作成します。

src/resources/views/items/edit.blade.php
@extends('base')
@section('content')
  <div class="container">
    <div class="row">
      <div class="col-12">
        <h2 class="text-center">データ入力</h2>
      </div>
    </div>
    <div class="row">
      <div class="col-12">
        <div class="float-right">
          <a class="btn btn-outline-secondary" href="{{ route('index') }}">戻る</a>
          <button class="btn btn-outline-secondary save" type="submit" form="myform">保存</button>
        </div>
      </div>
    </div>
    <div class="row">
      <div class="col-12">
        <form method="POST" action="{{ route('update', ['id' => $item->id ]) }}" id="myform">
          @if ($errors->any())
            <div class="alert alert-danger">
              <ul>
                @foreach( $errors->all() as $error)
                  <li>{{ $error }}</li>
                @endforeach
              </ul>
            </div>
          @endif
          {{ csrf_field() }}
          {{ method_field('PATCH') }}
          <div class="form-group">
            <label>名前</label>
            <input type="text" name="name" value="{{ $item->name }}"placeholder="記入例:山田 太郎" class="form-control">
          </div>
          <div class="form-group">
            <label>年齢</label>
            <input type="number" name="age" value="{{ $item->age }}" class="form-control">
          </div>
          <div class="form-group">
            <label>性別</label>
            <div class="form-check">
              <input type="radio" name="sex" id="sex-male" value="1" @if($item->sex === 1) checked @endif class="form-check-input">
              <label class="form-check-label" for="sex-male">男性</label>
            </div>
            <div class="form-check">
              <input type="radio" name="sex" id="sex-female" value="2" @if($item->sex === 2) checked @endif class="form-check-input">
              <label class="form-check-label" for="sex-female">女性</label>
            </div>
            <div class="form-check">
              <input type="radio" name="sex" id="sex-unspecified" value="3" @if($item->sex === 3) checked @endif class="form-check-input">
              <label class="form-check-label" for="sex-unspecified">指定なし</label>
            </div>
          </div>
          <div class="form-group">
            <label>備考</label>
            <textarea name="memo" class="form-control">{{ $item->memo }}</textarea>
          </div>
        </form>
      </div>
    </div>
    <div class="row">
      <div class="col-12">
        <div class="float-right">
          <a class="btn btn-outline-secondary" href="{{ route('index') }}">戻る</a>
          <button class="btn btn-outline-secondary save" type="submit" form="myform">保存</button>
        </div>
      </div>
    </div>
  </div>
@endsection
  • <form>タグ内に{{ method_field('PATCH') }}を指定することで、<input type="hidden" name="_method" value="PATCH"> が埋め込まれます。これにより、LaravelではPOSTであるリクエストをPATCHとして処理します。

HTMLフォームはPUT、PATCH、DELETEアクションをサポートしていません。ですから、HTMLフォームから呼ばれるPUT、PATCH、DELETEルートを定義する時、フォームに_method隠しフィールドを追加する必要があります。_methodフィールドとして送られた値は、HTTPリクエストメソッドとして使用されます。

Laravel 5.5 ルーティング - 擬似フォームメソッドより抜粋

  • 画面の見た目としては、create.blade.php(登録画面)と全く一緒であるため、ビューファイルを共通化できそうですが、以下の違いをビューファイル側で吸収する必要があるため、今回は別ビューファイルとしました。
    • 保存をクリックした後の遷移先の違い(/items/{id}/items)
    • 擬似フォームメソッドの有無({{ method_field('PATCH') }}の有無)
    • コントローラから渡されるデータの有無($itemsの有無)

2.7.4. 一覧画面から編集画面へのリンクを追加する

一覧画面の編集をクリックしたら、編集画面へ遷移するようにします。

src/resources/views/items/index.blade.php
//略
<a class="btn btn-outline-secondary " href="{{ route('edit', ['id' => $item->id]) }}">編集</a>
//略

2.8. 削除確認画面と削除処理

ここからは、itemsテーブルのデータを削除する際の削除確認画面と削除処理を作成します。

2.8.1. ルート定義とコントローラ 〜 Route Model Bindingの利用 〜

まず、削除確認画面へのルートと、削除確認画面からのリクエストを処理する削除処理へのルートを定義します。

src/routes/web.php
<?php

Route::get('/', 'ItemController@index')->name('index');
Route::get('/items/create', 'ItemController@create')->name('create');
Route::post('/items', 'ItemController@store')->name('store');
Route::get('/items/{id}', 'ItemController@show')->name('show')->where('id', '[0-9]+');
Route::get('/items/{id}/edit', 'ItemController@edit')->name('edit')->where('id', '[0-9]+');
Route::patch('/items/{id}', 'ItemController@update')->name('update')->where('id', '[0-9]+');
Route::get('/items/{item}/delete', 'ItemController@delete')->name('delete')->where('item', '[0-9]+'); //追加
Route::delete('/items/{item}', 'ItemController@destroy')->name('destroy')->where('item', '[0-9]+');  //追加

次に、ItemControllerに、削除確認画面を表示するためのdeleteメソッドと、削除処理を行うためのdestroyメソッドを追加します。

src/app/Http/Controllers/ItemController.php
//略
class ItemController extends Controller
{
//略
    /**
     * 削除確認画面の表示
     *
     * @param  Item $item
     * @return Response
     *
     */
    public function delete(Item $item){
        return view('items.delete')->with('item', $item);
    }
    /**
     * 削除処理
     *
     * @param Item $item
     * @return Response
     * @throws \Exception
     */
    public function destroy(Item $item){
        $item->delete();
        return redirect()->route('index');
    }
}

今回のルート定義では、コントローラへ渡すパラメータとして{id}ではなく{item}といったようにモデルの名称(小文字、単数形)にしました。

加えて、コントローラ側の各メソッドでは、(Item $item)といったようにそのモデルを型宣言しています。

すると、$itemには、パラメータに指定した値をidカラムとするレコードが代入されます。

こうした処理の流れをRoute Model Bindingと呼びます。

これにより、$item = Item::findOrFail($id);といった処理を記述する必要が無くなります。

2.8.2. ビューの新規作成

続いて、削除確認画面のビューとして、delete.blade.phpを新規作成します。

2.8.2.1. データ内容表示部分を詳細画面と共通化する

削除確認画面は以下のイメージとなります。

laravel_delete1.png

名前〜更新日までの表示内容は、詳細画面(show.blade.php)と全く同じです。

そこで、その部分を別ファイルとして切り出し、削除確認画面から呼び出すようにします。同様に詳細画面からも呼び出すように変更します。

2.8.2.2. 共通部分の新規作成

まず、共通の部分を(card.blade.php)として作成します。

src/resources/views/items/card.blade.php
<div class="row">
  <div class="col-3">
    <p>名前</p>
  </div>
  <div class="col-9">
    <p>{{ $item->name }}</p>
  </div>
</div>
<div class="row">
  <div class="col-3">
    <p>年齢</p>
  </div>
  <div class="col-9">
    <p>{{ $item->age }}</p>
  </div>
</div>
<div class="row">
  <div class="col-3">
    <p>性別</p>
  </div>
  <div class="col-9">
    @if($item->sex === 1)
      <p>男性</p>
    @elseif($item->sex === 2)
      <p>女性</p>
    @elseif($item->sex === 3)
      <p>指定なし</p>
    @else
      <p></p>
    @endif
  </div>
</div>
<div class="row">
  <div class="col-3">
    <p>備考</p>
  </div>
  <div class="col-9">
    <p>{{ $item->memo }}</p>
  </div>
</div>
<div class="row">
  <div class="col-3">
    <p>登録日</p>
  </div>
  <div class="col-9">
    <p>{{ $item->created_at }}</p>
  </div>
</div>
<div class="row">
  <div class="col-3">
    <p>更新日</p>
  </div>
  <div class="col-9">
    <p>{{ $item->updated_at }}</p>
  </div>
</div>

2.8.2.3. 削除確認画面本体の新規作成と共通部分の呼び出し 〜 @includeの利用 〜

次に、削除画面本体のビューファイルとして以下を作成します。

/src/resources/views/items/delete.blade.php
@extends('base')
@section('content')
  <div class="container">
    <h2 class="text-center">データ削除</h2>
    <p>このデータを削除します。よろしいですか?</p>

    <form action="{{ route('destroy', ['item' => $item]) }}" method="POST">
      {{ csrf_field() }}
      {{ method_field('DELETE') }}
      <div class="row">
        <div class="col-12">
          <div class="float-right">
            <a class="btn btn-outline-secondary" href="{{ route('index') }}">戻る</a>
            <input type="submit" class="btn btn-outline-secondary" value="削除" />
          </div>
        </div>
      </div>

      @include('items.card')

      <div class="row">
        <div class="col-12">
          <div class="float-right">
            <a class="btn btn-outline-secondary" href="{{ route('index') }}">戻る</a>
            <input type="submit" class="btn btn-outline-secondary" value="削除" />
          </div>
        </div>
      </div>
    </form>
  </div>
@endsection
  • @include('items.card')により、この部分に``card.blade.php`が展開されます

  • <form>タグ内に{{ method_field('DELETE') }}を指定することで、<input type="hidden" name="_method" value="DELETE"> が埋め込まれます。これにより、LaravelではPOSTであるリクエストをDELETEとして処理します。

2.8.2.4. 詳細画面本体からの共通部分の呼び出し

詳細画面のビューファイルも以下の通り、@include('items.card')を利用します。

/src/resources/views/items/show.blade.php
@extends('base')
@section('content')
  <div class="container">
    <h2 class="text-center">詳細表示</h2>
    <div class="row">
      <div class="col-12">
        <a class="btn btn-outline-secondary float-right" href="{{ route('index') }}">戻る</a>
      </div>
    </div>

    @include('items.card')

    <div class="row">
      <div class="col-12">
        <a class="btn btn-outline-secondary float-right" href="{{ route('index') }}">戻る</a>
      </div>
    </div>
  </div>
@endsection

2.8.3. 一覧画面から削除確認画面へのリンクを追加する

一覧画面の削除をクリックしたら、削除確認画面へ遷移するようにします。

/src/resources/views/items/index.blade.php
//略
<a class="btn btn-outline-secondary " href="{{ route('delete', ['item' => $item]) }}">削除</a>
//略

最後に

以上でテーブルとCRUD画面の作成は完了です。

次の記事では、一覧画面に検索機能とページネーションを追加します。

参考

67
82
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
67
82

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?