- 投稿日:2019-10-23T23:14:54+09:00
【サルが書く】Laravel 基本情報編
Laravelのとくちょう
わきゃ
最近Laravelに触る機会ができたので、基本的なところをまとめたいと思います。1. 学習コストが低い
Laravelの特徴として、学習コストが低いことが挙げられます。
その特徴として、ファサードを使用することで、PHPのスタティックなクラスメソッドを呼び出すように各機能を使用することができます。
Laravel 5.5 ファサードLaravelのファサードはすべて、
Illuminate\Support\Facades名前空間下で定義されているので。
使いたいときはuse Illuminate\Support\Facades\Cache;のようにuseで指定して使うようですね。ファサードを使用するときの注意点としては、ファサードを使用しすぎてクラスの肥大化しないように気をつけること。クラスの責任範囲を小さくするようにファサードを使用しすぎない。
2. symfonyベース
Laravelはsymfonyベースで作られています。
symfonyのコアはModel View Controller(MVC)フレームワークである Mojavi、オブジェクトリレーショナルマッピング (ORM) である Propel、そして Ruby on Rails のテンプレートヘルパーなどがベースとなっています。3. 多機能
Laravelはフルスタックフレームワークであり、様々な機能を持っています。
ルーティングやコントローラー、ビュー、ORMなどの基本機能の他に、認証機能、UT (ユニットテスト)、などの応用的な機能も備えています。4. 積極的なバージョンアップ
Laravelは積極的なバージョンアップを常に行っています。
現時点では半年ごとにマイナーリリースが行われています。PHPのフレームワークの中では早めのリリースサイクルを保っています。そのため、2年毎にLTS(長期サポート版)がリリースされます。最近 (2019/09/11) ではLaravel6のリリースが発表されました。
https://laravel-news.com/laravel-65. 高い拡張性
Laravelではディレクトリ構成に高い拡張性を備えています。ディレクトリ構成は開発者が自由に決めることができます。そのためMVCパターン、ADR、レイヤードアーキテクチャ等の様々なアーキテクチャを採用することができます。
逆に言えば、自由なディレクトリ構造をできるので、ディレクトリの管理ができなくなりやすいのかなとも思います。
phpのフレームワークでCakePHPがありますが、そちらはLaravelとは逆にディレクトリ構成を規約で縛ることにより、崩れにくいディレクトリ管理をすることができます。
参考文献
- 投稿日:2019-10-23T17:56:26+09:00
LaradockでLaravel環境を構築する流れとMySQL接続について
LaradockでMySQLに接続したい!!
Laradockでlaravelの開発環境を構築する方法については、公式ドキュメントや多くのQiitaの記事で説明されており、基本的にはそれに従って行けば開発環境を立ち上げることができると思います。しかし、自分が公式ドキュメントや記事通りに作業していく中でハマった点が2つあり、その解決法を探すのに時間がかかったので、ハマった点とその対処法について記録を残しておくものです
ハマった点
1 MySQLのバージョンについて
MySQLの認証方法がLaravelにサポートされてないのでエラーが出る。
MySQL 8.0.4からデフォルトの認証方式が変わり、セキュリティが強化されました。しかし、Laravel側でサポートされていないためphp artisan migrateを打っても「The server requested authentication method unknown to the client」というエラーが出ます2 .envの編集について
Laradock,Laravelの.envの編集すれば良いか、MySQLのユーザー登録をどのようにすれば良いかがわからない
1 Laradockのインストール、env編集
今回はこのようなディレクトリ構造で作業を行なっていきます
- (root)
- sampleapp
- Laradock(Laradock側)
- Laravel
- wikiLearns(Laravel側)
まずは公式ドキュメント通り
$ git clone https://github.com/laradock/laradock.git $ cd laradock $ cp env-example .envここで.envファイルを編集します
laradock.env### MYSQL ################################################ MYSQL_VERSION=5.7 //latest MYSQL_DATABASE=default MYSQL_USER=default MYSQL_PASSWORD=secret MYSQL_PORT=3306 MYSQL_ROOT_PASSWORD=root MYSQL_ENTRYPOINT_INITDB=./mysql/docker-entrypoint-initdb.dMySQLのバージョンをlatestから5.7にしました
なのでハマった点1の答えは「認証方法変更前のMySQL5.7を使おう」です。現在インストールしているMySQLも8.0.4 -> 5.7に変更しましたそれともう一つ
laradock.env### NGINX ################################################ NGINX_HOST_HTTP_PORT=8888 // 80 NGINX_HOST_HTTPS_PORT=443 NGINX_HOST_LOG_PATH=./logs/nginx/ NGINX_SITES_PATH=./nginx/sites/ NGINX_PHP_UPSTREAM_CONTAINER=php-fpm NGINX_PHP_UPSTREAM_PORT=9000 NGINX_SSL_PATH=./nginx/ssl/NGINXのポート番号を80 -> 8888に変更しました
Apacheのポート番号とダブらないようにするためです2 コンテナの起動
コンテナを起動します
docker-compose up -d nginx mysql phpmyadminここで8.0 -> 5.7にバージョンを変えた場合、MySQLのコンテナだけ起動されないという不具合が発生します。その時はDATA_PATH_HOSTで設定したフォルダを綺麗にして、イメージを作成し直す必要があります
対処法
① パス確認
$ cat .env | grep DATA_PATH_HOST
自分の場合は DATA_PATH_HOST=~/.laradock/data が表示されました
② パス、イメージ、コンテナを消去
rm -rf ~/.laradock/data/mysql
docker rmi laradock_mysql -f
docker rmi mysql -f
③ mysqlをビルドし直し
docker-compose build --no-cache mysql
④ docker ps でちゃんと動いているか確認しましょう3 Laravelプロジェクトの作成
workspaceに入って
$ docker exec -it laradock_workspace_1 bashLaravelプロジェクトの作成
生成後、sampleapp/Laravel/wikiLearns 内にディレクトリwikiLearnsが出来るので、これをsample/Laravelに置き直してください(何かここ方法があるはず)# composer create-project laravel/laravel wikiLearns(任意のアプリ名)workspaceから出ます
# exit設定の変更をするために一時停止
$ docker-compose stopAPP_CODE_PATH_HOST=../をAPP_CODE_PATH_HOST=../laravel/wikiLearns(自分の作ったディレクトリに対するLaradock起点での任意のパス)に書き換えてください
laradock.envAPP_CODE_PATH_HOST=../laravel/wikiLearns // ../コンテナ再起動
$ docker-compose up -d nginx mysqlブラウザでhttp://localhost:8888/ につなぐと例の画面が出てくるはずです。(出てこない場合は多分APP_CODE_PATH_HOSTのパス間違い)
4 MySQLへの接続
ハマった点2の説明です
LaradockのMySQLに接続したい時、
Laravel.envDB_CONNECTION=mysql DB_HOST=mysql // 127.0.0.1 DB_PORT=3306 DB_DATABASE=wikiLearns DB_USERNAME=homestead DB_PASSWORD=secretLaravelのDB_HOSTを127.0.0.1 -> mysqlに変更すれば接続できるはず...なんですが、PDOException::("SQLSTATE[HY000] [2002] Connection refused")が出てきます。このエラーはMySQLに認証されていないユーザーで接続しようとした時に発生します。MySQLのコンテナに入って新たにユーザー生成しましょう
1 ユーザー生成
「laradockのworkspaceコンテナからmysqlコンテナに接続」します。そうするとworkspaceからMySQLに接続してマイグレーション等ができるようになります
まずdocker inspectでworkspaceのIPアドレスを確認しましょう$ docker-compose up -d nginx mysql $ docker ps (これでworkspaceのコンテナIDを見られる) $ docker inspect be68295513a3(workspaceのIPアドレス)下の方にIPアドレスが書いてあります
"Gateway": "172.20.0.1", "IPAddress": "172.20.0.3", <- これ "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "MacAddress": "02:42:ac:14:00:03", "DriverOpts": nullMySQLコンテナに入ります
$ docker-compose exec mysql bash # mysql -u root -p # (パスワードはLaradockの.envのMYSQL_ROOT_PASSWORDを見る デフォルトはroot)データベースを作成
> create database wikiLearns(任意の名前);ユーザー作成
> create user 'root(任意の名前)'@'172.20.0.3(workspaceのIPアドレス)'identified by 'secret(任意のパスワード)'; > grant all privileges on . to 'root'@'172.20.0.3';2 .env編集
MySQLを出てLaradock、Laravelの.env編集
laradock.envMYSQL_VERSION=5.7 MYSQL_DATABASE=wikiLearns(作ったデータベース名) MYSQL_USER=root(作ったユーザー名) MYSQL_PASSWORD=secret(設定したパスワード) MYSQL_PORT=3306 MYSQL_ROOT_PASSWORD=root MYSQL_ENTRYPOINT_INITDB=./mysql/docker-entrypoint-initdb.dLaravel.envDB_CONNECTION=mysql DB_HOST=mysql // 127.0.0.1 → mysql DB_PORT=3306 DB_DATABASE=dockapp(Laradockと同じに) DB_USERNAME=root(Laradockと同じに) DB_PASSWORD=secret(Laradockと同じに)dockerを停止して再起動すればマイグレーションができます
$ docker-compose up -d nginx mysql $ docker exec -it laradock_workspace_1 /bin/bash $ php artisan migrateこれでdockerコンテナを立ち上げてlaravelを利用できるようになりました
- 投稿日:2019-10-23T15:17:08+09:00
Laravel で 開始と終了 時刻 でのバリデーション
Laravel6を使用して動作を確認しています。
画面
form.blade.php<input type="text" placeholder="00:00" name="start_time" class="form-control"> @error('start_time') {{ $message }} @enderror <input type="text" placeholder="00:00" name="end_time" class="form-control"> @error('end_time') {{ $message }} @enderrorバリデーション
FormRequestで行います
php artisan make:make:request TimeRequestTimeRequest.php<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class TimeRequest 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 [ 'start_time' => 'required|date_format:H:i|', 'end_time' => 'required|date_format:H:i|after:start_time', ]; } }これで適当なControllerのメソッドへformをpostするようにして、引数に
TimeRequestを指定しますTekitoController<?php namespace App\Http\Controllers; use App\Http\Requests\TimeRequest; class TekitoControllerextends Controller { public function some(TimeRequest $request) { //バリデーション済み$requestが渡ってくる } }
- 投稿日:2019-10-23T12:33:22+09:00
vue-component内のinput-textタグのvalueにBlade上の変数を入れる
親コンポーネントから子コンポーネントへ単方向データバインディングさせる際、
v-modelディレクティブ名は
- 親側:ケバブケース (例:foo-bar)
- 子側:キャメルケース (例:fooBar)
で記述する必要がある(みたい)。
以下、サンプルコード。
/resources/js/components/SamplComponent.vue<template> <div> <div class="input-group"> <input type="text" class="form-control" name="text1" v-model="hoge"> </div> ...(省略) </div> </template> <script> export default { props: { inputVal: { type: String, } }, data() { return { hoge: this.inputVal, // プロパティのイミュータブル制約回避 ... } }, ... } </script>/resources/js/app.jsrequire('./bootstrap'); window.Vue = require('vue'); Vue.component('sample-component', require('./components/SamplComponent.vue').default); const app = new Vue({ el: '#app', });
$ npm run dev等でコンパイルし、/resources/views/sample.blade.php<body> <div id="app"> ... <sample-component input-val="{{ old('hogehoge') }}"></sample-component> <!-- ↑例:oldヘルパーを使って入力値を保持 --> ... </div> </body>
- 投稿日:2019-10-23T10:48:17+09:00
laravelのマルチauthで認証済みの独自定義ユーザを取得する方法
php artisan make:authで作成できるユーザの認証状態はAuth::user()で取得できますが、
マルチauthでの独自に定義したユーザは上記の方法では認証状態を取得することはできません。そこで、認証済み独自ユーザを取得する方法をadminという独自ユーザを例に解説します。
早速ですが、以下コードになります。Auth::guard('admin')->user()Authファサードのguardメソッドを使って、使用したい独自ユーザのガードを指定することで、
マルチauthでの認証済の独自ユーザの取得を実現しています。今回はadminを指定しているので、認証済adminの取得になります。
利用例
実際に使うときは、routesのapi.phpなどで以下のように利用しています。
Route::get('/admin', function () { return Auth::guard('admin')->user(); })->name('admin');マルチauth作成で参考にさせていただいたページ
- 投稿日:2019-10-23T10:48:17+09:00
Laravelのマルチauthで認証済みの独自定義ユーザを取得する方法
php artisan make:authで作成できるユーザの認証状態はAuth::user()で取得できますが、
マルチauthでの独自に定義したユーザは上記の方法では認証状態を取得することはできません。そこで、認証済み独自ユーザを取得する方法をadminという独自ユーザを例に解説します。
早速ですが、以下コードになります。Auth::guard('admin')->user()Authファサードのguardメソッドを使って、使用したい独自ユーザのガードを指定することで、
マルチauthでの認証済の独自ユーザの取得を実現しています。今回はadminを指定しているので、認証済adminの取得になります。
利用例(Laravel × Nuxt)
実際に使うときは、routesのapi.phpなどで以下のように利用しています。
Route::get('/admin', function () { return Auth::guard('admin')->user(); })->name('admin');Nuxtでログイン状態の維持のために以下のように使えます。
/store/index.jsexport const actions = { async nuxtServerInit ({ commit }, { app }) { await app.$axios.$get('/admin') .then(user => commit('auth/setAdmin', admin)) .catch(() => commit('auth/setAdmin', null)) } }SSR時のログインチェックを行わせることで、Nuxt側で認証をstoreにセットして、ログイン状態を維持できるわけですね。
LaravelとNuxtでの認証周りの開発の参考になれば幸いです。
マルチauth作成で参考にさせていただいたページ
- 投稿日:2019-10-23T10:48:17+09:00
Laravelのマルチauthで認証済みの独自定義ユーザを取得する方法・Nuxt SSR時のログイン状態維持
php artisan make:authで作成できるユーザの認証状態はAuth::user()で取得できますが、
マルチauthでの独自に定義したユーザは上記の方法では認証状態を取得することはできません。そこで、認証済み独自ユーザを取得する方法をadminという独自ユーザを例に解説します。
早速ですが、以下コードになります。Auth::guard('admin')->user()Authファサードのguardメソッドを使って、使用したい独自ユーザのガードを指定することで、
マルチauthでの認証済の独自ユーザの取得を実現しています。今回はadminを指定しているので、認証済adminの取得になります。
利用例(Laravel × Nuxt)
実際に使うときは、routesのapi.phpなどで以下のように利用しています。
Route::get('/admin', function () { return Auth::guard('admin')->user(); })->name('admin');Nuxtでログイン状態の維持のために以下のように使えます。
/store/index.jsexport const actions = { async nuxtServerInit ({ commit }, { app }) { await app.$axios.$get('/admin') .then(admin => commit('auth/setAdmin', admin)) .catch(() => commit('auth/setAdmin', null)) } }SSR時のログインチェックを行わせることで、Nuxt側で認証をstoreにセットして、ログイン状態を維持できるわけですね。
LaravelとNuxtでの認証周りの開発の参考になれば幸いです。
マルチauth作成で参考にさせていただいたページ
- 投稿日:2019-10-23T03:27:06+09:00
laravelのlogin処理のソースコードリーディング
login処理のソースコードリーディングをした過程を書いたのですが、ログイン周りをカスタムする必要性が出てきたら勉強すればよいと思います。
自分の場合は軽い気持ちで読んでみただけなので、カスタムする必要性が出てきてからでよかったなと思ってます。
本当にカスタムしようとすると、GuardだったりUserProviderも勉強しないといけないのでアレなんですけどね。
前提知識
認証を理解するにあたって知っておきたい知識を簡単に説明します。
Guard
認証情報の操作をするものです。このクラスは
Illuminate\Contracts\Auth\Guardを実装する必要があります。デフォルトではセッションを利用するものが使われます。
UserProvider
実際に認証処理を行うクラスです。idなどを利用してユーザーのデータを取得して返却します。
Illuminate\Contracts\Aurh\UserProviderを実装する必要があります。デフォルトではDBからユーザー情報の取得を行い、DB接続に利用するものとしてeloquentかdatabaseの2つが用意されており、デフォルトではeloquentが利用されます。
Authenticatable
UserProviderでは引数や返り値として
Illuminate\Contracts\Auth\Authenticatableを実装したクラスを扱います。デフォルトで使用される
User.phpもextends先でこれを実装しています。login処理
まずはじめに注意点として、掲載するコードは今回の説明に関係ないところを省略して書いていたりします。完全なコードが知りたい場合はlaravelのコードを見てください。
LoginControllerにはloginメソッドはなく、AuthenticatesUsersトレイトに実装されています。public function login(Request $request) { $this->validateLogin($request); // If the class is using the ThrottlesLogins trait, we can automatically throttle // the login attempts for this application. We'll key this by the username and // the IP address of the client making these requests into this application. if (method_exists($this, 'hasTooManyLoginAttempts') && $this->hasTooManyLoginAttempts($request)) { $this->fireLockoutEvent($request); return $this->sendLockoutResponse($request); } if ($this->attemptLogin($request)) { return $this->sendLoginResponse($request); } // If the login attempt was unsuccessful we will increment the number of attempts // to login and redirect the user back to the login form. Of course, when this // user surpasses their maximum number of attempts they will get locked out. $this->incrementLoginAttempts($request); return $this->sendFailedLoginResponse($request); }
validateLoginは入力された認証情報(標準ではメールアドレスとパスワード)をバリデーションしています。
attemptLoginの箇所以外はログイン試行回数が規定回数を超えたらロックするといった機能を実現するものなので、今回は割愛します。二個目のif文の中の
$this->attemptLoginを見ていきます。protected function attemptLogin(Request $request) { return $this->guard()->attempt( $this->credentials($request), $request->filled('remember') ); }
guard()メソッドを見ていきます。protected function guard() { return Auth::guard(); }Authファサードで
guardメソッドを呼び出しています。Authファサードは
authという名前でサービスコンテナから解決をします。
config/app.phpからAuthServiceProviderが登録されているのがわかるので、その中身を見ればどの具象クラスが返ってくるかがわかります。Authファサードでは
Illuminate\Auth\AuthManagerのインスタンスを返却するので、その中のguardメソッドを呼び出していることになります。なので上記のコードは以下のコードとほぼ同義になります。
protected function guard() { return (new AuthManager())->guard(); }では
guardメソッドの中身を見ていきましょう。public function guard($name = null) { $name = $name ?: $this->getDefaultDriver(); return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name); }
guardには引数を渡していないので、$this->getDefaultDriverが呼ばれます。public function getDefaultDriver() { return $this->app['config']['auth.defaults.guard']; }ここのコードの意味は
config/auth.phpの中身のdefaults => guardの値を取り出すといった意味です。auth.phpdefaults' => [ 'guard' => 'web', 'passwords' => 'users', ],標準ではこのように記載されているので、
webといった文字列を返却することになります。
なので$name = "web"となります。次は
$this->resolve($name)を見ていきます。protected function resolve($name) { $config = $this->getConfig($name); $driverMethod = 'create'.ucfirst($config['driver']).'Driver'; if (method_exists($this, $driverMethod)) { return $this->{$driverMethod}($name, $config); } }
getConfigは先程のgetDefaultDriverと同じ用にauth.phpから値を取ってきます。
['driver' => 'session', 'provider' => 'users']といった配列が返却されます。
$driverMethod = createSessionDriverとなります。if文の中でこの名前のメソッドを呼び出しています。
($this->createSessionDriver($name, $config))public function createSessionDriver($name, $config) { $provider = $this->createUserProvider($config['provider'] ?? null); $guard = new SessionGuard($name, $provider, $this->app['session.store']); return $guard; }
$this->createUserProviderでUserProviderを取得し、それらを利用してSessionGuardを生成しています。
attemptLoginメソッド内の$this->guard()はSessionGuardクラスのインスタンスを返すことがわかりました。いろんなところにとびすぎたので
attemptLoginメソッドを再掲します。protected function attemptLogin(Request $request) { return $this->guard()->attempt( $this->credentials($request), $request->filled('remember') ); // 以下と同値 return (new SessionGuard()->attempt( $this->credentials($request), $request->filled('remember') ); }
Illuminate\Auth\SessionGuardのattemptメソッドを見ていきます。public function attempt(array $credentials = [], $remember = false) { $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); if ($this->hasValidCredentials($user, $credentials)) { $this->login($user, $remember); return true; } }
$this->provider->retrieveByCredentials($credentials)はメールアドレスを元にユーザー情報をDBから取得しています。
$this->hasValidCredentialsは主にパスワードの照合をしています。パスワードが一致すればloginメソッドが呼ばれます。public function login(AuthenticatableContract $user, $remember = false) { $this->updateSession($user->getAuthIdentifier()); // If the user should be permanently "remembered" by the application we will // queue a permanent cookie that contains the encrypted copy of the user // identifier. We will then decrypt this later to retrieve the users. if ($remember) { $this->ensureRememberTokenIsSet($user); $this->queueRecallerCookie($user); } // If we have an event dispatcher instance set we will fire an event so that // any listeners will hook into the authentication events and run actions // based on the login and logout events fired from the guard instances. $this->fireLoginEvent($user, $remember); $this->setUser($user); }
loginメソッドでは、セッションにユーザーIDを格納したり、remember_tokenをDBにセットしたりしています。そうすることでログインという状態を維持しているわけです。
loginメソッドのあとにtrueを返却することで、一番最初のloginメソッドに戻り、ログインイベントが発火されるわけです。if ($this->attemptLogin($request)) { return $this->sendLoginResponse($request); }
- 投稿日:2019-10-23T01:10:52+09:00
Laravel6.0で新規作成・編集・一覧・ページネーションを作成したメモ
概要
前回の続き。
Laravel 6.0 基本のタスクリストを参考に、CRUD操作の一部を作成した。
Bootstrapが4であったりという点が異なっている。データベース
タグを今回は作成する
php artisan make:migration create_tags_tablewhitemap/database/migrations/create_tags_table.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateTagsTable extends Migration { public function up() { Schema::create('tags', function (Blueprint $table) { $table->bigIncrements('id'); $table->integer('create_user_id')->unsigned()->comment('作成者ID'); $table->string('name'); $table->integer('value')->default(0); $table->boolean('is_display')->default(true); $table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP')); $table->foreign('create_user_id')->references('id')->on('users'); }); } public function down() { Schema::dropIfExists('tags'); } }アプリケーション
モデル
php artisan make:model Models/Tag上記コマンドで以下が作成される。
規約通りに作ると、Eloquentが自動的にO/Rマッピングしてくれる。whitemap/app/Models/Tag.php<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Tag extends Model { // }コントローラ
php artisan make:controller Admin/TagController --resource --model=Models/Tag上記コマンドでモデルと紐づいたコントローラが作成される。
ただし、今回は新規作成画面や編集画面をそれぞれ作らない予定なので、createやeditメソッドは割愛。whitemap/app/Http/Controllers/Admin/TagController.php<?php namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Tag; use App\Http\Requests\Admin\StoreTagPost; use Illuminate\Http\Request; class TagController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { $tags = Tag::paginate(config('const.Paginator.PER_PAGE')); return view('admin/tags', [ 'tags' => $tags ]); } /** * Store a newly created resource in storage. * 新規作成 * * @param App\Http\Requests\Admin\StoreTagPost $request * @return \Illuminate\Http\Response */ public function store(StoreTagPost $request) { $user = $request->user(); $tag = new Tag(); $tag->name = $request->name; $tag->create_user_id = $user->id; $tag->value = $request->value; $tag->save(); return redirect('/tag'); } /** * Update the specified resource in storage. * 変更の保存 * Laravelはタイプヒントされた変数名とルートセグメント名が一致する場合、 * ルートかコントローラアクション中にEloquentモデルが定義されていると、自動的に依存解決する。 * * @param \Illuminate\Http\Request $request * @param \App\Models\Tag $tag * @return App\Http\Requests\Admin\StoreTagPost */ public function update(StoreTagPost $request, Tag $tag) { $tag->name = $request->name; $tag->value = $request->value; $tag->save(); return redirect('/tag'); } }ページネーション用に、1ページに何ページ表示するかの数字を定数にしている。
whitemap/config/const.php<?php return [ // Couponsで使う定数 'Coupons' => [ 'TYPE_GET' => 1, 'TYPE_USE' => 2, ], + 'Paginator'=>[ + 'PER_PAGE'=>30 + ] ];バリデーション
php artisan make:request Admin/StoreTagPostバリデーションの設定を行う。
タイプヒントで指定することでコントローラに使用を伝える。
例:update(StoreTagPost $request)whitemap/app/Http/Requests/Admin/StoreTagPost.php<?php namespace App\Http\Requests\Admin; use Illuminate\Foundation\Http\FormRequest; class StoreTagPost extends FormRequest { public function authorize() { // そもそも管理者しかタグの更新は行わないためここでは判定しない return true; } public function rules() { return [ 'name' => 'required|max:255', 'value' => 'required|integer' ]; } public function messages() { return [ 'name.required' => '名前は必須です', 'name.max' => '名前は255文字以内で入力してください', 'value.required' => '値は必須です', ]; } }ルーティング
{tag}にプライマリーキーを受け取ることを想定して、コントローラで$tagで受け取ることで暗黙的なモデルバインディングを実現している。whitemap/routes/web.phpRoute::group(['middleware' => ['auth', 'can:admin-access']], function () { Route::get('/admin', function (Illuminate\Http\Request $request) { return view('admin/dashboard'); }); + Route::get('/tag','Admin\TagController@index'); + Route::post('/tag','Admin\TagController@store'); + Route::put('/tag/{tag}','Admin\TagController@update'); });ビュー
fontawesomeは公式の
<link href="https://use.fontawesome.com/releases/v5.11.2/css/all.css" rel="stylesheet">よりもjsdelivrのほうが数段早い。whitemap/resources/views/layoutes/app.blade.php<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="description" content="異世界漂流TRPG ドリフトサヴァイブはサバイバルをして文明を築き上げるTRPGです。" /> <meta name="keywords" content="Laravel,laradock,gcp" /> <meta name="robots" content="index" /> @yield('meta') <!-- CSRF Token --> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>{{ config('app.name', 'Laravel') }} - @yield('title')</title> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.11.2/css/all.min.css" integrity="sha256-+N4/V/SbAFiW1MPBCXnfnP9QSN3+Keu+NlB+0ev/YKQ=" crossorigin="anonymous"> @yield('css') <link href="{{ asset('css/app.css') }}" rel="stylesheet"> + <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8=" crossorigin="anonymous"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> + <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script src="{{ mix('js/common/app.js') }}"></script> @yield('head-scripts') <!-- Google Tag Manager --> <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','GTM-PBSJTDV');</script> <!-- End Google Tag Manager --> </head> <body> <!-- Google Tag Manager (noscript) --> <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PBSJTDV" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript> <!-- End Google Tag Manager (noscript) --> <div id="app"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> @guest @else <a class="navbar-brand user-icon" href="{{ url('/') }}"> <i class="user-icon"> <img src="{{\Auth::user()->twitter_profile_image_url_https}}"> </i> </a> @endguest <!-- <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"> <ul class="navbar-nav mr-auto"> <li class="nav-item {{str_replace(url('/'),'',url()->current()) === '/' ? 'active' : ''}}"><a class="nav-link" href="{{ url('/') }}">トップ</span></a></li> <li class="nav-item {{str_replace(url('/'),'',url()->current()) === '/home' ? 'active' : ''}}"><a class="nav-link" href="{{ url('/home') }}">マイページ</span></a></li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> サイト情報 </a> <div class="dropdown-menu" aria-labelledby="navbarDropdown"> <a class="dropdown-item" href="/agreement">利用規約</a> <a class="dropdown-item" href="/privacy-policy">プライバシーポリシー</a> <div class="dropdown-divider"></div> <a class="dropdown-item" href="https://github.com/hibohiboo/whitemap">github</a> </div> </li> </ul> </div> @guest <div class="my-2 my-lg-0"> <ul class="nav navbar-nav navbar-right"> <li class="nav-item"><a href="{{ route('login') }}" class="nav-link">ログイン</a></li> </ul> </div> @else @endguest </nav> @yield('content') </div> <!-- Scripts --> <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js" integrity="sha256-VeNaFBVDhoX3H+gJ37DpT/nTuZTdjYro9yBruHjVmoQ=" crossorigin="anonymous"></script> @yield('scripts') </body> </html>bootstrap3の
panelがbootstrap4ではcardになっているなど微修正。whitemap/resouces/vies/admin/tags.blade.php@extends('layouts.app') @section('content') {{-- このコメントはレンダ後のHTMLには現れない --}} {{-- Bootstrapは一度に1つのモーダルウィンドウしかサポートしない。入れ子になったモーダルは、ユーザー経験が乏しいと思われるためサポートされていない。--}} {{-- 可能であれば、他の要素からの干渉を避けるために、モーダルHTMLを最上位に配置すること --}} <div id="editModal" class="modal fade" tabindex="-1" role="dialog"> <form id="edit-form" action="{{ url('tag')}}" method="POST"> @csrf @method('PUT') <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">編集</h5> <button type="button" class="close" data-dismiss="modal" aria-label="閉じる"> <span aria-hidden="true">×</span> </button> </div>{{-- /.modal-header --}} <div class="modal-body"> {{-- タグ名 --}} <div class="form-group"> <label for="edit-tag-name" class="col-sm-3 control-label">タグ名</label> <div class="col-sm-6"> <input type="text" name="name" id="edit-tag-name" class="form-control" value="{{ old('tag') }}"> </div> </div> {{-- タグ値 --}} <div class="form-group"> <label for="edit-tag-value" class="col-sm-3 control-label">値</label> <div class="col-sm-6"> <input type="number" name="value" id="edit-tag-value" class="form-control" value="{{ old('tag') }}"> </div> </div> </div>{{-- /.modal-body --}} <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-dismiss="modal">閉じる</button> <button type="submit" class="btn btn-primary">変更を保存</button> </div>{{-- /.modal-footer --}} </div>{{-- /.modal-content --}} </div>{{-- /.modal-dialog --}} </form> </div>{{-- /.modal --}} <main class="container"> <div class="col-sm-offset-2 col-sm-8"> <div class="card"> <div class="card-header">新しいタグ</div> <div class="card-body"> {{-- バリデーションエラーの表示 --}} @include('common.errors') {{-- 新タグフォーム --}} <form action="{{ url('tag')}}" method="POST" class="form-horizontal"> @csrf {{-- タグ名 --}} <div class="form-group"> <label for="tag-name" class="col-sm-3 control-label">タグ名</label> <div class="col-sm-6"> <input type="text" name="name" id="tag-name" class="form-control" value="{{ old('tag') }}"> </div> </div> {{-- タグ値 --}} <div class="form-group"> <label for="tag-value" class="col-sm-3 control-label">値</label> <div class="col-sm-6"> <input type="number" name="value" id="tag-value" class="form-control" value="{{ old('tag') }}"> </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> タグ追加 </button> </div> </div> </form> </div> </div> @if (count($tags) > 0) <div class="card"> <div class="card-header">タグ一覧</div> <div class="card-body"> <table class="table table-striped tag-table"> {{-- テーブルヘッダ --}} <thead> <tr> <th>タグ</th> <th>値</th> <th> </th> </tr> </thead> {{-- テーブル本体 --}} <tbody> @foreach ($tags as $tag) <tr> <td class="table-text"> <div>{{ $tag->name }}</div> </td> <td class="table-text"> <div>{{ $tag->value }}</div> </td> <td> <button type="button" class="btn" data-toggle="modal" data-target="#editModal" data-action="{{ url('tag/' . $tag->id) }}" data-name="{{$tag->name}}" data-value="{{$tag->value}}" > <i class="fa fa-btn fa-edit"></i> </button> </td> </tr> @endforeach </tbody> </table> </div> </div> @endif {{ $tags->links() }} </div> </div> @endsection @section('scripts') <script src="{{ mix('js/admin/tag/index.js') }}"></script> @endsectionmixで呼び出すjs用の設定が以下。externalsで外部からリソースを取得することを表す。
npm install --save-dev @types/bootstrapwhitemap/webpack.mix.jsconst mix = require('laravel-mix'); mix.ts('resources/ts/common/app.ts', 'public/js/common') .sass('resources/sass/app.scss', 'public/css') .version(); mix.ts('resources/ts/welcome/index.ts', 'public/js/welcome').version(); mix.ts('resources/ts/home/index.ts', 'public/js/home').version(); mix.ts('resources/ts/login/index.ts', 'public/js/login').version(); mix.webpackConfig({ externals: { + jquery: 'jQuery', firebase: 'firebase', firebaseui: 'firebaseui', axios: 'axios', lodash: 'lodash', + bootstrap: 'bootstrap', + popper: 'popper.js' } }); + mix.ts('resources/ts/admin/tag/index.ts', 'public/js/admin/tag').version();resouces/admin/tag/index.tsimport { ModalEventHandler } from 'bootstrap'; $('#editModal').on('show.bs.modal', function(event: ModalEventHandler<HTMLElement>) { const target = event.relatedTarget; console.log('modal', target); if (target === undefined) { return; } const $button = $(target); // モーダル切替えボタン const action = $button.data('action'); // data-* 属性から情報を抽出 const name = $button.data('name'); const value = $button.data('value'); // モーダルの内容を更新。ここではjQueryを使用するが、代わりにデータ・バインディング・ライブラリまたは他のメソッドを使用することも可能 const $modal = $(this); $modal.find('#edit-tag-name').val(name); $modal.find('#edit-tag-value').val(value); $modal.find('#edit-form').attr('action', action); });npm run watch-poll参考
Laravel 6.0 基本のタスクリスト
bootstrap3 -> 4
@types/bootstrap
fontawesome は公式より jsdeliver のほうが早い
FontAwesome の読み込み速度を公式サイトと CDN サービスで比較してみた
暗黙のモデル結合
Laravel 6.0 データベース:ペジネーション
Laravel 6.0 コントローラ
Laravel 6.0 バリデーション
- 投稿日:2019-10-23T00:28:23+09:00
Laravel6.0で管理者用画面を作ったメモ
概要
前回の続き。
今回は、ログインユーザに管理者権限を与えて、管理者専用画面を表示する。データベース
ER図
今回、DBは以下のようにする。
管理者クーポンが発行されたユーザが管理者権限を持つとする。マイグレーション
以下のコマンドでマイグレーションファイルを作成できる。
php artisan make:migration create_coupons_table上記で作成したファイルに含まれる
$table->timestamps();はnullableなcreated_atとupdated_atを作成する。
DBに作ってもらいたかったので、作成時、更新時の時刻を挿入する設定にしている。whitemap/database/migrations/create_coupons_table.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateCouponsTable extends Migration { public function up() { Schema::create('coupons', function (Blueprint $table) { $table->string('id'); $table->integer('type')->default(config('const.Coupons.TYPE_GET', 1))->comment('1:取得, 2:使用'); $table->string('name'); $table->boolean('is_display')->default(true); $table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP')); }); } public function down() { Schema::dropIfExists('coupons'); } }クーポン種別は定数で定義してみた。
whitemap/config/const.php<?php return [ // Couponsで使う定数 'Coupons' => [ 'TYPE_GET' => 1, 'TYPE_USE' => 2, ], ];ユーザとクーポンの紐づけテーブルでは外部キーを設定し、
ユーザテーブルやクーポンテーブルにないものは登録できないようにした。whitemap/database/create_user_coupons_table.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateUserCouponsTable extends Migration { public function up() { Schema::create('user_coupons', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('coupon_id')->comment('クーポンID'); $table->integer('subscribe_user_id')->unsigned()->comment('受取ユーザID'); $table->integer('publish_user_id')->unsigned()->comment('発行ユーザID'); $table->dateTime('expire')->nullable()->comment('利用期限'); $table->timestamp('created_at')->useCurrent(); $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP')); $table->softDeletes(); $table->foreign('subscribe_user_id')->references('id')->on('users'); $table->foreign('publish_user_id')->references('id')->on('users'); $table->foreign('coupon_id')->references('id')->on('coupons'); }); } public function down() { Schema::dropIfExists('user_coupons'); } }
php artisan migareteでデータベース更新。シーダの設定
php artisan make:seeder UsersTableSeederシステム用のユーザと初期ユーザを作成。
whitemap/database/seeds/UserSeeder.php<?php use Illuminate\Database\Seeder; class UserSeeder extends Seeder { public function run() { $user = [ 'id' => 1, 'firebase_uid' => 'system', 'name' => 'システム管理者', 'twitter_screen_name' => '', 'twitter_profile_image_url_https' => '', ]; DB::table('users')->insert($user); $user = [ 'id' => 2, 'firebase_uid' => env('FIRST_USER_FIREBASE_UID'), 'name' => env('FIRST_USER_NAME'), 'twitter_screen_name' => env('FIRST_USER_TWITTER_SCREEN_NAME'), 'twitter_profile_image_url_https' => env('FIRST_USER_TWITTER_PROFILE_IMAGE_URL'), ]; DB::table('users')->insert($user); } }管理者クーポンを追加。
whitemap/database/seeds/Couponseeder.php<?php use Illuminate\Database\Seeder; use App\Enums\Coupon\CouponIds; class CouponsSeeder extends Seeder { public function run() { $coupon = [ 'id' => CouponIds::ADMIN(), 'name' => '管理者クーポン', 'is_display' => false, ]; DB::table('coupons')->insert($coupon); } }クーポンIDはEnumで登録してみた。
composer require myclabs/php-enumwhitemap/app/Enums/Coupon.php<?php namespace App\Enums\Coupon; use MyCLabs\Enum\Enum; class CouponIds extends Enum { const ADMIN = 'admin'; }システムユーザから初期ユーザに向けて管理者クーポンを発行
whitemap/database/seeds/UserCouponsSeeder.php<?php use Illuminate\Database\Seeder; use App\Enums\Coupon\CouponIds; class UserCouponsSeeder extends Seeder { public function run() { $user_coupon = [ 'id' => 1, 'coupon_id' => CouponIds::ADMIN(), 'subscribe_user_id' => 2, 'publish_user_id' => 1, ]; DB::table('user_coupons')->insert($user_coupon); } }実行するSeederを指定
whitemap/database/seeds/DatabaseSeeder.php<?php use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { public function run() { // $this->call(UsersTableSeeder::class); $this->call([ UserSeeder::class, CouponsSeeder::class, UserCouponsSeeder::class, ]); } }シーダの実行
php artisan db:seedアプリケーション
モデルの設定
php artisan make:model Models/UserCouponwhitemap/app/Models//UserCoupon.php<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class UserCoupon extends Model { protected $table = 'user_coupons'; }
subscribe_user_idでユーザクーポンとユーザを紐づける。whitemap/app/User.php<?php namespace App; use Laravel\Passport\HasApiTokens; // 追加 use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use App\Enums\Coupon\CouponIds; class User extends Authenticatable { use Notifiable, HasApiTokens; // HasApiTokens を追加 public function __construct(array $attributes = []){ } protected $fillable = [ 'name', 'twitter_screen_name','twitter_profile_image_url_https', 'firebase_uid' ]; protected $hidden = [ 'password', 'remember_token', ]; protected $casts = [ 'email_verified_at' => 'datetime', ]; + public function userCoupons(){ + return $this->hasMany('App\Models\UserCoupon', 'subscribe_user_id'); + } }ゲートの設定
whitemap/app/Gate/AdminAccess<?php namespace App\Gate; use App\User; use App\Enums\Coupon\CouponIds; final class AdminAccess { public function __invoke(User $user): bool { // 管理者用のクーポンを持っているかDBに問い合わせる。 return $user->userCoupons()->where('coupon_id',CouponIds::ADMIN())->exists(); } }上記で設定したゲートを
admin-accessの名前で登録する。whitemap/app/Providers/AuthServiceProvider.php<?php namespace App\Providers; use App\Auth\MySessionGuard; use App\Auth\MyEloquentUserProvider; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; use Auth; use App\Gate\UserAccess; use App\Gate\AdminAccess; use \Psr\Log\LoggerInterface; class AuthServiceProvider extends ServiceProvider { protected $policies = [ // 'App\Model' => 'App\Policies\ModelPolicy', ]; public function boot(LoggerInterface $logger) { $this->registerPolicies(); Auth::provider('my_eloquent', function($app, array $config) { return new MyEloquentUserProvider($app['hash'], $config['model']); }); + // 認可 + Gate::define('admin-access', new AdminAccess); // 認可の前にロギング Gate::before(function ($user, $ability) use ($logger) { // Log::info("Hello my log,"); $logger->info($ability, ['firebase_uid'=>$user->getAuthIdentifier()]); }); } }ルーティング
whitemap/routes/web.phpRoute::group(['middleware' => ['auth', 'can:admin-access']], function () { // この中は管理者権限の場合のみルーティングされる Route::get('/admin', function (Illuminate\Http\Request $request) { return view('admin/dashboard'); }); });管理者ページへのリンク
管理者権限があるときだけ、管理者ページへのリンクを表示。
whitemap/resources/views/home.blade.php@extends('layouts.app') @section('title') マイページ @endsection @section('head-scripts') @if(Auth::check()) <script src="https://www.gstatic.com/firebasejs/7.2.0/firebase-app.js"></script> <script src="https://www.gstatic.com/firebasejs/7.2.0/firebase-auth.js"></script> <script src="{{ mix('js/home/index.js') }}"></script> @endif @endsection @section('content') <main role="main" class="container"> <div class="starter-template"> <h1>マイページ</h1> @if(Auth::check()) <ul> + @can('admin-access') + <li><a href="/admin">管理者画面へ</a></li> + @endcan <li><a href="#" id="logout">ログアウト</a></li> </ul> @else こんにちは! ゲストさん <br /> <a href="/login">ログイン</a> @endif </div></main> @endsection参考
Laravel で定数をつかうよ
Laravel で Enum を使う
Laravel 6.0 Artisan コンソール
全 68 種類!Laravel 5.6 の artisan コマンドまとめ
【メモ】【Laravel】外部キー制約付き Migrate がさっぱり動かないときのチェック・ポイント(Mysql)
Laravel の DB migration で日付のデフォルトを指定
Laravel(Eloquent)の save メソッドを使ったら MySQL の timestamp 型で謎な挙動が発生した話
管理者クーポンによるタグ画面の制御
blade テンプレートでの切替
Laravel 6.0 基本のタスクリスト
laravel
readouble laravel
【Laravel】 認証や認可に関する補足資料
Laravel 6.0 認可
Laravel 6.0 ルーティング
Laravel 6.0 ミドルウェア
Laravel の Gate(ゲート)機能で権限(ロール)によるアクセス制限を実装する
Laravel 6.0 バリデーション


