- 投稿日:2020-12-01T23:17:07+09:00
Docker for Windows で postgresのコンテナが起動できない
Docker for Windows で laradockのデータベースをpostgresにしたところ、うまくできない場合・・・
psqlで接続できない・・・
laradockを利用して、postgresにデータベースを作成する為に
docker-compose exec workspace psql -U default -h postgresを実行したところ、以下のエラー
psql: could not translate host name "postgres" to address: No address associated with hostnameそもそもコンテナが起動してない・・・
docker ps してコンテナを確認すると、postgresは起動していない
docker-compose up -d で特にエラーが出ていなかったのだが・・・docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 1370b45bf0b5 nginx:alpine "/docker-entrypoint.…" 40 minutes ago Up 10 seconds 0.0.0.0:8082->80/tcp laravel-sns_nginx_1 e92f90d8dd0f laravel-sns_php-fpm "docker-php-entrypoi…" 6 days ago Up 12 seconds 0.0.0.0:9000->9000/tcp laravel-sns_php-fpm_1 4fc742fdaba5 laravel-sns_workspace "docker-php-entrypoi…" 6 days ago Up 12 seconds 9000/tcp laravel-sns_workspace_1laradockディレクトリ内の.env は永続化OK
DATA_PATH_HOST=../data.envのDATA_PATH_HOSTがデフォルトの状態だとエラーになるという記事がヒットするが、私の環境は変更済み
はて?
解決!!!
laradockディレクトリのdocker-compose.yml を変更する事で解決しました!
postgres: image: postgres:${POSTGRES_VERSION}-alpine depends_on: - php-fpm ports: - ${POSTGRES_PORT}:5432 volumes: # - ${DATA_PATH_HOST}/postgres:/var/lib/postgresql/data #変更前 - ${DATA_PATH_HOST}/postgres:/var/lib/postgresql #変更後 environment:vokumesのディレクトリ指定で最後のdataを削除
これで、再度、コンテナを起動
docker-compose up -d workspace php-fpm nginx postgres Starting laravel-sns_workspace_1 ... done Starting laravel-sns_php-fpm_1 ... done Starting laravel-sns_nginx_1 ... done Recreating laravel-sns_postgres_1 ... doneでコンテナを確認
docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 07773a4cbf64 postgres:11.6-alpine "docker-entrypoint.s…" 6 seconds ago Up 4 seconds 0.0.0.0:5432->5432/tcp laravel-sns_postgres_1 1370b45bf0b5 nginx:alpine "/docker-entrypoint.…" 44 minutes ago Up 4 seconds 0.0.0.0:8082->80/tcp laravel-sns_nginx_1 e92f90d8dd0f laravel-sns_php-fpm "docker-php-entrypoi…" 6 days ago Up 6 seconds 0.0.0.0:9000->9000/tcp laravel-sns_php-fpm_1 4fc742fdaba5 laravel-sns_workspace "docker-php-entrypoi…" 6 days ago Up 6 seconds 9000/tcp laravel-sns_workspace_1無事にpostgresを起動できました!
しかし、同じ環境でmacのdockerだと問題なく動くのに、Docker for Windowsだと動かない原因までは追っていません!
- 投稿日:2020-12-01T21:03:14+09:00
Wordpressの投稿画面みたいなやつの名前
下記のように呼ばれるっぽい。
・ビジュアルエディタ
・リッチテキストエディタ
・テキストエディタ
- 投稿日:2020-12-01T21:02:07+09:00
laravel 6.x migrations-generatorがインストールできない問題対応
既存DBのmigrationsファイルを作成したい
migrations-generatorをインストールする
composer require --dev "xethron/migrations-generator"larabel 6.xでは下記のようなエラーになる
Your requirements could not be resolved to an installable set of packages. Problem 1 - xethron/migrations-generator v2.0.0 requires way/generators dev-feature/laravel-five-stable -> no matching package found. - illuminate/support v5.7.9 requires nesbot/carbon ^1.26.3 -> satisfiable by nesbot/carbon[1.26.3, 1.26.4, 1.26.5, 1.26.6, 1.27.0, 1.28.0, 1.29.0, 1.29.1, 1.29.2, 1.30.0, 1.31.0, 1.31.1, 1.32.0, 1.33.0, 1.34.0, 1.34.1, 1.34.2, 1.34.3, 1.34.4, 1.35.0, 1.35.1, 1.36.0, 1.36.1, 1.36.2, 1.37.0, 1.37.1, 1.38.0, 1.38.1, 1.38.2, 1.38.3, 1.38.4, 1.39.0, 1.39.1] but these conflict with your requirements or minimum-stability. (省略) - don't install illuminate/support v5.0.4|don't install laravel/framework v6.18.20 - Installation request for laravel/framework (locked at v6.18.20, required as 6.x) -> satisfiable by laravel/framework[v6.18.20]. - Installation request for xethron/migrations-generator ^2.0 -> satisfiable by xethron/migrations-generator[v2.0.0, v2.0.1, v2.0.2]. Potential causes: - A typo in the package name - The package is not available in a stable-enough version according to your minimum-stability setting see <https://getcomposer.org/doc/04-schema.md#minimum-stability> for more details. - It's a private package and you forgot to add a custom repository to find it Read <https://getcomposer.org/doc/articles/troubleshooting.md> for further common problems. Installation failed, reverting ./composer.json to its original content.ググると同じ問題に直面してるひとが多数。(https://github.com/Xethron/migrations-generator/issues/191)
これや https://github.com/oscarafdev/migrations-generatorcomposer require oscarafdev/migrations-generator --devこれを https://github.com/kitloong/laravel-migrations-generator
composer require --dev "kitloong/laravel-migrations-generator"試してみろと書かれているのがこれらもうまく行かない・・・
解決方法
laravelのバージョンの互換性の問題だしlaravelのバージョン下げてしまえばいい
そりゃそうでしょって話だが今回migrationsファイル作るだけなので
別プロジェクトをlaravel5.4で立ち上げてそこで作ったものをコピーすれば良いと考えたVersion指定してlaravelをインストール
composer create-project "laravel/laravel=5.4.*" sampleproject cd sampleproject composer require oscarafdev/migrations-generator --devうまくインストールできた。
app.phpに下記を追加してconfig/app.phpWay\Generators\GeneratorsServiceProvider::class, Xethron\MigrationsGenerator\MigrationsGeneratorServiceProvider::class,.envを修正して既存DBにつなげたあと
php artisan migrate:generate Do you want to log these migrations in the migrations table? [Y/n] : > Y Next Batch Number is: 2. We recommend using Batch Number 0 so that it becomes the "first" migration [Default: 0] : > 0でmigrationsファイル作成してくれました
ちなみに
php artisan migrate:generate table1,table2,table3,table4,table5でテーブルの指定ができて
php artisan migrate:generate --ignore="table3,table4,table5"で実行しないテーブルの指定ができるみたいです。
互換性の問題ってわかってるならあれこれやる前にバージョン下げてしまえばよいだけの話でした
- 投稿日:2020-12-01T20:07:18+09:00
Laravelでどのリンク先からアクセスしたのか知る方法【リファラ】
下記コードで実現可能。
url()->previous();
- 投稿日:2020-12-01T19:30:49+09:00
Laravel6.xからLaravel8.xへアップグレード
まえがき
6.xがLTSだし6.xで開発しはじめていたが、ローンチ時期的に次のLTSバージョンが出る、もしくは近いことが想定されるのでいっそ次にバージョンを上げる際のコストを下げようの気持ちで8.xまでアップグレードするよ
アップデート前情報
PHP
PHP 7.4.3 (cli) (built: Feb 20 2020 21:53:46) ( NTS ) Copyright (c) The PHP Group Zend Engine v3.4.0, Copyright (c) Zend Technologies with Zend OPcache v7.4.3, Copyright (c), by Zend Technologies with Xdebug v2.9.6, Copyright (c) 2002-2020, by Derick RethansLaravel
Laravel Framework 6.18.35composer.json
- 実際に利用しているPJなのでアップデートとかには関係ないライブラリもあります
"require": { "php": "^7.4", "ext-fileinfo": "*", "ext-json": "*", "ext-pdo": "*", "ext-redis": "*", "aws/aws-sdk-php-laravel": "~3.0", "encore/laravel-admin": "^1.8", "fideloper/proxy": "^4.0", "guzzlehttp/guzzle": "^7.0", "lampager/lampager": "^0.4.0", "lampager/lampager-laravel": "^0.4.5", "laravel/framework": "^6.2", "laravel/tinker": "^2.0", "lcobucci/jwt": "^3.3", "vinkla/hashids": "^7.0" }, "require-dev": { "barryvdh/laravel-ide-helper": "^2.6", "brianium/paratest": "^4.2", "deployer/deployer": "^6.8", "deployer/recipes": "^6.2", "facade/ignition": "^1.4", "friendsofphp/php-cs-fixer": "^2.16", "fzaninotto/faker": "^1.9.1", "laravel/telescope": "^3.2", "mockery/mockery": "^1.0", "nunomaduro/collision": "^3.0", "phpmd/phpmd": "^2.9", "phpunit/phpunit": "^9.0", "squizlabs/php_codesniffer": "^3.5" }変更していく
下記のドキュメントを参考にしながら行っていく
- 6.xから7.0へのアップグレード
- 7.xから8.0へのアップグレード
PHP要件
今回はもともとPHP7.4だったので特に変更無し
Laravel依存パッケージのアップデート
6 to 7
laravel/frameworkを^7.0へ
nunomaduro/collisionを^4.1へ
phpunit/phpunitを^8.5へ
laravel/tinkerを^2.0へ
facade/ignitionを^2.0へ7 to 8
guzzlehttp/guzzleを^7.0.1へ
facade/ignitionを^2.3.6へ
laravel/frameworkを^8.0へ
laravel/uiを^3.0へ
nunomaduro/collisionを^5.0へ
phpunit/phpunitを^9.0へ変更したcomposer.json
- laravelの依存ライブラリ以外にも対応が必要な場合もあるのでPJごとによしなに
"require": { "php": "^7.4", "ext-fileinfo": "*", "ext-json": "*", "ext-pdo": "*", "ext-redis": "*", "aws/aws-sdk-php-laravel": "~3.0", "encore/laravel-admin": "^1.8", "fideloper/proxy": "^4.0", - "guzzlehttp/guzzle": "^7.0", + "guzzlehttp/guzzle": "^7.0.1", "lampager/lampager": "^0.4.0", "lampager/lampager-laravel": "^0.4.5", - "laravel/framework": "^6.2", + "laravel/framework": "^8.0", "laravel/tinker": "^2.0", + "laravel/ui": "^3.0", "lcobucci/jwt": "^3.3", - "vinkla/hashids": "^7.0" + "vinkla/hashids": "^9.0" }, "require-dev": { "barryvdh/laravel-ide-helper": "^2.6", - "brianium/paratest": "^6.0", + "brianium/paratest": "^6.8", "deployer/deployer": "^6.8", "deployer/recipes": "^6.2", - "facade/ignition": "^1.4", + "facade/ignition": "^2.3.6", "friendsofphp/php-cs-fixer": "^2.16", "fzaninotto/faker": "^1.9.1", - "laravel/telescope": "^3.2", + "laravel/telescope": "^4.0", "mockery/mockery": "^1.0", - "nunomaduro/collision": "^3.0", + "nunomaduro/collision": "^5.0", "phpmd/phpmd": "^2.9", "phpunit/phpunit": "^9.0", "squizlabs/php_codesniffer": "^3.5" }このまま
compose update
すると最後のコマンド実行でエラーになるので先にSynfony5対応を行ってしまうWriting lock file Generating optimized autoload files > Illuminate\Foundation\ComposerScripts::postAutoloadDump > @php artisan package:discover --ansi Script @php artisan package:discover --ansi handling the post-autoload-dump event returned with error code 255Symfony5へのアップグレード
App\Exceptions\Handlerの変更
app/Exceptions/Handler.php
を8.xの内容でコピーする- 見た感じ継承して親の処理呼んでただけの部分が消えた
- 残したければ
Exception
を受けてるところをThrowable
へ- 自前実装分があればよしなに
https://github.com/laravel/laravel/blob/8.x/app/Exceptions/Handler.php
セッションの設定を変更
config/session.php
を8.xの内容でコピーする- コメント以外は下記のような違いになっている
- 自前実装分があればよしなに
app\config\session.php~~ - 'secure' => env('SESSION_SECURE_COOKIE', false), + 'secure' => env('SESSION_SECURE_COOKIE'), ~~ - 'same_site' => null, + 'same_site' => 'lax', ];https://github.com/laravel/laravel/blob/8.x/config/session.php
compose updateの実行
上記の対応まで行ってからcomposer updateを行い、エラーが出なければOK
エラーが出た場合は内容見て解決するのが良いとは思いますここまでで最低限の対応
$ composer update
気になればキャッシュも消しておく
$ artisan optimize:clear
更新後バージョン
Laravel Framework 8.16.1Next step
PJというか環境によっては上記までの対応では不足しているので動作確認をしながら、エラーや挙動がおかしい箇所に関してアップグレードガイドを参考にしていく
特に「影響の可能性: 高い」の部分は要チェック
- 6.xから7.0へのアップグレード
- 7.xから8.0へのアップグレード
参考までに、「影響の可能性: 低い」とされていても 自分のPJでは
$model->getOriginal
を利用していたので対応が必要になったり、 app/Providers/RouteServiceProvider.php をごにょごにょしていたので変更が必要だったりライブラリを更新した影響でLaravelと関係ないところで更新を迫られたりとまぁちょいちょいありました
アップグレードガイド見るよりコード差分見て行くほうが早いかもしれない
https://github.com/laravel/laravel/compare/6.x...8.x
- 投稿日:2020-12-01T18:51:28+09:00
cordova-plugin-purchase用の消耗型課金レシート検証APIをLaravelで実装する
Cordovaで課金処理を行うためのプラグイン、cordova-plugin-purchaseには各プラットフォームのレシートを検証するためのAPI呼び出し処理を追加することができます。
https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/api.md#receipt-validationこのレシート検証APIをLaravelで作ってみます。
課金を実装したCordovaアプリ自体の作成はこちらの記事でまとめています。
Cordova(Monaca)でアプリ内課金を実装するAPIのリクエスト、レスポンス仕様
プラグイン側でリクエストレスポンスの仕様が決められています。
https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/api.md#validatorリクエスト
URL
好きなURLを設定できます。
標準以外のパラメータを渡したい場合はパスパラメータなどを利用しましょう。メソッド
POST
リクエストボディ
{ "additionalData" : null, "alias" : "monthly1", "currency" : "USD", "description" : "Monthly subscription", "id" : "subscription.monthly", "loaded" : true, "price" : "$12.99", "priceMicros" : 12990000, "state" : "approved", "title" : "The Monthly Subscription Title", "transaction" : { // 各ストアのレシート情報が入る }, "type" : "paid subscription", "valid" : true }transactionの中身は各ストア事にこのようになります。
https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/api.md#transactionsiOS
"transaction" : { "appStoreReceipt":"appStoreReceiptString", // BASE64エンコーディングされたレシート情報 "id" : "idString", // トランザクションID "original_transaction_id":"transactionIdString", // 購読型の時にセットされる "type": "ios-appstore" // ストアの識別 }Android
"transaction" : { "developerPayload" : undefined, // オプション "id" : "idString", // トランザクションID "purchaseToken" : "purchaseTokenString", // レシート情報 "receipt" : "{\"autoRenewing\":true,\"orderId\":\"orderIdString\",\"packageName\":\"com.mycompany\",\"purchaseTime\":1555217574101,\"purchaseState\":0,\"purchaseToken\":\"purchaseTokenString\"}", "signature" : "signatureString", // 署名 "type": "android-playstore" // ストアの識別 }レスポンス
返すレスポンスによって呼び出し元に戻った時に
.verified()
に入るか.unverified()
に入るか決まります。成功(verified)
レスポンスコード
200
レスポンスボディ
{ "ok" : true, "data" : { "transaction" : { // リクエストボディのトランザクションをセット } } }失敗(unverified)
レスポンスコード
200または200以外でも失敗と判断される
レスポンスボディ
{ "ok" : false, "data" : { // エラーコード "code" : 6778003 }, "error" : { // エラーメッセージ。好きに設定できる。 "message" : "The subscription is expired." } }アプリ側でハンドルするためにエラーコードは以下が定義されています。
store.INVALID_PAYLOAD = 6778001; store.CONNECTION_FAILED = 6778002; store.PURCHASE_EXPIRED = 6778003; store.PURCHASE_CONSUMED = 6778004; store.INTERNAL_ERROR = 6778005; store.NEED_MORE_DATA = 6778006;消耗型課金の検証
消耗型(Androidなら消費型)の課金に関して、各プラットフォーム側でレシートの検証方法が用意されています。
プラットフォームごとの検証
iOS(App Store)
iOSの場合はApp Storeの用意するAPIにレシートを送信することで検証することができます。
https://developer.apple.com/documentation/appstorereceipts/verifyreceipt
環境 エンドポイント メソッド Sandbox https://sandbox.itunes.apple.com/verifyReceipt POST Production https://buy.itunes.apple.com/verifyReceipt POST Sandboxアカウントで発行したレシートはSandboxのエンドポイントに送信しなければいけません。
本番のエンドポイントにSandboxのレシートを送信すると「{"status":21007}
」が返ります。レスポンスのステータスが0なら正しいレシート。
戻されたバンドルIDが正しいかなどのチェックを追加で行います。Android(Google Play)
Androidの場合は、レシートの署名(signature)を検証することで、レシートが改ざんされていないかチェックできます。
リクエストで送られてきたreceipt
とsignature
をGoogle Play Consoleから取得できるRSA公開鍵で検証します。公開鍵のBase64文字列はGoogle Play Consoleから取得します。
取得した文字列を
public.txt
に保存して下記コマンドを実行してPEM形式にします。$ base64 -d public.txt > public.der $ openssl rsa -inform DER -outform PEM -pubin -in public.der -out public.pem writing RSA key $ cat public.pem -----BEGIN PUBLIC KEY----- **************************************************************** **************************************************************** **************************************************************** **************************************************************** **************************************************************** **************************************************************** ******** -----END PUBLIC KEY-----これで検証が行えます。
その他の検証
その他にも以下を検証しておいた方がよさそうです。
- バンドルID(パッケージ名)がアプリの識別子と一致すること
- プロダクトIDが存在する商品のIDであること
- トランザクションIDがまだ処理されていないこと
- アプリのビジネスロジックに関するサーバサイドチェック
APIの実装例
Laravelでの実装例です。
api.phpRoute::post('/verify-purchase', 'Api\PurchaseController@verifyReceipt');PurchaseController.php<?php namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Log; use GuzzleHttp\Client; use GuzzleHttp\Exception\ClientException; class PurchaseController extends Controller { private $bundle_id; private $product_ids; private $pubkey; private $error_codes = [ "INVALID_PAYLOAD" => 6778001, "CONNECTION_FAILED" => 6778002, "PURCHASE_EXPIRED" => 6778003, "PURCHASE_CONSUMED" => 6778004, "INTERNAL_ERROR" => 6778005, "NEED_MORE_DATA" => 6778006, ]; public function __construct() { $this->bundle_id = env('BUNDLE_ID'); $this->product_ids = env('PRODUCT_IDS'); $this->pubkey = env('PUBKEY'); } public function verifyReceipt(Request $request) { $success_response = [ 'ok' => true, 'data' => [ 'transaction' => null, ], ]; $error_response = [ 'ok' => false, 'data' => [ 'code' => $this->error_codes['INVALID_PAYLOAD'], ], 'error' => [ 'message' => "invalid receipt" ], ]; // Validation check $validator = Validator::make($request->all(), [ 'id' => "required|string|in:{$this->product_ids}", 'transaction' => 'required|array', 'transaction.type' => 'required|in:ios-appstore,android-playstore', ]); // Validation error if($validator->fails()) { $errors = $validator->errors()->all(); Log::notice($errors); $error_response['error']['message'] = $errors; return $error_response; } $product_id = $request->input('id'); $transaction = $request->input('transaction'); Log::info($transaction); $success_response['data']['transaction'] = $transaction; // Verify each platform switch($transaction['type']){ case 'ios-appstore': Log::info('Verify App Store.'); if(!$this->verifyAppStore($transaction)){ $error_response['error']['message'] = 'Verify App Store Failed.'; return $error_response; } break; case 'android-playstore': Log::info('Verify Google Play.'); if(!$this->verifyGooglePlay($transaction)){ $error_response['error']['message'] = 'Verify Google Play Failed.'; return $error_response; } break; } return $success_response; } /** * Verify App Store receipt * @param array $transaction * @return boolean */ private function verifyAppStore($transaction){ // endpoint $production_url = 'https://buy.itunes.apple.com/verifyReceipt'; $sandbox_url = 'https://sandbox.itunes.apple.com/verifyReceipt'; $params = [ 'verify' => false, 'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json', ], 'json' => [ 'receipt-data' => $transaction['appStoreReceipt'], ], ]; $http_client = new Client(); // Production try { Log::info('Send iOS receipt production.'); $response = $http_client->request('POST', $production_url, $params); if($response->getStatusCode() !== 200) { Log::notice('Response not 200 OK.'); return false; } $body = json_decode($response->getBody()->getContents(), true); Log::info($body); }catch(ClientException $e) { Log::error($e->getMessage()); return false; } // Sandbox if($body['status'] === 21007) { Log::info('Send iOS receipt sandbox'); try { $response = $http_client->request('POST', $sandbox_url, $params); if($response->getStatusCode() !== 200) { Log::notice('Response not 200 OK.'); return false; } $body = json_decode($response->getBody()->getContents(), true); Log::info($body); }catch(ClientException $e) { Log::error($e->getMessage()); return false; } } if ($body['status'] !== 0) { Log::notice('Receipt status not 0.'); return false; } // Check bundle id if ($body['receipt']['bundle_id'] !== $this->bundle_id) { Log::notice('Invalid bundle id.'); return false; } return true; } /** * Verify Google Play receipt * @param array $transaction * @return boolean */ private function verifyGooglePlay($transaction){ $receipt = $transaction['receipt']; $signature = $transaction['signature']; // RSA public key generation $pubkey = openssl_get_publickey($this->pubkey); // Base64 decode signature $signature = base64_decode($signature); // Signature verification $result = (int)openssl_verify($receipt, $signature, $pubkey, OPENSSL_ALGO_SHA1); if($result !== 1){ Log::notice('Signature invalid.'); return false; } openssl_free_key($pubkey); // Check package name $receipt = json_decode($transaction['receipt']); if($receipt->packageName !== $this->bundle_id) { Log::notice('Invalid package id.'); return false; } return true; } }.envBUNDLE_ID=com.example.billingtest PRODUCT_IDS=coins100,coins200 PUBKEY="-----BEGIN PUBLIC KEY----- **************************************************************** **************************************************************** **************************************************************** **************************************************************** **************************************************************** **************************************************************** ******** -----END PUBLIC KEY-----"仕様通りできていればcordova-plugin-purchaseから呼び出せるはずです。
- 投稿日:2020-12-01T16:22:52+09:00
[PHP]require_once
PHPの require_once について
require_once(リクワイア ワンス)は、プログラムファイルを読み込む命令。読み込んだファイルをコードとして実行する。ワンスと付いているので、一度だけ読み込む。
読み込むファイルの拡張子は、何でもいいが、今回はPHPのコードを含んだファイルを読み込むので、phpという拡張子にしている
HTMLテンプレート<!DOCTYPE html> <html lang='ja'> <head> <meta charset='utf-8'> <title>PHP-Web - wawawa</title> <style>body {padding: 10px;}</style> </head> <body> <h1>Hello templates</h1> </body> </html>index.php<?php // ここにテンプレートを読み込む require_once'views/content.tpl.php';結果> Hello templates以上!
- 投稿日:2020-12-01T14:57:30+09:00
【Laravel8.16】ORMで指定したカラムだけ取得する方法
Laravel歴2日の初心者です。
間違えてたらごめんなさい。指定したカラムだけ取って来たい!!どないすんねん!!ってなったときの備忘録です。
公式マニュアルにも書いておらず、ググっても答えが出てこなかったのでソース直読みすることに。クエリビルダとORMでの全件取得方法
クエリビルダ$users = DB::table('users')->get();ORM$users= App\Models\Users::all();指定したカラムだけ取得する方法
クエリビルダ$users = DB::table('users')->select('name', 'email as user_email')->get();ORM$users= App\Models\Users::all('name', 'email as user_email');ソース
Model.phppublic static function all($columns = ['*']) { return static::query()->get( is_array($columns) ? $columns : func_get_args() ); }
- 投稿日:2020-12-01T14:05:48+09:00
[PHP] エラー解決 npm 脆弱性の解決
前回のエラーを解決する際に脆弱性が2つあるよというエラーに初遭遇。
前回の記事のURLはこちらになります。(省略) found 2 vulnerabilities (1 low, 1 high) run `npm audit fix` to fix them, or `npm audit` for details (省略)上記のエラーを突破しましたのでその備忘録として投稿します。
解説
まず何が脆弱性なのかを確認するためターミナルで
npm audit
を実行する//ターミナルにて npm audit今回は
SEMVER WARNING: Recommended action is a potentially breaking change
というエラーが2つ出てきました。ここでターミナルでnpm audit fix
を実行しましょう。//ターミナルにて npm audit fixしかし、下記の画像のようにまだエラーを修復できませんでした。
これでも突破できる方法がnpm audit fix --force
というものがあります。
--forceはオプションになり強制的に修復させるというやり方です。//ターミナルにて npm audit fix --force
- 投稿日:2020-12-01T13:52:22+09:00
【Laravel】 Laravel SocialiteでSNS認証を実装する
Laravel初学者がSocialiteでSNS認証を実装しようとして、苦戦したので備忘録としての投稿です。
Google、Facebook、TwitterのほかにLINEでもログインできるようにしています。
今回はxamppを使用して実装していきます。
完全に仕組みを理解できない部分もあるので、冗長な記述が多いとは思いますが、
私が実装できた方法のメモ書きみたいな物なので、ご容赦ください。開発環境
・Windows10 64bit
・Laravel Framework 7.29.3
・XAMPP 7.4.11
・PHP 7.4.11
・Apache 2.4.46
・MariaDB 10.4.14実装手順
Laravelやxampp等の環境構築は出来ているものとして説明していきます。
xamppのSSL化もしてあります。なのでURLの最初が
https://
から始まっています。
SSL化が必要ない方はローカルURLのhttps://
をhttp://
で読み替えてください。SSL化については以下の記事を参考にさせていただきました。
XAMPP for WindowsでSSLを有効にする
https://qiita.com/sutara79/items/21a068494bc3a08a48031.Laravelプロジェクトを作成
C:\xampp\htdocsに移動してプロジェクトを作成します。
今回はLaravel7を使いたいので、バージョン指定をしています。
バージョン指定は
composer create-project "laravel/laravel=バージョン" プロジェクト名
で行います。$ composer create-project "laravel/laravel=7.*" social-loginプロジェクト作成後、
cd social-login
でディレクトリ移動2.Socialiteをインストールする
$ composer require laravel/socialite3.ログイン機能を実装する
まずはlaravel/uiライブラリをインストールします。
laravel7ではuiのバージョンを指定しないとエラーが出るのでバージョン2.4を指定しています。$ composer require laravel/ui:^2.4インストール後、↓のコマンドで認証機能をログイン機能を実装します
$ php artisan ui vue --authApacheを起動させてhttps://localhost/social-login/public/ にアクセスすると上の画面が表示されます。
xamppを使ってない人は
$ php artisan serveでビルトインサーバ起動させて、http://localhost:8000/ にアクセスすればOKです。
右上にLOGINが追加されて、ログイン機能が実装されます。
4.ログイン画面のレイアウトを整える
フロントエンドに必要なパッケージをインストールしていきます。
node.jsのインストール
node.jsをまだインストールしてない人は、Node.jsのホームページから
推奨版をインストールしてPCを再起動してください。$ node --version上のコマンドでnode.jsのバージョン出れば、インストールできています。
パッケージのインストール
$ npm install↑パッケージをインストールして
$ npm run dev↑ビルドします
5.データベースの準備
xamppを使っている人はDBから作成します。使ってない人は飛ばしてください。
xammpを使っている人が必要な準備
Apacheをスタートさせた後、mySQLをスタートさせて、右横のAdminをクリック。
データベース名にsocial_loginと入力して作成をクリックこれでDBが作成されます。
次に、今作ったDBをアプリで使用できるようにします。
C:\xampp\htdocs\social-login\.env
.envDB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=social_login //ここを書き換える DB_USERNAME=root DB_PASSWORD=
DB_DATABASE=laravel
となっているところを上記に書き換えます。これで、作成したDBが使えるようになります。
7.Userテーブルを作成する
プロジェクトを作成したときに自動でマイグレーションファイルが出来るので、そのままマイグレートを実行します。
$ php artisan migrateこれでUsersテーブルが作成されます。
マイグレーションファイルの作成
SNSログインではユーザーのパスワードを必要としないので、Usersテーブルのpasswordカラムをnull(からっぽ)でも許容できるようにしなければなりません。
また、SNSからユーザーの名前とメールアドレスの情報を取得できるのですが、
今回はそれらの情報をDBに保存しないようにしたいので、emailカラムもnullを許容できるようにします。
(個人情報を取り扱いたくないのと、LINEなどはemailを取得する際に必要な手順があるため)
では、Usersテーブルの変更を行うマイグレーションファイルを作成していきます。
$ php artisan make:migration [マイグレーションファイル名] --table=[テーブル名]のコマンドでマイグレーションファイルが作成できます。
$ php artisan make:migration change_users_table --table=users上記コマンドを実行すると
C:\xampp\htdocs\social-login\database\migrations
のなかに新しいマイグレーションファイルが作成されるので、変更内容をそこに記述します。<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class ChangeUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('users', function (Blueprint $table) { $table->enum('provider', ['google', 'facebook', 'twitter', 'line'])->nullable(); //カラム追加 $table->string('provided_user_id')->nullable(); $table->string('email')->nullable()->change(); //nullを許容 $table->string('password')->nullable()->change(); $table->unique(['provider', 'provided_user_id']); //ユニーク制約 }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('users', function (Blueprint $table) { $table->dropUnique(['provider', 'provided_user_id']); //ユニーク制約を解除 $table->dropColumn('provider'); //カラムの削除 $table->dropColumn('provided_user_id'); $table->string('email')->nullable(false)->change(); //nullを許容しない $table->string('password')->nullable(false)->change(); }); } }
テーブルを変更する場合には、doctrine/dbalをインストールする必要があります。
入れてない方は下のコマンドをでインストールしてください。$ composer require doctrine/dbal:2.9.3現在の最新バージョンの2.10だとうまくいかなかったので今回はバージョン指定しています。
問題なそうな方は末尾の:2.9.3
の記述を外してください。$ php artisan migrate上のコマンドでマイグレーションを実行してください。
そうするとUsersテーブルがこのようになります。
8.Usersモデルに記述をする
C:\xampp\htdocs\social-login\app\User.php
を以下の記述をします。<?php namespace App; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; use Illuminate\Auth\Authenticatable; class User extends Model implements AuthenticatableContract { use Authenticatable, Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'email', 'password', 'provider', 'provided_user_id', ]; /** * 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', ]; public function getAuthPassword() { return ''; } /** * Get the column name for the "remember me" token. * * @return string */ public function getRememberTokenName() { return ''; } }9.コントローラーに記述をする
C:\xampp\htdocs\social-login\app\Http\Controllers\Auth\LoginController.php
に以下の記述をします。<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Socialite; use App\User; use Illuminate\Support\Facades\Auth; class LoginController extends Controller { /* |-------------------------------------------------------------------------- | Login Controller |-------------------------------------------------------------------------- | | This controller handles authenticating users for the application and | redirecting them to your home screen. The controller uses a trait | to conveniently provide its functionality to your applications. | */ use AuthenticatesUsers; /** * Where to redirect users after login. * * @var string */ protected $redirectTo = RouteServiceProvider::HOME; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest')->except('logout'); } /** * OAuth認証先にリダイレクト * * @param str $provider * @return \Illuminate\Http\Response */ public function redirectToProvider($provider) { return Socialite::driver($provider)->redirect(); } /** * OAuth認証の結果受け取り * * @param str $provider * @return \Illuminate\Http\Response */ public function handleProviderCallback($provider) { $provided_user = Socialite::driver($provider)->user(); $user = User::where('provider', $provider) ->where('provided_user_id', $provided_user->id) ->first(); if ($user === null) { // redirect confirm $user = User::create([ 'name' => $provided_user->name, 'provider' => $provider, 'provided_user_id' => $provided_user->id, ]); } Auth::login($user); return redirect($this->redirectTo); } }10.ルーティングを記述する
C:\xampp\htdocs\social-login\routes\web.php
の一番下に以下の記述を追加します。Route::get('/login/{provider}', 'Auth\LoginController@redirectToProvider'); Route::get('/login/{provider}/callback', 'Auth\LoginController@handleProviderCallback');これでSNSログイン機能の記述はOKです。
11.socialiteの拡張
LINEなどのsocialiteが元から対応しているプロバイダ以外を追加する場合は以下の記述が必要です。
(Google、Facebook、Twitter、Linkedin、GitHub、GitLab、Bitbucketが元から対応しているはずなので、これらだけのSNS認証ならここの記述は不要です。)追加するsoccialiteドライバのインストールする
以下のコマンドで追加するsocialiteのドライバをインストールします
LINE用
$ composer require socialiteproviders/line上記以外のドライバをインストールしたい場合はhttps://socialiteproviders.com/about/ にアクセスして対応したプロバイダのコマンドを探してください。
次に
C:\xampp\htdocs\social-login\config\app.php
の'providers' =>の中に以下の記述を追加します。'providers' => [ /* * 追加のプロバイダ */ SocialiteProviders\Manager\ServiceProvider::class, // この一行を追加 ];次に
C:\xampp\htdocs\social-login\app\Providers\EventServiceProvider.php
に記述を追加します。protected $listen = [ \SocialiteProviders\Manager\SocialiteWasCalled::class => [ 'SocialiteProviders\\Line\\LineExtendSocialite@handle', //LINE ], ];今回はLINEの記述だけをしていますが、ほかのプロバイダを追加したい場合にもhttps://socialiteproviders.com/about/ にアクセスして、追加したいプロバイダのページに飛んで
Add provider event listener
のところにあるコードをコピペしてください。12.クライアントIDとシークレットキーを取得する
今回はクライアントIDとシークレットキーの取得方法は割愛します。
以下のサイトからAPIキーを取得してください。
https://console.developers.google.com/
https://developers.facebook.com
https://apps.twitter.com
・LINE
https://developers.line.biz/ja/services/line-login/13.取得したクライアントIDを記述する
C:\xampp\htdocs\social-login\config\services.php
のreturnの中に追加したプロバイダを追記します。return [ 'facebook' => [ 'client_id' => env('FACEBOOK_CLIENT_ID'), 'client_secret' => env('FACEBOOK_CLIENT_SECRET'), 'redirect' => env('FACEBOOK_REDIRECT_URI'), ], 'google' => [ 'client_id' => env('GOOGLE_CLIENT_ID'), 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 'redirect' => env('GOOGLE_REDIRECT_URL'), ], 'twitter' => [ 'client_id' => env('TWITTER_CLIENT_ID'), 'client_secret' => env('TWITTER_CLIENT_SECRET'), 'redirect' => env('TWITTER_REDIRECT_URI') ], 'line' => [ 'client_id' => env('LINE_CLIENT_ID'), 'client_secret' => env('LINE_CLIENT_SECRET'), 'redirect' => env('LINE_REDIRECT_URL'), ], ];次に
C:\xampp\htdocs\social-login\.env
の一番下に取得したクライアントIDとシークレットキーを追記します。
リダイレクト先のURLも同時に入力していきます。「取得したクライアントID」と「取得したシークレットキー」のところはそれぞれ書き換えてください。
FACEBOOK_CLIENT_ID=取得したクライアントID FACEBOOK_CLIENT_SECRET=取得したシークレットキー FACEBOOK_REDIRECT_URI=https://localhost/social-login/public/login/facebook/callback GOOGLE_CLIENT_ID=取得したクライアントID GOOGLE_CLIENT_SECRET=取得したシークレットキー GOOGLE_REDIRECT_URI=https://localhost/social-login/public/login/google/callback TWITTER_CLIENT_ID=取得したクライアントID TWITTER_CLIENT_SECRET=取得したシークレットキー TWITTER_REDIRECT_URI=https://localhost/social-login/public/login/twitter/callback LINE_CLIENT_ID=取得したクライアントID LINE_CLIENT_SECRET=取得したシークレットキー LINE_REDIRECT_URI=https://localhost/social-login/public/login/line/callback14.動作確認
これでソーシャルログイン機能ができたはずなので、
https://localhost/social-login/public/login/(ログインしたいSNS名)
にアクセスすると、ソーシャルログインができるはずです。お疲れさまでした。
参考にさせていただいた記事
Laravel Socialiteでソーシャルログイン
https://tdomy.com/2020/08/how-to-use-laravel-socialite/socialiteを使ったOAuth認証の実装
https://note.com/kodokuna_dancer/n/n9556f4ad17f2
- 投稿日:2020-12-01T12:44:01+09:00
Laravelでschedule:runしたときのoutputがdockerで標準出力されない問題を解消
はじめに(何が問題か)
以下のようなコマンドがあって、
app/Console/Commands/Hello.php/** * Execute the console command. * * @return mixed */ public function handle() { $this->output->writeLn('Hello !!'); return 0; }下記のように、スケジュール登録していたとして、
app/Console/Kernel.php/** * Define the application's command schedule. * * @param Schedule $schedule * @return void */ protected function schedule(Schedule $schedule) { $schedule->command('app:hello')->everyMinute(); }
artisan schedule:run
しても$ docker-compose run --rm task-scheduler Creating bizany-api_task-scheduler_run ... done Running scheduled command: '/usr/local/bin/php' 'artisan' app:hello >> '/dev/null' 2>&1
Hello !!
は、標準出力されないんですね。
これを解消したという記事になります。
appendOutputTo('/dev/stdout')
は効かない
\Illuminate\Console\Scheduling\Event
にはappendOutputTo
というメソッドがあって、出力先を指定することができます。
指定しないと/dev/null
になってしまいます。
とはいえ、結論として、appendOutputTo('/dev/stdout')
は効きません。app/Console/Kernel.php/** * Define the application's command schedule. * * @param Schedule $schedule * @return void */ protected function schedule(Schedule $schedule) { $schedule->command('app:hello')->appendOutputTo('/dev/stdout')->everyMinute(); }どうやって解消したか
答えは、
stackoverflow
にありました。Dockerfileにて、
RUN ln -sf /proc/1/fd/1 /var/log/laravel-scheduler.logとした上で、出力先を
/var/log/laravel-scheduler.log
とします。app/Console/Kernel.php/** * Define the application's command schedule. * * @param Schedule $schedule * @return void */ protected function schedule(Schedule $schedule) { $schedule->command('app:hello')->appendOutputTo('/var/log/laravel-scheduler.log')->everyMinute(); }すると、
Hello !!
が標準出力されるようになります。
(Dockerコンテナのリビルドをお忘れなく)$ docker-compose run --rm task-scheduler Creating bizany-api_task-scheduler_run ... done Running scheduled command: '/usr/local/bin/php' 'artisan' app:hello >> '/var/log/laravel-scheduler.log' 2>&1 Hello !!おわりに
Laravel
に限った話ではないのかもしれませんが、とにかく何も出力されなくて困り果てていたので解決できて良かったです。
同じ状況で困っている方のお役に立てればと思います。余談
世の中、アドベントカレンダーの季節ですが、普通に記事書いてしまいました 。。
まあ、今後も書きたいときに書いていきます。ではでは。
- 投稿日:2020-12-01T12:35:12+09:00
Laravel 8にてRouteの記述につまずいた話
- 投稿日:2020-12-01T10:40:22+09:00
Laravel Excelの結合セルに罫線を引きたい
laravelにspreadsheetをインストール
/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/ReaderHtml.php
を開いて
if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
}
の最後に以下を追加する$sheet->getStyle($range)->getBorders()->getTop()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN); $sheet->getStyle($range)->getBorders()->getBottom()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN); $sheet->getStyle($range)->getBorders()->getRight()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN); $sheet->getStyle($range)->getBorders()->getLeft()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);以下のようにする
if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { //create merging rowspan and colspan $columnTo = $column; for ($i = 0; $i < $attributeArray['colspan'] - 1; ++$i) { ++$columnTo; } $range = $column . $row . ':' . $columnTo . ($row + $attributeArray['rowspan'] - 1); foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) { $this->rowspan[$value] = true; } $sheet->mergeCells($range); $column = $columnTo; $sheet->getStyle($range)->getBorders()->getTop()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN); $sheet->getStyle($range)->getBorders()->getBottom()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN); $sheet->getStyle($range)->getBorders()->getRight()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN); }
- 投稿日:2020-12-01T10:14:33+09:00
Laravel × Docker AlpineでER図を自動生成する
こんにちは。むらってぃーです。
Laravel Advent Calendar 2020の3日目を担当させていただきます。皆さんはER図使っていますか?
エンティティ同士の関係が一目でわかるドキュメントであるため、手元にあれば非常に便利なものとして効力を発揮します。
一方で、DBスキーマを更新するたびにER図を書き換える必要があり、メンテナンスコストが少々高めです。今回はLaravelでアプリケーションを開発する際、Eloquent ModelからER図を自動生成するツールを紹介します。
DockerのAlpineイメージでLaravelを動かし、その上で生成します。
そのため、チームで運用する際にも導入しやすいです。Laravel ER Diagram Generator
https://github.com/beyondcode/laravel-er-diagram-generator
LaravelのEloquentModelから、ER図を自動生成するライブラリです。
Star数は1000超えで、多くの方に利用されているみたいです。インストール
Dockerfile
Laravelを動かしているAlpineイメージに下記を追加します。
内部でgraphvizというツールを使っているため、apk経由でそちらをインストールします。
フォントもインストールしないと、ER図に日本語や英語が出力されないので入れておきます。RUN apk --no-cache add graphviz fontconfig \ && rm -rf /var/cache/apk/* \ # graphviz用フォントインストール && curl -O https://noto-website.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip \ && mkdir -p /usr/share/fonts/NotoSansCJKjp \ && unzip NotoSansCJKjp-hinted.zip -d /usr/share/fonts/NotoSansCJKjp/ \ && rm NotoSansCJKjp-hinted.zip \ && fc-cache -fvcomposer.json
Alpineイメージに入り込み、下記コマンドでLaravel ER Diagram Generatorを入れます。
composer require beyondcode/laravel-er-diagram-generator --devServiceProvider
ローカルでのみこのライブラリが読み込まれるようにします。
AppServiceProvider.phpclass AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { if ($this->app->environment('local')) { // ER図生成用のProviderはcomposer の require-devでインストールしているので、localでのみ使用 $this->app->register(\BeyondCode\ErdGenerator\ErdGeneratorServiceProvider::class); } } ...config用意・修正
下記コマンドで、configファイルをライブラリ内からプロジェクト内にコピー。
cp ./vendor/beyondcode/laravel-er-diagram-generator/config/config.php config/erd-generator.php今回は App/Models配下にモデルファイルを置くため、configでModelファイルの読み込み先を下記に変更します。
config/erd-generator.php<?php return [ /* * All models in these directories will be scanned for ER diagram generation. * By default, the `app` directory will be scanned recursively for models. */ 'directories' => [ base_path('app') . '/Models', ], ...Model用意
では、Modelを用意します。
Model概要
今回は下記のモデルにしてみます。
Book(本)
カラム名 備考 ID 自動採番ID Title タイトル AuthorID 著者ID. 著者テーブルへのリレーション. Author(著者)
カラム名 備考 ID 自動採番ID Name 著者名 マイグレーションファイル用意
authorsテーブルとbooksテーブルを作ります。
2020_11_09_154434_create_tables.php... class CreateTables extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('authors', function (Blueprint $table) { $table->increments('id'); $table->integer('name'); $table->timestamps(); }); Schema::create('books', function (Blueprint $table) { $table->increments('id'); $table->string('title'); $table->integer('author_id'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('books'); Schema::dropIfExists('authors'); } }こちらはそのまま流します。
php artisan migrateModelファイル用意
app/Models/Author.php<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Author extends Model { }app/Models/Book.php<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Book extends Model { public function author(): BelongsTo { return $this->belongsTo('App\Models\Author'); } }Bookには、Authorへのリレーションを記載します。
なお、ER図に記載されるその他のフィールドは、アプリケーションが繋がっているDBから読み取ってくれる動きです。
ER図生成
Alpineコンテナの中に入り、下記のコマンドを打ちます。
php artisan generate:erd er-diagram.pngすると、プロジェクトルートに er-diagram.pngというファイルが出来上がります。
Userは、Laravelで最初から組み込まれているものです。
このようにBookとAuthorの関係がER図として出力されています。BelongsToの矢印が色付けされているのが個人的には嬉しいです。
最後に
今回はLaravel ER Diagram Generatorを使い、Larvel × Docker AlpineでER図を自動生成する方法を紹介しました。
一度このように土台を作れば、migrationファイルとModelファイルの管理のみでER図を自動生成できます。
メンテナンスコストがほとんどかからないのが嬉しいですね。
ER図生成のコマンドはCIに組み込むなりして、常に最新に保つことができればさらに幸せになれそうですね!
- 投稿日:2020-12-01T09:21:29+09:00
Web エンジニアが始める Server-side Swift "Vapor"
はじめに
2014年の WWDC で電撃発表された Swift は、もはや iOS アプリ開発では定番となっていますが、Web 開発界隈のエンジニアにとっては「何それ美味しいの?」という感じであり、ましてや「サーバサイドで Swift が動く!」とか言われても、どうせ最低限の事しかできず「プレステで Linux が動く!」くらいのお遊びだと思われている方も一定数いらっしゃるのでは無いでしょうか?(シランケド)とか言う私も、当時の WWDC の Swift 発表は興奮して朝まで眠れなかったくらいですが、最近は Swift からは少し離れて Web アプリ開発にどっぷりです。
そんなわけで、今回は、Web 開発者の目線で、Swift の Web アプリケーションフレームワーク Vapor を触ってみたいと思います。
Swift ってどんな言語?
2014年以前は、iOS や macOS のアプリは Objective-C という
変態言語での開発が主流でした。WWDC での突然の発表で世に出た Swift ですが、普及するには4、5年はかかると見られていた予想とは裏腹に、あっと言う間に多くのプロジェクトが Swift に移行していきました。一つの iOS プロジェクト内で Objective-C と Swift を混在して書けるというバイナリ互換が後押ししたというのもあるでしょう。さらに翌年にはオープンソース化して GitHub で公開したことによって、他のプラットフォームへの移植のハードルが下がりました。今では Linux だけでなく Windows でも動作するようになっています。
じゃあ Linux や Windows でも iOS のようなアプリが作れるのかというとそうではなく、あくまでも言語の基礎部分だけがオープンソース化しているに過ぎず、スマートフォンの画面を構成する UiKit や、マルチメディアな機能を扱う AVFoundation など iPhone 環境に特化した様々な非公開フレームワーク群が備わった環境が iOS SDK としてデベロッパーにのみ提供されており、Xcode でのアプリ開発を可能にしているわけです。
そんな Swift さんですが、Objective-C という技術的負債をバッサリ捨てたことにより、何の障壁もなく様々なプログラムバラダイムのトレンドを実装できたという背景もあって、クロージャーやジェネリクス、Optional 型、タイプセーフな構文などは他のナウい言語と共通する部分もあって学びやすい言語だと思います。
Vapor とは?
Vapor はそんな Swift で書かれた Web アプリケーションフレームワークです。Laravel (Lumen) にインスパイアされて作ったとか作らなかったとか(すみません以前公式に書かれてた気がしましたがソースが見つかりませんでした)。Web アプリケーション開発では一般的な MVC の構成で、DB接続 (PostgreSQL / MySQL / SQLite / MongoDB) はもちろん、ORM やテンプレートエンジンなどの Web に必須となる機能もエコシステムとして提供されています。
- Vapor 公式
- Vapor GitHub
Vapor は今のところ Mac と Linux で利用可能です。Mac だと Homebrew、Linux では yum や apt などでパッケージが提供されています。あくまでも実行環境ということなので docker や Homestead を使えば Windows でも開発は可能です。
余談ですが、Swift 製の Web アプリケーションフレームワークと言えば当初は IBM が開発する Kitura が有名でしたが、調べたら2020年1月に(IBMとして)開発を終了しているようでした。
- Kitura 公式
- Kitura GitHub
サーバサイドを Swift で書くメリット
Swift で iOS アプリを開発しているエンジニアにとっては、馴染みの無い PHP や Ruby に手を出してサーバサイドの実装を行うことは大きなハードルになりますが、サーバサイドも Swift で書けるということで、システム全体を得意言語でカバーできることになります。何より頭の中で言語のコンテキストをスイッチすることが無くなると言うメリットは大きいでしょう。Web エンジニア風に言えば、シングルページアプリケーションを JavaScript で書きつつ、サーバ API は node.js で実装するような「JavaScript ボーダレス」な開発環境と言えば分かりやすいんじゃないでしょうか。
では、PHP などの非コンパイル言語での開発を生業としているエンジニアにとって、Swift でサーバサイドのプログラムを実装するメリットは何でしょうか?
Swift を使ったことがなければ答えは「メリットなし」でしょう。餅は餅屋。得意言語で実装するに越したことはないです。ただ、少しでも Swift を触ったことがあれば、(もしくはこれから学ぼうという意欲があれば)、読みやすい、書きやすいで定評のある Swift を使うメリットはあると思います。個人的には Xcode での開発ができる点もメリットだと思います。Web 開発の IDE は Electron や Javas ベースのものがほとんどで、Web 開発者の多くは、メモリ食い過ぎ問題や動作もっさり問題に悩まされています。 macOS ネイティブで快適に安定動作する IDE は皆無でした。賛否はありますが、Xcode が使えるのは開発効率的にもメリットだと思います。
環境準備
言語比較やIDEの批評をすると石が飛んでくるらしいので早速 Vapor のインストールに入りましょう。
まずは事前に下記をインストール。Xcode は初回立ち上げ時に License Agreement に同意しないと Vapor のインストールで怒られるので同意しておいてください。Developer Program への登録は不要なので、AppStore からインストールしてください。
インストール
Vapor 本体は Homebrew でインストールします。下記コマンドで一発です。
$ brew install vapor完了したら vapor コマンドが使用できるので、バージョンが表示されればOKです。
$ vapor --version framework: 4.36.0プロジェクトの作成
では早速 Web アプリケーションを作成しましょう。ここでは ApiTest というプロジェクトを作成します。vapor new コマンドでプロジェクト名を渡します。
$ vapor new ApiTest Cloning template... name: ApiTest Would you like to use Fluent? (--fluent/--no-fluent) y/n>Fluent を使用するかどうかを聞かれるので、y か n を入力。
Fluent は Laravel で言うところの Eloquent のようなライブラリみたいです。とりあえず y で入れておきます。Which database would you like to use? (--fluent.db) 1: Postgres (Recommended) 2: MySQL 3: SQLite 4: MongoFluent を使用すると言うことはその後ろにデータベースがあるわけですので、次でデータベース製品を選択します。PostgreSQL が Recomended なので、1 を入力。
Would you like to use Leaf? (--leaf/--no-leaf) y/n>次は Leaf の追加。こちらは Laravel で言うところの Blade テンプレートシステムです。もちろん y を入力。
すると完了メッセージと共に下記のようなアスキーアートが表示されます。
アスキーアートと言えば、昔アドベントカレンダーで書いた自分の記事を思い出しました。全然関係ないけど。
- 【アスキーアート職人歓喜?】画像からアスキーアートを生成
Xcode の起動
プロジェクトディレクトリに移動して下記コマンドで Xcode が起動します。
$ cd cd ApiTest $ vapor xcode初回起動時は依存パッケージがダウンロードされるのでしばらく待って、左上のビルドボタン(再生ボタン)がアクティブになるまで待ってください。
依存バッケージのダウンロードが完了したら、ビルドを実行してみましょう。ショートカットキー「⌘ + R」で実行できます。
初回は時間がかかりますが、完了すると Xcode のコンソールに下記のようなメッセージが表示されます。[ WARNING ] No custom working directory set for this scheme, using /Users/shigeta/Library/Developer/Xcode/DerivedData/ApiTest-awtfxnyehqlqwxclpfyylflkbavb/Build/Products/Debug [ NOTICE ] Server starting on http://127.0.0.1:8080ちょうど npm run dev したような感じですね。ではブラウザでアクセスしましょう。
軽く構成を眺める
初期構成のファイル構成を軽く見てみましょう。
既視感があると言うかなんと言うか、もうフォルダ名のファイル名で大体わかっちゃいますよね。主要なファイルをさくっと紹介。Resources/Views/index.leaf
Leafのテンプレートファイルです。Webエンジニアであれば説明は不要でしょう。
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>#(title)</title> </head> <body> <h1>#(title)</h1> </body> </html>Sources/App/Controllers/TodoController.swift
こちらはコントローラーです。
予め TODO の CRUD 遷移が実装されています。import Fluent import Vapor struct TodoController: RouteCollection { func boot(routes: RoutesBuilder) throws { let todos = routes.grouped("todos") todos.get(use: index) todos.post(use: create) todos.group(":todoID") { todo in todo.delete(use: delete) } } func index(req: Request) throws -> EventLoopFuture<[Todo]> { return Todo.query(on: req.db).all() } func create(req: Request) throws -> EventLoopFuture<Todo> { let todo = try req.content.decode(Todo.self) return todo.save(on: req.db).map { todo } } func delete(req: Request) throws -> EventLoopFuture<HTTPStatus> { return Todo.find(req.parameters.get("todoID"), on: req.db) .unwrap(or: Abort(.notFound)) .flatMap { $0.delete(on: req.db) } .transform(to: .ok) } }Migrations/CreateTodo.swift
マイグレーションファイルもありますね。Swift 知らなくても Laravel や Rails 使ってる人であれば直感的に理解できるんじゃないでしょうか。
import Fluent struct CreateTodo: Migration { func prepare(on database: Database) -> EventLoopFuture<Void> { return database.schema("todos") .id() .field("title", .string, .required) .create() } func revert(on database: Database) -> EventLoopFuture<Void> { return database.schema("todos").delete() } }Models/Todo.swift
モデルクラスです。
import Fluent import Vapor final class Todo: Model, Content { static let schema = "todos" @ID(key: .id) var id: UUID? @Field(key: "title") var title: String init() { } init(id: UUID? = nil, title: String) { self.id = id self.title = title } }Sources/App/routes.swift
URLルート定義ファイル。URLのマッピングをここに記載。これも Web アプリケーションフレームワークではおなじみですね。
import Fluent import Vapor func routes(_ app: Application) throws { app.get { req in return req.view.render("index", ["title": "Hello Vapor!"]) } app.get("hello") { req -> String in return "Hello, world!" } try app.register(collection: TodoController()) }まとめ
Vapor はざっと見た感じ Rails や Laravel に近い構成なので、Web アプリケーションエンジニアにとっては取っ付きやすいのでは無いでしょうか。
Swift は言語的にもとてもスマートで、本当に洗練された無駄の無い言語なので、iOS アプリだけで使うのは勿体無いですが、とは言え、PHP などの軽量言語のエンジニアにとってはまだまだ敷居が高いです。特にブラウザ上で動作するメイン言語がまだまだ JavaScript 全盛なので、Web アプリエンジニアにとってはあまりメリットが感じられないかもしれません。
そうなると願いは一つですね。
いつの日か、Swift がブラウザ上で動作する時代が来ますように!
告知
猫会社 猫会社で有名な qnote は今年もアドベントカレンダーに参加しています!
応援よろしくお願いします!
qnote Advent Calendar 2020
過去の猫会社アドベントカレンダー
- 投稿日:2020-12-01T01:42:20+09:00
Laravel6でマルチ認証(管理者/ユーザー)の基本設定
(コントローラー構成イメージ)
app\Http\Controllers | (一般ページ〜) ├── HomeController.php ├── CompanyController.php └── Admin(管理者ダッシュボード〜) | ├ Auth | | ├ LoginController.php | | └ RegisterController.php | ├ DashboardController.php | └ ... └── User(会員ページ〜) ├ Auth | ├ LoginController.php | └ RegisterController.php ├ DashboardController.php └ ...Controllerのディレクトリ準備
■Admin/Usreディレクトリの作成
$ mkdir ./app/Http/Controllers/{User,Admin}■元のAuthディレクトリをUser以下に移動
$ mv ./app/Http/Controllers/Auth ./app/Http/Controllers/User/Auth■AuthディレクトリをコピーしてAdmin以下にも配置
$ cp -r ./app/Http/Controllers/User/Auth ./app/Http/Controllers/Admin/Auth■namespaceの変更: User側のController
namespace App\Http\Controllers\User\Auth;■namespaceの変更: Admin側のController
namespace App\Http\Controllers\Admin\Auth;Migrationの作成
$ php artisan make:model Admin -m(database/migrations以下にMigrationファイルが生成されます)
database/migrations ├── 2014_10_12_000000_create_users_table.php ├── 2014_10_12_100000_create_password_resets_table.php ├── 2019_08_19_000000_create_failed_jobs_table.php └── 2020_11_29_015635_create_admins_table.phpAdminテーブルのMigrationファイルの編集
Schema::create部分をUserモデル参考に変更します。2020_11_29_015635_create_admins_table.phpSchema::create('admins', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); });migrationを実行します。(tableが作成されます)
$ php artisan migrateSeederの準備
■テストユーザー作成のためのシーダーを準備
$ php artisan make:seeder UsersTableSeeder $ php artisan make:seeder AdminTableSeeder■シーダーで作成する、ユーザー/管理者のデータを設定します。
database/seeds/UsersTableSeeder.php<?php use Illuminate\Database\Seeder; class UsersTableSeeder extends Seeder { public function run() { DB::table('users')->insert([ 'name' => 'member', 'email' => 'member@example.com', 'password' => Hash::make('pass'), 'remember_token' => Str::random(10), ]); } }database/seeds/AdminsTableSeeder.php<?php use Illuminate\Database\Seeder; class AdminTableSeeder extends Seeder { public function run() { DB::table('admins')->insert([ 'name' => 'kanri', 'email' => 'admin@example.com', 'password' => Hash::make('word'), 'remember_token' => Str::random(10), ]); } }database/seeds/DatabaseSeeder.php<?php use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { public function run() { $this->call([ UsersTableSeeder::class, AdminsTableSeeder::class, ]); } }準備ができたところでSeedを実行します。(データがはいります)
$ php artisan db:seedModelの準備
User.phpを参考にAdmin.phpを編集します。(Class名以外はUserと一緒)
app/Admin.php<?php namespace App; 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', ]; }管理者(Admin)認証設定:config/auth.php
■デフォルトのguardをwebからusersに変更
'defaults' => [ 'guard' => 'user', 'passwords' => 'users', ],■guardsのwebをuserに変更/adminを追加
'guards' => [ 'user' => [ 'driver' => 'session', 'provider' => 'users', ], 'admin' => [ 'driver' => 'session', 'provider' => 'admins', ], 'api' => [ 'driver' => 'token', 'provider' => 'users', 'hash' => false, ], ],■providersにadminsを追加
'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\User::class, ], 'admins' => [ 'driver' => 'eloquent', 'model' => App\Admin::class, ], ],■passwordsにadminを追加
'passwords' => [ 'users' => [ 'provider' => 'users', 'table' => 'password_resets', 'expire' => 60, 'throttle' => 60, ], 'admins' => [ 'provider' => 'admins', 'table' => 'password_resets', 'expire' => 60, 'throttle' => 60, ], ],〜(長いので一旦)User側の認証をまず作ります〜
ログイン後のコントローラーの準備
ログイン後のダッシュボードのコントローラーを作成。
$ php artisan make:controller User/DashboardControllerUser/DashboardController.php<?php namespace App\Http\Controllers\User; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class DashboardController extends Controller { public function index() { return view('user.index'); } }ルーティング設定
以下のルーティングの設定を行います。
- トップページのviewの変更
- Userのプリフィックスの設定
- ルーティングにAuthの制限
routes/web.php<?php Route::get('/', function () { return view('index'); }); Route::namespace('User')->prefix('user')->name('user.')->group(function () { Auth::routes(); Route::middleware('auth:user')->group(function () { Route::get('/', 'DashboardController@index')->name('index'); }); });ログイン前後のリダイレクト先の設定
以下の3ケースについて設定していきます。
- ログイン未:制限ページへのアクセス
- ログイン済:ログインページへのアクセス
- ログイン後:リダイレクト先ページ
1.ログイン未:制限ページへのアクセス
ルートのnameがuser/adminの場合で分岐しています。
app/Http/Middleware/Authenticate.php<?php namespace App\Http\Middleware; use Illuminate\Auth\Middleware\Authenticate as Middleware; use Illuminate\Support\Facades\Route; class Authenticate extends Middleware { /** * Get the path the user should be redirected to when they are not authenticated. * * @param \Illuminate\Http\Request $request * @return string|null */ protected function redirectTo($request) { if (! $request->expectsJson()) { if(Route::is('user.*')){ return route('user.login'); }else if(Route::is('admin.*')){ return route('admin.login'); }else{ abort(403, 'Unauthorized action.'); } } } }2.ログイン済:ログインページへのアクセス
guardのタイプでリダイレクト先を変更
app/Http/Middleware/RedirectIfAuthenticated.php<?php namespace App\Http\Middleware; use App\Providers\RouteServiceProvider; use Closure; use Illuminate\Support\Facades\Auth; class RedirectIfAuthenticated { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @param string|null $guard * @return mixed */ public function handle($request, Closure $next, $guard = null) { if (Auth::guard($guard)->check()) { $home = ''; switch($guard){ case 'user': $home = RouteServiceProvider::HOME; break; case 'admin': $home = RouteServiceProvider::HOME_ADMIN; break; default: $home = RouteServiceProvider::HOME; } return redirect($home); } return $next($request); } }3. ログイン後:リダイレクト先ページ
User用のHOMEを変更しています。
Admin用のHOME_ADMINを追記しています。app/Providers/RouteServiceProvider.phppublic const HOME = '/user'; public const HOME_ADMIN = '/admin';LoginControllerの設定
パスの変更などがあるため以下をOverrideして微調整します。
- showLoginForm()をOverride
- loggedOut()をOverride
app/Http/Controllers/User/Auth/LoginController.php<?php namespace App\Http\Controllers\User\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; class LoginController extends Controller { use AuthenticatesUsers; protected $redirectTo = RouteServiceProvider::HOME; public function __construct() { $this->middleware('guest')->except('logout'); } public function showLoginForm() { return view('user.auth.login'); } protected function loggedOut(Request $request) { return redirect(route('user.login')); } }RegisterControllerの設定
- ネームスペースを変更
- showRegistrationForm()をOverride
app/Http/Controllers/User/Auth/RegisterController.php<?php namespace App\Http\Controllers\User\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use App\User; use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; class RegisterController extends Controller { use RegistersUsers; protected $redirectTo = RouteServiceProvider::HOME; public function __construct() { $this->middleware('guest'); } protected function validator(array $data) { return Validator::make($data, [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'string', 'min:8', 'confirmed'], ]); } protected function create(array $data) { return User::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), ]); } public function showRegistrationForm() { return view('user.auth.register'); } }パスワードリセットURLの設定
パスワードリセットのURLを
http://example.com/user/password/reset/{token}
のように/user/以下に配置したいためメールを生成しているクラスを変更します。■リセットメール送信Notification作成
$ php artisan make:notification PasswordResetUserNotification■app/Notifications/PasswordResetUserNotification.php
※ResetPasswordを継承してtoMailのみをオーバーライドしています。app/Notifications/PasswordResetUserNotification.php<?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 PasswordResetUserNotification extends ResetPassword { 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(route('user.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.')); } }■app/User.phpでsendPasswordResetNotification(Illuminate\Auth\Passwords\CanResetPassword)のオーバーライド
app/User.phppublic function sendPasswordResetNotification($token) { $this->notify(new PasswordResetUserNotification($token)); }パスワードリセットフォームの表示設定
- showLinkRequestForm()のオーバーライド
app/Http/Controllers/User/Auth/ForgotPasswordController.php<?php namespace App\Http\Controllers\User\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\SendsPasswordResetEmails; class ForgotPasswordController extends Controller { use SendsPasswordResetEmails; public function showLinkRequestForm() { return view('user.auth.passwords.email'); } }パスワードリセットフォームの設定
- showResetFormメソッドのオーバーライド
app/Http/Controllers/User/Auth/ResetPasswordController.php<?php namespace App\Http\Controllers\User\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Http\Request; class ResetPasswordController extends Controller { use ResetsPasswords; protected $redirectTo = RouteServiceProvider::HOME; public function showResetForm(Request $request, $token = null) { return view('user.auth.passwords.reset')->with( ['token' => $token, 'email' => $request->email] ); } }viewの設定
user側のviewを以下の通り構成していきます。
実用的ではないのですが、極力マルチ認証をシンプルに一旦完成できるようにしてみました。resources/views | ├── index.blade.php ├── layouts | └ default.blade.php └── user ├ index.blade.php └ auth ├ login.blade.php ├ register.blade.php ├ verify.blade.php └ passwords ├ reset.blade.php └ email.blade.phpファイル/ディレクトリ作成
$ mv resources/views/welcome.blade.php resources/views/index.blade.php $ mkdir resources/views/{user,layouts} $ touch resources/views/layouts/default.blade.php $ touch resources/views/user/index.blade.php $ mkdir resources/views/user/auth $ mkdir resources/views/user/auth/passwords $ touch resources/views/user/auth/{login.blade.php,register.blade.php,verify.blade.php} $ touch resources/views/user/auth/passwords/{reset.blade.php,email.blade.php}resources/views/layouts/default.blade.php<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>マルチ認証テスト</title> <script src="{{ asset('js/app.js') }}" defer></script> <link href="{{ asset('css/app.css') }}" rel="stylesheet"> </head> <body> <h2>ユーザー関連</h2> <ul> <li><a href="{{ route('user.login') }}">ユーザーログイン</a></li> <li><a href="{{ route('user.register') }}">ユーザー登録</a></li> <li><a href="{{ route('user.index') }}">ユーザーダッシュボード</a></li> </ul> @auth('user') <p>ようこそ{{ Auth::user()->name }}</p> <form method="POST" action="{{ route('user.logout') }}"> @csrf <button type="submit">ログアウト</button> </form> @else <p>ユーザー未ログイン</p> @endauth <hr> <h2>管理者関連</h2> <ul> <li><a href="{{ route('admin.login') }}">管理者ログイン</a></li> <li><a href="{{ route('admin.register') }}">管理者登録</a></li> <li><a href="{{ route('admin.index') }}">管理者ダッシュボード</a></li> </ul> @auth('admin') <p>ようこそ{{ auth('admin')->user()->name }}</p> <form method="POST" action="{{ route('admin.logout') }}"> @csrf <button type="submit">ログアウト</button> </form> @else <p>管理者未ログイン</p> @endauth <hr> <main> @yield('content') </main> </body> </html>resources/views/index.blade.php@extends('layouts.default') @section('content') <h1>トップページ</h1> @endsectionresources/views/user/index.blade.php@extends('layouts.default') @section('content') <h1>ユーザーダッシュボード</h1> @endsectionresources/views/user/auth/login.blade.php@extends('layouts.default') @section('content') <form method="POST" action="{{ route('user.login') }}"> @csrf <div> <input type="email" name="email" value="{{ old('email') }}"> @error('email')<p>{{ $message }}</p>@enderror </div> <div> <input type="password" name="password"> @error('password')<p>{{ $message }}</p>@enderror </div> <div> <button type="submit">ログイン</button> </div> </form> <a href="{{ route('user.password.request') }}">パスワードを忘れた方</a> @endsectionresources/views/user/auth/register.blade.php@extends('layouts.default') @section('content') <form method="POST" action="{{ route('user.register') }}"> @csrf <div> <input type="text" name="name" value="{{ old('name') }}" placeholder="name"> @error('name')<p>{{ $message }}</p>@enderror </div> <div> <input type="email" name="email" value="{{ old('email') }}" placeholder="email"> @error('email')<p>{{ $message }}</p>@enderror </div> <div> <input type="password" name="password" placeholder="password"><br> <input type="password" name="password_confirmation" placeholder="password_confirmation"> @error('password')<p>{{ $message }}</p>@enderror </div> <button type="submit">登録</button> </form> @endsectionresources/views/user/auth/passwords/email.blade.php@extends('layouts.default') @section('content') @if (session('status')) <p>{{ session('status') }}</p> @endif <form method="post" action="{{ route('user.password.email') }}"> @csrf <input type="email" name="email" value="{{ old('email') }}"> @error('email') <p>リンクの送信に失敗しました。</p> @enderror <button type="submit">パスワードリセットリンクを送信</button> </form> @endsectionresources/views/user/auth/passwords/reset.blade.php@extends('layouts.default') @section('content') <form method="POST" action="{{ route('user.password.update') }}"> @csrf <input type="hidden" name="token" value="{{ $token }}"> <div> <input type="email" name="email" value="{{ $email ?? old('email') }}" placeholder="email"> @error('email')<p>{{ $message }}</p>@enderror </div> <div> <input type="password" name="password" placeholder="password"><br> <input type="password" name="password_confirmation" placeholder="password_confirmation"> @error('password')<p>{{ $message }}</p>@enderror </div> <button type="submit">パスワードのリセット</button> </form> @endsection〜ユーザー側ができたのでやっと管理者側〜
User側を参考にAdmin側を作っていきます。
ログイン後のコントローラーの準備
$ php artisan make:controller Admin/DashboardControllerapp/Http/Controllers/Admin/DashboardController.php<?php namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class DashboardController extends Controller { public function index() { return view('admin.index'); } }ルーティング設定
- Adminのプリフィックスの設定を追加
routes/web.php<?php Route::get('/', function () { return view('index'); }); Route::namespace('User')->prefix('user')->name('user.')->group(function () { Auth::routes(); Route::middleware('auth:user')->group(function () { Route::get('/', 'DashboardController@index')->name('index'); }); }); Route::namespace('Admin')->prefix('admin')->name('admin.')->group(function () { Auth::routes(); Route::middleware('auth:admin')->group(function () { Route::get('/', 'DashboardController@index')->name('index'); }); });LoginControllerの設定
- showLoginForm()をOverride
- guard()をOverride
- logout()をOverride
- loggedOut()をOverride
app/Http/Controllers/Admin/Auth/LoginController.php<?php namespace App\Http\Controllers\Admin\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class LoginController extends Controller { use AuthenticatesUsers; protected $redirectTo = RouteServiceProvider::HOME_ADMIN; public function __construct() { $this->middleware('guest')->except('logout'); } public function showLoginForm() { return view('admin.auth.login'); } protected function guard() { return Auth::guard('admin'); } public function logout(Request $request) { Auth::guard('admin')->logout(); return $this->loggedOut($request); } protected function loggedOut(Request $request) { return redirect(route('admin.login')); } }RegisterControllerの設定
- ネームスペースを変更
- guard()をOverride
- showRegistrationForm()をOverride
app/Http/Controllers/Admin/Auth/RegisterController.php<?php namespace App\Http\Controllers\Admin\Auth; use App\Admin; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; class RegisterController extends Controller { use RegistersUsers; protected $redirectTo = RouteServiceProvider::HOME; public function __construct() { $this->middleware('guest'); } protected function validator(array $data) { return Validator::make($data, [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:admins'], 'password' => ['required', 'string', 'min:8', 'confirmed'], ]); } protected function guard() { return Auth::guard('admin'); } protected function create(array $data) { return Admin::create([ 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), ]); } public function showRegistrationForm() { return view('admin.auth.register'); } }パスワードリセットURLの設定
パスワードリセットのURLを
http://example.com/admin/password/reset/{token}
のように/admin/以下に配置したいためメールを生成しているクラスを変更します。■リセットメール送信Notification作成
$ php artisan make:notification PasswordResetAdminNotification※ResetPasswordを継承してtoMailのみをオーバーライドしています。
app/Notifications/PasswordResetAdminNotification.php<?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 PasswordResetAdminNotification extends ResetPassword { 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(route('admin.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.')); } }
- Illuminate\Auth\Passwords\CanResetPasswordのオーバーライド
app/Admin.phpでsendPasswordResetNotification.phppublic function sendPasswordResetNotification($token) { $this->notify(new PasswordResetAdminNotification($token)); }パスワードリセットフォームの設定
- showLinkRequestForm()のオーバーライド
- sendResetLinkEmail()のオーバーライド
app/Http/Controllers/Admin/Auth/ForgotPasswordController.php<?php namespace App\Http\Controllers\Admin\Auth; use App\Admin; use App\Http\Controllers\Controller; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Illuminate\Foundation\Auth\SendsPasswordResetEmails; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Password; use UnexpectedValueException; class ForgotPasswordController extends Controller { use SendsPasswordResetEmails; public function showLinkRequestForm() { return view('admin.auth.passwords.email'); } public function sendResetLinkEmail(Request $request) { $this->validateEmail($request); $broker = Password::broker('admins'); $response = $broker->sendResetLink( $this->credentials($request) ); return $response == Password::RESET_LINK_SENT ? $this->sendResetLinkResponse($request, $response) : $this->sendResetLinkFailedResponse($request, $response); } }パスワードリセットフォームの設定
- redirectToの変更
- showResetFormメソッドのオーバーライド
app/Http/Controllers/Admin/Auth/ResetPasswordController.php<?php namespace App\Http\Controllers\Admin\Auth; use App\Http\Controllers\Controller; use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Http\Request; class ResetPasswordController extends Controller { use ResetsPasswords; /** * Where to redirect users after resetting their password. * * @var string */ protected $redirectTo = RouteServiceProvider::HOME_ADMIN; public function showResetForm(Request $request, $token = null) { return view('admin.auth.passwords.reset')->with( ['token' => $token, 'email' => $request->email] ); } }viewの設定
Admin側のviewを以下の通り構成していきます。
resources/views(に追加) └── admin ├ index.blade.php └ auth ├ login.blade.php ├ register.blade.php ├ verify.blade.php └ passwords ├ reset.blade.php └ email.blade.phpファイル/ディレクトリ作成
$ mkdir resources/views/admin $ touch resources/views/admin/index.blade.php $ mkdir resources/views/admin/auth $ mkdir resources/views/admin/auth/passwords $ touch resources/views/admin/auth/{login.blade.php,register.blade.php,verify.blade.php} $ touch resources/views/admin/auth/passwords/{reset.blade.php,email.blade.php}resources/views/admin/index.blade.php@extends('layouts.default') @section('content') <h1>管理者ダッシュボード</h1> @endsectionresources/views/admin/auth/login.blade.php@extends('layouts.default') @section('content') <form method="POST" action="{{ route('admin.login') }}"> @csrf <div> <input type="email" name="email" value="{{ old('email') }}"> @error('email')<p>{{ $message }}</p>@enderror </div> <div> <input type="password" name="password"> @error('password')<p>{{ $message }}</p>@enderror </div> <div> <button type="submit">ログイン</button> </div> </form> <a href="{{ route('admin.password.request') }}">パスワードを忘れた方</a> @endsectionresources/views/admin/auth/register.blade.php@extends('layouts.default') @section('content') <form method="POST" action="{{ route('admin.register') }}"> @csrf <div> <input type="text" name="name" value="{{ old('name') }}" placeholder="name"> @error('name')<p>{{ $message }}</p>@enderror </div> <div> <input type="email" name="email" value="{{ old('email') }}" placeholder="email"> @error('email')<p>{{ $message }}</p>@enderror </div> <div> <input type="password" name="password" placeholder="password"><br> <input type="password" name="password_confirmation" placeholder="password_confirmation"> @error('password')<p>{{ $message }}</p>@enderror </div> <button type="submit">登録</button> </form> @endsectionresources/views/admin/auth/passwords/email.blade.php@extends('layouts.default') @section('content') @if (session('status')) <p>{{ session('status') }}</p> @endif <form method="post" action="{{ route('user.password.email') }}"> @csrf <input type="email" name="email" value="{{ old('email') }}"> @error('email') <p>リンクの送信に失敗しました。</p> @enderror <button type="submit">パスワードリセットリンクを送信</button> </form> @endsectionuser/auth/passwords/reset.blade.php@extends('layouts.default') @section('content') <form method="POST" action="{{ route('user.password.update') }}"> @csrf <input type="hidden" name="token" value="{{ $token }}"> <div> <input type="email" name="email" value="{{ $email ?? old('email') }}" placeholder="email"> @error('email')<p>{{ $message }}</p>@enderror </div> <div> <input type="password" name="password" placeholder="password"><br> <input type="password" name="password_confirmation" placeholder="password_confirmation"> @error('password')<p>{{ $message }}</p>@enderror </div> <button type="submit">パスワードのリセット</button> </form> @endsection参考にさせて頂いたページ。
Laravel6でマルチ認証を実装する(UserとAdminの階層を分ける)
https://qiita.com/namizatork/items/5d56d96d4c255a0e3a87
- 投稿日:2020-12-01T00:21:00+09:00
[PHP]標準入力で与えられる値を取得する
- 投稿日:2020-12-01T00:20:06+09:00
Laravel 全テーブルをTruncateしてからシーディングする方法
Laravel Advent Calendar 2020 - Qiita の 1日目 の記事です。
明日は @yoshikyoto さんのレガシーなプロダクトに Laravel を導入する第一歩(Laravel DI と Facade)の記事です!環境
- PHP: 7.4.12
- Laravel: 8.16.1
- MySQL: 8.0.21
- doctrine/dbal: 3.0.0
予備知識
Laravel Seeding(シーディング) とは
Laravelにて標準搭載されている初期データ(テストデータ)を登録する機能です。
Laravel Migration(マイグレーション) とは
Laravelにて標準搭載されているデータベースのテーブル定義の変遷のバージョン管理をしてくれる機能です。
困ったこと
Laravel migrate:fresh コマンド
migrate:fresh
コマンドはすべてのテーブルをドロップして、すべてのマイグレーションを再度実行してくれるコマンドです。$ php artisan migrate:fresh # --seed オプションを付けるとシーディングの実行まで行ってくれます。 $ php artisan migrate:fresh --seedマイグレーションはプロジェクトの開発が進めば進むほど、履歴が長くなってしまいまっさらな状態を作るためにマイグレーションファイルをすべて適応するのはどんどん長くなってきてしまいます。
ダカーポを使ったり、Laravel8で新しく実装されたマイグレーションスカッシングを使う手もありますが、もっと手軽に実行できる方法が我々には求められていました。
Laravel db:seed コマンド
Laravelのシーディングのみ行ってくれるコマンド。
$ php artisan db:seedテストデータが入った状態でシーディングし直そうとするも、古いレコードが残ったり、外部キー制約でうまく削除できない問題が起こる。
$ php artisan migrate:freshで解決するが、マイグレーション適用時間がかかってしまう。
やはり、このコマンドだけでデータをクリアする方法が我々には求められていた。解決方法
全テーブルを
truncate
するシーダーを作ろう。$ composer require doctrine/dbal
doctrine/dbal
ライブラリが必要です。
テーブル定義を変更する場合には必須なライブラリなので、この機能を求める頃にはおそらく入っているはず...$ php artisan make:seeder TruncateAllTables生成された
database/seeds/TruncateAllTables.php
ファイルを下記のように上書きします。database/seeds/TruncateAllTables.php<?php declare(strict_types=1); use Illuminate\Database\Seeder; class TruncateAllTables extends Seeder { /** * Run the database seeds. * * @return void */ public function run(): void { Schema::disableForeignKeyConstraints(); foreach ($this->getTargetTableNames() as $tableName) { DB::table($tableName)->truncate(); } Schema::enableForeignKeyConstraints(); } /** * @return array */ private function getTargetTableNames(): array { $excludes = ['migrations']; return array_diff($this->getAllTableNames(), $excludes); } /** * @return array */ private function getAllTableNames(): array { return DB::connection()->getDoctrineSchemaManager()->listTableNames(); } }やってることはシンプルで全テーブル名を取得して各テーブルで
truncate
して削除しています。
migrations
テーブルを消す訳には行かないので除外してます。外部キー制約が張られてあるとレコードを削除できない場合があるので一時的に無効化してます。
テーブル削除後に外部キー制約を有効化してます。作成した
TruncateAllTables
クラスをDatabaseSeeder
クラスに登録します。database/seeds/DatabaseSeeder.php<?php declare(strict_types=1); namespace Database\Seeders; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run(): void { $this->call(TruncateAllTables::class); // シーダーをたくさん書く ... \App\Models\User::factory(10)->create(); } }コマンドを実行すればokです。
$ php artisan db:seed Seeding: Database\Seeders\TruncateAllTables Seeded: Database\Seeders\TruncateAllTables (153.14ms) Database seeding completed successfully.これでシーディングを実行しても古いレコードが残ったり、外部キー制約で怒られることもなくなりました。我々の完全勝利だ!??
ちなみにTruncateAllTables
だけ呼び出したい場合はクラス名をオプション指定して実行すればokです。$ php artisan db:seed --class=TruncateAllTables Database seeding completed successfully.参考
- https://laravel.com/api/8.x/Illuminate/Database/Schema/Builder.html#method_enableForeignKeyConstraints
- https://laravel.com/api/8.x/Illuminate/Database/MySqlConnection.html
- https://github.com/doctrine/dbal/blob/3.0.x/src/Schema/MySQLSchemaManager.php
- https://github.com/doctrine/dbal/blob/3.0.x/src/Schema/AbstractSchemaManager.php#L213-L228
補足: Laravel7 以前のコード例
Laravel8の場合、名前空間が付いてるのでLaravel7以前で利用する場合は注意が必要です。
database/seeders/DatabaseSeeder.php<?php declare(strict_types=1); use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run(): void { $this->call(TruncateAllTables::class); $this->call(UserSeeder::class); } }database/seeds/TruncateAllTables.php<?php declare(strict_types=1); use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; class TruncateAllTables extends Seeder { /** * Run the database seeds. * * @return void */ public function run(): void { Schema::disableForeignKeyConstraints(); foreach ($this->getTargetTableNames() as $tableName) { DB::table($tableName)->truncate(); } Schema::enableForeignKeyConstraints(); } /** * @return array */ private function getTargetTableNames(): array { $excludes = ['migrations']; return array_diff($this->getAllTableNames(), $excludes); } /** * @return array */ private function getAllTableNames(): array { return DB::connection()->getDoctrineSchemaManager()->listTableNames(); } }補足: 速度のお試し
- マイグレーションファイル数: 45
- テーブル数: 24
上記の環境だと約2.5倍ほどの速度差が見られました。
もし実際に試した人がいたらコメント欄で実行時間教えてもらえると嬉しいです。$ time php artisan migrate:fresh real 0m5.013s user 0m0.327s sys 0m0.178s約5.0秒
$ time php artisan db:seed --class=TruncateAllTables real 0m2.274s user 0m0.273s sys 0m0.145s約2.2秒
- 投稿日:2020-12-01T00:20:06+09:00
Laravel 全テーブルをTrancateしてからシーディングを行う方法
Laravel Advent Calendar 2020 - Qiita の 1日目 の記事です。
環境
- PHP: 7.4.12
- Laravel: 8.16.1
- MySQL: 8.0.21
- doctrine/dbal: 3.0.0
予備知識
Laravel Seeding(シーディング) とは
Laravelにて標準搭載されている初期データ(テストデータ)を登録する機能です。
Laravel Migration(マイグレーション) とは
Laravelにて標準搭載されているデータベースのテーブル定義の変遷のバージョン管理をしてくれる機能です。
困ったこと
Laravel migrate:fresh コマンド
migrate:fresh
コマンドはすべてのテーブルをドロップして、すべてのマイグレーションを再度実行してくれるコマンドです。$ php artisan migrate:fresh # --seed オプションを付けるとシーディングの実行まで行ってくれます。 $ php artisan migrate:fresh --seedマイグレーションはプロジェクトの開発が進めば進むほど、履歴が長くなってしまいまっさらな状態を作るためにマイグレーションファイルをすべて適応するのはどんどん長くなってきてしまいます。
ダカーポを使ったり、Laravel8で新しく実装されたマイグレーションスカッシングを使う手もありますが、もっと手軽に実行できる方法が我々には求められていました。
Laravel db:seed コマンド
Laravelのシーディングのみ行ってくれるコマンド。
$ php artisan db:seedテストデータが入った状態でシーディングし直そうとするも、古いレコードが残ったり、外部キー制約でうまく削除できない問題が起こる。
$ php artisan migrate:fresh --seedで解決するが、マイグレーション適用時間がかかってしまう。
やはり、このコマンドだけでデータをクリアする方法が我々には求められていた。解決方法
全テーブルを
truncate
するシーダーを作ろう。$ composer require doctrine/dbal
doctrine/dbal
ライブラリが必要です。
テーブル定義を変更する場合には必須なライブラリなので、この機能を求める頃にはおそらく入っているはず...$ php artisan make:seeder TruncateAllTables生成された
database/seeds/TruncateAllTables.php
ファイルを下記のように上書きします。database/seeds/TruncateAllTables.php<?php declare(strict_types=1); use Illuminate\Database\Seeder; class TruncateAllTables extends Seeder { /** * Run the database seeds. * * @return void */ public function run(): void { Schema::disableForeignKeyConstraints(); foreach ($this->getTargetTableNames() as $tableName) { DB::table($tableName)->truncate(); } Schema::enableForeignKeyConstraints(); } /** * @return array */ private function getTargetTableNames(): array { $excludes = ['migrations']; return array_diff($this->getAllTableNames(), $excludes); } /** * @return array */ private function getAllTableNames(): array { return DB::connection()->getDoctrineSchemaManager()->listTableNames(); } }やってることはシンプルで全テーブル名を取得して各テーブルで
truncate
して削除しています。
migrations
テーブルを消す訳には行かないので除外してます。外部キー制約が張られてあるとレコードを削除できない場合があるので一時的に無効化してます。
テーブル削除後に外部キー制約を有効化してます。作成した
TruncateAllTables
クラスをDatabaseSeeder
クラスに登録します。database/seeds/DatabaseSeeder.php<?php declare(strict_types=1); namespace Database\Seeders; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run(): void { $this->call(TruncateAllTables::class); // シーダーをたくさん書く ... \App\Models\User::factory(10)->create(); } }コマンドを実行すればokです。
$ php artisan db:seed Seeding: Database\Seeders\TruncateAllTables Seeded: Database\Seeders\TruncateAllTables (153.14ms) Database seeding completed successfully.これでシーディングを実行しても古いレコードが残ったり、外部キー制約で怒られることもなくなりました。我々の完全勝利だ!??
ちなみにTruncateAllTables
だけ呼び出したい場合はクラス名をオプション指定して実行すればokです。$ php artisan db:seed --class=TruncateAllTables Database seeding completed successfully.参考
- https://laravel.com/api/8.x/Illuminate/Database/Schema/Builder.html#method_enableForeignKeyConstraints
- https://laravel.com/api/8.x/Illuminate/Database/MySqlConnection.html
- https://github.com/doctrine/dbal/blob/3.0.x/src/Schema/MySQLSchemaManager.php
- https://github.com/doctrine/dbal/blob/3.0.x/src/Schema/AbstractSchemaManager.php#L213-L228
補足: Laravel7 以前のコード例
Laravel8の場合、名前空間が付いてるのでLaravel7以前で利用する場合は注意が必要です。
database/seeders/DatabaseSeeder.php<?php declare(strict_types=1); use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run(): void { $this->call(TruncateAllTables::class); $this->call(UserSeeder::class); } }database/seeds/TruncateAllTables.php<?php declare(strict_types=1); use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; class TruncateAllTables extends Seeder { /** * Run the database seeds. * * @return void */ public function run(): void { Schema::disableForeignKeyConstraints(); foreach ($this->getTargetTableNames() as $tableName) { DB::table($tableName)->truncate(); } Schema::enableForeignKeyConstraints(); } /** * @return array */ private function getTargetTableNames(): array { $excludes = ['migrations']; return array_diff($this->getAllTableNames(), $excludes); } /** * @return array */ private function getAllTableNames(): array { return DB::connection()->getDoctrineSchemaManager()->listTableNames(); } }
- 投稿日:2020-12-01T00:20:06+09:00
Laravel 全テーブルをTruncateしてからシーディングを行う方法
Laravel Advent Calendar 2020 - Qiita の 1日目 の記事です。
明日は @yoshikyoto さんの記事です!環境
- PHP: 7.4.12
- Laravel: 8.16.1
- MySQL: 8.0.21
- doctrine/dbal: 3.0.0
予備知識
Laravel Seeding(シーディング) とは
Laravelにて標準搭載されている初期データ(テストデータ)を登録する機能です。
Laravel Migration(マイグレーション) とは
Laravelにて標準搭載されているデータベースのテーブル定義の変遷のバージョン管理をしてくれる機能です。
困ったこと
Laravel migrate:fresh コマンド
migrate:fresh
コマンドはすべてのテーブルをドロップして、すべてのマイグレーションを再度実行してくれるコマンドです。$ php artisan migrate:fresh # --seed オプションを付けるとシーディングの実行まで行ってくれます。 $ php artisan migrate:fresh --seedマイグレーションはプロジェクトの開発が進めば進むほど、履歴が長くなってしまいまっさらな状態を作るためにマイグレーションファイルをすべて適応するのはどんどん長くなってきてしまいます。
ダカーポを使ったり、Laravel8で新しく実装されたマイグレーションスカッシングを使う手もありますが、もっと手軽に実行できる方法が我々には求められていました。
Laravel db:seed コマンド
Laravelのシーディングのみ行ってくれるコマンド。
$ php artisan db:seedテストデータが入った状態でシーディングし直そうとするも、古いレコードが残ったり、外部キー制約でうまく削除できない問題が起こる。
$ php artisan migrate:freshで解決するが、マイグレーション適用時間がかかってしまう。
やはり、このコマンドだけでデータをクリアする方法が我々には求められていた。解決方法
全テーブルを
truncate
するシーダーを作ろう。$ composer require doctrine/dbal
doctrine/dbal
ライブラリが必要です。
テーブル定義を変更する場合には必須なライブラリなので、この機能を求める頃にはおそらく入っているはず...$ php artisan make:seeder TruncateAllTables生成された
database/seeds/TruncateAllTables.php
ファイルを下記のように上書きします。database/seeds/TruncateAllTables.php<?php declare(strict_types=1); use Illuminate\Database\Seeder; class TruncateAllTables extends Seeder { /** * Run the database seeds. * * @return void */ public function run(): void { Schema::disableForeignKeyConstraints(); foreach ($this->getTargetTableNames() as $tableName) { DB::table($tableName)->truncate(); } Schema::enableForeignKeyConstraints(); } /** * @return array */ private function getTargetTableNames(): array { $excludes = ['migrations']; return array_diff($this->getAllTableNames(), $excludes); } /** * @return array */ private function getAllTableNames(): array { return DB::connection()->getDoctrineSchemaManager()->listTableNames(); } }やってることはシンプルで全テーブル名を取得して各テーブルで
truncate
して削除しています。
migrations
テーブルを消す訳には行かないので除外してます。外部キー制約が張られてあるとレコードを削除できない場合があるので一時的に無効化してます。
テーブル削除後に外部キー制約を有効化してます。作成した
TruncateAllTables
クラスをDatabaseSeeder
クラスに登録します。database/seeds/DatabaseSeeder.php<?php declare(strict_types=1); namespace Database\Seeders; use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run(): void { $this->call(TruncateAllTables::class); // シーダーをたくさん書く ... \App\Models\User::factory(10)->create(); } }コマンドを実行すればokです。
$ php artisan db:seed Seeding: Database\Seeders\TruncateAllTables Seeded: Database\Seeders\TruncateAllTables (153.14ms) Database seeding completed successfully.これでシーディングを実行しても古いレコードが残ったり、外部キー制約で怒られることもなくなりました。我々の完全勝利だ!??
ちなみにTruncateAllTables
だけ呼び出したい場合はクラス名をオプション指定して実行すればokです。$ php artisan db:seed --class=TruncateAllTables Database seeding completed successfully.参考
- https://laravel.com/api/8.x/Illuminate/Database/Schema/Builder.html#method_enableForeignKeyConstraints
- https://laravel.com/api/8.x/Illuminate/Database/MySqlConnection.html
- https://github.com/doctrine/dbal/blob/3.0.x/src/Schema/MySQLSchemaManager.php
- https://github.com/doctrine/dbal/blob/3.0.x/src/Schema/AbstractSchemaManager.php#L213-L228
補足: Laravel7 以前のコード例
Laravel8の場合、名前空間が付いてるのでLaravel7以前で利用する場合は注意が必要です。
database/seeders/DatabaseSeeder.php<?php declare(strict_types=1); use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run(): void { $this->call(TruncateAllTables::class); $this->call(UserSeeder::class); } }database/seeds/TruncateAllTables.php<?php declare(strict_types=1); use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; class TruncateAllTables extends Seeder { /** * Run the database seeds. * * @return void */ public function run(): void { Schema::disableForeignKeyConstraints(); foreach ($this->getTargetTableNames() as $tableName) { DB::table($tableName)->truncate(); } Schema::enableForeignKeyConstraints(); } /** * @return array */ private function getTargetTableNames(): array { $excludes = ['migrations']; return array_diff($this->getAllTableNames(), $excludes); } /** * @return array */ private function getAllTableNames(): array { return DB::connection()->getDoctrineSchemaManager()->listTableNames(); } }補足: 速度のお試し
- マイグレーションファイル数: 45
- テーブル数: 24
上記の環境だと約2.5倍ほどの速度差が見られました。
もし実際に試した人がいたらコメント欄で実行時間教えてもらえると嬉しいです。$ time php artisan migrate:fresh real 0m5.013s user 0m0.327s sys 0m0.178s約5.0秒
$ time php artisan db:seed --class=TruncateAllTables real 0m2.274s user 0m0.273s sys 0m0.145s約2.2秒