はじめに
を座右に置きながら、 Laravel 5.5 で実践したときの記録をすべて載せました。上記ページと本投稿に沿って実践すれば、 Laravel 5.5 での中級者向けタスクリストを作ることができます。
最終の完全なソースコードは次のページから取得可能です。
早速、始めていきます。
イントロダクション
インストール
Laravelのインストール/
ローカル環境は Laravel Homestead 5.2 Laravel ではなく、自身で作成した Docker 環境、 oki2a24/docker_lemp_laravel: Laravel 用の Docker Compose (Nginx、MySQL、PHP) です。 、としました。
ディレクトリ構成は次のようにしました。
- sample_laravel_my_quickstart_intermediate: Docker 環境と Laravel 環境をおいています。
- docker_my_qucikstart_intermediate: Docker 環境。Docker Compose のために必要なものがおいてあります。oki2a24/docker_lemp_laravel: Laravel 用の Docker Compose (Nginx、MySQL、PHP) です。
- my-quickstart-intermediate: 中級者向けタスクリストの Laravel 5.5 です。
. └── sample_laravel_my_quickstart_intermediate ├── docker_my_qucikstart_intermediate └── my-quickstart-intermediate
Laravel のインストールですけれども、入門の次のコマンドでは、 5.5 ではなく、もっと新しい 5.7 などの最新バージョンが入ってしまいます。ダメです。
composer create-project laravel/laravel quickstart --prefer-dist
そこで次のようにしました。
.
: Docker 環境の関係で、すでにmy-quickstart-intermediate
ディレクトリが作成済みだった。そこで、my-quickstart-intermediate
に移動し、.
を指定することでカレントディレクトリへ Laravel をインストールした。"5.5.*"
: Laravel のバージョン指定。 5.5 の最新バージョンを指定。
composer create-project --prefer-dist laravel/laravel . "5.5.*"
次のバージョンがインストールされました。
root@37d62dbbf13d:/var/www# php artisan --version Laravel Framework 5.5.44 root@37d62dbbf13d:/var/www#
なお、ターミナルというのは Mac のターミナルから実行したコマンドで、 php_fpmコンテナというのは docker-compose exec php_fpm bash
によって PHP-FPM コンテナに入ってそこで実行したコマンドとなります。
クイックスタートのインストール (任意)
ここはやってはいけません。今回の目的は、自分の手で作り上げることです。それも、 Laravel 5.2 ではなく、 Laravel 5.5 で。
データベースの準備
データベースマイグレーション
特に行うことはありません。解説を読みましょう。
users
テーブル
入門にある通り、最初からLaravelに用意されている基本的な users
テーブルを作成するマイグレーションに手を加えないでそのまま使いました。
tasks
テーブル
入門にある通り次のコマンドで大丈夫でした。
php artisan make:migration create_tasks_table --create=tasks
実際に実行すると、次のようになりました。
root@37d62dbbf13d:/var/www# php artisan make:migration create_tasks_table --create=tasks Created Migration: 2018_11_18_231801_create_tasks_table root@37d62dbbf13d:/var/www#
マイグレーションファイルを編集しました。入門との違いは次の部分です。
- 自動生成時に
use Illuminate\Support\Facades\Schema;
が追加されていた。 - 自動生成時のコードが
Schema::drop('tasks');
ではなく、Schema::dropIfExists('tasks');
へと変わった。 - 自動生成コードに、 user_id と name の行を追加した。
<?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->integer('user_id')->unsigned()->index(); $table->string('name'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('tasks'); } }
マイグレーションを実行します。
php artisan migrate
こうなりま、、、あっ><。
root@37d62dbbf13d:/var/www# php artisan migrate In Connection.php line 664: SQLSTATE[HY000] [2002] Connection refused (SQL: select * from information_schema.tables where table_schema = homestead and table_name = migrations) In Connector.php line 67: SQLSTATE[HY000] [2002] Connection refused root@37d62dbbf13d:/var/www#
Laravel に DB 設定をするのを忘れていました。
my-quickstart-intermediate/.env
の DB_HOST
、 DB_DATABASE
、 DB_USERNAME
、 DB_PASSWORD
を修正しました。
root@37d62dbbf13d:/var/www# 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: 2018_11_18_231801_create_tasks_table Migrated: 2018_11_18_231801_create_tasks_table root@37d62dbbf13d:/var/www#
これで大丈夫でした。
Eloquentモデル
User
モデル
こちらも、users
テーブルのマイグレーションファイルと同様、最初からある my-quickstart-intermediate/app/User.php
をそのまま使用しました。
Task
モデル
入門の通り作成しました。
root@37d62dbbf13d:/var/www# php artisan make:model Task Model created successfully. root@37d62dbbf13d:/var/www#
次に、これも入門の通り、「複数代入」ができるよう name
属性を fillable
に登録しました。入門と、全く同じ内容となりました。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Task extends Model { /** * 複数代入を行う属性 * * @var array */ protected $fillable = ['name']; }
Eloquentリレーション
ここは、設定後にどうやって使えるようになるかを解説するコードなため、とくに行うことはありませんでした。
tasks
のリレーション
入門どおりです。
<?php namespace App; use Illuminate\Notifications\Notifiable; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { use Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'email', 'password', ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ 'password', 'remember_token', ]; /** * 特定ユーザーの全タスク取得 */ public function tasks() { return $this->hasMany(Task::class); } }
user
のリレーション
こちらも、入門どおりです。
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Task extends Model { /** * 複数代入を行う属性 * * @var array */ protected $fillable = ['name']; /** * タスク所有ユーザーの取得 */ public function user() { return $this->belongsTo(User::class); } }
ルーティング
ビューの表示
もちろんこのビューを実際に定義する必要があります。後ほど行いましょう!
とありましたが、行っている箇所はありませんでした><。よって、この部分はそのままとしました。
認証
入門と異なる部分です。したがって最も難しい箇所の一つとなります。
- 入門には、 “既にapp/Http/Controllers/Auth/AuthControllerがLaravelアプリケーションに用意されていることに注目しましょう。” とあるが、実際には、そんなファイルは Laravel 5.5 には存在しない。
app/Http/Controllers/Auth/
は存在する。 AuthenticatesAndRegistersUsers
もファイル検索してみたが存在しない。
認証ルートとビュー
不安にかられますけれども、進めてまいりましょう。
さて、残りは何でしょう?えーと、ユーザ登録とログインのテンプレートが必要ですし、認証コントローラへのルート定義もまだです。これらは全部、make:auth Artisanコマンドで片付きます。
とある通り、やってみましょう。
root@37d62dbbf13d:/var/www# php artisan make:auth Authentication scaffolding generated successfully. root@37d62dbbf13d:/var/www#
うまくいきました。何が変わったのでしょう?概要です。
$ git add -A $ git status On branch routing Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: app/Http/Controllers/HomeController.php new file: app/Http/Controllers/HomeController.php new file: resources/views/auth/passwords/email.blade.php new file: resources/views/auth/passwords/reset.blade.php new file: resources/views/auth/register.blade.php new file: resources/views/home.blade.php new file: resources/views/layouts/app.blade.php modified: routes/web.php $
では、少し長くなりますが、何が変わったかも見ていきます。
まずは既存ファイルの、 my-quickstart-intermediate/routes/web.php
の差分です。
<?php /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return view('welcome'); }); Auth::routes(); Route::get('/home', 'HomeController@index')->name('home');
以下、新規追加されたファイルです。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class HomeController extends Controller { /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('auth'); } /** * Show the application dashboard. * * @return \Illuminate\Http\Response */ public function index() { return view('home'); } }
@extends('layouts.app') @section('content') <div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading">Login</div> <div class="panel-body"> <form class="form-horizontal" method="POST" action="{{ route('login') }}"> {{ csrf_field() }} <div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}"> <label for="email" class="col-md-4 control-label">E-Mail Address</label> <div class="col-md-6"> <input id="email" type="email" class="form-control" name="email" value="{{ old('email') }}" required autofocus> @if ($errors->has('email')) <span class="help-block"> <strong>{{ $errors->first('email') }}</strong> </span> @endif </div> </div> <div class="form-group{{ $errors->has('password') ? ' has-error' : '' }}"> <label for="password" class="col-md-4 control-label">Password</label> <div class="col-md-6"> <input id="password" type="password" class="form-control" name="password" required> @if ($errors->has('password')) <span class="help-block"> <strong>{{ $errors->first('password') }}</strong> </span> @endif </div> </div> <div class="form-group"> <div class="col-md-6 col-md-offset-4"> <div class="checkbox"> <label> <input type="checkbox" name="remember" {{ old('remember') ? 'checked' : '' }}> Remember Me </label> </div> </div> </div> <div class="form-group"> <div class="col-md-8 col-md-offset-4"> <button type="submit" class="btn btn-primary"> Login </button> <a class="btn btn-link" href="{{ route('password.request') }}"> Forgot Your Password? </a> </div> </div> </form> </div> </div> </div> </div> </div> @endsection
@extends('layouts.app') @section('content') <div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading">Reset Password</div> <div class="panel-body"> @if (session('status')) <div class="alert alert-success"> {{ session('status') }} </div> @endif <form class="form-horizontal" method="POST" action="{{ route('password.email') }}"> {{ csrf_field() }} <div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}"> <label for="email" class="col-md-4 control-label">E-Mail Address</label> <div class="col-md-6"> <input id="email" type="email" class="form-control" name="email" value="{{ old('email') }}" required> @if ($errors->has('email')) <span class="help-block"> <strong>{{ $errors->first('email') }}</strong> </span> @endif </div> </div> <div class="form-group"> <div class="col-md-6 col-md-offset-4"> <button type="submit" class="btn btn-primary"> Send Password Reset Link </button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection
@extends('layouts.app') @section('content') <div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading">Reset Password</div> <div class="panel-body"> <form class="form-horizontal" method="POST" action="{{ route('password.request') }}"> {{ csrf_field() }} <input type="hidden" name="token" value="{{ $token }}"> <div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}"> <label for="email" class="col-md-4 control-label">E-Mail Address</label> <div class="col-md-6"> <input id="email" type="email" class="form-control" name="email" value="{{ $email or old('email') }}" required autofocus> @if ($errors->has('email')) <span class="help-block"> <strong>{{ $errors->first('email') }}</strong> </span> @endif </div> </div> <div class="form-group{{ $errors->has('password') ? ' has-error' : '' }}"> <label for="password" class="col-md-4 control-label">Password</label> <div class="col-md-6"> <input id="password" type="password" class="form-control" name="password" required> @if ($errors->has('password')) <span class="help-block"> <strong>{{ $errors->first('password') }}</strong> </span> @endif </div> </div> <div class="form-group{{ $errors->has('password_confirmation') ? ' has-error' : '' }}"> <label for="password-confirm" class="col-md-4 control-label">Confirm Password</label> <div class="col-md-6"> <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required> @if ($errors->has('password_confirmation')) <span class="help-block"> <strong>{{ $errors->first('password_confirmation') }}</strong> </span> @endif </div> </div> <div class="form-group"> <div class="col-md-6 col-md-offset-4"> <button type="submit" class="btn btn-primary"> Reset Password </button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection
@extends('layouts.app') @section('content') <div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading">Register</div> <div class="panel-body"> <form class="form-horizontal" method="POST" action="{{ route('register') }}"> {{ csrf_field() }} <div class="form-group{{ $errors->has('name') ? ' has-error' : '' }}"> <label for="name" class="col-md-4 control-label">Name</label> <div class="col-md-6"> <input id="name" type="text" class="form-control" name="name" value="{{ old('name') }}" required autofocus> @if ($errors->has('name')) <span class="help-block"> <strong>{{ $errors->first('name') }}</strong> </span> @endif </div> </div> <div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}"> <label for="email" class="col-md-4 control-label">E-Mail Address</label> <div class="col-md-6"> <input id="email" type="email" class="form-control" name="email" value="{{ old('email') }}" required> @if ($errors->has('email')) <span class="help-block"> <strong>{{ $errors->first('email') }}</strong> </span> @endif </div> </div> <div class="form-group{{ $errors->has('password') ? ' has-error' : '' }}"> <label for="password" class="col-md-4 control-label">Password</label> <div class="col-md-6"> <input id="password" type="password" class="form-control" name="password" required> @if ($errors->has('password')) <span class="help-block"> <strong>{{ $errors->first('password') }}</strong> </span> @endif </div> </div> <div class="form-group"> <label for="password-confirm" class="col-md-4 control-label">Confirm Password</label> <div class="col-md-6"> <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required> </div> </div> <div class="form-group"> <div class="col-md-6 col-md-offset-4"> <button type="submit" class="btn btn-primary"> Register </button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection
@extends('layouts.app') @section('content') <div class="container"> <div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading">Dashboard</div> <div class="panel-body"> @if (session('status')) <div class="alert alert-success"> {{ session('status') }} </div> @endif You are logged in! </div> </div> </div> </div> </div> @endsection
<!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"> <!-- CSRF Token --> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>{{ config('app.name', 'Laravel') }}</title> <!-- Styles --> <link href="{{ asset('css/app.css') }}" rel="stylesheet"> </head> <body> <div id="app"> <nav class="navbar navbar-default navbar-static-top"> <div class="container"> <div class="navbar-header"> <!-- Collapsed Hamburger --> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#app-navbar-collapse" aria-expanded="false"> <span class="sr-only">Toggle Navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <!-- Branding Image --> <a class="navbar-brand" href="{{ url('/') }}"> {{ config('app.name', 'Laravel') }} </a> </div> <div class="collapse navbar-collapse" id="app-navbar-collapse"> <!-- Left Side Of Navbar --> <ul class="nav navbar-nav"> </ul> <!-- Right Side Of Navbar --> <ul class="nav navbar-nav navbar-right"> <!-- Authentication Links --> @guest <li><a href="{{ route('login') }}">Login</a></li> <li><a href="{{ route('register') }}">Register</a></li> @else <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" aria-haspopup="true" v-pre> {{ Auth::user()->name }} <span class="caret"></span> </a> <ul class="dropdown-menu"> <li> <a href="{{ route('logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();"> Logout </a> <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;"> {{ csrf_field() }} </form> </li> </ul> </li> @endguest </ul> </div> </div> </nav> @yield('content') </div> <!-- Scripts --> <script src="{{ asset('js/app.js') }}"></script> </body> </html>
さて、ここからが混乱します。
これで、残りはルートファイルで認証ルートを追加すれば全て完了です。Routeファサードのauthメソッドで行います。このメソッドは、私達が必要としている認証、ログイン、パスワードリセットすべてのルートをまとめて登録してくれます。
// 認証ルート
Route::auth();
とありますが、 my-quickstart-intermediate/routes/web.php
には、すでに Auth::routes();
という行が追記されています。
微妙に異なりますので非常に不安にかられますけれども、このままとしました。
そして、
authルートを登録したら、app/Http/Controllers/Auth/AuthControllerコントローラの$redirectToプロパティに、/tasksを確実にセットしてください。
protected $redirectTo = ‘/tasks’;
については、そのようなファイルがありません。 my-quickstart-intermediate/app/Http/Controllers/Auth/
ディレクトリはあるのですけれども。リダイレクトを設定したいですので、このディレクトリにある、該当するプロパティを編集して、次のようにしました。
なお、 my-quickstart-intermediate/app/Http/Controllers/Auth/ForgotPasswordController.php
については、該当するプロパティがありませんでしたので、何もしていません。
<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; class LoginController extends Controller { /* |-------------------------------------------------------------------------- | Login Controller |-------------------------------------------------------------------------- | | This controller handles authenticating users for the application and | redirecting them to your home screen. The controller uses a trait | to conveniently provide its functionality to your applications. | */ use AuthenticatesUsers; /** * Where to redirect users after login. * * @var string */ protected $redirectTo = '/tasks'; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest')->except('logout'); } }
<?php namespace App\Http\Controllers\Auth; use App\User; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Validator; use Illuminate\Foundation\Auth\RegistersUsers; class RegisterController extends Controller { /* |-------------------------------------------------------------------------- | Register Controller |-------------------------------------------------------------------------- | | This controller handles the registration of new users as well as their | validation and creation. By default this controller uses a trait to | provide this functionality without requiring any additional code. | */ use RegistersUsers; /** * Where to redirect users after registration. * * @var string */ protected $redirectTo = '/tasks'; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest'); } /** * Get a validator for an incoming registration request. * * @param array $data * @return \Illuminate\Contracts\Validation\Validator */ protected function validator(array $data) { return Validator::make($data, [ 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255|unique:users', 'password' => 'required|string|min:6|confirmed', ]); } /** * Create a new user instance after a valid registration. * * @param array $data * @return \App\User */ protected function create(array $data) { return User::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => bcrypt($data['password']), ]); } }
<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\ResetsPasswords; class ResetPasswordController extends Controller { /* |-------------------------------------------------------------------------- | Password Reset Controller |-------------------------------------------------------------------------- | | This controller is responsible for handling password reset requests | and uses a simple trait to include this behavior. You're free to | explore this trait and override any methods you wish to tweak. | */ use ResetsPasswords; /** * Where to redirect users after resetting their password. * * @var string */ protected $redirectTo = '/tasks'; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest'); } }
さらに、
さらに、app/Http/Middleware/RedirectIfAuthenticated.phpファイルのリダイレクトパスも変更する必要があります。
return redirect(‘/tasks’);
は、、、、大丈夫でした。ありました。編集して、次のようにしました。
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Support\Facades\Auth; class RedirectIfAuthenticated { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @param string|null $guard * @return mixed */ public function handle($request, Closure $next, $guard = null) { if (Auth::guard($guard)->check()) { return redirect('/tasks'); } return $next($request); } }
make:auth 後のここまでの操作は、要は、ブラウザからなにか操作した後のリダイレクト先を設定しよう!ということが意図 のはずです。ですので、これで良いはずと判断して、進めました。
ログイン周りについては、次のページが参考になりました。ありがとうございます!
タスクコントローラー
TaskController
の作成は入門どおりです。
php artisan make:controller TaskController
これでコントローラーが生成できました。続いてこのコントローラーを実行するルートをapp/Http/routes.phpファイルへスタブ(空の代用コード)として作成します。
については、またしても該当のファイルがありません。ですがルートを設定できれば良いので、 my-quickstart-intermediate/routes/web.php
に記述すればよいはずです。次のようにしました。
<?php /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return view('welcome'); }); Auth::routes(); Route::get('/home', 'HomeController@index')->name('home'); Route::get('/tasks', 'TaskController@index'); Route::post('/task', 'TaskController@store'); Route::delete('/task/{task}', 'TaskController@destroy');
全タスクルートの認証
こちらも、入門どおりです。 TaskController の全アクション対して認証を要求するようにしました。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class TaskController extends Controller { /** * 新しいコントローラインスタンスの生成 * * @return void */ public function __construct() { $this->middleware('auth'); } }
レイアウトとビューの構築
ビューに関しては、入門では理解を深めるために簡略化されています。完全なコードは laravel/quickstart-intermediate: A sample task list application with authentication. にあります。このことを頭の片隅において、続けましょう。
レイアウトの定義
追記: my-quickstart-intermediate/resources/views/layouts/app.blade.php
の編集は、不要でした。詳しくは、 [エラー対処 4. MethodNotAllowedHttpException No message
] の見出しを参照してください。
quickstart-intermediate/app.blade.php at master · laravel/quickstart-intermediate に当たります。そのまま既存の内容を書き換えました。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Laravel Quickstart - Intermediate</title> <!-- Fonts --> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.4.0/css/font-awesome.min.css" rel='stylesheet' type='text/css'> <link href="https://fonts.googleapis.com/css?family=Lato:100,300,400,700" rel='stylesheet' type='text/css'> <!-- Styles --> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet"> {{-- <link href="{{ elixir('css/app.css') }}" rel="stylesheet"> --}} <style> body { font-family: 'Lato'; } .fa-btn { margin-right: 6px; } </style> </head> <body id="app-layout"> <nav class="navbar navbar-default"> <div class="container"> <div class="navbar-header"> <!-- Collapsed Hamburger --> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#app-navbar-collapse"> <span class="sr-only">Toggle Navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <!-- Branding Image --> <a class="navbar-brand" href="{{ url('/') }}"> Task List </a> </div> <div class="collapse navbar-collapse" id="app-navbar-collapse"> <!-- Left Side Of Navbar --> <ul class="nav navbar-nav"> </ul> <!-- Right Side Of Navbar --> <ul class="nav navbar-nav navbar-right"> <!-- Authentication Links --> @if (Auth::guest()) <li><a href="{{ url('/login') }}">Login</a></li> <li><a href="{{ url('/register') }}">Register</a></li> @else <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"> {{ Auth::user()->name }} <span class="caret"></span> </a> <ul class="dropdown-menu" role="menu"> <li><a href="{{ url('/logout') }}"><i class="fa fa-btn fa-sign-out"></i>Logout</a></li> </ul> </li> @endif </ul> </div> </div> </nav> @yield('content') <!-- JavaScripts --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script> {{-- <script src="{{ elixir('js/app.js') }}"></script> --}} </body> </html>
子ビューの定義
TaskController
のindex
メソッドに対応する、resources/views/tasks.blade.php
を定義しましょう。
とありますが、そのようなファイルを作るのではなく、その下のコードにある、 <!-- resources/views/tasks/index.blade.php -->
のパスでファイルを作ります。
GitHub の完全なコードでも、こちらのパスとなっています。
quickstart-intermediate/index.blade.php at master · laravel/quickstart-intermediate をそのまま追加しました。
@extends('layouts.app') @section('content') <div class="container"> <div class="col-sm-offset-2 col-sm-8"> <div class="panel panel-default"> <div class="panel-heading"> New Task </div> <div class="panel-body"> <!-- Display Validation Errors --> @include('common.errors') <!-- New Task Form --> <form action="{{ url('task') }}" method="POST" class="form-horizontal"> {{ csrf_field() }} <!-- Task Name --> <div class="form-group"> <label for="task-name" class="col-sm-3 control-label">Task</label> <div class="col-sm-6"> <input type="text" name="name" id="task-name" class="form-control" value="{{ old('task') }}"> </div> </div> <!-- Add Task Button --> <div class="form-group"> <div class="col-sm-offset-3 col-sm-6"> <button type="submit" class="btn btn-default"> <i class="fa fa-btn fa-plus"></i>Add Task </button> </div> </div> </form> </div> </div> <!-- Current Tasks --> @if (count($tasks) > 0) <div class="panel panel-default"> <div class="panel-heading"> Current Tasks </div> <div class="panel-body"> <table class="table table-striped task-table"> <thead> <th>Task</th> <th> </th> </thead> <tbody> @foreach ($tasks as $task) <tr> <td class="table-text"><div>{{ $task->name }}</div></td> <!-- Task Delete Button --> <td> <form action="{{url('task/' . $task->id)}}" method="POST"> {{ csrf_field() }} {{ method_field('DELETE') }} <button type="submit" id="delete-task-{{ $task->id }}" class="btn btn-danger"> <i class="fa fa-btn fa-trash"></i>Delete </button> </form> </td> </tr> @endforeach </tbody> </table> </div> </div> @endif </div> </div> @endsection
簡単な説明
これでアプリケーションの基本レイアウトとビューが定義できました。続けて
TaskController
のindex
メソッドから、このビューを返しましょう。
は入門どおりに追加し、次のようになりました。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class TaskController extends Controller { /** * 新しいコントローラインスタンスの生成 * * @return void */ public function __construct() { $this->middleware('auth'); } /** * ユーザーの全タスクをリスト表示 * * @param Request $request * @return Response */ public function index(Request $request) { return view('tasks.index'); } }
タスク追加
バリデーション
TaskController@store
メソッドの追加は、タスク作成の見出しで行います。
$errors変数
quickstart-intermediate/errors.blade.php at master · laravel/quickstart-intermediate の内容を追加しました。
@if (count($errors) > 0) <!-- Form Error List --> <div class="alert alert-danger"> <strong>Whoops! Something went wrong!</strong> <br><br> <ul> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif
タスク作成
入門の TaskController@store
メソッドをそのまま追加しました。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class TaskController extends Controller { /** * 新しいコントローラインスタンスの生成 * * @return void */ public function __construct() { $this->middleware('auth'); } /** * ユーザーの全タスクをリスト表示 * * @param Request $request * @return Response */ public function index(Request $request) { return view('tasks.index'); } /** * 新しいタスクの作成 * * @param Request $request * @return Response */ public function store(Request $request) { $this->validate($request, [ 'name' => 'required|max:255', ]); $request->user()->tasks()->create([ 'name' => $request->name, ]); return redirect('/tasks'); } }
既存タスク表示
ここのコードは追加せず、次の依存注入へと進みました。
依存注入
リポジトリーの作成
入門どおり、追加しました。
<?php namespace App\Repositories; use App\User; class TaskRepository { /** * 指定ユーザーの全タスク取得 * * @param User $user * @return Collection */ public function forUser(User $user) { return $user->tasks() ->orderBy('created_at', 'asc') ->get(); } }
リポジトリーの注入
追記: 追加の漏れにより、後々エラーが発生しました。エラー内容と対処は、 [エラー対処 1. ReflectionException Class App\Http\Controllers\TaskRepository does not exist
] を参照してください。足りない部分は、 use App\Repositories\TaskRepository;
となります。
これも、入門どおりに追加しました。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class TaskController extends Controller { /** * タスクリポジトリーインスタンス * * @var TaskRepository */ protected $tasks; /** * 新しいコントローラーインスタンスの生成 * * @param TaskRepository $tasks * @return void */ public function __construct(TaskRepository $tasks) { $this->middleware('auth'); $this->tasks = $tasks; } /** * ユーザーの全タスクをリスト表示 * * @param Request $request * @return Response */ public function index(Request $request) { return view('tasks.index', [ 'tasks' => $this->tasks->forUser($request->user()), ]); } /** * 新しいタスクの作成 * * @param Request $request * @return Response */ public function store(Request $request) { $this->validate($request, [ 'name' => 'required|max:255', ]); $request->user()->tasks()->create([ 'name' => $request->name, ]); return redirect('/tasks'); } }
タスク表示
入門の本見出し部分は、すでに [子ビューの定義] で作成した my-quickstart-intermediate/resources/views/tasks/index.blade.php
に含まれています。
quickstart-intermediate/index.blade.php at master · laravel/quickstart-intermediate の 40 から 75 行が該当します。
タスク削除
削除ボタンの追加
こちらも、 [子ビューの定義] で作成した my-quickstart-intermediate/resources/views/tasks/index.blade.php
に含まれています。
quickstart-intermediate/index.blade.php at master · laravel/quickstart-intermediate の 58 から 68 行が該当します。
見せかけのメソッドの説明
行うことは特にありません。入門の内容を理解します。
ルートモデル結合
ここでも何も行いません。 destroy
メソッドの追加は、 [タスク削除] の見出しで行います。今は入門の内容を理解し、今後の流れを予想します。
認可
ここでも行うことは特にありません。入門の内容を理解し、認可を使って何をしたいのかを理解します。
ポリシー
入門の通り、ポリシーをコマンドで生成しました。
root@37d62dbbf13d:/var/www# php artisan make:policy TaskPolicy Policy created successfully. root@37d62dbbf13d:/var/www#
次のファイルが追加されました。
<?php namespace App\Policies; use App\User; use Illuminate\Auth\Access\HandlesAuthorization; class TaskPolicy { use HandlesAuthorization; /** * Create a new policy instance. * * @return void */ public function __construct() { // } }
次に destroy
メソッドをポリシーへ追加しました。入門の通りです。
追記: 次のコードで後々エラーとなりました。エラー内容と対処は、 [エラー対処 3. Type error: Argument 2 passed to App\Policies\TaskPolicy::destroy() must be an instance of App\Policies\Task, instance of App\Task given, called in /var/www/vendor/laravel/framework/src/Illuminate/Auth/Access/Gate.php on line 481
] を参照してください。修正するには、 use App\Task;
を追加してください。
<?php namespace App\Policies; use App\User; use Illuminate\Auth\Access\HandlesAuthorization; class TaskPolicy { use HandlesAuthorization; /** * Create a new policy instance. * * @return void */ public function __construct() { // } /** * 指定されたユーザーが指定されたタスクを削除できるか決定 * * @param User $user * @param Task $task * @return bool */ public function destroy(User $user, Task $task) { return $user->id === $task->user_id; } }
次に、入門の通り、 Task
モデルを TaskPolicy
と関連付けました。
<?php namespace App\Providers; use Illuminate\Support\Facades\Gate; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; class AuthServiceProvider extends ServiceProvider { /** * The policy mappings for the application. * * @var array */ protected $policies = [ 'App\Model' => 'App\Policies\ModelPolicy', 'App\Task' => 'App\Policies\TaskPolicy', ]; /** * Register any authentication / authorization services. * * @return void */ public function boot() { $this->registerPolicies(); // } }
アクションの認可
ここでは書いてある内容を理解します。行うことは特にありません。
タスク削除
次のようにしました。
追記: 次のコードで後々エラーとなりました。詳細と対処方法は [エラー対処 2. ReflectionException Class App\Http\Controllers\Task does not exist
] の見出しを参照してください。修正するには use App\Task;
を追加してください。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class TaskController extends Controller { /** * タスクリポジトリーインスタンス * * @var TaskRepository */ protected $tasks; /** * 新しいコントローラーインスタンスの生成 * * @param TaskRepository $tasks * @return void */ public function __construct(TaskRepository $tasks) { $this->middleware('auth'); $this->tasks = $tasks; } /** * ユーザーの全タスクをリスト表示 * * @param Request $request * @return Response */ public function index(Request $request) { return view('tasks.index', [ 'tasks' => $this->tasks->forUser($request->user()), ]); } /** * 新しいタスクの作成 * * @param Request $request * @return Response */ public function store(Request $request) { $this->validate($request, [ 'name' => 'required|max:255', ]); $request->user()->tasks()->create([ 'name' => $request->name, ]); return redirect('/tasks'); } /** * 指定タスクの削除 * * @param Request $request * @param Task $task * @return Response */ public function destroy(Request $request, Task $task) { $this->authorize('destroy', $task); $task->delete(); return redirect('/tasks'); } }
入門は以上です!次は実際に確認してみましょう♪
確認
確認するには、 http://localhost/tasks へウェブブラウザからアクセスします。
エラー対処 1. ReflectionException Class App\Http\Controllers\TaskRepository does not exist
http://localhost/tasks へウェブブラウザからアクセスするといきなりこのエラーです><。
TaskRepository は、 namespace App\Repositories
として追加しました。ですので、 App\Http\Controllers
に見つからないのは当たり前です。
どこかでタイポしたのでしょうか?
調べてみると、 TaskController
の頭で、 use App\Repositories\TaskRepository;
をしていません。抜けています><。 これにより、タスク削除で追加した destroy メソッドの引数の $task の型 Task を見つけられず、エラーとなっています。
もっと見てみると、他にも use
が抜けているのですが、それはおいておきます。今は、目的のエラーひとつだけに集中します。
次のようにして、この問題は解決しました。
<?php namespace App\Http\Controllers; use App\Repositories\TaskRepository; use Illuminate\Http\Request; ... 略 ...
ウェブブラウザから http://localhost/tasks へアクセスすると、 http://localhost/login へリダイレクトされ、ログイン画面が表示されました。
ログインユーザーは作成していないので、 [Register] をクリックして http://localhost/register へ移動し、登録しました。
- Name: test
- E-Mail Address: test@example.com
- Password: testpass
すると、 http://localhost/tasks ページへとリダイレクトし無事に Task ページを表示することができました♪
タスクを追加してみました。。。登録され、同じページにタスクが表示されました。やったぜ!
次に、登録したタスクを削除してみました。。。おや?
エラー対処 2. ReflectionException Class App\Http\Controllers\Task does not exist
似たようなエラーです。そして、このエラーは予想通りです。Task
クラスを TaskController
で use
していないので、TaskController
では Task
クラスを見つけられず使えないのです。
先程と同様に修正しました。
<?php namespace App\Http\Controllers; use App\Repositories\TaskRepository; use App\Task; use Illuminate\Http\Request; ... 略 ...
再び http://localhost/tasks へアクセスし、タスクを [Delete] しました。
すると、このエラーは解消されましたが、、、
エラー対処 3. Type error: Argument 2 passed to App\Policies\TaskPolicy::destroy() must be an instance of App\Policies\Task, instance of App\Task given, called in /var/www/vendor/laravel/framework/src/Illuminate/Auth/Access/Gate.php on line 481
TaskPolicy
クラスの destroy
メソッドが呼ばれたものの、その第 2 引数の型が想定している App\Policies\Task
または App\Task
ではないために発生した、とエラーメッセージが言っているように思えます。
調べてみましょう。
TaskController
の destroy
メソッドに渡ってきた $task
の中身を確認したいです。
dd($task);
を埋め込んでみました。
public function destroy(Request $request, Task $task) { dd($task); // TODO デバッグ $this->authorize('destroy', $task);
出力はこうでした。
Task {#226 ▼ #fillable: array:1 [▼ 0 => "name" ] #connection: "mysql" #table: null #primaryKey: "id" #keyType: "int" +incrementing: true #with: [] #withCount: [] #perPage: 15 +exists: true +wasRecentlyCreated: false #attributes: array:5 [▼ "id" => 1 "user_id" => 1 "name" => "最初のタスク" "created_at" => "2018-11-23 00:26:32" "updated_at" => "2018-11-23 00:26:32" ] #original: array:5 [▼ "id" => 1 "user_id" => 1 "name" => "最初のタスク" "created_at" => "2018-11-23 00:26:32" "updated_at" => "2018-11-23 00:26:32" ] #changes: [] #casts: [] #dates: [] #dateFormat: null #appends: [] #dispatchesEvents: [] #observables: [] #relations: [] #touches: [] +timestamps: true #hidden: [] #visible: [] #guarded: array:1 [▼ 0 => "*" ] }
問題なさそうですよね。。。
ということは、 TaskPolicy
の destroy
メソッドに渡すための指定の仕方あたりに問題があるのでしょうか? TaskPolicy
クラスを重点的に確認してみましょう。
あ、多分これです。またしても、 use
です。 Task
を読み込んでいないために、型を解決できていません。次のようにしました。
<?php namespace App\Policies; use App\Task; use App\User; use Illuminate\Auth\Access\HandlesAuthorization; ... 略 ...
これでタスクページから [Delete] すると、無事タスクを削除できました!やったぜ♪
では最後に、ログアウトしてみましょう。。。おや?
エラー対処 4. MethodNotAllowedHttpException No message
ログアウトできませんでした><。しかも、今回は MethodNotAllowedHttpException であることはわかりましたが、 No message と情報がありません。
ですので、エラーメッセージで検索してみました。
次のページが該当しそうです。
どうやら、ログアウト時に CSRF のトークンを渡せていなさそうです。
そういえば、 feat: make:auth した結果を追加する · oki2a24/my-quickstart-intermediate@2e38a05 後、 feat: レイアウトの定義をする · oki2a24/my-quickstart-intermediate@de846c9 で入門のとおりに書き換えたのでした。
これによって、 CSRF トークンを渡すための処理が消えているように見えます。
改めて、 make:auth した結果のコードを見てみますと、このままで良かったのではないかと思えます。ですので戻してみました。
git checkout 2e38a05 -- resources/views/layouts/app.blade.php
この状態で、 http://localhost へアクセスし、右上の [LOGIN] からログインし、タスクを追加、削除し、ログアウトし、 http://localhost へと戻ってくる、全部できました!
これで 中級者向けタスクリスト 5.2 Laravel は完了です♪ Laravel 5.5 完全に理解した
おわりに
完全に理解した、とは、チュートリアルが終わった、という意味です。
さて、この入門は、結構難しいです。なぜなら、少しコードを書いてウェブブラウザからアクセスして確認する、というサイクルが組み込まれていないからです。
最後までコードを全部書いて、始めて動かすのでエラー対処が非常に大変です。 Laravel 達人なら問題ないでしょうけれども、 Laravel 素人には優しくありません。
それで、確認のときにエラー対処も記録に残しました。
以上です。