■目次
概要環境構築LaravelをインストールLaravelでプログラム作成シンプルなCRUDの新規作成例(カテゴリ管理を作成)複雑なCRUDの新規作成例(記事管理を作成)デバッグテストHomesteadHomestead トラブル対応メモFTPアップロードで稼働させるメモトラブル
■概要
Laravel.txt の内容を踏まえて、実際にLaravel6でプログラムを作成したときのメモ Laravel5から仕様変更されている箇所があるので注意 詳細な解説は公式のドキュメントを参照 6.x Laravel https://readouble.com/laravel/6.x/ja/ 以下に検証用のリポジトリがある refirio/laravel6 at develop https://github.com/refirio/laravel6/tree/develop
■環境構築
VagrantでCentOS7を新規にインストールし、そこに開発環境を構築する 作業フォルダは、ここでは C:\Users\refirio\Vagrant\laravel6 とする VagrantのIPアドレスは、ここでは 192.168.33.10 とする Ansibleの https://github.com/refirio/ansible-nginx_php7_mariadb でローカル開発環境を構築しているが、 Laravelの仕様に合わせてあらかじめ公開フォルダを /var/www/main/html から /var/www/main/public に変更して使用している
■Laravelをインストール
■配置 C:\Users\refirio\Vagrant\laravel6\code\main を C:\Users\refirio\Vagrant\laravel6\code\main_backup に変更する $ sudo su -s /bin/bash - nginx $ cd /var/www $ composer create-project --prefer-dist "laravel/laravel=6.0.*" main ブラウザから以下にアクセスして確認 http://192.168.33.10/ インストール 6.x Laravel https://readouble.com/laravel/6.x/ja/installation.html ■hostsを設定 Vagrant に laravel6.local でアクセスできるようにする C:\Windows\System32\drivers\etc\hosts
192.168.33.10 laravel6.local
ブラウザから以下にアクセスして確認 http://laravel6.local/ ■プログラムの調整 MySQLやMariaDBのバージョンによっては、マイグレーションがエラーになる app/Providers/AppServiceProvider.php を以下のように修正しておくことで対応できる
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Schema; … 追加 〜中略〜 public function boot() { Schema::defaultStringLength(191); … 追加
ただし MariaDB 5.5 は2020年4月11日でサポートが終了されているため、 今なら素直に新しいバージョンのものを使う方が良さそう データベース:マイグレーション 6.x Laravel https://readouble.com/laravel/6.x/ja/migrations.html 「バージョン5.7.7より古いMySQLや、バージョン10.2.2より古いMariaDBを使用している場合、マイグレーションにより生成されるデフォルトのインデックス用文字列長を明示的に設定する必要があります。」 ■初期設定 .env の設定を以下のように編集する(サイトのURLとデータベースへの接続情報を設定)
APP_URL=http://laravel6.local DB_CONNECTION=mysql DB_HOST=localhost DB_PORT=3306 DB_DATABASE=main DB_USERNAME=webmaster DB_PASSWORD=1234
設定 6.x Laravel https://readouble.com/laravel/6.x/ja/configuration.html ■動作確認 http://laravel6.local/
■Laravelでプログラム作成
■言語ファイルを配置 $ cd /var/www/main $ php -r "copy('https://readouble.com/laravel/6.x/ja/install-ja-lang-files.php', 'install-ja-lang.php');" $ php -f install-ja-lang.php $ php -r "unlink('install-ja-lang.php');" validation.php言語ファイル 6.x Laravel https://readouble.com/laravel/6.x/ja/validation-php.html ■タイムゾーンとロケールを変更 config/app.php でタイムゾーンとロケールを変更
'timezone' => 'Asia/Tokyo', 'locale' => 'ja',
設定 6.x Laravel https://readouble.com/laravel/6.x/ja/configuration.html ■デフォルトのマイグレーションを調整 ※調整内容は一例。案件に合わせて調整する 不要なマイグレーションを削除 database/migrations/2019_08_19_000000_create_failed_jobs_table.php デフォルトのマイグレーションを調整 database/migrations/2014_10_12_000000_create_users_table.php
public function up() { Schema::create('users', function (Blueprint $table) { $table->engine = 'InnoDB'; $table->bigIncrements('id'); $table->string('name')->comment('名前'); $table->string('email')->unique()->comment('メールアドレス'); $table->timestamp('email_verified_at')->nullable()->comment('メールアドレス確認日時'); $table->string('password')->comment('パスワード'); $table->rememberToken()->comment('トークン'); $table->timestamps(); }); DB::statement('ALTER TABLE users COMMENT \'ユーザ\''); }
database/migrations/2014_10_12_100000_create_password_resets_table.php
public function up() { Schema::create('password_resets', function (Blueprint $table) { $table->engine = 'InnoDB'; $table->string('email')->index()->comment('メールアドレス'); $table->string('token')->comment('トークン'); $table->timestamp('created_at')->nullable(); }); DB::statement('ALTER TABLE password_resets COMMENT \'パスワードリセット\''); }
マイグレーションを実行 $ php artisan migrate データベース:マイグレーション 6.x Laravel https://readouble.com/laravel/6.x/ja/migrations.html ■認証機能を作成 ユーザ登録と認証機能を作成する ※「php artisan make:auth」はLaravel6では使えない ※「composer require laravel/ui --dev」もLaravel7が出たからかLaravel6ではエラーになるようになった $ composer require laravel/ui 1.* $ php artisan ui vue --auth 画面の右上に「LOGIN」「REGISTER」が表示されていて、ここからログインと登録ができる この時点ではCSSとJavaScriptがコンパイルされていないので、見た目は質素なものになるが動作はする 認証処理の流れはLaravel5から大きく変わっていないようなので、「Laravel.txt」の「認証」にある処理の流れを参考にする 認証 6.x Laravel https://readouble.com/laravel/6.x/ja/authentication.html 更新! Laravel6/7「make:Auth」が無くなった 〜Laravel6/7でのLogin機能の実装方法〜MyMemo - Qiita https://qiita.com/daisu_yamazaki/items/a914a16ca1640334d7a5 Laravel6 ログイン機能を実装する - Qiita https://qiita.com/ucan-lab/items/bd0d6f6449602072cb87 [PHP]Laravel6でmake:authするには - Qiita https://qiita.com/kusumoto-t/items/fc6ef3f5bf1dbe5dc579 Laravel 6でユーザー登録機能・ログイン機能を実装する https://www.no-title.com/programming/laravel/authentication ■CSSとJavaScriptをコンパイルしない代わりにBootstrapを導入 ※npmでコンパイルするのが本来の手順だと思われるが、動作しないのでBootstrapで対応する 以下にBootstrapがある。今回はこれを使用する https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css resources/views/layouts/app.blade.php
<title>{{ config('app.name', 'Laravel') }}</title> <!-- Scripts --> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" defer></script> <!-- Fonts --> <link rel="dns-prefetch" href="//fonts.gstatic.com"> <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet"> <!-- Styles --> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet">
■Userモデルの配置場所を変更 app直下にモデルを大量に作ると管理しづらいので、フォルダにまとめるようにする Laravelは「モデルの役割と置き場所は自分で決めてくれ」という思想 app/User.php を app/Models/User.php へ移動
namespace App; ↓ namespace App\Models;
それに伴い、以下も調整 config/auth.php
'model' => App\User::class, ↓ 'model' => App\Models\User::class,
app/Http/Controllers/Auth/RegisterController.php
use App\User; ↓ use App\Models\User;
■メールの送信 「MAIL_DRIVER=sendmail」での対応だと、ローカルではメール送信にかなりタイムラグがあるみたい いったんGmailのSMTPで送信してみる .env
MAIL_DRIVER=smtp MAIL_HOST=smtp.gmail.com MAIL_PORT=465 MAIL_USERNAME=xxxxx@gmail.com MAIL_PASSWORD=xxxxx MAIL_ENCRYPTION=ssl
パスワードリセット機能でメール送信を試す メールで以下のようなリンクが送られてきて、ここからリセットできる http://laravel6.local/password/reset/dc56b315b5cd842e1ae6b888897bf62e8739902d35186fd02d542f942dd6fc0... ■パスワードリセットメールのカスタマイズ 通知クラスを作成 $ php artisan make:notification ResetPasswordNotification 以下にパスワードリセットの実際のクラスがあるので、これを参考に上記クラスを調整する vendor/laravel/framework/src/Illuminate/Auth/Notifications/ResetPassword.php 完成した ResetPassword.php は以下のとおり。ここではメールの件名のみカスタマイズしている(実際は変数化してconfigなどで管理するといい) なお、「['text' => 'emails.password_reset']」部分を「'emails.password_reset'」にするとHTMLメールとして送信される
<?php namespace App\Notifications; use Illuminate\Auth\Notifications\ResetPassword; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; use Illuminate\Support\Facades\Lang; class ResetPasswordNotification extends ResetPassword { use Queueable; /** * Build the mail representation of the notification. * * @param mixed $notifiable * @return \Illuminate\Notifications\Messages\MailMessage */ public function toMail($notifiable) { if (static::$toMailCallback) { return call_user_func(static::$toMailCallback, $notifiable, $this->token); } return (new MailMessage) /* ->subject(Lang::get('Reset Password Notification')) ->line(Lang::get('You are receiving this email because we received a password reset request for your account.')) ->action(Lang::get('Reset Password'), url(config('app.url').route('password.reset', ['token' => $this->token, 'email' => $notifiable->getEmailForPasswordReset()], false))) ->line(Lang::get('This password reset link will expire in :count minutes.', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')])) ->line(Lang::get('If you did not request a password reset, no further action is required.')); */ ->subject('パスワードリセット通知') ->view(['text' => 'emails.password_reset'], [ 'reset_url' => url(config('app.url').route('password.reset', ['token' => $this->token, 'email' => $notifiable->getEmailForPasswordReset()], false)) ]); } }
メール本文は resources/views/emails/password_reset.blade.php に作成する 文字列に「&」が入るとエスケープされるため、URLをテキストで表示する場合は「!!」を使っておくと無難(この機能に限れば「&」は入らないので問題無いが)
パスワードリセット通知 {!! $reset_url !!}
app/Models/User.php に sendPasswordResetNotification メソッドを追加する このメソッドの実態は vendor/laravel/framework/src/Illuminate/Auth/Passwords/CanResetPassword.php にある CanResetPassword クラスは vendor/laravel/framework/src/Illuminate/Foundation/Auth/User.php 経由で呼ばれている 使用するクラスは、上で作成した ResetPasswordNotification にする
use App\Notifications\ResetPasswordNotification; /** * Send the password reset notification. * * @param string $token * @return void */ public function sendPasswordResetNotification($token) { $this->notify(new ResetPasswordNotification($token)); }
パスワードリセットを行い、「パスワードリセット通知」という件名で通知が来ることを確認する 本文も上で指定したものになっていることを確認する Laravel6.xでパスワードリセットメールをカスタマイズする - Qiita https://qiita.com/daijin/items/8bc658c9b14cdd15260c ■マイページと編集機能を実装 http://laravel6.local/mypage/ から、マイページにログインできるようにする app/Http/Controllers/Auth/LoginController.php app/Http/Controllers/Auth/RegisterController.php app/Http/Controllers/Auth/ResetPasswordController.php app/Http/Controllers/Auth/VerificationController.php を編集
protected $redirectTo = '/home'; ↓ protected $redirectTo = '/mypage';
app/Http/Middleware/RedirectIfAuthenticated.php を編集
return redirect('/home'); ↓ return redirect('/mypage');
app/Http/Controllers/MypageController.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Hash; use App\Http\Controllers\Controller; use App\Models\User; class MypageController extends Controller { /** * インスタンス作成 * * @return void */ public function __construct() { } /** * マイページ * * @param Request $request * @return \Illuminate\Contracts\Support\Renderable */ public function index(Request $request) { return view('mypage/index'); } /** * 基本情報編集画面 * * @param Request $request * @return \Illuminate\Contracts\Support\Renderable */ public function basis(Request $request) { // ユーザー情報取得 $user = Auth::guard()->user(); return view('mypage/basis', [ 'user' => $user, ]); } /** * 基本情報編集 * * @param Request $request * @return \Illuminate\Http\RedirectResponse */ public function basisUpdate(Request $request) { // 入力内容をチェック $validator = Validator::make($request->all(), [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255'], 'password' => ['nullable', 'string', 'min:8', 'confirmed'], ]); if ($validator->fails()) { return redirect('mypage/basis')->withErrors($validator)->withInput(); } // 編集 $userId = Auth::guard()->user()->id; $user = User::find($userId); $user->name = $request->name; $user->email = $request->email; if (!empty($request->password)) { $user->password = Hash::make($request->password); } $user->save(); return redirect('mypage/basis')->with('message', '基本情報を編集しました。'); } }
resources/views/mypage/index.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">マイページ</div> <div class="card-body"> <ul> <li><a href="{{ route('mypage.basis') }}">基本情報編集</a></li> <li><a href="{{ route('logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a></li> </ul> </div> </div> </div> </div> </div> @endsection
resources/views/mypage/basis.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">基本情報編集</div> <div class="card-body"> <form method="POST" action="{{ route('mypage.basis.update') }}"> @csrf @if (session('message')) <div class="box"> <div class="alert alert-success"> {{ session('message') }} </div> </div> @endif <div class="form-group row"> <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label> <div class="col-md-6"> <input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name', $user->name ?? '') }}" required autocomplete="name" autofocus> @error('name') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label> <div class="col-md-6"> <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email', $user->email ?? '') }}" required autocomplete="email"> @error('email') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label> <div class="col-md-6"> <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" autocomplete="new-password"> @error('password') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label> <div class="col-md-6"> <input id="password-confirm" type="password" class="form-control" name="password_confirmation" autocomplete="new-password"> </div> </div> <div class="form-group row mb-0"> <div class="col-md-6 offset-md-4"> <button type="submit" class="btn btn-primary"> {{ __('Register') }} </button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection
routes/web.php にルーティングを追加
// 認証後のコンテンツ Route::group(['middleware' => 'auth'], function () { // マイページ Route::group(['as' => 'mypage'], function () { // マイページ Route::get('/mypage', 'MypageController@index')->name('.index'); // 基本情報編集画面 Route::get('/mypage/basis', 'MypageController@basis')->name('.basis'); // 基本情報編集 Route::post('/mypage/basis_update', 'MypageController@basisUpdate')->name('.basis.update'); }); });
バリデーション 6.x Laravel https://readouble.com/laravel/6.x/ja/validation.html Laravel|【保存版】バリデーションルールのまとめ - わくわくBank https://www.wakuwakubank.com/posts/376-laravel-validation/ Laravel ログイン中、ログイン画面にアクセスした際のリダイレクト先変更 - Qiita https://qiita.com/Ioan/items/551d379fe99ffcbd06b4 ■マルチ認証 http://laravel6.local/mypage/ のマイページは残しつつ http://laravel6.local/admin/login から、管理画面にログインできるようにする database/migrations/2020_04_08_000000_create_admins_table.php
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateAdminsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('admins', function (Blueprint $table) { $table->engine = 'InnoDB'; $table->bigIncrements('id'); $table->string('name')->comment('名前'); $table->string('email')->unique()->comment('メールアドレス'); $table->timestamp('email_verified_at')->nullable()->comment('メールアドレス確認日時'); $table->string('password')->comment('パスワード'); $table->rememberToken()->comment('トークン'); $table->timestamps(); }); DB::statement('ALTER TABLE admins COMMENT \'管理者\''); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('admins'); } }
database/seeds/AdminsTableSeeder.php
<?php use Illuminate\Database\Seeder; class AdminsTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { if (DB::table('admins')->count() > 0) { return; } DB::table('admins')->insert([ 'name' => 'admin', 'email' => 'admin@example.com', 'password' => Hash::make('abcd1234'), 'remember_token' => Str::random(10), ]); } }
database/seeds/DatabaseSeeder.php
// $this->call(UsersTableSeeder::class); ↓ $this->call(AdminsTableSeeder::class);
マイグレーションとシーダーを実行 Seederを追加した直後に「php artisan db:seed」を実行すると、AdminsTableSeederクラスが見つからないとなった 「composer dump-autoload」を実行すれば解決した $ composer dump-autoload $ php artisan migrate $ php artisan db:seed main/app/Models/Admin.php
<?php namespace App\Models; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; class Admin 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', ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'email_verified_at' => 'datetime', ]; }
config/auth.php にある guards の設定に以下を追加
'admin' => [ 'driver' => 'session', 'provider' => 'admins', ],
同ファイルにある providers の設定に以下を追加
'admins' => [ 'driver' => 'eloquent', 'model' => App\Models\Admin::class, ],
同ファイルにある passwords の設定に以下を追加(テーブルは password_resets 以外にすべきか?要検証)
'admins' => [ 'provider' => 'admins', 'table' => 'password_resets', 'expire' => 60, ],
app/Http/Middleware/Authenticate.php に admin 用の分岐を追加
use Illuminate\Support\Facades\Route; if (! $request->expectsJson()) { if (Route::is('admin.*')) { return route('admin.login'); } else { return route('login'); } }
app/Http/Middleware/RedirectIfAuthenticated.php に admin 用の分岐を追加
if (Auth::guard($guard)->check() && $guard === 'user') { return redirect('/mypage'); } elseif (Auth::guard($guard)->check() && $guard === 'admin') { return redirect('/admin/home'); }
app/Http/Controllers/Admin/HomeController.php
<?php namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class HomeController extends Controller { public function __construct() { $this->middleware('auth:admin'); } public function index() { return view('admin.home'); } }
app/Http/Controllers/Admin/Auth/LoginController.php
<?php namespace App\Http\Controllers\Admin\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class LoginController extends Controller { use AuthenticatesUsers; protected $redirectTo = '/admin/home'; public function __construct() { $this->middleware('guest:admin')->except('logout'); } protected function guard() { return Auth::guard('admin'); } public function showLoginForm() { return view('admin.auth.login'); } public function logout(Request $request) { Auth::guard('admin')->logout(); return $this->loggedOut($request); } public function loggedOut(Request $request) { return redirect(route('admin.login')); } }
resources/views/layouts/admin.blade.php
<!doctype html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <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> <!-- Scripts --> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" defer></script> <!-- Fonts --> <link rel="dns-prefetch" href="//fonts.gstatic.com"> <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet"> <!-- Styles --> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div id="app"> <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm"> <div class="container"> <a class="navbar-brand" href="{{ url('/') }}"> {{ config('app.name', 'Laravel') }} </a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <!-- Left Side Of Navbar --> <ul class="navbar-nav mr-auto"> </ul> <!-- Right Side Of Navbar --> <ul class="navbar-nav ml-auto"> <!-- Authentication Links --> @guest <li class="nav-item"> <a class="nav-link" href="{{ route('admin.login') }}">{{ __('Login') }}</a> </li> @else <li class="nav-item dropdown"> <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre> {{ Auth::user()->name }} <span class="caret"></span> </a> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown"> <a class="dropdown-item" href="{{ route('admin.logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();"> {{ __('Logout') }} </a> <form id="logout-form" action="{{ route('admin.logout') }}" method="POST" style="display: none;"> @csrf </form> </div> </li> @endguest </ul> </div> </div> </nav> <main class="py-4"> @yield('content') </main> </div> </body> </html>
resources/views/admin/auth/login.blade.php
@extends('layouts.admin') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">{{ __('Login') }}</div> <div class="card-body"> <form method="POST" action="{{ route('admin.login') }}"> @csrf <div class="form-group row"> <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label> <div class="col-md-6"> <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus> @error('email') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label> <div class="col-md-6"> <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password"> @error('password') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <div class="col-md-6 offset-md-4"> <div class="form-check"> <input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}> <label class="form-check-label" for="remember"> {{ __('Remember Me') }} </label> </div> </div> </div> <div class="form-group row mb-0"> <div class="col-md-8 offset-md-4"> <button type="submit" class="btn btn-primary"> {{ __('Login') }} </button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection
resources/views/admin/home.blade.php
@extends('layouts.admin') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">管理者用ページ</div> <div class="card-body"> <ul> <li><a href="{{ route('admin.logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a></li> </ul> </div> </div> </div> </div> </div> @endsection
routes/web.php に admin 用のルーティングを追加
// 管理者 Route::namespace('Admin')->prefix('admin')->group(function () { Route::name('admin.')->group(function () { // ログイン認証関連 Auth::routes([ 'register' => false, 'reset' => false, 'verify' => false ]); }); // 認証後のコンテンツ Route::middleware('auth:admin')->group(function () { Route::group(['as' => 'admin'], function () { // ダッシュボード Route::get('/home', 'HomeController@index')->name('.home'); }); }); });
Laravel6でマルチ認証を実装する(UserとAdminの階層を分ける) - Qiita https://qiita.com/namizatop/items/5d56d96d4c255a0e3a87 Laravelマルチ認証でセッション用テーブル、設定を分ける(Laravel6で確認済み) - Qiita https://qiita.com/lixwork/items/11f7463d6cf35cb46553 Laravel6.7でマルチログインをできる限り自分好みに実装したはなし - Qiita https://qiita.com/wasipo/items/b06be610bb7ba95954df Laravel6x マルチ認証(userとadmin)の実装でハマった事|Makoto|note https://note.com/makoto0419/n/n9d0e1c1dc90d ■バリデーションをリクエストに移行 デフォルトのコードではコントローラ内でバリデーションを行っているが、リクエストクラスにまとめることが推奨されている ユーザ情報のバリデーションをリクエストクラスにまとめてみる もともとのユーザ新規登録処理は vendor/laravel/framework/src/Illuminate/Foundation/Auth/RegistersUsers.php にある 一部このファイルの処理を参考にしている $ php artisan make:request StoreUserRequest $ php artisan make:request UpdateUserRequest app/Http/Requests/StoreUserRequest.php
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class StoreUserRequest 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', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'string', 'min:8', 'confirmed'], ]; } /** * バリデーションエラーのカスタム属性の取得 * * @return array */ public function attributes() { return [ 'name' => '名前', 'email' => 'メールアドレス', 'password' => 'パスワード', ]; } }
main/app/Http/Requests/UpdateUserRequest.php
<?php namespace App\Http\Requests; class UpdateUserRequest extends StoreUserRequest { /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { $rules = parent::rules(); $rules['email'] = ['required', 'string', 'email', 'max:255']; $rules['password'] = ['nullable', 'string', 'min:8', 'confirmed']; return $rules; } }
main/app/Http/Controllers/MypageController.php
use App\Http\Requests\UpdateUserRequest; /** * 基本情報編集 * * @param Request $request * @return \Illuminate\Http\RedirectResponse */ public function basisUpdate(UpdateUserRequest $request) { // 編集 $userId = Auth::guard()->user()->id;
app/Http/Controllers/Auth/RegisterController.php
use App\Http\Requests\StoreUserRequest; use Illuminate\Auth\Events\Registered; // validatorメソッドは削除 /** * Handle a registration request for the application. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function register(StoreUserRequest $request) { event(new Registered($user = $this->create($request->all()))); $this->guard()->login($user); return $this->registered($request, $user) ?: redirect($this->redirectPath()); }
バリデーション 6.x Laravel https://readouble.com/laravel/6.x/ja/validation.html なお、バリデーション時にエラーがあると自動でリダイレクト処理が走るが、場合によってはその処理が邪魔になることがある これは、StoreUserRequest 内で以下のようにカラの failedValidation を定義することで抑制できるみたい (未検証)
protected function failedValidation(Validator $validator) { }
この場合、コントローラー側で以下のようにしてエラー判定できるみたい (未検証)
protected function register(StoreUserRequest $request) { if ($request->getValidator()->fails()) { $validator = $request->getValidator();
必要に応じて、この書き方を使用するといいかもしれない 【Laravel】フォームリクエストを使いつつ勝手にリダイレクトさせない - Qiita https://qiita.com/rana_kualu/items/77134af5477d27bf2cc2 ■CRUDを作成 管理画面でユーザ情報の登録編集削除をできるようにする app/Http/Controllers/Admin/UserController.php
<?php namespace App\Http\Controllers\Admin; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use App\Http\Controllers\Controller; use App\Http\Requests; use App\Http\Requests\StoreUserRequest; use App\Http\Requests\UpdateUserRequest; use App\Models\User; class UserController extends Controller { /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('auth'); } /** * Display a list of all users. * * @param Request $request * @return Response */ public function index(Request $request) { return view('admin.user.index', [ 'users' => User::orderBy('created_at', 'asc')->get() ]); } /** * Display a form of new user. * * @param Request $request * @return Response */ public function create(Request $request) { return view('admin.user.form'); } /** * Create a new user. * * @param Request $request * @return Response */ public function store(StoreUserRequest $request) { $user = new User; $user->name = $request->name; $user->email = $request->email; $user->password = Hash::make($request->password); $user->save(); return redirect()->route('admin.user.index')->with('message', '登録しました。'); } /** * Display a form of edit user. * * @param Request $request * @return Response */ public function edit(Request $request, $id) { $user = User::find($id); return view('admin.user.form', [ 'user' => $user, ]); } /** * Update a user. * * @param Request $request * @return Response */ public function update(UpdateUserRequest $request, $id) { $user = User::find($id); $user->name = $request->name; $user->email = $request->email; if (!empty($request->password)) { $user->password = Hash::make($request->password); } $user->save(); return redirect()->route('admin.user.index')->with('message', '編集しました。'); } /** * Destroy the given user. * * @param Request $request * @param string $id * @return Response */ public function destroy(Request $request, $id) { User::findOrFail($id)->delete(); return redirect()->route('admin.user.index')->with('message', '削除しました。'); } }
resources/views/admin/home.blade.php
<div class="card-body"> <ul> <li><a href="{{ route('admin.home') }}">ホーム</a></li> <li><a href="{{ route('admin.user.index') }}">ユーザ管理</a></li> <li><a href="{{ route('admin.logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">ログアウト</a></li> </ul> </div>
resources/views/admin/user/index.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">ユーザ一覧</div> <div class="card-body"> @if (session('message')) <div class="box"> <div class="alert alert-success"> {{ session('message') }} </div> </div> @endif <p><a href="{{ route('admin.user.create') }}" class="btn btn-primary">ユーザ登録</a></p> <table class="table table-striped"> <thead> <th>名前</th> <th>メール</th> <th>編集</th> <th>削除</th> </thead> <tbody> @foreach ($users as $user) <tr> <td class="table-text"><div>{{ $user->name }}</div></td> <td class="table-text"><div>{{ $user->email }}</div></td> <td class="table-text"><a href="{{ route('admin.user.edit', ['id' => $user->id]) }}" class="btn btn-primary">Edit</a></td> <td> <form action="{{ route('admin.user.delete', ['id' => $user->id]) }}" method="POST"> {{ csrf_field() }} {{ method_field('DELETE') }} <button type="submit" class="btn btn-danger"> Delete </button> </form> </td> </tr> @endforeach </tbody> </table> </div> </div> </div> </div> @endsection
resources/views/admin/user/form.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">ユーザ @if (!Request::is('*/create')) {{ '編集' }} @else {{ '登録' }} @endif</div> <div class="card-body"> @if (count($errors) > 0) <div class="alert alert-danger"> @foreach ($errors->all() as $error) {{ $error }}<br> @endforeach </div> @endif <form action="{{ Request::is('*/create') ? route('admin.user.create') : route('admin.user.edit', ['id' => $user->id]) }}" method="POST" class="form-horizontal"> @if (!Request::is('*/create')) {{ method_field('put') }} @endif {{ csrf_field() }} <div class="form-group"> <label for="name" class="col-sm-3 control-label">Name</label> <div class="col-sm-6"> <input type="text" name="name" id="name" class="form-control" value="{{ old('name', isset($user) ? $user->name : '') }}"> </div> </div> <div class="form-group"> <label for="email" class="col-sm-3 control-label">Email</label> <div class="col-sm-6"> <input type="text" name="email" id="email" class="form-control" value="{{ old('email', isset($user) ? $user->email : '') }}"> </div> </div> <div class="form-group"> <label for="password" class="col-sm-3 control-label">Password</label> <div class="col-sm-6"> <input type="password" name="password" id="password" class="form-control" value="{{ old('password') }}"> </div> </div> <div class="form-group"> <label for="password-confirm" class="col-sm-3 control-label">Password</label> <div class="col-sm-6"> <input type="password" name="password_confirmation" id="password-confirm" class="form-control"> </div> </div> <div class="form-group"> <div class="col-sm-offset-3 col-sm-6"> <button type="submit" class="btn btn-primary"> <i class="fa fa-btn fa-plus"></i>@if (!Request::is('*/create')) {{ '編集' }} @else {{ '登録' }} @endif </button> </div> </div> </form> </div> </div> </div> </div> @endsection
routes/web.php
// 管理者 Route::namespace('Admin')->prefix('admin')->group(function () { Route::name('admin.')->group(function () { // ログイン認証関連 Auth::routes([ 'register' => false, 'reset' => false, 'verify' => false ]); }); // 認証後のコンテンツ Route::middleware('auth:admin')->group(function () { Route::group(['as' => 'admin'], function () { // ダッシュボード Route::get('/home', 'HomeController@index')->name('.home'); // ユーザ管理 Route::get('/user', 'UserController@index')->name('.user.index'); Route::get('/user/create', 'UserController@create')->name('.user.create'); Route::post('/user/create', 'UserController@store')->name('.user.store'); Route::get('/user/edit/{id}', 'UserController@edit')->name('.user.edit'); Route::put('/user/edit/{id}', 'UserController@update')->name('.user.update'); Route::delete('/user/delete/{id}', 'UserController@destroy')->name('.user.delete'); }); }); });
■サービスとリポジトリを作成 コントローラ内で直接モデルを呼び出さず、サービスとリポジトリを経由するように変更する 設計方針は人それぞれだが、ここでは 「コントローラからは常にサービスを呼び出し、リポジトリとモデルは呼び出さない」 「データベース操作はリポジトリに閉じ込め、テストのために差し替え可能な状態にする」 とする ※app/Http/Controllers/Auth/RegisterController.php ファイルの register メソッド内に event(new Registered($user)); の処理があると、ユーザ登録時にnginxタイムアウトになってしまうことがあった 冒頭で use Illuminate\Auth\Events\Registered; をインポートするようにして解決したみたい ※似て非なる処理が増えて管理しづらくなってしまうので、 「interface Repository」を作って以下にあるような雛形を定義し、 それを継承して「abstract class UserRepository implements Repository」を作って具体的な処理を実装し、 それを「use App\Contracts\Repositories\UserRepository as UserRepositoryContract;」として読み込んで継承して「class UserRepository implements UserRepositoryContract」を作って必要な追加処理があれば実装する …とする方がいいか app/Contracts/Repositories/UserRepository.php
<?php namespace App\Contracts\Repositories; interface UserRepository { /** * 取得 * * @param int $id * @return mixed */ public function find($id); /** * 検索 * * @param array $conditions * @param array $orders * @param int|null $limit * @return mixed */ public function search(array $conditions, array $orders, $limit); /** * 件数 * * @return int */ public function count(); /** * 保存 * * @param array $data * @param int|null $id * @return mixed */ public function save(array $data, $id); /** * 削除 * * @param int $id * @return mixed */ public function delete($id); }
app/Repositories/UserRepository.php
<?php namespace App\Repositories; use App\Contracts\Repositories\UserRepository as UserRepositoryContract; use App\Models\User; class UserRepository implements UserRepositoryContract { /** @var User */ protected $user; /** * コンストラクタ * * @param User $user * @return void */ public function __construct(User $user) { $this->user = $user; } /** * 取得 * * @param int $id * @return mixed */ public function find($id) { return $this->user->find($id); } /** * 検索 * * @param array $conditions * @param array $orders * @param int|null $limit * @return mixed */ public function search(array $conditions = array(), array $orders = array(), $limit = null) { $query = $this->user->query(); $query = $this->setConditions($query, $conditions); foreach ($orders as $order) { $query->orderBy($order[0], $order[1]); } if ($limit == null) { return $query->get(); } else { return $query->paginate($limit); } } /** * 件数 * * @param array $conditions * @return int */ public function count(array $conditions = array()) { $query = $this->user->query(); $query = $this->setConditions($query, $conditions); return $query->count(); } /** * 保存 * * @param array $data * @param int|null $id * @return User */ public function save(array $data, $id = null) { return $this->user->updateOrCreate(['id' => $id], $data); } /** * 削除 * * @param int $id * @return mixed */ public function delete($id) { return $this->user->findOrFail($id)->delete(); } /** * 検索条件を設定 * * @param int $query * @param array $conditions * @return \Illuminate\Database\Query\Builder */ private function setConditions($query, array $conditions = array()) { if (isset($conditions['id'])) { $query->where('id', $conditions['id']); } if (isset($conditions['name'])) { $query->where('name', $conditions['name']); } if (isset($conditions['name_like'])) { $query->where('name', 'like', $conditions['name_like']); } return $query; } }
app/Services/UserService.php
<?php namespace App\Services; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use App\Contracts\Repositories\UserRepository; use App\Http\Requests\StoreUserRequest; use App\Http\Requests\UpdateUserRequest; class UserService { /** @var UserRepository */ protected $userRepository; /** * コンストラクタ * * @param UserRepository $userRepository * @return void */ public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; } /** * 1件取得 * * @param int $id * @return mixed */ public function getUser($id) { return $this->userRepository->find($id); } /** * 検索して取得 * * @param array $conditions * @param array $orders * @param int $limit * @return mixed */ public function getUsers(array $conditions = array(), array $orders = array(), $limit = null) { return $this->userRepository->search($conditions, $orders, $limit); } /** * 件数を取得 * * @param array $conditions * @return int */ public function countUsers(array $conditions = array()) { return $this->userRepository->count($conditions); } /** * 登録 * * @param StoreUserRequest $request * @return \App\Models\User */ public function storeUser(StoreUserRequest $request) { $input = $request->only([ 'name', 'email', 'password', ]); // パスワードを暗号化 $input['password'] = Hash::make($input['password']); // 保存 try { return DB::transaction(function () use ($input) { $result = $this->userRepository->save($input); return $result; }); } catch (\Exception $e) { Log::error('UserService:storeUser', ['message' => $e->getMessage(), 'input' => $input]); return null; } } /** * 編集 * * @param UpdateUserRequest $request * @param int $id * @return \App\Models\User */ public function updateUser(UpdateUserRequest $request, $id) { $input = $request->only([ 'name', 'email', 'password', ]); // パスワードを暗号化 if (empty($input['password'])) { unset($input['password']); } else { $input['password'] = Hash::make($input['password']); } // 保存 try { return DB::transaction(function () use ($input, $id) { $result = $this->userRepository->save($input, $id); return $result; }); } catch (\Exception $e) { Log::error('UserService:updateUser', ['message' => $e->getMessage(), 'input' => $input, 'id' => $id]); return null; } } /** * 削除 * * @param int $id * @return \App\Models\User */ public function deleteUser($id) { try { return DB::transaction(function () use ($id) { return $this->userRepository->delete($id); }); } catch (\Exception $e) { Log::error('UserService:deleteUser', ['message' => $e->getMessage(), 'id' => $id]); return null; } } }
app/Providers/AppServiceProvider.php
public function register() { $this->app->bind( \App\Contracts\Repositories\UserRepository::class, \App\Repositories\UserRepository::class ); }
resources/views/admin/user/index.blade.php
@elseif (session('error')) <div class="box"> <div class="alert alert-danger"> {{ session('error') }} </div> </div> @endif
app/Http/Controllers/Auth/RegisterController.php
<?php namespace App\Http\Controllers\Auth; use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Auth\Events\Registered; use App\Http\Controllers\Controller; use App\Http\Requests\StoreUserRequest; use App\Models\User; use App\Services\UserService; 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 = '/mypage'; /** * Create a new controller instance. * * @param UserService $userService * @return void */ public function __construct(UserService $userService) { $this->middleware('guest'); $this->userService = $userService; } /** * Handle a registration request for the application. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function register(StoreUserRequest $request) { event(new Registered($user = $this->userService->storeUser($request))); $this->guard()->login($user); return $this->registered($request, $user) ?: redirect($this->redirectPath()); } }
app/Http/Controllers/MypageController.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Hash; use App\Http\Controllers\Controller; use App\Http\Requests\UpdateUserRequest; use App\Models\User; use App\Services\UserService; class MypageController extends Controller { /** * インスタンス作成 * * @param UserService $userService * @return void */ public function __construct(UserService $userService) { $this->userService = $userService; } /** * マイページ * * @param Request $request * @return \Illuminate\Contracts\Support\Renderable */ public function index(Request $request) { return view('mypage/index'); } /** * 基本情報編集画面 * * @param Request $request * @return \Illuminate\Contracts\Support\Renderable */ public function basis(Request $request) { // ユーザー情報取得 $user = Auth::guard()->user(); return view('mypage/basis', [ 'user' => $user, ]); } /** * 基本情報編集 * * @param Request $request * @return \Illuminate\Http\RedirectResponse */ public function basisUpdate(UpdateUserRequest $request) { // 編集 $id = Auth::guard()->user()->id; if ($this->userService->updateUser($request, $id)) { return redirect()->route('mypage.basis')->with('message', '基本情報を編集しました。'); } else { return redirect()->route('mypage.basis')->with('message', '基本情報を編集できませんでした。'); } } }
app/Http/Controllers/Admin/UserController.php
<?php namespace App\Http\Controllers\Admin; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Http\Requests; use App\Http\Requests\StoreUserRequest; use App\Http\Requests\UpdateUserRequest; use App\Models\User; use App\Services\UserService; class UserController extends Controller { /** * Create a new controller instance. * * @param UserService $userService * @return void */ public function __construct(UserService $userService) { $this->middleware('auth'); $this->userService = $userService; } /** * Display a list of all users. * * @param Request $request * @return Response */ public function index(Request $request) { return view('admin.user.index', [ 'users' => $this->userService->getUsers([], [['id', 'desc']]), ]); } /** * Display a form of new user. * * @param Request $request * @return Response */ public function create(Request $request) { return view('admin.user.form'); } /** * Create a new user. * * @param Request $request * @return Response */ public function store(StoreUserRequest $request) { if ($this->userService->storeUser($request)) { return redirect()->route('admin.user.index')->with('message', 'ユーザを登録しました。'); } else { return redirect()->route('admin.user.index')->with('error', 'ユーザを登録できませんでした。'); } } /** * Display a form of edit user. * * @param Request $request * @return Response */ public function edit(Request $request, $id) { return view('admin.user.form', [ 'user' => $this->userService->getUser($id), ]); } /** * Update a user. * * @param Request $request * @return Response */ public function update(UpdateUserRequest $request, $id) { if ($this->userService->updateUser($request, $id)) { return redirect()->route('admin.user.index')->with('message', 'ユーザを編集しました。'); } else { return redirect()->route('admin.user.index')->with('error', 'ユーザを編集できませんでした。'); } } /** * Destroy the given user. * * @param Request $request * @param string $id * @return Response */ public function destroy(Request $request, $id) { if ($this->userService->deleteUser($id)) { return redirect()->route('admin.user.index')->with('message', 'ユーザを削除しました。'); } else { return redirect()->route('admin.user.index')->with('error', 'ユーザを削除できませんでした。'); } } }
■新規ユーザ登録時に確認画面を表示 データ登録前に確認画面を表示する例として作成 データの一時的な保持にはフォームのhiddenではなく、セッションを使う routes/web.php
Auth::routes(); Route::post('confirm', 'Auth\RegisterController@confirm')->name('confirm');
app/Http/Controllers/Auth/RegisterController.php
use Illuminate\Http\Request; /** * Show the application registration form. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function showRegistrationForm(Request $request) { if ($request->referer === 'confirm' && $request->session()->has('post.register')) { $post = $request->session()->get('post.register'); } else { $post = []; } return view('auth.register', [ 'user' => $post, ]); } /** * Confirm a registration request for the application. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Contracts\Support\Renderable */ protected function confirm(StoreUserRequest $request) { $post = $request->all(); $request->session()->put('post.register', $post); return view('auth.confirm', [ 'user' => $post, ]); } /** * Handle a registration request for the application. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function register(Request $request) { if (!$request->session()->has('post.register')) { return redirect()->route('register'); } $post = $request->session()->get('post.register'); $postRequest = new StoreUserRequest(); $postRequest->merge($post); $request->session()->forget('post.register'); event(new Registered($user = $this->userService->storeUser($postRequest))); $this->guard()->login($user); return $this->registered($request, $user) ?: redirect($this->redirectPath()); }
resources/views/auth/register.blade.php
<form method="POST" action="{{ route('confirm') }}"> <input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name', $user['name'] ?? '') }}" required autocomplete="name" autofocus> <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email', $user['email'] ?? '') }}" required autocomplete="email"> <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" value="{{ old('password', $user['password'] ?? '') }}" required autocomplete="new-password"> <input id="password-confirm" type="password" class="form-control" name="password_confirmation" value="{{ old('password_confirmation', $user['password_confirmation'] ?? '') }}" required autocomplete="new-password">
resources/views/auth/confirm.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">{{ __('Register') }}</div> <div class="card-body"> <form method="POST" action="{{ route('register') }}"> @csrf <div class="form-group row"> <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label> <div class="col-md-6"> {{ $user['name'] }} </div> </div> <div class="form-group row"> <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label> <div class="col-md-6"> {{ $user['email'] }} </div> </div> <div class="form-group row"> <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label> <div class="col-md-6"> ********** </div> </div> <div class="form-group row mb-0"> <div class="col-md-6 offset-md-4"> <a href="{{ route('register') }}?referer=confirm">Back</a> <button type="submit" class="btn btn-primary"> {{ __('Register') }} </button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection
■論理削除に対応 データを物理削除から論理削除に変更する まずはマイグレーションを作成 $ php artisan make:migration alter_tables 作成されたマイグレーションを以下のように修正
/** * Run the migrations. * * @return void */ public function up() { Schema::table('users', function (Blueprint $table) { $table->softDeletes(); }); Schema::table('admins', function (Blueprint $table) { $table->softDeletes(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('users', function (Blueprint $table) { $table->dropColumn('deleted_at'); }); Schema::table('admins', function (Blueprint $table) { $table->dropColumn('deleted_at'); }); }
マイグレーションを実行 $ php artisan migrate プログラムを修正 app\Models\User.php
use Illuminate\Database\Eloquent\SoftDeletes; use Notifiable, SoftDeletes;
app\Models\Admin.php
use Illuminate\Database\Eloquent\SoftDeletes; use Notifiable, SoftDeletes;
■CSSファイルやJSファイルのキャッシュ読み込み対策を行う ファイル名の後ろに、自動的にタイムスタンプが付くようにする app\Providers\BladeServiceProvider.php を作成
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Blade; class BladeServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { // } /** * Bootstrap any application services. * * @return void */ public function boot() { /** * CSSファイルやJSファイルのパスに、ファイルのタイムスタンプを追加 * * @param string $file * @return string */ Blade::directive('asset', function ($file) { $file = str_replace(["'", '"'], '', $file); $path = public_path() . '/' . $file; try { $timestamp = "?<?php echo \File::lastModified('{$path}') ?>"; } catch (\Exception $e) { $timestamp = ''; } return asset($file) . $timestamp; /* // ロードバランサーを考慮する場合(環境によってはURLを自動取得できないので、.envのAPP_URLを参照) $app_url = env('APP_URL'); if (empty($app_url)) { return asset($file) . $timestamp; } else { return $app_url . '/' . $file . $timestamp; } */ }); } }
config\app.php を編集
'providers' => [ 〜略〜 App\Providers\BladeServiceProvider::class,
ビュー内で以下のように asset を使用すると、ファイル名の最後に「?1597309521」のような値が追加される
<script src="@asset('js/common.js')" defer></script> <link href="@asset('css/common.css')" rel="stylesheet">
■シンプルなCRUDの新規作成例(カテゴリ管理を作成)
マイグレーションを作成 $ php artisan make:migration create_categories_table 作成されたマイグレーションを以下のように修正
/** * Run the migrations. * * @return void */ public function up() { Schema::create('categories', function (Blueprint $table) { $table->engine = 'InnoDB'; $table->bigIncrements('id'); $table->string('name')->nullable()->comment('名前'); $table->integer('sort')->comment('並び順'); $table->timestamps(); $table->softDeletes(); }); DB::statement('ALTER TABLE categories COMMENT \'カテゴリ\''); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('categories'); }
マイグレーションを実行 $ php artisan migrate プログラムを作成 app/Models/Category.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Category extends Model { use SoftDeletes; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'sort', ]; }
app/Contracts/Repositories/CategoryRepository.php
<?php namespace App\Contracts\Repositories; interface CategoryRepository { /** * 取得 * * @param int $id * @return mixed */ public function find($id); /** * 検索 * * @param array $conditions * @param array $orders * @param int|null $limit * @return mixed */ public function search(array $conditions, array $orders, $limit); /** * 件数 * * @return int */ public function count(); /** * 保存 * * @param array $data * @param int|null $id * @return mixed */ public function save(array $data, $id); /** * 削除 * * @param int $id * @return mixed */ public function delete($id); }
app/Repositories/CategoryRepository.php
<?php namespace App\Repositories; use App\Contracts\Repositories\CategoryRepository as CategoryRepositoryContract; use App\Models\Category; class CategoryRepository implements CategoryRepositoryContract { /** @var Category */ protected $category; /** * コンストラクタ * * @param Category $category * @return void */ public function __construct(Category $category) { $this->category = $category; } /** * 取得 * * @param int $id * @return mixed */ public function find($id) { return $this->category->find($id); } /** * 検索 * * @param array $conditions * @param array $orders * @param int|null $limit * @return mixed */ public function search(array $conditions = array(), array $orders = array(), $limit = null) { $query = $this->category->query(); $query = $this->setConditions($query, $conditions); foreach ($orders as $order) { $query->orderBy($order[0], $order[1]); } if ($limit == null) { return $query->get(); } else { return $query->paginate($limit); } } /** * 件数 * * @param array $conditions * @return int */ public function count(array $conditions = array()) { $query = $this->category->query(); $query = $this->setConditions($query, $conditions); return $query->count(); } /** * 保存 * * @param array $data * @param int|null $id * @return Category */ public function save(array $data, $id = null) { return $this->category->updateOrCreate(['id' => $id], $data); } /** * 削除 * * @param int $id * @return mixed */ public function delete($id) { return $this->category->findOrFail($id)->delete(); } /** * 検索条件を設定 * * @param int $query * @param array $conditions * @return \Illuminate\Database\Query\Builder */ private function setConditions($query, array $conditions = array()) { if (isset($conditions['id'])) { $query->where('id', $conditions['id']); } if (isset($conditions['name'])) { $query->where('name', $conditions['name']); } if (isset($conditions['name_like'])) { $query->where('name', 'like', $conditions['name_like']); } return $query; } }
app/Services/CategoryService.php
<?php namespace App\Services; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use App\Contracts\Repositories\CategoryRepository; use App\Http\Requests\StoreCategoryRequest; use App\Http\Requests\UpdateCategoryRequest; class CategoryService { /** @var CategoryRepository */ protected $categoryRepository; /** * コンストラクタ * * @param CategoryRepository $categoryRepository * @return void */ public function __construct(CategoryRepository $categoryRepository) { $this->categoryRepository = $categoryRepository; } /** * 1件取得 * * @param int $id * @return mixed */ public function getCategory($id) { return $this->categoryRepository->find($id); } /** * 検索して取得 * * @param array $conditions * @param array $orders * @param int $limit * @return mixed */ public function getCategories(array $conditions = array(), array $orders = array(), $limit = null) { return $this->categoryRepository->search($conditions, $orders, $limit); } /** * 件数を取得 * * @param array $conditions * @return int */ public function countCategories(array $conditions = array()) { return $this->categoryRepository->count($conditions); } /** * 登録 * * @param StoreCategoryRequest $request * @return \App\Models\Category */ public function storeCategory(StoreCategoryRequest $request) { $input = $request->only([ 'name', 'sort', ]); // 保存 try { return DB::transaction(function () use ($input) { $result = $this->categoryRepository->save($input); return $result; }); } catch (\Exception $e) { Log::error('CategoryService:storeCategory', ['message' => $e->getMessage(), 'input' => $input]); return null; } } /** * 編集 * * @param UpdateCategoryRequest $request * @param int $id * @return \App\Models\Category */ public function updateCategory(UpdateCategoryRequest $request, $id) { $input = $request->only([ 'name', 'sort', ]); // 保存 try { return DB::transaction(function () use ($input, $id) { $result = $this->categoryRepository->save($input, $id); return $result; }); } catch (\Exception $e) { Log::error('CategoryService:updateCategory', ['message' => $e->getMessage(), 'input' => $input, 'id' => $id]); return null; } } /** * 削除 * * @param int $id * @return \App\Models\Category */ public function deleteCategory($id) { try { return DB::transaction(function () use ($id) { return $this->categoryRepository->delete($id); }); } catch (\Exception $e) { Log::error('CategoryService:deleteCategory', ['message' => $e->getMessage(), 'id' => $id]); return null; } } }
app/Providers/AppServiceProvider.php
$this->app->bind( \App\Contracts\Repositories\CategoryRepository::class, \App\Repositories\CategoryRepository::class );
app/Http/Requests/StoreCategoryRequest.php
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class StoreCategoryRequest 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', 'string', 'max:255'], 'sort' => ['required', 'integer'], ]; } /** * バリデーションエラーのカスタム属性の取得 * * @return array */ public function attributes() { return [ 'name' => '名前', 'sort' => '並び順', ]; } }
main/app/Http/Requests/UpdateCategoryRequest.php
<?php namespace App\Http\Requests; class UpdateCategoryRequest extends StoreCategoryRequest { }
app/Http/Controllers/Admin/CategoryController.php
<?php namespace App\Http\Controllers\Admin; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Http\Requests; use App\Http\Requests\StoreCategoryRequest; use App\Http\Requests\UpdateCategoryRequest; use App\Models\Category; use App\Services\CategoryService; class CategoryController extends Controller { /** * Create a new controller instance. * * @param CategoryService $categoryService * @return void */ public function __construct(CategoryService $categoryService) { $this->middleware('auth'); $this->categoryService = $categoryService; } /** * Display a list of all categories. * * @param Request $request * @return Response */ public function index(Request $request) { return view('admin.category.index', [ 'categories' => $this->categoryService->getCategories([], [['id', 'desc']]), ]); } /** * Display a form of new category. * * @param Request $request * @return Response */ public function create(Request $request) { return view('admin.category.form'); } /** * Create a new category. * * @param Request $request * @return Response */ public function store(StoreCategoryRequest $request) { if ($this->categoryService->storeCategory($request)) { return redirect()->route('admin.category.index')->with('message', 'カテゴリを登録しました。'); } else { return redirect()->route('admin.category.index')->with('error', 'カテゴリを登録できませんでした。'); } } /** * Display a form of edit category. * * @param Request $request * @return Response */ public function edit(Request $request, $id) { return view('admin.category.form', [ 'category' => $this->categoryService->getCategory($id), ]); } /** * Update a category. * * @param Request $request * @return Response */ public function update(UpdateCategoryRequest $request, $id) { if ($this->categoryService->updateCategory($request, $id)) { return redirect()->route('admin.category.index')->with('message', 'カテゴリを編集しました。'); } else { return redirect()->route('admin.category.index')->with('error', 'カテゴリを編集できませんでした。'); } } /** * Destroy the given category. * * @param Request $request * @param string $id * @return Response */ public function destroy(Request $request, $id) { if ($this->categoryService->deleteCategory($id)) { return redirect()->route('admin.category.index')->with('message', 'カテゴリを削除しました。'); } else { return redirect()->route('admin.category.index')->with('error', 'カテゴリを削除できませんでした。'); } } }
resources/views/admin/home.blade.php
<li><a href="{{ route('admin.category.index') }}">カテゴリ管理</a></li>
resources/views/admin/category/index.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">カテゴリ一覧</div> <div class="card-body"> @if (session('message')) <div class="box"> <div class="alert alert-success"> {{ session('message') }} </div> </div> @elseif (session('error')) <div class="box"> <div class="alert alert-danger"> {{ session('error') }} </div> </div> @endif <p><a href="{{ route('admin.category.create') }}" class="btn btn-primary">カテゴリ登録</a></p> <table class="table table-striped"> <thead> <th>名前</th> <th>並び順</th> <th>編集</th> <th>削除</th> </thead> <tbody> @foreach ($categories as $category) <tr> <td class="table-text"><div>{{ $category->name }}</div></td> <td class="table-text"><div>{{ $category->sort }}</div></td> <td class="table-text"><a href="{{ route('admin.category.edit', ['id' => $category->id]) }}" class="btn btn-primary">Edit</a></td> <td> <form action="{{ route('admin.category.delete', ['id' => $category->id]) }}" method="POST"> {{ csrf_field() }} {{ method_field('DELETE') }} <button type="submit" class="btn btn-danger"> Delete </button> </form> </td> </tr> @endforeach </tbody> </table> </div> </div> </div> </div> @endsection
resources/views/admin/category/form.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">カテゴリ @if (!Request::is('*/create')) {{ '編集' }} @else {{ '登録' }} @endif</div> <div class="card-body"> @if (count($errors) > 0) <div class="alert alert-danger"> @foreach ($errors->all() as $error) {{ $error }}<br> @endforeach </div> @endif <form action="{{ Request::is('*/create') ? route('admin.category.create') : route('admin.category.edit', ['id' => $category->id]) }}" method="POST" class="form-horizontal"> @if (!Request::is('*/create')) {{ method_field('put') }} @endif {{ csrf_field() }} <div class="form-group"> <label for="name" class="col-sm-3 control-label">名前</label> <div class="col-sm-6"> <input type="text" name="name" id="name" class="form-control" value="{{ old('name', isset($category) ? $category->name : '') }}"> </div> </div> <div class="form-group"> <label for="sort" class="col-sm-3 control-label">並び順</label> <div class="col-sm-6"> <input type="text" name="sort" id="sort" class="form-control" value="{{ old('sort', isset($category) ? $category->sort : '') }}"> </div> </div> <div class="form-group"> <div class="col-sm-offset-3 col-sm-6"> <button type="submit" class="btn btn-primary"> <i class="fa fa-btn fa-plus"></i>@if (!Request::is('*/create')) {{ '編集' }} @else {{ '登録' }} @endif </button> </div> </div> </form> </div> </div> </div> </div> @endsection
routes/web.php
// カテゴリ管理 Route::get('/category', 'CategoryController@index')->name('.category.index'); Route::get('/category/create', 'CategoryController@create')->name('.category.create'); Route::post('/category/create', 'CategoryController@store')->name('.category.store'); Route::get('/category/edit/{id}', 'CategoryController@edit')->name('.category.edit'); Route::put('/category/edit/{id}', 'CategoryController@update')->name('.category.update'); Route::delete('/category/delete/{id}', 'CategoryController@destroy')->name('.category.delete');
■複雑なCRUDの新規作成例(記事管理を作成)
マイグレーションを作成 $ php artisan make:migration create_entries_table 作成されたマイグレーションを以下のように修正 bigInteger の引数は、「カラム名」「自動採番するか」「0以上の正の整数とするか」となっている 「$table->bigInteger('user_id')->unsigned();」と書けるかも。それならその方が直感的でいいかも 要検証
/** * Run the migrations. * * @return void */ public function up() { Schema::create('entries', function (Blueprint $table) { $table->engine = 'InnoDB'; $table->bigIncrements('id'); $table->dateTime('datetime')->comment('日時'); $table->string('title')->comment('タイトル'); $table->text('body')->comment('本文'); $table->bigInteger('user_id', false, true); $table->timestamps(); $table->softDeletes(); }); DB::statement('ALTER TABLE entries COMMENT \'記事\''); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('entries'); }
マイグレーションを実行 $ php artisan migrate プログラムを作成 app/Models/Entry.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Entry extends Model { use SoftDeletes; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'datetime', 'title', 'body', 'user_id', ]; /** * The attributes that should be cast to native types. * * @var array */ protected $casts = [ 'datetime' => 'datetime', ]; }
app/Contracts/Repositories/EntryRepository.php
<?php namespace App\Contracts\Repositories; interface EntryRepository { /** * 取得 * * @param int $id * @return mixed */ public function find($id); /** * 検索 * * @param array $conditions * @param array $orders * @param int|null $limit * @return mixed */ public function search(array $conditions, array $orders, $limit); /** * 件数 * * @return int */ public function count(); /** * 保存 * * @param array $data * @param int|null $id * @return mixed */ public function save(array $data, $id); /** * 削除 * * @param int $id * @return mixed */ public function delete($id); }
app/Repositories/EntryRepository.php
<?php namespace App\Repositories; use App\Contracts\Repositories\EntryRepository as EntryRepositoryContract; use App\Models\Entry; class EntryRepository implements EntryRepositoryContract { /** @var Entry */ protected $entry; /** * コンストラクタ * * @param Entry $entry * @return void */ public function __construct(Entry $entry) { $this->entry = $entry; } /** * 取得 * * @param int $id * @return mixed */ public function find($id) { return $this->entry->find($id); } /** * 検索 * * @param array $conditions * @param array $orders * @param int|null $limit * @return mixed */ public function search(array $conditions = array(), array $orders = array(), $limit = null) { $query = $this->entry->query(); $query = $this->setConditions($query, $conditions); foreach ($orders as $order) { $query->orderBy($order[0], $order[1]); } if ($limit == null) { return $query->get(); } else { return $query->paginate($limit); } } /** * 件数 * * @param array $conditions * @return int */ public function count(array $conditions = array()) { $query = $this->entry->query(); $query = $this->setConditions($query, $conditions); return $query->count(); } /** * 保存 * * @param array $data * @param int|null $id * @return Entry */ public function save(array $data, $id = null) { return $this->entry->updateOrCreate(['id' => $id], $data); } /** * 削除 * * @param int $id * @return mixed */ public function delete($id) { return $this->entry->findOrFail($id)->delete(); } /** * 検索条件を設定 * * @param int $query * @param array $conditions * @return \Illuminate\Database\Query\Builder */ private function setConditions($query, array $conditions = array()) { if (isset($conditions['id'])) { $query->where('id', $conditions['id']); } if (isset($conditions['title'])) { $query->where('title', $conditions['title']); } if (isset($conditions['title_like'])) { $query->where('title', 'like', $conditions['title_like']); } return $query; } }
app/Services/EntryService.php
<?php namespace App\Services; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use App\Contracts\Repositories\EntryRepository; use App\Http\Requests\StoreEntryRequest; use App\Http\Requests\UpdateEntryRequest; class EntryService { /** @var EntryRepository */ protected $entryRepository; /** * コンストラクタ * * @param EntryRepository $entryRepository * @return void */ public function __construct(EntryRepository $entryRepository) { $this->entryRepository = $entryRepository; } /** * 1件取得 * * @param int $id * @return mixed */ public function getEntry($id) { return $this->entryRepository->find($id); } /** * 検索して取得 * * @param array $conditions * @param array $orders * @param int $limit * @return mixed */ public function getEntries(array $conditions = array(), array $orders = array(), $limit = null) { return $this->entryRepository->search($conditions, $orders, $limit); } /** * 件数を取得 * * @param array $conditions * @return int */ public function countEntries(array $conditions = array()) { return $this->entryRepository->count($conditions); } /** * 登録 * * @param StoreEntryRequest $request * @return \App\Models\Entry */ public function storeEntry(StoreEntryRequest $request) { $input = $request->only([ 'datetime', 'title', 'body', 'user_id', ]); // 保存 try { return DB::transaction(function () use ($input) { $result = $this->entryRepository->save($input); return $result; }); } catch (\Exception $e) { Log::error('EntryService:storeEntry', ['message' => $e->getMessage(), 'input' => $input]); return null; } } /** * 編集 * * @param UpdateEntryRequest $request * @param int $id * @return \App\Models\Entry */ public function updateEntry(UpdateEntryRequest $request, $id) { $input = $request->only([ 'datetime', 'title', 'body', 'user_id', ]); // 保存 try { return DB::transaction(function () use ($input, $id) { $result = $this->entryRepository->save($input, $id); return $result; }); } catch (\Exception $e) { Log::error('EntryService:updateEntry', ['message' => $e->getMessage(), 'input' => $input, 'id' => $id]); return null; } } /** * 削除 * * @param int $id * @return \App\Models\Entry */ public function deleteEntry($id) { try { return DB::transaction(function () use ($id) { return $this->entryRepository->delete($id); }); } catch (\Exception $e) { Log::error('EntryService:deleteEntry', ['message' => $e->getMessage(), 'id' => $id]); return null; } } }
app/Providers/AppServiceProvider.php
$this->app->bind( \App\Contracts\Repositories\EntryRepository::class, \App\Repositories\EntryRepository::class );
app/Http/Requests/StoreEntryRequest.php
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class StoreEntryRequest 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 [ 'datetime' => ['required'], 'title' => ['required', 'string', 'max:255'], 'body' => ['required'], 'user_id' => ['required', 'integer'], ]; } /** * バリデーションエラーのカスタム属性の取得 * * @return array */ public function attributes() { return [ 'datetime' => '日時', 'title' => 'タイトル', 'body' => '本文', 'user_id' => 'ユーザ', ]; } }
main/app/Http/Requests/UpdateEntryRequest.php
<?php namespace App\Http\Requests; class UpdateEntryRequest extends StoreEntryRequest { }
app/Http/Controllers/Admin/EntryController.php
<?php namespace App\Http\Controllers\Admin; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Http\Requests; use App\Http\Requests\StoreEntryRequest; use App\Http\Requests\UpdateEntryRequest; use App\Models\Entry; use App\Services\EntryService; use App\Services\UserService; class EntryController extends Controller { /** * Create a new controller instance. * * @param EntryService $entryService * @param UserService $userService * @return void */ public function __construct( EntryService $entryService, UserService $userService ) { $this->middleware('auth'); $this->entryService = $entryService; $this->userService = $userService; } /** * Display a list of all entries. * * @param Request $request * @return Response */ public function index(Request $request) { return view('admin.entry.index', [ 'entries' => $this->entryService->getEntries([], [['id', 'desc']]), ]); } /** * Display a form of new entry. * * @param Request $request * @return Response */ public function create(Request $request) { return view('admin.entry.form', [ 'users' => $this->userService->getUsers(), ]); } /** * Create a new entry. * * @param Request $request * @return Response */ public function store(StoreEntryRequest $request) { if ($this->entryService->storeEntry($request)) { return redirect()->route('admin.entry.index')->with('message', '記事を登録しました。'); } else { return redirect()->route('admin.entry.index')->with('error', '記事を登録できませんでした。'); } } /** * Display a form of edit entry. * * @param Request $request * @return Response */ public function edit(Request $request, $id) { return view('admin.entry.form', [ 'entry' => $this->entryService->getEntry($id), 'users' => $this->userService->getUsers(), ]); } /** * Update a entry. * * @param Request $request * @return Response */ public function update(UpdateEntryRequest $request, $id) { if ($this->entryService->updateEntry($request, $id)) { return redirect()->route('admin.entry.index')->with('message', '記事を編集しました。'); } else { return redirect()->route('admin.entry.index')->with('error', '記事を編集できませんでした。'); } } /** * Destroy the given entry. * * @param Request $request * @param string $id * @return Response */ public function destroy(Request $request, $id) { if ($this->entryService->deleteEntry($id)) { return redirect()->route('admin.entry.index')->with('message', '記事を削除しました。'); } else { return redirect()->route('admin.entry.index')->with('error', '記事を削除できませんでした。'); } } }
resources/views/admin/home.blade.php
<li><a href="{{ route('admin.entry.index') }}">記事管理</a></li>
resources/views/admin/entry/index.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">記事一覧</div> <div class="card-body"> @if (session('message')) <div class="box"> <div class="alert alert-success"> {{ session('message') }} </div> </div> @elseif (session('error')) <div class="box"> <div class="alert alert-danger"> {{ session('error') }} </div> </div> @endif <p><a href="{{ route('admin.entry.create') }}" class="btn btn-primary">記事登録</a></p> <table class="table table-striped"> <thead> <th>日時</th> <th>タイトル</th> <th>編集</th> <th>削除</th> </thead> <tbody> @foreach ($entries as $entry) <tr> <td class="table-text"><div>{{ $entry->datetime->format('Y/m/d H:i:s') }}</div></td> <td class="table-text"><div>{{ $entry->title }}</div></td> <td class="table-text"><a href="{{ route('admin.entry.edit', ['id' => $entry->id]) }}" class="btn btn-primary">Edit</a></td> <td> <form action="{{ route('admin.entry.delete', ['id' => $entry->id]) }}" method="POST"> {{ csrf_field() }} {{ method_field('DELETE') }} <button type="submit" class="btn btn-danger"> Delete </button> </form> </td> </tr> @endforeach </tbody> </table> </div> </div> </div> </div> @endsection
resources/views/admin/entry/form.blade.php
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">記事 @if (!Request::is('*/create')) {{ '編集' }} @else {{ '登録' }} @endif</div> <div class="card-body"> @if (count($errors) > 0) <div class="alert alert-danger"> @foreach ($errors->all() as $error) {{ $error }}<br> @endforeach </div> @endif <form action="{{ Request::is('*/create') ? route('admin.entry.create') : route('admin.entry.edit', ['id' => $entry->id]) }}" method="POST" class="form-horizontal"> @if (!Request::is('*/create')) {{ method_field('put') }} @endif {{ csrf_field() }} <div class="form-group"> <label for="datetime" class="col-sm-3 control-label">日時</label> <div class="col-sm-6"> <input type="text" name="datetime" id="datetime" class="form-control" value="{{ old('datetime', isset($entry) ? $entry->datetime : '') }}"> </div> </div> <div class="form-group"> <label for="title" class="col-sm-3 control-label">タイトル</label> <div class="col-sm-6"> <input type="text" name="title" id="title" class="form-control" value="{{ old('title', isset($entry) ? $entry->title : '') }}"> </div> </div> <div class="form-group"> <label for="body" class="col-sm-3 control-label">本文</label> <div class="col-sm-12"> <textarea name="body" id="body" rows="10" cols="10" class="form-control">{{{ old('body', isset($entry) ? $entry->body : '') }}}</textarea> </div> </div> <div class="form-group"> <label for="user_id" class="col-sm-3 control-label">ユーザ</label> <div class="col-sm-6"> <select name="user_id" class="form-control"> <option value=""></option> @foreach ($users as $user) <option value="{{ $user->id }}" @if (old('user_id', isset($entry) ? $entry->user_id : '') == $user->id) selected @endif>{{ $user->name }}</option> @endforeach </select> </div> </div> <div class="form-group"> <div class="col-sm-offset-3 col-sm-6"> <button type="submit" class="btn btn-primary"> <i class="fa fa-btn fa-plus"></i>@if (!Request::is('*/create')) {{ '編集' }} @else {{ '登録' }} @endif </button> </div> </div> </form> </div> </div> </div> </div> @endsection
routes/web.php
// 記事管理 Route::get('/entry', 'EntryController@index')->name('.entry.index'); Route::get('/entry/create', 'EntryController@create')->name('.entry.create'); Route::post('/entry/create', 'EntryController@store')->name('.entry.store'); Route::get('/entry/edit/{id}', 'EntryController@edit')->name('.entry.edit'); Route::put('/entry/edit/{id}', 'EntryController@update')->name('.entry.update'); Route::delete('/entry/delete/{id}', 'EntryController@destroy')->name('.entry.delete');
■1対1でリレーション(記事に関連するユーザを取得) app/Models/Entry.php に以下を追加
/** * 記事に関連するユーザを取得 */ public function user() { return $this->belongsTo('App\Models\User'); }
resources/views/admin/entry/index.blade.php のテーブルヘッダ部分とテーブルデータ部分に、それぞれ以下を追加
<th>ユーザ</th> <td class="table-text"><div>{{ $entry->user->name }}</div></td>
このとき、一覧表示のために発行されるSQLは以下のようになる 記事数に応じてselectの数が増える、いわゆる「N+1問題」によって処理速度が遅くなる select * from `entries` where `entries`.`deleted_at` is null order by `id` desc select * from `users` where `users`.`id` = 14 and `users`.`deleted_at` is null limit 1 select * from `users` where `users`.`id` = 1 and `users`.`deleted_at` is null limit 1 select * from `users` where `users`.`id` = 47 and `users`.`deleted_at` is null limit 1 select * from `users` where `users`.`id` = 2 and `users`.`deleted_at` is null limit 1 select * from `users` where `users`.`id` = 1 and `users`.`deleted_at` is null limit 1 app/Repositories/EntryRepository.php の search() 内にある以下の部分を変更
$query = $this->entry->query(); ↓ $query = $this->entry->query()->with('user');
これなら最初に必要なデータを一括して取得するので、上記の問題は解消できる select * from `entries` where `entries`.`deleted_at` is null order by `id` desc select * from `users` where `users`.`id` in (1, 2, 14, 47) and `users`.`deleted_at` is null Eloquent:リレーション 6.x Laravel https://readouble.com/laravel/6.x/ja/eloquent-relationships.html ■多対多でリレーション(記事に関連するカテゴリを複数登録して取得) マイグレーションを作成 $ php artisan make:migration create_category_entry_table 作成されたマイグレーションを以下のように修正
/** * Run the migrations. * * @return void */ public function up() { Schema::create('category_entry', function (Blueprint $table) { $table->bigInteger('category_id', false, true)->index(); $table->bigInteger('entry_id', false, true)->index(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('category_entry'); }
マイグレーションを実行 $ php artisan migrate 引き続きプログラムを実装する app/Models/Entry.php に以下を追加
/** * 記事に関連するカテゴリを取得 */ public function categories() { return $this->belongsToMany('App\Models\Category'); }
app/Repositories/EntryRepository.php
/** * 取得 * * @param int $id * @return mixed */ public function find($id) { return $this->entry->with(['categories' => function($query) { $query->orderBy('sort'); }, 'user'])->find($id); } /** * 検索 * * @param array $conditions * @param array $orders * @param int|null $limit * @return mixed */ public function search(array $conditions = array(), array $orders = array(), $limit = null) { $query = $this->entry->query()->with(['categories' => function($query) { $query->orderBy('sort'); }, 'user']); 〜中略〜 /** * 保存 * * @param array $data * @param int|null $id * @return Entry */ public function save(array $data, $id = null) { $entry = $this->entry->updateOrCreate(['id' => $id], $data); $model = $this->entry->find($entry->id); $model->categories()->sync($data['categories']); return $entry; }
app/Services/EntryService.php
/** * 登録 * * @param StoreEntryRequest $request * @return \App\Models\Entry */ public function storeEntry(StoreEntryRequest $request) { $input = $request->only([ 'datetime', 'title', 'body', 'categories', 'user_id', ]); 〜中略〜 /** * 編集 * * @param UpdateEntryRequest $request * @param int $id * @return \App\Models\Entry */ public function updateEntry(UpdateEntryRequest $request, $id) { $input = $request->only([ 'datetime', 'title', 'body', 'categories', 'user_id', ]);
app/Http/Requests/StoreEntryRequest.php
/** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'datetime' => ['required'], 'title' => ['required', 'string', 'max:255'], 'body' => ['required'], 'categories' => ['required'], 'user_id' => ['required', 'integer'], ]; } /** * バリデーションエラーのカスタム属性の取得 * * @return array */ public function attributes() { return [ 'datetime' => '日時', 'title' => 'タイトル', 'body' => '本文', 'categories' => 'カテゴリ', 'user_id' => 'ユーザ', ]; }
app/Http/Controllers/Admin/EntryController.php
<?php namespace App\Http\Controllers\Admin; use Illuminate\Http\Request; use App\Http\Controllers\Controller; use App\Http\Requests; use App\Http\Requests\StoreEntryRequest; use App\Http\Requests\UpdateEntryRequest; use App\Models\Entry; use App\Services\EntryService; use App\Services\CategoryService; use App\Services\UserService; class EntryController extends Controller { /** * Create a new controller instance. * * @param EntryService $entryService * @param CategoryService $categoryService * @param UserService $userService * @return void */ public function __construct( EntryService $entryService, CategoryService $categoryService, UserService $userService ) { $this->middleware('auth'); $this->entryService = $entryService; $this->categoryService = $categoryService; $this->userService = $userService; } /** * Display a list of all entries. * * @param Request $request * @return Response */ public function index(Request $request) { return view('admin.entry.index', [ 'entries' => $this->entryService->getEntries([], [['id', 'desc']]), ]); } /** * Display a form of new entry. * * @param Request $request * @return Response */ public function create(Request $request) { return view('admin.entry.form', [ 'categories' => $this->categoryService->getCategories(), 'users' => $this->userService->getUsers(), ]); } /** * Create a new entry. * * @param Request $request * @return Response */ public function store(StoreEntryRequest $request) { if ($this->entryService->storeEntry($request)) { return redirect()->route('admin.entry.index')->with('message', '記事を登録しました。'); } else { return redirect()->route('admin.entry.index')->with('error', '記事を登録できませんでした。'); } } /** * Display a form of edit entry. * * @param Request $request * @return Response */ public function edit(Request $request, $id) { $entry = $this->entryService->getEntry($id); return view('admin.entry.form', [ 'entry' => $entry, 'entry_categories' => array_column($entry->categories->toArray(), 'id'), 'categories' => $this->categoryService->getCategories(), 'users' => $this->userService->getUsers(), ]); } /** * Update a entry. * * @param Request $request * @return Response */ public function update(UpdateEntryRequest $request, $id) { if ($this->entryService->updateEntry($request, $id)) { return redirect()->route('admin.entry.index')->with('message', '記事を編集しました。'); } else { return redirect()->route('admin.entry.index')->with('error', '記事を編集できませんでした。'); } } /** * Destroy the given entry. * * @param Request $request * @param string $id * @return Response */ public function destroy(Request $request, $id) { if ($this->entryService->deleteEntry($id)) { return redirect()->route('admin.entry.index')->with('message', '記事を削除しました。'); } else { return redirect()->route('admin.entry.index')->with('error', '記事を削除できませんでした。'); } } }
resources/views/admin/entry/index.blade.php
<th>カテゴリ</th> <td class="table-text"> @foreach ($entry->categories as $category) {{ $category->name }} @endforeach </td>
resources/views/admin/entry/form.blade.php
<div class="form-group"> <label class="col-sm-3 control-label">カテゴリ</label> <div class="col-sm-6"> @foreach ($categories as $category) <div> <label class="control-label conditions"><input type="checkbox" name="categories[]" value="{{ $category->id }}" @if (in_array($category->id, old('categories', isset($entry) ? $entry_categories : []))) checked @endif> {{ $category->name }}</label> </div> @endforeach </div> </div>
Eloquent:リレーション 6.x Laravel https://readouble.com/laravel/6.x/ja/eloquent-relationships.html
■デバッグ
■デバッグモード .env
APP_DEBUG=true
■デバッグバー barryvdh/laravel-debugbar をインストールすると、 デバッグモードが有効のときにデバッグバーが表示される また、storage/debugbar/.gitignore が作成される $ composer require barryvdh/laravel-debugbar Using version ^3.4 for barryvdh/laravel-debugbar ./composer.json has been updated Loading composer repositories with package information Updating dependencies (including require-dev) Package operations: 2 installs, 0 updates, 0 removals - Installing maximebf/debugbar (v1.16.3): Downloading (100%) - Installing barryvdh/laravel-debugbar (v3.4.1): Downloading (100%) maximebf/debugbar suggests installing kriswallsmith/assetic (The best way to manage assets) maximebf/debugbar suggests installing predis/predis (Redis storage) Package jakub-onderka/php-console-color is abandoned, you should avoid using it. Use php-parallel-lint/php-console-color instead. Package jakub-onderka/php-console-highlighter is abandoned, you should avoid using it. Use php-parallel-lint/php-console-highlighter instead. Package phpunit/php-token-stream is abandoned, you should avoid using it. No replacement was suggested. Writing lock file Generating optimized autoload files > Illuminate\Foundation\ComposerScripts::postAutoloadDump > @php artisan package:discover --ansi Discovered Package: barryvdh/laravel-debugbar Discovered Package: facade/ignition Discovered Package: fideloper/proxy Discovered Package: laravel/tinker Discovered Package: laravel/ui Discovered Package: nesbot/carbon Discovered Package: nunomaduro/collision Package manifest generated successfully. 16 packages you are using are looking for funding. Use the `composer fund` command to find out more! ■ログ 以下の場所にアプリケーションログが保存される storage/logs
■テスト
■概要 テストの方針は案件ごとに決めておく 今回の方針は ・コントローラからはサービスを呼び出す(リポジトリやモデルを直接呼び出さない) ・サービスの各メソッドに対してテストを書く ・データベースアクセスなしでテストできるように、Mockeryでリポジトリのモックを作る ・HTTPリクエストで各ページの参照をテスト とする ■テストの実行方法 テスト: テストの準備 6.x Laravel https://readouble.com/laravel/6.x/ja/testing.html $ sudo su -s /bin/bash - nginx $ cd /var/www/main $ ./vendor/bin/phpunit PHPUnit 8.5.2 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 5.47 seconds, Memory: 20.00 MB OK (2 tests, 2 assertions) デフォルトでは、以下にあるテストが実行される tests\Unit\ExampleTest.php tests\Feature\ExampleTest.php UnitとFeatureの違いは、公式サイトでは以下のように紹介されている Unit: ユニットテストは極小さい、コードの独立した一部をテストします。 実際、ほとんどのユニット(Unit)テストは一つのメソッドに焦点をあてます。 Feature: 機能テストは、多くのオブジェクトがそれぞれどのように関しているかとか、 JSONエンドポイントへ完全なHTTPリクエストを送ることさえ含む、コードの幅広い範囲をテストします。 以下はユニットテストの例 (以下ははじめから作成されているテスト。値の一致をテストする) tests\Unit\ExampleTest.php
<?php namespace Tests\Unit; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class ExampleTest extends TestCase { /** * A basic test example. * * @return void */ public function testBasicTest() { $this->assertTrue(true); } }
以下は機能テストの例 (以下ははじめから作成されているテスト。トップページへのHTTPリクエストをテストする) tests\Feature\ExampleTest.php
<?php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; class ExampleTest extends TestCase { /** * A basic test example. * * @return void */ public function testBasicTest() { $response = $this->get('/'); $response->assertStatus(200); } }
■テストの作成方法 $ php artisan make:test UserServiceTest --unit Test created successfully. 「--unit」を付けるとUnitディレクトリにテストの雛形が作成され、 付けなければFeatureディレクトリにテストの雛形が作成される 作成された雛形をもとに、独自のテストを作成するといい テスト: テストの準備 7.x Laravel https://readouble.com/laravel/7.x/ja/testing.html 以降で、UserServiceのテストを作成していく ■ダミーデータの作成 テスト時にダミーデータが必要な場合、Fakerを使うことでデータを作成してくれる まずは、Fakerが作成するダミーデータを日本語にする config\app.php
'faker_locale' => 'en_US', ↓ 'faker_locale' => 'ja_JP',
以下でFactoryを作成できる ただし UserFactory ははじめから作成済になっている $ php artisan make:Factory UserFactory 内容は以下のとおり。ダミーのユーザデータを作成してくれる ただし今回のプログラムに合わせて、「use App\User;」を「use App\Models\User;」に変更している database\factories\UserFactory.php
<?php /** @var \Illuminate\Database\Eloquent\Factory $factory */ use App\Models\User; use Faker\Generator as Faker; use Illuminate\Support\Str; /* |-------------------------------------------------------------------------- | Model Factories |-------------------------------------------------------------------------- | | This directory should contain each of the model factory definitions for | your application. Factories provide a convenient way to generate new | model instances for testing / seeding your application's database. | */ $factory->define(User::class, function (Faker $faker) { return [ 'name' => $faker->name, 'email' => $faker->unique()->safeEmail, 'email_verified_at' => now(), 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'remember_token' => Str::random(10), ]; });
[Laravel5.1]Fakerチートシート - Qiita https://qiita.com/tosite0345/items/1d47961947a6770053af ■テストの編集 先の手順において $ php artisan make:test UserServiceTest --unit で作成したテストを元に作り込むものとする PHPUnit\Framework\TestCase (PHPUnitのクラス)を Tests\TestCase (Laravelのクラス)に差し替えることにより、 Laravelのファサードなどが使えるようになる Laravelで "A facade root has not been set." というエラーが出た場合の対処法 - Qiita https://qiita.com/aminevsky/items/784573234d071c769288 Fakerで作成したデータを各メソッドで使い回す場合など、 共通の初期処理が必要なら setup() で行う PHPUnitのsetUp()を使ったら「must be compatible」とエラーが表示される - Qiita https://qiita.com/stoneBK7/items/fcc898f38ee161b38ef4 tests\Unit\UserServiceTest.php
<?php namespace Tests\Unit; //use Illuminate\Foundation\Testing\RefreshDatabase; //use PHPUnit\Framework\TestCase; use Tests\TestCase; class UserServiceTest extends TestCase { private $users; /** * Setup the test environment. * * @return void */ protected function setUp(): void { parent::setUp(); // ダミーデータを作成 $this->users = factory(\App\Models\User::class, 3)->make(); /* foreach ($this->users as $user) { echo '[' . $user->name . ']'; echo '[' . $user->email . ']'; } */ } /** * Clean up the testing environment before the next test. * * @return void */ protected function tearDown(): void { parent::tearDown(); // ダミーデータを削除 $this->users = null; } /** * 1件取得 * * @return void */ public function testGetUser() { // 1件取得 $user = $this->users; // リポジトリのモックを作成 $userRepositoryMock = \Mockery::mock(\App\Repositories\UserRepository::class); $userRepositoryMock->shouldReceive('find')->andReturn($user); // リポジトリをモックに差し替えてサービスを作成 $userService = new \App\Services\UserService( $userRepositoryMock ); $this->assertEquals($user, $userService->getUser(1)); } /** * 検索して取得 * * @return void */ public function testGetUsers() { // リポジトリのモックを作成 $userRepositoryMock = \Mockery::mock(\App\Repositories\UserRepository::class); $userRepositoryMock->shouldReceive('search')->andReturn($this->users); // リポジトリをモックに差し替えてサービスを作成 $userService = new \App\Services\UserService( $userRepositoryMock ); $this->assertEquals($this->users, $userService->getUsers()); } /** * 件数を取得 * * @return void */ public function testCountUsers() { // リポジトリのモックを作成 $userRepositoryMock = \Mockery::mock(\App\Repositories\UserRepository::class); $userRepositoryMock->shouldReceive('count')->andReturn(count($this->users)); // リポジトリをモックに差し替えてサービスを作成 $userService = new \App\Services\UserService( $userRepositoryMock ); $this->assertEquals(3, $userService->countUsers()); } /** * 登録 * * @return void */ public function testStoreUser() { // リポジトリのモックを作成 $userRepositoryMock = \Mockery::mock(\App\Repositories\UserRepository::class); $userRepositoryMock->shouldReceive('save')->andReturn(new \App\Models\User); // リポジトリをモックに差し替えてサービスを作成 $userService = new \App\Services\UserService( $userRepositoryMock ); $postRequest = new \App\Http\Requests\StoreUserRequest(); $postRequest->merge([ 'name' => 'Taro Yamada', 'email' => 'taro@example.com', 'password' => 'abcd1234', ]); $this->assertInstanceOf('\App\Models\User', $userService->storeUser($postRequest)); } /** * 編集 * * @return void */ public function testUpdateUser() { // リポジトリのモックを作成 $userRepositoryMock = \Mockery::mock(\App\Repositories\UserRepository::class); $userRepositoryMock->shouldReceive('save')->andReturn(new \App\Models\User); // リポジトリをモックに差し替えてサービスを作成 $userService = new \App\Services\UserService( $userRepositoryMock ); $postRequest = new \App\Http\Requests\StoreUserRequest(); $postRequest->merge([ 'name' => 'Taro Yamada', 'email' => 'taro@example.com', 'password' => 'abcd1234', ]); $this->assertInstanceOf('\App\Models\User', $userService->storeUser($postRequest, 1)); } /** * 削除 * * @return void */ public function testDeleteUser() { // リポジトリのモックを作成 $userRepositoryMock = \Mockery::mock(\App\Repositories\UserRepository::class); $userRepositoryMock->shouldReceive('delete')->andReturn(new \App\Models\User); // リポジトリをモックに差し替えてサービスを作成 $userService = new \App\Services\UserService( $userRepositoryMock ); $this->assertInstanceOf('\App\Models\User', $userService->deleteUser(1)); } }
プログラム内の以下の部分で、ダミーデータを作成している factory(\App\Models\User::class, 3)->make(); 以下のように make() ではなく create() を使うと、実際にデータベースにも登録される factory(\App\Models\User::class, 3)->create();
■Homestead
Laravel Homestead 6.x Laravel https://readouble.com/laravel/6.x/ja/homestead.html Laravel環境構築 Homestead-Vagrant(Windows10) | -ANDROIDろいど- プログラム開発入門&情報共有ブログ http://androidroid.info/laravel-homestead-install/ Homesteadをインストールするだけで Ubuntu18 + PHP7 + Nginx + MySQL の環境を構築できる 以下、VirtualBox + Vagrant の環境は構築済みとする ■Homesteadのインストール Homesteadをダウンロードする >cd C:\Users\refirio\Vagrant >vagrant box add laravel/homestead ==> box: Loading metadata for box 'laravel/homestead' box: URL: https://vagrantcloud.com/laravel/homestead This box can work with multiple providers! The providers that it can work with are listed below. Please review the list and choose the provider you will be working with. 1) hyperv 2) parallels 3) virtualbox Enter your choice: 3 … virtualboxで使うので3を入力してEnter ==> box: Adding box 'laravel/homestead' (v11.4.0) for provider: virtualbox box: Downloading: https://vagrantcloud.com/laravel/boxes/homestead/versions/11.4.0/providers/virtualbox.box box: box: Calculating and comparing box checksum... ==> box: Successfully added box 'laravel/homestead' (v11.4.0) for 'virtualbox'! … 5分ほどで完了 >vagrant box list centos/7 (virtualbox, 2004.01) laravel/homestead (virtualbox, 11.4.0) … Homesteadが追加されたことを確認できる Homestead用の領域を作成して起動する ※これは「案件のためのファイルをインストール」ではなく「Homesteadの仕組みそのもののインストール」なので、 案件ごとの作業領域ではなく、Homestead用の領域としてインストールする方が良さそう Homesteadで複数案件を扱いたければ、Homestead内の設定で対応する(案件ごとの領域を作成する) >cd C:\Users\refirio\Vagrant >git clone https://github.com/laravel/homestead.git homestead >cd homestead >git checkout release … 最新の安定バージョンは https://github.com/laravel/homestead/releases で確認。もしくは常に最新の安定バージョンを用意しているreleaseブランチにチェックアウト >init.bat … 設定ファイルを作成(Windowsの場合。Mac/LinuxもしくはWindowsgit bash環境なら「bash init.sh」とする) インストールしたHomesteadのバージョンは、以下に記載されている C:\Users\refirio\Vagrant\homestead\bin\homestead $app = new Symfony\Component\Console\Application('Laravel Homestead', '12.4.1'); ■鍵の作成 gitを使っているなら Git Bash で作成できる WindowsでGitを始めたらまず確認!Git Bashの設定&ショートカット | 株式会社グランフェアズ http://www.granfairs.com/blog/staff/gitbash-setting-shortcut $ mkdir C:\Users\refirio\.ssh $ cd C:\Users\refirio\.ssh $ ssh-keygen -t rsa … もしくは「ssh-keygen -b 4096」とするか Generating public/private rsa key pair. Enter file in which to save the key (/c/Users/refirio/.ssh/id_rsa): … 空ENTER Enter passphrase (empty for no passphrase): … 空ENTER Enter same passphrase again: … 空ENTER Your identification has been saved in /c/Users/refirio/.ssh/id_rsa. Your public key has been saved in /c/Users/refirio/.ssh/id_rsa.pub. The key fingerprint is: SHA256:vjqdyEjv9TO5Ywmox+PfV7yDoF5ljEqbIR/SRFuknBQ refirio@WorkComputer The key's randomart image is: +---[RSA 4096]----+ | Eoo | | + = | | * | | o o | | o.S . +. | | . .*.*.o o | | . * oB+.+ o . | | o O.+=B o o | | ++*+.+* . | +----[SHA256]-----+ これで C:\Users\ユーザ名\.ssh 内に id_rsa と id_rsa.pub が作成される。ここはHomesteadからみると ~/.ssh/ にあたる ■Homesteadの設定 C:\Users\refirio\Vagrant\homestead\Homestead.yaml を編集する 鍵の設定は以下にあるが、上の手順で作成した場所がデフォルトとなっているため変更は不要 他の方法で鍵を作成した場合、必要に応じて設定を変更する
authorize: C:\Users\refirio\.ssh\id_rsa.pub keys: - C:\Users\refirio\.ssh\id_rsa
また、同ファイルで共有フォルダの設定を調整する C:\Users\refirio\Vagrant\homestead\code\homestead\public はあらかじめ作成しておく
folders: - map: ~/code to: /home/vagrant/code sites: - map: homestead.test to: /home/vagrant/code/public ↓ folders: - map: C:\Users\refirio\Vagrant\homestead\code\homestead to: /home/vagrant/homestead sites: - map: homestead.test to: /home/vagrant/homestead/public
■hostsの設定 http://homestead.test/ でアクセスできるようにする C:\Windows\System32\drivers\etc\hosts
192.168.10.10 homestead.test
■Vagrantの起動 初回起動時、「変更を許可しますか?」の確認ダイアログが2回表示された すべて許可して進めた >cd C:\Users\refirio\Vagrant\homestead >vagrant up Bringing machine 'homestead' up with 'virtualbox' provider... ==> homestead: Checking if box 'laravel/homestead' version '11.4.0' is up to date... ==> homestead: Clearing any previously set forwarded ports... ==> homestead: Clearing any previously set network interfaces... ==> homestead: Preparing network interfaces based on configuration... homestead: Adapter 1: nat homestead: Adapter 2: hostonly ==> homestead: Forwarding ports... homestead: 80 (guest) => 8000 (host) (adapter 1) homestead: 443 (guest) => 44300 (host) (adapter 1) homestead: 22 (guest) => 2222 (host) (adapter 1) ==> homestead: Running 'pre-boot' VM customizations... ==> homestead: Booting VM... ==> homestead: Waiting for machine to boot. This may take a few minutes... homestead: SSH address: 127.0.0.1:2222 homestead: SSH username: vagrant homestead: SSH auth method: private key ==> homestead: Machine booted and ready! ==> homestead: Checking for guest additions in VM... ==> homestead: Setting hostname... ==> homestead: Configuring and enabling network interfaces... ==> homestead: Mounting shared folders... homestead: /vagrant => C:/Users/refirio/Vagrant/homestead homestead: /home/vagrant/homestead => C:/Users/refirio/Vagrant/homestead/code/homestead ==> homestead: Detected mount owner ID within mount options. (uid: 1000 guestpath: /home/vagrant/homestead) ==> homestead: Detected mount group ID within mount options. (gid: 1000 guestpath: /home/vagrant/homestead) ==> homestead: Machine already provisioned. Run `vagrant provision` or use the `--provision` ==> homestead: flag to force provisioning. Provisioners marked to run always will still run. ■SSHでの接続 コマンドプロンプトから以下を実行して接続する >vagrant ssh Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-81-generic x86_64) * Homestead v12.4.1 | Thanks for using Homestead * Settler v11.4.0 $ ll homestead/ total 4 drwxrwxrwx 1 vagrant vagrant 0 Aug 21 14:48 ./ drwxr-xr-x 10 vagrant vagrant 4096 Aug 21 15:26 ../ drwxrwxrwx 1 vagrant vagrant 0 Aug 21 14:48 public/ homestead/public/index.php を作成し、以下を入力する
<?php phpinfo() ?>
以下でアクセスできる。PHPは「8.0.9」だった http://homestead.test/ もしくはPoderosaなどで以下に接続する 接続先: 127.0.0.1 ポート: 2222 ユーザ名: vagrant パスワード: vagrant 環境によっては「The negotiation of kex algorithm is failed」というエラーになって接続できないことがあったが、 これはPoderosaのバージョンが古いだけ、かもしれない Poderosaで「The negotiation of encryption algorithm is failed」エラー https://mook.jpn.org/archives/2016/01/post-4505.html 鍵を作成する際に「ssh-keygen -b 4096」ではなく「ssh-keygen -t rsa」とすればいいかと思ったが、これでも解消しなかった もしくはVirtualBoxの「設定」アイコンのある画面で「表示」をクリックするとコンソールが開く ログインを求められるが、 vagrant login: vagrant Password: vagrant でログインできる ■MariaDBを使用する C:\Users\refirio\Vagrant\homestead\Homestead.yaml
features: - mysql: true - mariadb: false - postgresql: false - ohmyzsh: false - webdriver: false ↓ features: - mysql: true - mariadb: true - postgresql: false - ohmyzsh: false - webdriver: false
Laravel Homestead 6.x Laravel https://readouble.com/laravel/6.x/ja/homestead.html#installing-optional-features 「MariaDBを有効にすると、MySQLを削除してMariaDBをインストールします。 MariaDBはMySQLのそのまま置き換え可能な代替機能として動作します。 そのため、アプリケーションのデータベース設定では、mysqlデータベースドライバをそのまま使ってください。」 ■homestead内部に作業領域を追加した例 Vagrantを停止しておく >vagrant halt C:\Users\refirio\Vagrant\homestead\code\sample\public を作成 C:\Users\refirio\Vagrant\homestead\Homestead.yaml を編集
folders: - map: C:\Users\refirio\Vagrant\homestead\code\homestead to: /home/vagrant/homestead - map: C:\Users\refirio\Vagrant\homestead\code\sample to: /home/vagrant/sample sites: - map: homestead.test to: /home/vagrant/homestead/public - map: sample.homestead.test to: /home/vagrant/sample/public databases: - homestead - sample
C:\Windows\System32\drivers\etc\hosts を編集
192.168.10.10 sample.homestead.test
以下で実行 >vagrant up --provision 以下でアクセス http://sample.homestead.test/ ■homestead外部に作業領域を追加した例 C:\Users\refirio\Vagrant\homestead\code はHomestead自体のGit管理領域なので、本格的に作業するなら、この外に作る方がいいかもしれない Vagrantを停止しておく >vagrant halt C:\Users\refirio\Vagrant\homestead_happy-link\code\public を作成 C:\Users\refirio\Vagrant\homestead\Homestead.yaml を編集
folders: - map: C:\Users\refirio\Vagrant\homestead\code\homestead to: /home/vagrant/homestead - map: C:\Users\refirio\Vagrant\homestead\code\sample to: /home/vagrant/sample - map: C:\Users\refirio\Vagrant\homestead_happy-link\code to: /home/vagrant/happy-link sites: - map: homestead.test to: /home/vagrant/homestead/public - map: sample.homestead.test to: /home/vagrant/sample/public - map: happy-link.local to: /home/vagrant/happy-link/public databases: - homestead - sample - happy-link
C:\Windows\System32\drivers\etc\hosts を編集
192.168.10.10 happy-link.local
以下で実行 >vagrant up --provision 以下でアクセス http://happy-link.local/ ■データベースに接続 Laravel Homestead 6.x Laravel https://readouble.com/laravel/6.x/ja/homestead.html#connecting-to-databases 以下で接続できる rootでは接続できない?要確認 $ mysql -u homestead -p secret 以下でPHPプログラムから接続できる
<?php try { $pdo = new PDO( 'mysql:dbname=sample;host=localhost', 'homestead', 'secret' ); $stmt = $pdo->query('SELECT NOW() AS now;'); $data = $stmt->fetch(PDO::FETCH_ASSOC); echo "<p>" . $data['now'] . "</p>\n"; $pdo = null; } catch (PDOException $e) { exit($e->getMessage()); }
■PHPバージョンを指定 Laravel Homestead 6.x Laravel https://readouble.com/laravel/6.x/ja/homestead.html#daily-usage 「複数のPHPバージョン」 C:\Users\refirio\Vagrant\homestead\Homestead.yaml を編集
sites: - map: homestead.test to: /home/vagrant/homestead/public - map: sample.homestead.test to: /home/vagrant/sample/public php: "7.4"
>vagrant reload --provision 以下のエラーが出るようになってしまった Timed out while waiting for the machine to boot. This means that Vagrant was unable to communicate with the guest machine within the configured ("config.vm.boot_timeout" value) time period. 削除すると起動できるようになった ただしこれはたまたまで、本当の問題は別にある…かもしれない もしくは、SSHで接続して以下のようにすることでも変更できる >vagrant ssh $ php74 update-alternatives: using /usr/bin/php7.4 to provide /usr/bin/php (php) in manual mode update-alternatives: using /usr/bin/php-config7.4 to provide /usr/bin/php-config (php-config) in manual mode update-alternatives: using /usr/bin/phpize7.4 to provide /usr/bin/phpize (phpize) in manual mode $ php -v PHP 7.4.22 (cli) (built: Jul 30 2021 13:08:17) ( NTS ) Copyright (c) The PHP Group Zend Engine v3.4.0, Copyright (c) Zend Technologies with Zend OPcache v7.4.22, Copyright (c), by Zend Technologies $ exit >vagrant halt >vagrant up
■Homestead トラブル対応メモ
■起動時にエラー ※未解決 >cd C:\Users\refirio\Vagrant\homestead >vagrant up Bringing machine 'homestead' up with 'virtualbox' provider... ==> homestead: Importing base box 'laravel/homestead'... ==> homestead: Matching MAC address for NAT networking... ==> homestead: Checking if box 'laravel/homestead' version '11.4.0' is up to date... ==> homestead: Setting the name of the VM: homestead ==> homestead: Clearing any previously set network interfaces... ==> homestead: Preparing network interfaces based on configuration... homestead: Adapter 1: nat homestead: Adapter 2: hostonly ==> homestead: Forwarding ports... homestead: 80 (guest) => 8000 (host) (adapter 1) homestead: 443 (guest) => 44300 (host) (adapter 1) homestead: 22 (guest) => 2222 (host) (adapter 1) ==> homestead: Running 'pre-boot' VM customizations... ==> homestead: Booting VM... There was an error while executing `VBoxManage`, a CLI used by Vagrant for controlling VirtualBox. The command and stderr is shown below. Command: ["startvm", "883f2b65-3c93-4840-ac71-c6fe2c9b19fd", "--type", "headless"] Stderr: VBoxManage.exe: error: Failed to open/create the internal network 'HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter #3' (VERR_INTNET_FLT_IF_NOT_FOUND). VBoxManage.exe: error: Failed to attach the network LUN (VERR_INTNET_FLT_IF_NOT_FOUND) VBoxManage.exe: error: Details: code E_FAIL (0x80004005), component ConsoleWrap, interface IConsole VirtualBox で Failed to open/create the internal network 'HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter' が出た時の対処 - Qiita https://qiita.com/ExA_DEV/items/ae80a7d767144c2e1992 VirtualBoxでネットワーク関連のエラー CodeLab(コードラボ)技術ブログ https://www.codelab.jp/blog/?p=3025 "ネットワークと共有センター" を開く(コントロールパネル → ネットワークとインターネット → ネットワークと共有センター) "アダプターの設定の変更" を開く "VirtualBox Host-Only Network #N" のプロパティを開く "VirtualBox NDIS6 Bridged Networking Driver" にチェックを入れる "インターネットプロトコル バージョン6(TCP/IPv6)" のチェックを外す [OK] を押し、プロパティウィンドウを閉じる "VirtualBox Host-Only Network #N" の右クリックメニューで「無効」にする 再度、「有効」にし直す 「VirtualBox Host-Only Network #N」に該当するものとして、「イーサネット4」「イーサネット5」「イーサネット6」があった 「イーサネット4」と「イーサネット6」で「インターネットプロトコル バージョン6(TCP/IPv6)」にチェックが入っていたので、これらに対して作業した >vagrant halt さらにWindowsも再起動 >cd C:\Users\refirio\Vagrant\homestead >vagrant up Bringing machine 'homestead' up with 'virtualbox' provider... ==> homestead: Checking if box 'laravel/homestead' version '11.4.0' is up to date... ==> homestead: Clearing any previously set forwarded ports... ==> homestead: Clearing any previously set network interfaces... ==> homestead: Preparing network interfaces based on configuration... homestead: Adapter 1: nat homestead: Adapter 2: hostonly ==> homestead: Forwarding ports... homestead: 80 (guest) => 8000 (host) (adapter 1) homestead: 443 (guest) => 44300 (host) (adapter 1) homestead: 22 (guest) => 2222 (host) (adapter 1) ==> homestead: Running 'pre-boot' VM customizations... ==> homestead: Booting VM... ==> homestead: Waiting for machine to boot. This may take a few minutes... homestead: SSH address: 127.0.0.1:2222 homestead: SSH username: vagrant homestead: SSH auth method: private key Timed out while waiting for the machine to boot. This means that Vagrant was unable to communicate with the guest machine within the configured ("config.vm.boot_timeout" value) time period. If you look above, you should be able to see the error(s) that Vagrant had when attempting to connect to the machine. These errors are usually good hints as to what may be wrong. If you're using a custom box, make sure that networking is properly working and you're able to connect to the machine. It is a common problem that networking isn't setup properly in these boxes. Verify that authentication configurations are also setup properly, as well. If the box appears to be booting properly, you may want to increase the timeout ("config.vm.boot_timeout") value. まだエラーになる Laravel の Homestead を立ち上げる時に Timed out while waiting for the machine to boot がでる - Qiita https://qiita.com/pugiemonn/items/e4aef6afe3b17d63f9fb 起動したボックスの一覧から「homestead」を選択 右画面に表示される「設定」アイコンをクリック ネットワーク → アダプター1 → 高度 で表示される「ケーブル接続」にチェックを入れる…とあるが、チェックは入っている 特に変更せず、ボックスを再起動してみる >vagrant halt ==> homestead: Attempting graceful shutdown of VM... homestead: homestead: Vagrant insecure key detected. Vagrant will automatically replace homestead: this with a newly generated keypair for better security. homestead: homestead: Inserting generated public key within guest... homestead: Removing insecure key from the guest if it's present... homestead: Key inserted! Disconnecting and reconnecting using new SSH key... >vagrant up Bringing machine 'homestead' up with 'virtualbox' provider... ==> homestead: Checking if box 'laravel/homestead' version '11.4.0' is up to date... ==> homestead: Clearing any previously set forwarded ports... ==> homestead: Clearing any previously set network interfaces... ==> homestead: Preparing network interfaces based on configuration... homestead: Adapter 1: nat homestead: Adapter 2: hostonly ==> homestead: Forwarding ports... homestead: 80 (guest) => 8000 (host) (adapter 1) homestead: 443 (guest) => 44300 (host) (adapter 1) homestead: 22 (guest) => 2222 (host) (adapter 1) ==> homestead: Running 'pre-boot' VM customizations... ==> homestead: Booting VM... ==> homestead: Waiting for machine to boot. This may take a few minutes... homestead: SSH address: 127.0.0.1:2222 homestead: SSH username: vagrant homestead: SSH auth method: private key homestead: Warning: Connection reset. Retrying... homestead: Warning: Connection aborted. Retrying... ==> homestead: Machine booted and ready! ==> homestead: Checking for guest additions in VM... ==> homestead: Setting hostname... ==> homestead: Configuring and enabling network interfaces... ==> homestead: Mounting shared folders... homestead: /vagrant => C:/Users/refirio/Vagrant/homestead homestead: /home/vagrant/homestead => C:/Users/refirio/Vagrant/homestead/code/homestead ==> homestead: Detected mount owner ID within mount options. (uid: 1000 guestpath: /home/vagrant/homestead) ==> homestead: Detected mount group ID within mount options. (gid: 1000 guestpath: /home/vagrant/homestead) ==> homestead: Running provisioner: file... homestead: C:/Users/refirio/Vagrant/homestead/aliases => /tmp/bash_aliases ==> homestead: Running provisioner: handle_aliases (shell)... homestead: Running: inline script ==> homestead: Running provisioner: setting authorize key (shell)... homestead: Running: inline script homestead: homestead: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDHSTaGwKmzv23Uv+HQBFKfXmoh2KJnXXMOMoJsKES4XAoUtoLVWUC4cB04+LTcgtz7FzgTfzTolwdYHdG3Q1waAaoPTBnB0A4hSBQCg9WrAeTMkWao7fjCbsCY9weJuRxIvyE92Y0bC29N0dDNvl2vjOM0XOBCBE154vrJtUt6n0pg2p6/Vv9aZjsrvqhi8LcQsJgf6jXTyTf0L/uPfo6YJ/tLw3wFUR8AUTsKSXJDzpMtTknYPPgYfLgpBFu0pwwFlBNmC7WpDHA/tOJV9XNryKD6FXbFLT27uU+LIHNAKn6JWv8feAK2qRWEH0N4bPQybG+rp8SEDFN8Vw7Ku8wHD2Zir0+HW4+lhtAyh1sccFYeGe9FKlMI3Mr6xkzeph2fPwl7v674XG5IfVLe9+kKq+gpUJTBfiT+Yvpc7j6eLry9nViNo5XC+5GCuQ6hPav1/Q0fAi/SO3WU5Rl9OGHtNrDISDYz5GtglD8m3FJWjTHxfoOgYhYozI4woUZAafIuCQ+DuryAgZ4MpwT07n29Ro80L7t/xspbX0iNW8RXbK8oIT4yEMjb+tZFSswjKxPZo+rJUfWk9FNp2XfDy4q0XIsMh+oSOFhXWyG71M5k5nJTaCDGiE5FP/3hXiTbhDF75ILlV+UmxQ/ScmoI/4pxUGapW3/dlZA3/dL4wyhOWw== refirio@Surface-Laptop ==> homestead: Running provisioner: setting authorize permissions (shell)... homestead: Running: inline script ==> homestead: Running provisioner: mk_features (shell)... homestead: Running: inline script ==> homestead: Running provisioner: own_features (shell)... homestead: Running: inline script ==> homestead: Running provisioner: apt_update (shell)... homestead: Running: inline script homestead: Get:1 https://download.docker.com/linux/ubuntu focal InRelease [52.1 kB] homestead: Get:2 http://security.ubuntu.com/ubuntu focal-security InRelease [114 kB] homestead: Hit:3 http://ppa.launchpad.net/chris-lea/redis-server/ubuntu focal InRelease homestead: Hit:4 http://archive.ubuntu.com/ubuntu focal InRelease homestead: Get:5 http://apt.postgresql.org/pub/repos/apt focal-pgdg InRelease [86.7 kB] homestead: Hit:6 https://deb.nodesource.com/node_14.x focal InRelease homestead: Get:7 http://archive.ubuntu.com/ubuntu focal-updates InRelease [114 kB] homestead: Hit:8 http://ppa.launchpad.net/ondrej/php/ubuntu focal InRelease homestead: Get:9 http://archive.ubuntu.com/ubuntu focal-backports InRelease [101 kB] homestead: Get:10 http://security.ubuntu.com/ubuntu focal-security/main amd64 Packages [822 kB] homestead: Get:11 http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages [1,170 kB] homestead: Get:12 http://apt.postgresql.org/pub/repos/apt focal-pgdg/main amd64 Packages [213 kB] homestead: Get:13 http://security.ubuntu.com/ubuntu focal-security/main i386 Packages [274 kB] homestead: Get:14 http://security.ubuntu.com/ubuntu focal-security/main Translation-en [161 kB] homestead: Get:15 http://archive.ubuntu.com/ubuntu focal-updates/main i386 Packages [522 kB] homestead: Get:16 http://security.ubuntu.com/ubuntu focal-security/restricted amd64 Packages [364 kB] homestead: Get:17 http://security.ubuntu.com/ubuntu focal-security/restricted Translation-en [52.1 kB] homestead: Get:18 http://security.ubuntu.com/ubuntu focal-security/universe amd64 Packages [640 kB] homestead: Get:19 http://archive.ubuntu.com/ubuntu focal-updates/main Translation-en [252 kB] homestead: Get:20 http://archive.ubuntu.com/ubuntu focal-updates/restricted amd64 Packages [410 kB] homestead: Get:21 http://security.ubuntu.com/ubuntu focal-security/universe i386 Packages [509 kB] homestead: Get:22 http://security.ubuntu.com/ubuntu focal-security/universe Translation-en [100 kB] homestead: Get:23 http://archive.ubuntu.com/ubuntu focal-updates/restricted Translation-en [58.7 kB] homestead: Get:24 http://archive.ubuntu.com/ubuntu focal-updates/universe i386 Packages [632 kB] homestead: Get:25 http://archive.ubuntu.com/ubuntu focal-updates/universe amd64 Packages [851 kB] homestead: Get:26 http://archive.ubuntu.com/ubuntu focal-updates/universe Translation-en [179 kB] homestead: Fetched 7,676 kB in 32s (237 kB/s) homestead: Reading package lists... ==> homestead: Running provisioner: shell... homestead: Running: inline script homestead: Invalid feature: mysql ==> homestead: Running provisioner: shell... homestead: Running: inline script homestead: Ignoring feature: mariadb because it is set to false ==> homestead: Running provisioner: shell... homestead: Running: inline script homestead: Ignoring feature: postgresql because it is set to false ==> homestead: Running provisioner: shell... homestead: Running: inline script homestead: Ignoring feature: ohmyzsh because it is set to false ==> homestead: Running provisioner: shell... homestead: Running: inline script homestead: Ignoring feature: webdriver because it is set to false ==> homestead: Running provisioner: shell... homestead: Running: C:/Users/refirio/AppData/Local/Temp/vagrant-shell20210822-3508-1j39xiq.sh ==> homestead: Running provisioner: shell... homestead: Running: C:/Users/refirio/AppData/Local/Temp/vagrant-shell20210822-3508-e4dd1k.sh ==> homestead: Running provisioner: Creating Certificate: homestead.test (shell)... homestead: Running: script: Creating Certificate: homestead.test homestead: Updating certificates in /etc/ssl/certs... homestead: rehash: warning: skipping duplicate certificate in ca.homestead.homestead.pem homestead: 1 added, 0 removed; done. homestead: Running hooks in /etc/ca-certificates/update.d... homestead: done. ==> homestead: Running provisioner: Creating Site: homestead.test (shell)... homestead: Running: script: Creating Site: homestead.test ==> homestead: Running provisioner: shell... homestead: Running: inline script ==> homestead: Running provisioner: shell... homestead: Running: C:/Users/refirio/AppData/Local/Temp/vagrant-shell20210822-3508-zr5pv9.sh ==> homestead: Running provisioner: Checking for old Schedule (shell)... homestead: Running: script: Checking for old Schedule ==> homestead: Running provisioner: Clear Variables (shell)... homestead: Running: script: Clear Variables ==> homestead: Running provisioner: Restarting Cron (shell)... homestead: Running: script: Restarting Cron ==> homestead: Running provisioner: Restart Webserver (shell)... homestead: Running: script: Restart Webserver ==> homestead: Running provisioner: Creating MySQL / MariaDB Database: homestead (shell)... homestead: Running: script: Creating MySQL / MariaDB Database: homestead homestead: We didn't find a PID for mariadb, skipping $DB creation ==> homestead: Running provisioner: Update Composer (shell)... homestead: Running: script: Update Composer homestead: Upgrading to version 2.1.6 (stable channel). homestead: homestead: Use composer self-update --rollback to return to version 2.1.5 ==> homestead: Running provisioner: shell... homestead: Running: C:/Users/refirio/AppData/Local/Temp/vagrant-shell20210822-3508-fpn9o0.sh ==> homestead: Running provisioner: Update motd (shell)... homestead: Running: script: Update motd ==> homestead: Running provisioner: Run after.sh (shell)... homestead: Running: C:/Users/refirio/AppData/Local/Temp/vagrant-shell20210822-3508-14hyo7e.sh これで起動できた…ことがある 駄目な場合、さらに以下を修正 C:\Users\refirio\Vagrant\homestead\Vagrantfile
if Vagrant.has_plugin?('vagrant-notify-forwarder') config.notify_forwarder.enable = true end end
if Vagrant.has_plugin?('vagrant-notify-forwarder') config.notify_forwarder.enable = true end if Vagrant.has_plugin?('vagrant-vbguest') config.vbguest.auto_update = false end end
vagrant upでGuestAdditions seems to be installed (6.0.14) correctly, but not running. The following SSH command responded with a non-zero exit status. - Qiita https://qiita.com/Kosuke-nick/items/7e7b49804219d04071e2 Vagrant/VirtualBoxで「GuestAdditions seems to be installed (6.0.8) correctly, but not running.」出たときの対処法 - Qiita https://qiita.com/sola-msr/items/ce6ea764b5236554cb32 これで起動できた…ことがある 改めて起動を試して、また「Timed out while waiting for the machine to boot.」のエラーになることがあった Vagrantの停止&起動で正常に起動できたようだが原因は不明 以下をインストールすることで、フォルダ同期が高速になるとされる ただしインストールしても状況は変わらず >vagrant plugin install vagrant-winnfsd 整理すると以下の状況 現状、タイムアウトになったりならなかったり…なので開発環境として使うのは難しい ・まったく起動できないわけでは無いので、「根本的にWindowsが対応していない」ということは無いはず ・正常に起動できたものを「vagrant reload」や「vagrant halt」「vagrant up」で再起動するとタイムアウトになることがある ・不意に起動できるようになることがある…ような いったん起動できたものを、 何も変更せずに「vagrant reload」して再起動できることを確認し、 再度実行しても問題なく、さらに実行すると5分ほどでタイムアウトになる …という状態 なお、Homestead以外のボックスは問題なく起動できる 原因不明 ■Poderosaで接続エラー ※未解決 PoderosaからSSHで接続しようとすると 「The negotiation of kex algorithm is failed」 というエラーになった Poderosaで「The negotiation of encryption algorithm is failed」エラー https://mook.jpn.org/archives/2016/01/post-4505.html Poderosaのバージョンが古いだけ、みたい? ただ、上記ページでは 「新しいbetaの Poderosa 4.3.16 を使ってみたところ、ちゃんと接続することが出来ました。」 とあるが、上記エラーが発生したのは 4.3.16 環境だった
■FTPアップロードで稼働させる
■サーバ要件 以下が必要な要件で、この環境が整っている前提 ここにFTPアップロードでLaravel6を設置し、動作させる インストール 6.x Laravel https://readouble.com/laravel/6.x/ja/installation.html ■テスト環境の作成 今回は、EC2上にテスト環境を作成するものとする $ sudo su - # amazon-linux-extras install ansible2 -y # amazon-linux-extras install nginx1.12 -y # amazon-linux-extras install php7.2 -y 以下のPlaybookを実行する。あらかじめ、公開ディレクトリは html から public に変更しておく nginx_php7_mariadb # cd /var/www # mv main main_backup # mkdir main # chown ec2-user. main SFTPの ec2-user ユーザで以下を作成し、ブラウザからアクセスして動作を確認する /var/www/main/public/index.php ■プロジェクトの作成 まずはローカルのVagrantなど、環境の整った場所でプロジェクトを作成する このファイルを後ほどテスト環境に設置する $ sudo su -s /bin/bash - nginx $ /var/www $ composer create-project --prefer-dist "laravel/laravel=6.0.*" main ■プログラムの設置と動作確認 あらかじめ作成しておいたプロジェクトを、テスト環境にSFTPの ec2-user ユーザでアップロードする vendorフォルダのアップロードには、高速な回線でも10分程度かかった(アップロード先によっては30分程度かかった) それ以外はすぐにアップロードできた この状態でブラウザからアクセスすると、以下のエラーが表示された
UnexpectedValueException The stream or file "/var/www/laravel/storage/logs/laravel-2020-08-11.log" could not be opened: failed to open stream: Permission denied
/var/www/main/storage のディレクトリのパーミッションを777にする(下層含めて) また、ファイルパーミッションを666にする(下層含めて) これでLaravelが動作した ■artisanの実行 一例だが、以下のPHPプログラムを作成し、ブラウザからアクセスするとartisanを起動できる ただし重い処理を行うと、nginxやApacheでタイムアウトになる可能性があるので注意
<?php echo shell_exec('cd ..;php artisan --version;');
■プログラムの更新 ・ファイルはFTPで上書きアップロードすることになる ・追加で「compsoer install」が必要な場合、まずは vendor_20200811 などにアップロードし、 完了したら vendor と vendor_20200811 の名前を入れ替え、その状態で致命的なエラーにならないか確認するか ・マイグレーションやキャッシュの削除は、上記の方法でartisanを実行して対応するか ■メモ Laravelアプリケーションをレンタルサーバーで動作させる - Qiita https://qiita.com/hitotch/items/5c8c2858c883e69cd507 ロリポップでLaravelをSSHを使わずにFTPでアップロードする - forget for get http://lightwill.hatenablog.com/entry/2019/10/04/155314 Laravelをさくらインターネットのレンタルサーバにデプロイ・アップロードする方法をまとめてみた | Arrown https://arrown-blog.com/laravel-sakura-deploy/
■メモ
■引き続き Laravel.txt の内容を再検証して移植する 最低限のモデル作成&操作をメモしておきたい マイグレーションの列作成は「$table->bigInteger('user_id')->unsigned();」のように書けるかも。それならその方が直感的でいいかも 以下にusersとrolesを中間テーブルrole_userで紐付けてbelongsToManyで関係を定義する方法が紹介されている 参考にしてEntryとCategoryを多対多で紐付けてみたい Eloquent:リレーション 6.x Laravel https://readouble.com/laravel/6.x/ja/eloquent-relationships.html $request->only のカラムはモデル内で定義しておくと集中管理できていいか https://bitbucket.org/terraportinc/nokai_school/src/ed44b21f598fbb1deedf9c7d9aee4a6d5f510636/app/Ser... 登録直前に再度バリデーション。ValidationErrorExceptionの例外処理も含めて https://bitbucket.org/terraportinc/nokai_school/src/ed44b21f598fbb1deedf9c7d9aee4a6d5f510636/app/Htt... 現状Contractがあまり生かされていない?もしくは現状で十分? 一応、今サービスで呼び出しているのはリポジトリのContractではある 以下で紹介されている「クラスの差し替えを容易にする」「機能のドキュメントとして利用できるようにする」は実現できているとは思う 契約 6.x Laravel https://readouble.com/laravel/6.x/ja/contracts.html マイグレーションファイル内のクラス名が重複していても、マイグレーションの一括実行はできるか 今もできないなら、自分で新規にマイグレーションを作成した場合は CreateAdminsTable20200408000000 のようにクラス名に日時を含めるか マイグレーションの not null や unsigned の書き方を検討したい マイグレーションでのコメント追加、単体テスト時にエラーになるか ALTER TABLE の実行を「if (!app()->runningUnitTests()) { 〜 }」で囲う必要があるか fallback_locale も最初に設定するか と思ったが、以下で「ここは該当言語が見つからない場合の言語です。「en」がいいでしょう。」とあるので、デフォルトのままで良さそう 【Laravel5.6】インストール直後にやること3点 - console dot log https://blog.capilano-fw.com/?p=289 Laravel管理画面「laravel-admin」を日本語化する。設定ファイルで対応 https://diy.hot-maker.com/454/ 汎用的な検索用に getUsers メソッドがあるが、原則 getAdminUsers など専用にサービスのメソッドを設けるべきか 直接リポジトリを呼ぶという手もあるが、煩雑になるので常にサービス経由で呼ぶ方が良さそう またそれなら、サービスに対してだけテストを書けば良くなる 記事管理を作る ユーザの単一選択、カテゴリの複数選択、ファイルアップロード、プレビュー などを検証したい 単体テストを書く リポジトリクラスを差し替えてテストしたりも試したい LaravelでCRUD - Qiita https://qiita.com/apricotcomic/items/07f172a957c1f342fd91 【Laravel】DB登録値取得時のfind()、get()、first()の返り値早見表 - Qiita https://qiita.com/sola-msr/items/fac931c72e1c46ae5f0f デバッグ用の命令 Laravelのdd() - Qiita https://qiita.com/kyohei121877/items/ba596b57d6027cc21166 以下はLaravelに関する後発の記事。参考になるかも Laravel 6 基本のタスクリスト - Qiita https://qiita.com/ucan-lab/items/36f6e89abad26a68f69a Laravel入門 - 使い方チュートリアル - - Qiita https://qiita.com/sano1202/items/6021856b70e4f8d3dc3d ■更新する場合 SSHで接続して以下を実行 $ sudo su -s /bin/bash - nginx $ cd /var/www/main $ composer install $ php artisan migrate $ php artisan db:seed $ php artisan config:clear $ php artisan view:clear ■複雑なクエリ ※検証中 コントローラーに複雑なクエリは書かずに、 サービス経由でリポジトリを呼び出して、その中で具体的なクエリを定義するか 例えば、リポジトリで以下のように定義しておく
if (isset($conditions['payment_at'])) { $query->leftJoin('user_campaigns', 'user_accounts.user_campaign_id', '=', 'user_campaigns.id'); $query->where(function($query) use ($conditions) { $query->where(function($query) use ($conditions) { $query->whereNull('user_campaign_id'); $query->where('registered_at', '<', $conditions['payment_at']); }); $query->orWhere(function($query) use ($conditions) { $query->whereNotNull('user_campaign_id'); $query->whereRaw('DATE_ADD(registered_at, INTERVAL user_campaigns.continuation MONTH) < ?', [$conditions['payment_at']]); }); }); }
コントローラーから以下のように呼び出す
$users = $this->userAccountService->searchPaymentAccounts([ 'payment_at' => '2021-10-01 16:55:00', ], [['user_accounts.id', 'asc']], null); foreach ($users as $user) { // 何かしらの処理 }
■トラブル
■Laravelインストール時に「GitHub API limit (0 calls/hr) is exhausted」と表示される composerでGitHub apiのアクセス制限対処法 - Qiita https://qiita.com/ma_me/items/7d1823c27d6ef45cdf9f $ curl -u 'refirio' -d '{"scopes":["repo"],"note":"Help example"}' https://api.github.com/authorizations $ Enter host password for user 'refirio': { "message": "Bad credentials", "documentation_url": "https://developer.github.com/v3" } -bash-4.2$ Enter host password for user 'refirio': { "id": 401877446, "url": "https://api.github.com/authorizations/401877446", "app": { "name": "Help example", "url": "https://developer.github.com/v3/oauth_authorizations/", "client_id": "00000000000000000000" }, "token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "hashed_token": "8175101dd3d26e7217010770ef6df4a35d9681c10572b46cd51cb29137937e96", "token_last_eight": "27a14caf", "note": "Help example", "note_url": null, "created_at": "2020-03-26T01:58:26Z", "updated_at": "2020-03-26T01:58:26Z", "scopes": [ "repo" ], "fingerprint": null } $ composer config -g github-oauth.github.com XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX $ composer create-project --prefer-dist "laravel/laravel=6.0.*" main ■キャッシュクリア時に「Failed to clear cache.」と表示される 以下のようにエラーになる $ php artisan cache:clear Failed to clear cache. Make sure you have the appropriate permissions. 以下のフォルダ内にある、すべてのフォルダを削除することで解消できた(dataフォルダ自体を削除するのでは無い) storage/framework/cache/data $ php artisan cache:clear Application cache cleared! LaravelでキャッシュクリアするとFailed to clear cacheエラーが出る-faster than the speed of light http://faster-than-the-sol.blogspot.com/2020/04/laravel-cache-clear-error.html Laravel 5.7 で php artisan cache:clear を叩くと Failed to clear cache. Make sure you have the appropriate permissions. になる。。 - Qiita https://qiita.com/tyamahori/items/a94b5717f1f1cab5e1b9 ■CSSやJavaScriptを作成(解決できず) 認証機能作成のために $ php artisan ui vue --auth を実行すると以下が表示されるので、それぞれインストールする Please run "npm install && npm run dev" to compile your fresh scaffolding. $ npm install でインストールすると以下のエラーになる EPROTO: protocol error, symlink '../@babel/parser/bin/babel-parser.js' -> '/var/www/main/node_modules/.bin/parser' $ npm install --no-bin-links でインストールすると通った $ npm run dev で実行すると以下のエラーになる sh: cross-env: コマンドが見つかりません C:\Users\refirio\Vagrant\laravel6\code\main\package.json にある「cross-env」をすべて「cross-env」に置換して再度実行する node.js が無い場合、あらかじめ以下でインストールしておく インストールしたユーザが使えるようになるようなので、nginxユーザのままで作業する $ cd $ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash $ . ~/.nvm/nvm.sh $ nvm install stable $ node -v v13.11.0 AWSのEC2インスタンスにNode.jsを入れる - Qiita https://qiita.com/knxrvb/items/1edcf2bab47f4b238ea2 Windows環境でnpm installでエラーが発生する場合の対処法 - Qiita https://qiita.com/sansaisoba/items/0fd5c77c9fbbfe61a629 MACにLaravel6をインストール | アールエフェクト https://reffect.co.jp/laravel/install-laravel6-in-mac Vue環境作る時のnpmで出るエラーの対処 覚書 | だいぢんのアイドル日記ブログ https://ameblo.jp/minister0926/entry-12463135199.html laravelでnpm run devを実行すると「cross-env: not found」というエラーが出る件対応したった - Qiita https://qiita.com/Yorinton/items/fd9dae33c6748abcdfbc 以下にyumでのインストール手順がある 基本的にこの方がいいかも?でも複数バージョンの切り替えを考えるなら上の手順?要確認 CentOS 7 Node.js のインストール手順 (yum を利用) - Qiita https://qiita.com/daskepon/items/16a77868d38f8e585840