Laravelで開発環境構築からWeb APIとページ遷移型CRUDアプリを作るまで

はじめに

概要

Laravelで、開発環境構築から、Web APIの作成と、ページ遷移型CRUDアプリの作成をするまでをまとめてみました。モデル、マイグレーション、コントローラ、ルーティング、ファクトリ、シーダー、ユニットテスト、ビュー、バリデーションといった概念に一通り触れて、その他実用的なTipsも追記しています。

他のWebフレームワークの経験がある方向けに、Laravelではどうするのか、が概観できるようなチュートリアルを目指しました。

動作確認したバージョンは以下の通りです。

  • Laravel 5.6
  • Homestead 5.2

筆者の環境

  • Windows 10 Pro 64ビット / macOS Sierra
  • VirtualBox
  • vagrant
  • IntelliJ IDEA 2018

必要なソフトウェアのインストール

Vagrant Boxのダウンロード

ダウンロードに時間がかかるので、Laravel開発用仮想環境であるHomesteadのVagrant Boxのダウンロードを先に行っておきます。なお、ここで紹介する方法は、Homesteadをプロジェクト毎にインストールする方法となります。

$ vagrant box add laravel/homestead

ホストマシンへのPHPとComposerのインストール

コードの生成や開発用サーバの動作は仮想環境であるHomestead上で行うことになりますが、最初にプロジェクトを作成してHomesteadをインストールするまでの作業はホストマシンで行うため、PHP 7.1と、依存性管理ツールのComposerをホストマシンにインストールします。

Composerの高速化

Composerの高速化を行います。

Composerの実行速度を高速化する方法

プロジェクトの作成とブラウザからのアクセス

プロジェクトの作成

作成するプロジェクト名をhellolaravelとして説明します。親ディレクトリで以下を実行して、プロジェクトを作成します。プロジェクトを作成したら、hellolaravelディレクトリに入ってください。

$ composer create-project --prefer-dist laravel/laravel hellolaravel
$ cd hellolaravel

Homesteadのインストールと起動

Homesteadをインストールします。

$ composer require laravel/homestead --dev

以下を実行し、VagrantfileとHomestead.yamlを作成します。

Windowsの場合

> .\vendor\bin\homestead.bat make

Macの場合

$ php vendor/bin/homestead make

以下を実行し、仮想マシンを起動します。

$ vagrant up

ここで Check your Homestead.yaml file, the path to your private key does not exist.とエラーが出た場合は、以下のように鍵ペアを作成してください。

Windowsの場合

各種記事をご参照ください。

Macの場合

$ ssh-keygen

以下を実行し、仮想マシンのシェルに入ります。以降のコマンドライン操作は全てこのシェルから行います。

$ vagrant ssh

仮想マシン内で、ホストマシンのコードを指すディレクトリに移動します。

$ cd code

以下を実行し、HomesteadのSSLサーバ証明書をホストマシンにコピーします。

$ cp /etc/nginx/ssl/ca.*.crt ./

ホストマシン上で、 ca.*.crt をダブルクリックし、信頼されたルート証明機関に追加します。Macの場合はキーチェーンアクセスが起動するので、追加した証明書をダブルクリックし、[信頼]-[SSL]を[常に信頼]にしてください。これにより、ブラウザからSSL/TLSでHomesteadにアクセスできるようになります。なお、万が一この証明書が悪意のある第三者によって作成されていた場合は、セキュリティ上の問題となりますのでご注意ください。

Homesteadへの接続

ホストマシンのhostsファイルを編集し、ホスト名homestead.testからHomesteadにアクセスできるようにします。hostsファイルは、Macの場合/etc/hosts、Windowsの場合C:¥Windows¥System32¥drivers¥etc¥hostsです。

hosts
192.168.10.10    homestead.test

ブラウザから、https://homestead.test にアクセスし、デフォルトのトップページが表示されることを確認します。

Composerの高速化

改めて、Homestead内で、Composerの高速化を行います。

Composerの実行速度を高速化する方法

IntelliJ IDEAの設定

プラグインのインストール

以下のプラグインをインストールします。

  • PHP
  • Laravel Plugin
  • Blade Support

[Open]より、hellolaravelを開きます。

[Preferences(File-Settings)]-[Languages & Frameworks]-[PHP]-[Composer]より、以下の設定をします。

  • [Path to composer.json]を設定。
  • [Add packages as libraries], [Synchronize IDE Settings with composer.json]をチェック。
  • [composer executable]を設定。
    • Windowsの場合: 例えば、 C:\ProgramData\ComposerSetup\bin\composer.bat
    • Macの場合: 例えば、/usr/local/bin/composer

Laravel 5 IDE Helper Generatorのインストール

以下を実行します。

$ composer require barryvdh/laravel-ide-helper --dev
$ composer require doctrine/dbal --dev

config/app.phpprovidersBarryvdh\LaravelIdeHelper\IdeHelperServiceProvider::classを追加します。

config/app.php
'providers' => [
    // ...
    Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
],

以下を実行し、コード補完用ファイルを生成します。これらは必要に応じて随時実行してください。

$ php artisan ide-helper:generate
$ php artisan ide-helper:models
$ php artisan ide-helper:meta

モデルとマイグレーションの追加

モデルの追加

このアプリで取り扱う対象物を、モデルとして作成します。このモデルがデータベース上ではあるテーブルに対応し、このモデルのインスタンスがデータベース上ではテーブルのレコードに対応します。また、モデルを作成すると同時に、データベース上でテーブルやカラムの構築や破棄を行うコードであるマイグレーションも作成します。(公式ドキュメント参照

$ php artisan make:model Task --migration

上記例では、モデルとしてapp/Task.php、マイグレーションとしてdatabase/migrations/yyyy_mm_dd_hhmmss_create_tasks_table.phpが生成されます。

モデルのクラスの実装は、Illuminate\Database\Eloquent\Modelを継承しているのみで、特に中身はありませんが、Laravel標準装備のORMであるEloquentが、Taskモデルとデータベース上のtasksテーブルがマッピングされていることを認識しています。

app/Task.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    //
}

テーブル名はデフォルトでモデル名の複数形となりますが、モデルに$tableプロパティを設定すれば、個別に設定することもできます。

マイグレーションの追加と実行

マイグレーションでは、以下のようにテーブルの作成と破棄を実装します。マイグレーションのファイル名に日付時刻がついているのは、マイグレーションの実行順序を決定するためです。以下では、文字列型(DB上では、通常varchar等になります)のカラムであるdescriptionを追加しています。カラムの種類については、公式ドキュメントをご参照ください。

database/migrations/yyyy_mm_dd_hhmmss_create_tasks_table.php
<?php

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

class CreateTasksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->increments('id');
            $table->string('description'); // この行を追加
            $table->timestamps();
        });
    }

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

では、以下で実際にマイグレーションを実行してデータベース上にテーブルを作成してみます。Homestead環境では、標準でMySQLが動いていますので、特にデータベースサーバのセットアップは必要ありません。

$ php artisan migrate:refresh

以下のように、実際にtasksテーブルが作成されたことを確認できます。

$ mysql -uhomestead -psecret -Dhomestead
mysql> SHOW TABLES;
+---------------------+
| Tables_in_homestead |
+---------------------+
| migrations          |
| password_resets     |
| tasks               |
| users               |
+---------------------+
4 rows in set (0.00 sec)

Web APIの作成

作成したモデルを読み書きするようなWeb APIを作成してみます。

コントローラの作成

まずは、HTTPリクエストを処理するコントローラを作成します。

$ php artisan make:controller TaskApiController

上記コマンドで、app/Http/Controllers/TaskApiController.phpが生成されるので、まずはTaskを作成、取得するようなAPIの実装を書いてみます。

app/Http/Controllers/TaskApiController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;

class TaskApiController extends Controller
{
    /**
     * @param Request $request
     * @return Response
     */
    public function store(Request $request)
    {
        $task = new \App\Task;
        $task->description = $request->input('description');
        $task->save();
        return response('OK', 200, [
            'Location' => url('/api/tasks/' . $task->id)
        ]);
    }

    /**
     * @param Request $request
     * @param string $id
     * @return Response
     */
    public function show(Request $request, $id)
    {
        $task = \App\Task::findOrFail($id);
        return response()->json([
              'id' => $task->id,
              'description' => $task->description,
        ]);
    }
}

ルーティングの設定

次に、URLとコントローラ内のメソッドを関連づけるルートを作ります。

routes/api.phpに以下の行を追加します。

routes/api.php
Route::post('/tasks', 'TaskApiController@store');
Route::get('/tasks/{id}', 'TaskApiController@show');

Route::post()Route::get()の第1パラメータがURL上のパスを表します。

なお、後述のweb.phpの場合と異なり、api.phpの場合、上で記述したパスの先頭に/api/が付加されたものが実際のURL上のパスとなります。これが不要な場合は、\App\Providers\RouteServiceProvidermapApiRoutes()prefix('api')の呼び出しを削除してください。

protected function mapApiRoutes()
{
        Route::/* prefix('api')
                 -> */middleware('api')
                 ->namespace($this->namespace)
                 ->group(base_path('routes/api.php'));
}

実際にcurlコマンドを使ってPOSTやGETリクエストを送って、正常にレスポンスが返ってくることを確認します。

$ curl http://localhost/api/tasks --data 'description=My Task' -X POST -v
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying ::1...
* connect to ::1 port 80 failed: Connection refused
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 80 (#0)
> POST /api/tasks HTTP/1.1
> Host: localhost
> User-Agent: curl/7.47.0
> Accept: */*
> Content-Length: 19
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 19 out of 19 bytes
< HTTP/1.1 302 Moved Temporarily
< Server: nginx/1.13.6
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Location: http://localhost/api/tasks/1
< Cache-Control: no-cache, private
< Date: Thu, 05 Apr 2018 07:32:14 GMT
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 59
<
* Connection #0 to host localhost left intact
$ curl http://localhost/api/tasks/1 -v
*   Trying ::1...
* connect to ::1 port 80 failed: Connection refused
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /api/tasks/1 HTTP/1.1
> Host: localhost
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.13.6
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Cache-Control: no-cache, private
< Date: Thu, 05 Apr 2018 07:33:56 GMT
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 59
<
* Connection #0 to host localhost left intact
{"id":1,"description":"My Task"}

ファクトリとSeederの作成

テストのためのダミーデータを生成するファクトリを登録します。Fakerを使えば、いかにもらしいダミーデータを生成できます。config/app.phpに以下を追加して、Fakerが生成するダミーデータの言語を指定してください。

config/app.php
'faker_locale' => 'ja_JP',

以下を実行して、database/factories/TaskFactory.phpを作成してください。

$ php artisan make:factory TaskFactory --model=Task

以下のように、実装してください。

database/factories/TaskFactory.php
<?php

use Faker\Generator as Faker;

$factory->define(App\Task::class, function (Faker $faker) {
    return [
        'description' => $faker->streetAddress, // 追加
    ];
});

次に、ダミーデータをDB上に保存するためのSeederを作成します。

$ php artisan make:seeder TasksTableSeeder

TasksTableSeederクラスに、Taskモデルを生成・保存するコードを書きます。

database/seeds/TasksTableSeeder.php
<?php

use Illuminate\Database\Seeder;

class TasksTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // 個別にプロパティを指定することも出来ます。
        factory(App\Task::class)->create([
            'description' => 'TEST DESCRIPTION',
        ]);
        // 複数個のインスタンスを生成することもできます。
        factory(App\Task::class, 50)->create();
    }
}

デフォルトのSeederであるDatabaseSeederから、作成したTasksTableSeederが呼び出されるようにします。

database/seeder/DatabaseSeeder.php
<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $this->call([
            TasksTableSeeder::class,
        ]);
    }
}

以下を実行し、DB上にテーブルを再作成し、Seederにレコードを生成させます。

$ php artisan migrate:refresh --seed

もし、レコードが生成されないときは、新しく作成したSeederファイルが認識されていない可能性があるので、以下を実行します。

$ composer dump-autoload

DB上にダミーデータが生成されているのが確認できます。

$ mysql -uhomestead -psecret -Dhomestead
mysql> SELECT * FROM tasks;
+----+----------------------------+---------------------+---------------------+
| id | description                | created_at          | updated_at          |
+----+----------------------------+---------------------+---------------------+
|  1 | TEST DESCRIPTION           | 2018-04-05 07:46:00 | 2018-04-05 07:46:00 |
|  2 | 佐藤町三宅5-1-2            | 2018-04-05 07:46:00 | 2018-04-05 07:46:00 |
|  3 | 中島町中村10-5-9           | 2018-04-05 07:46:00 | 2018-04-05 07:46:00 |
|  4 | 鈴木町青田6-10-2           | 2018-04-05 07:46:00 | 2018-04-05 07:46:00 |
|  5 | 山田町加藤1-9-3            | 2018-04-05 07:46:00 | 2018-04-05 07:46:00 |
|  6 | 石田町浜田4-2-1            | 2018-04-05 07:46:00 | 2018-04-05 07:46:00 |
|  7 | 小泉町藤本4-7-7            | 2018-04-05 07:46:00 | 2018-04-05 07:46:00 |
|  8 | 江古田町宇野6-8-7          | 2018-04-05 07:46:00 | 2018-04-05 07:46:00 |
|  9 | 山本町田辺10-1-1           | 2018-04-05 07:46:00 | 2018-04-05 07:46:00 |
| 10 | 井高町山本7-5-4            | 2018-04-05 07:46:00 | 2018-04-05 07:46:00 |

なお、開発中にマイグレーションに失敗した場合などは、直接MySQLから全テーブルを削除してやり直すのが、手っ取り早いと思われます。

ユニットテスト

以下を実行し、tests/Feature、もしくはtests/Unit以下に、ユニットテストを作ります。

$ php artisan make:test TasksTest
$ #もしくは
$ php artisan make:test TasksTest --unit

GETリクエストをテストする例を以下に示します。HTTPレベルのテストの例は、公式ドキュメントもご参照ください。

tests/Feature/TasksTest.php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class TasksTest extends TestCase
{
    public function testGet()
    {
        $response = $this->get('/api/tasks/1');
        $response->assertStatus(200);
        $response->assertJsonStructure(
            [
                'id',
                'description',
            ]
        );
        $response->assertJsonFragment([
            'id' => 1,
            'description' => 'TEST DESCRIPTION',
        ]);
    }
}

作成したテストは、以下のように、phpunitで実行できます。

$ phpunit

ページの作成

次に、ブラウザから操作できる、ページ遷移型のCRUD機能を作成します。

コントローラの作成

以下のように、CRUDを行うコントローラを、各ページのアクションのプレースホルダと共に作成します。(公式ドキュメント参照

$ php artisan make:controller TaskController --resource --model=Task

以下のように、各アクションを実装します。

app/Http/Controllers/TaskController.php
<?php

namespace App\Http\Controllers;

use App\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;

class TaskController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        return view('tasks', [
            'tasks' => \App\Task::orderBy('id')->get(),
        ]);
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        return view('tasks-create', [
        ]);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        $this->validateRequest($request);
        $task = new \App\Task;
        DB::transaction(function() use ($task, $request) {
            $task->description = $request->input('description');
            $task->save();
        });
        return redirect(route('tasks.show', [$task]));
    }

    /**
     * Display the specified resource.
     *
     * @param  \App\Task  $task
     * @return \Illuminate\Http\Response
     */
    public function show(Task $task)
    {
        return view('tasks-show', [
            'task' => $task
        ]);
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Task  $task
     * @return \Illuminate\Http\Response
     */
    public function edit(Request $request, Task $task)
    {
        // 前回の入力が無いときは、モデルから読み込む
        if (is_null($request->old('description'))) {
            $request->merge([
                'description' => $task->description,
            ]);
            $request->flash();
        }
        return view('tasks-create', [
        ]);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \App\Task  $task
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, Task $task)
    {
        $this->validateRequest($request);
        DB::transaction(function() use ($task, $request) {
            $task->description = $request->input('description');
            $task->save();
        });
        return redirect(route('tasks.show', [$task]));
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  \App\Task  $task
     * @return \Illuminate\Http\Response
     */
    public function destroy(Request $request, Task $task)
    {
        $task->delete();
        $request->session()->flash('status', '削除しました。');
        return redirect(route('tasks.index'));
    }

    /**
     * @param Request $request
     */
    private function validateRequest(Request $request)
    {
        $request->validate([
            'description' => 'required|max:20',
        ]);
    }
}

ルーティングの設定

以下のようにルーティングを追加します。

routes/web.php
Route::resource('tasks', 'TaskController');

これは、個別に以下の設定を行うのと同様の効果があります。

Route::get('/tasks', 'TaskController@index')->name('tasks.index');
Route::get('/tasks/create', 'TaskController@create')->name('tasks.create');
Route::post('/tasks', 'TaskController@store')->name('tasks.store');
Route::get('/tasks/{task}', 'TaskController@show')->name('tasks.show');
Route::get('/tasks/{task}/edit', 'TaskController@edit')->name('tasks.edit');
Route::put('/tasks/{task}', 'TaskController@update')->name('tasks.update');
Route::delete('/tasks/{task}', 'TaskController@destroy')->name('tasks.destroy');

Bootstrap 4のBootstrap 3への置き換え

Laravel 5.6ではBootstrap 4が標準CSSフレームワークとなっていますが、ここではレガシーブラウザへの対応のためBootstrap 3を利用します。

package.json内の"bootstrap": "^4...の行を"bootstrap-sass": "^3...に変更します。

package.json
        "bootstrap-sass": "^3.3.7",

以下を実行し、必要なnodeモジュールをインストールします。

$ yarn install --no-bin-links
$ yarn global add cross-env

bootstrap.jsをBootstrap 3用に書き換えます。

resources/assets/js/bootstrap.js
//require('bootstrap');
require('bootstrap-sass');

app.scssをBootstrap 3用に書き換えます。

resources/assets/sass/app.scss
//@import '~bootstrap/scss/bootstrap';
@import "~bootstrap-sass/assets/stylesheets/bootstrap";

Bootstrap 3との単位の整合性のため、以下の行を変更します。

resources/assets/sass/_variables.scss
//$font-size-base: 0.9rem;
$font-size-base: 14px;

public/css/app.csspublic/js/app.jsを生成します。

$ yarn run dev

ビューの作成

ビューのフォームを作成する際に、生のHTML要素を用いても良いのですが、バリデーションが失敗したときに、再度入力フォームに以前の値を表示する処理などを書く必要があり、面倒です。このような処理を自動的に行ってくれるLaravel Collectiveを用いてビューを実装します。

以下で、Laravel Collectiveをインストールします。

$ composer require laravelcollective/html

ビューはBladeというテンプレートエンジンで表示させます。

まず、複数ページ間で共有するレイアウトを定義します。(もちろん、レイアウトを定義せずに、直接ビューのページを書くことも可能です。)

resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>チュートリアル</title>
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <link href="{{ asset('css/app.css') }}?v=20180405" rel="stylesheet">
</head>
<body>
<div id="app">
    <nav class="navbar navbar-default navbar-static-top">
        <div class="container-fluid">
            <div class="navbar-header">
                <a class="navbar-brand" href="/tasks/">チュートリアル</a>
            </div>
        </div>
    </nav>
    @yield('content')
</div>
<script src="{{ asset('js/app.js') }}?v=20180405"></script>
@yield('script')
</body>
</html>

一覧のビューを作成します。@extendsで指定したレイアウトで表示されます。なお、ビューのファイル名の.blade.phpの前の部分が、コントローラ等から呼ばれるview()関数の引数となっています。

resources/views/tasks.blade.php
@extends('layouts.app')

@section('content')
  <div class="container-fluid">
      <div class="row">
          <div class="col-md-12">
              @if (session('status'))
                  <div class="alert alert-success">
                      {{ session('status') }}
                  </div>
              @endif
              <div class="panel panel-default">
                  <div class="panel-body">
                      <div class="pull-right">
                          <a href="{{ route('tasks.create') }}" class="btn btn-primary" role="button">新規作成</a>
                      </div>
                  </div>
              </div>
              <div class="clearfix"></div>
              <table class="table table-bordered table-striped table-condensed table-hover">
                  <thead>
                    <tr>
                        <th>ID</th>
                        <th>description</th>
                    </tr>
                  </thead>
                  <tbody>
                    @foreach ($tasks as $task)
                      <tr class="task" id="{{ $task->id }}">
                        <td>{{ $task->id }}</td>
                        <td>{{ $task->description }}</td>
                      </tr>
                    @endforeach
                  </tbody>
              </table>
          </div>
      </div>
  </div>
@endsection
@section('script')
  <script type="text/javascript">
      $(function() {
          $('.task').on('click', function(e) {
              window.location = '/tasks/' + $(this).attr('id');
          });
      });
  </script>
@endsection

表示のビューを作成します。

resources/views/tasks-show.blade.php
@extends('layouts.app')

@section('content')
    <div class="container-fluid">
        <div class="col-md-12">
            @if ($errors->any())
                <div class="alert alert-danger">
                    <ul>
                        @foreach ($errors->all() as $error)
                            <li>{{ $error }}</li>
                        @endforeach
                    </ul>
                </div>
            @endif
            <div class="form-horizontal">
                <div class="form-group">
                    <label class="control-label col-sm-3">ID</label>
                    <div class="col-sm-9">
                        <div class="checkbox">{{ $task->id }}</div>
                    </div>
                </div>
                <div class="form-group">
                    <label class="control-label col-sm-3">description</label>
                    <div class="col-sm-9">
                        <div class="checkbox">{{ $task->description }}</div>
                    </div>
                </div>
            </div>
            <div class="pull-right">
                <a class="btn btn-primary" href="{{ route('tasks.edit', ['task' => Route::current()->parameter('task')]) }}" role="button">編集</a>
            </div>
        </div>
    </div>
@endsection

作成、および編集のビューを作成します。

resources/views/tasks-create.blade.php
@extends('layouts.app')

@section('content')
    <div class="container-fluid">
        <div class="col-md-12">
            @if ($errors->any())
                <div class="alert alert-danger">
                    <ul>
                        @foreach ($errors->all() as $error)
                            <li>{{ $error }}</li>
                        @endforeach
                    </ul>
                </div>
            @endif
            {!! Form::open([
                   'url' => (Route::current()->getName() === 'tasks.create') 
                   ? route('tasks.store') 
                   : route('tasks.update', ['tasks' => Route::current()->parameter('task')]),
                    'class' => 'form-horizontal'
                ]) !!}
            @if(Route::current()->getName() === 'tasks.create')
                {{ method_field('POST') }}
            @else
                {{ method_field('PUT') }}
            @endif
            <div class="pull-right">
                {!! Form::button('登録', ['class' => 'btn btn-primary', 'type' => 'submit', 'name' => 'action', 'value' => 'submit']) !!}
            </div>
            <div class="clearfix"></div>
            <div class="form-group">
                {!! Form::label('name', 'description', ['class' => 'control-label col-sm-3']) !!}
                <div class="col-sm-9">
                    {!! Form::text('description', null, ['class' => 'form-control', 'placeholder' => 'description']) !!}
                    <div class="text-danger">{{ $errors->first('description') }}</div>
                </div>
            </div>
            <div class="pull-right">
                {!! Form::button('登録', ['class' => 'btn btn-primary', 'type' => 'submit', 'name' => 'action', 'value' => 'submit']) !!}
            </div>
            {!! Form::close() !!}
            @if (!is_null(Route::current()->parameter('task')))
                {!! Form::open([
                       'url' => route('tasks.destroy', ['tasks' => Route::current()->parameter('task')]),
                        'class' => 'form-horizontal'
                    ]) !!}
                {{ method_field('DELETE') }}
                <div class="pull-left">
                    {!! Form::button('削除', ['class' => 'btn btn-danger', 'type' => 'submit']) !!}
                </div>
                {!! Form::close() !!}
            @endif
        </div>
    </div>
@endsection

これで、ブラウザからhttps://homestead.test/tasks/にアクセスすれば、ページからCRUD操作ができるようになりました。

バリデーション文言の日本語化

config/app.phpを以下のように書き換えてください。

config/app.php
    'locale' => 'ja',

そして、有志の方が作成されたコードなどを利用し、resources/lang/ja/以下のファイルを作成してください。

Tips

Middlewareによるフィルタ処理

リクエスト処理の前後に何らかの処理を追加するための、他のフレームワークで言うところの「フィルタ」は、LaravelではMiddlewareと呼ばれています。Middlewareの作成と適用については公式ドキュメントをご参照ください。

複雑なバリデーション

バリデーションには様々な種類が用意されています。(公式ドキュメント
独自のバリデーションを行いたいとき、公式で推奨されているバリデーションルールを追加する方法以外に、簡単に実装する方法としては以下のような方法があります。

use Illuminate\Support\Facades\Validator;
//...
    /**
     * @param Request $request
     */
    private function validateRequest(Request $request)
    {
        // 既存のバリデーション
        $validator = Validator::make($request->all(), [
            'description' => 'required|max:20',
        ]);
        // 独自のバリデーション
        $validator->after(function($validator) use ($request) {
            $description = $request->input('description');
            if ($description === 'SECRET') {
                $validator->errors()->add('description', 'SECRETは指定できません。');
            }
        });
        $validator->validate();
    }

ログのローテート

デフォルトでは、ログはstorage/logs/laravel.logに追記され続けます。以下のように設定すると、日次で追記先ファイルを変更できます。

config/logging.php
'stack' => [
    'driver' => 'stack',
    'channels' => ['daily'],
],

ソフトデリート(論理削除)

Laravelはソフトデリート(いわゆる論理削除)に対応しています。以下のようにマイグレーションとモデルのコードを書けば、deleted_atカラムの値に従ってEloquentのクエリが行われるようになります。

database/migrations/yyyy_mm_dd_hhmmss_create_tasks_table
<?php
// ...
class CreateTasksTable extends Migration
{
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            // ...
            $table->softDeletes();
        });
    }
}
app/Task.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Task extends Model
{
    use SoftDeletes;

    public $dates = ['deleted_at'];
}

MySQLのcollation

デフォルトでは、MySQLへの接続設定で、collationがutf8_unicode_ciになっています。濁点の有り無しを同一視するなど、日本語環境には向いていない場合もありますので、必要に応じて変更してください。

config/database.php
'collation' => 'utf8_general_ci',

Gitリポジトリからのclone

通常Gitリポジトリには.envは含めません。GitリポジトリからLaravelのプロジェクトをcloneしてきたときは、.env.exampleを元に.envを作り、以下のコマンドでAPP_KEYを設定する必要があります。

$ php artisan key:generate

また、composer installも行う必要があります。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.