20201201のlaravelに関する記事は20件です。

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_1

laradockディレクトリ内の.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だと動かない原因までは追っていません!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Wordpressの投稿画面みたいなやつの名前

下記のように呼ばれるっぽい。

・ビジュアルエディタ
・リッチテキストエディタ
・テキストエディタ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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-generator

composer 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.php
Way\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"

で実行しないテーブルの指定ができるみたいです。

互換性の問題ってわかってるならあれこれやる前にバージョン下げてしまえばよいだけの話でした

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでどのリンク先からアクセスしたのか知る方法【リファラ】

下記コードで実現可能。

url()->previous();

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 Rethans

Laravel

Laravel Framework 6.18.35

composer.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"
    }

変更していく

下記のドキュメントを参考にしながら行っていく

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 255

Symfony5へのアップグレード

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.1

Next step

PJというか環境によっては上記までの対応では不足しているので動作確認をしながら、エラーや挙動がおかしい箇所に関してアップグレードガイドを参考にしていく

特に「影響の可能性: 高い」の部分は要チェック

参考までに、「影響の可能性: 低い」とされていても 自分のPJでは $model->getOriginal を利用していたので対応が必要になったり、 app/Providers/RouteServiceProvider.php をごにょごにょしていたので変更が必要だったり

ライブラリを更新した影響でLaravelと関係ないところで更新を迫られたりとまぁちょいちょいありました

アップグレードガイド見るよりコード差分見て行くほうが早いかもしれない
https://github.com/laravel/laravel/compare/6.x...8.x

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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#transactions

iOS
"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)を検証することで、レシートが改ざんされていないかチェックできます。
リクエストで送られてきたreceiptsignatureをGoogle Play Consoleから取得できるRSA公開鍵で検証します。

公開鍵のBase64文字列はGoogle Play Consoleから取得します。
image.png

取得した文字列を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.php
Route::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;
    }
}
.env
BUNDLE_ID=com.example.billingtest
PRODUCT_IDS=coins100,coins200
PUBKEY="-----BEGIN PUBLIC KEY-----
****************************************************************
****************************************************************
****************************************************************
****************************************************************
****************************************************************
****************************************************************
********
-----END PUBLIC KEY-----"

仕様通りできていればcordova-plugin-purchaseから呼び出せるはずです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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

以上!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.php
    public static function all($columns = ['*'])
    {
        return static::query()->get(
            is_array($columns) ? $columns : func_get_args()
        );
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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  

すると下記のようなリストがターミナルで出現します。
“スクリーンショット” 2020-12-01 13.45.10.jpg

今回はSEMVER WARNING: Recommended action is a potentially breaking change
というエラーが2つ出てきました。ここでターミナルでnpm audit fixを実行しましょう。

//ターミナルにて
npm audit fix 

しかし、下記の画像のようにまだエラーを修復できませんでした。
“スクリーンショット” 2020-12-01 13.56.07.jpg
これでも突破できる方法がnpm audit fix --forceというものがあります。
--forceはオプションになり強制的に修復させるというやり方です。

//ターミナルにて
npm audit fix --force 

これを実行したら無事に下記の画像のように脆弱性がなくなりました。
“スクリーンショット” 2020-12-01 14.03.05.jpg

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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/21a068494bc3a08a4803

1.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/socialite 

3.ログイン機能を実装する

まずはlaravel/uiライブラリをインストールします。
laravel7ではuiのバージョンを指定しないとエラーが出るのでバージョン2.4を指定しています。

$ composer require laravel/ui:^2.4

インストール後、↓のコマンドで認証機能をログイン機能を実装します

$ php artisan ui vue --auth

laravel.jpg

Apacheを起動させて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

↑ビルドします

login.jpg
これでログイン画面のレイアウトが整います。

5.データベースの準備

xamppを使っている人はDBから作成します。使ってない人は飛ばしてください。

xammpを使っている人が必要な準備

xampp.jpg
Apacheをスタートさせた後、mySQLをスタートさせて、右横のAdminをクリック。

Untitled Project (1).jpg
左上の新規作成をクリック

Untitled Project (2).jpg
データベース名にsocial_loginと入力して作成をクリック

これでDBが作成されます。

次に、今作ったDBをアプリで使用できるようにします。

C:\xampp\htdocs\social-login\.env

.env
DB_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テーブルがこのようになります。
screenshot (1).png

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キーを取得してください。

・Google
 https://console.developers.google.com/
・Facebook
 https://developers.facebook.com
・Twitter
 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/callback

14.動作確認

これでソーシャルログイン機能ができたはずなので、

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


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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にありました。

https://stackoverflow.com/questions/46586916/how-can-i-redirect-output-to-stdout-with-laravel-scheduler-command/46767123

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に限った話ではないのかもしれませんが、とにかく何も出力されなくて困り果てていたので解決できて良かったです。
同じ状況で困っている方のお役に立てればと思います。

余談

世の中、アドベントカレンダーの季節ですが、普通に記事書いてしまいました :grinning:。。
まあ、今後も書きたいときに書いていきます。

ではでは。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel 8にてRouteの記述につまずいた話

Laravel6以前と8ではRouteのアクションの記述が違う

Laravel6以前の場合

web.php
Route::get('test', 'testController@index');

Laravel8の場合

web.php
Route::get('test', [test::class, 'index']);

まとめ

これらのような記述の違いで頭を悩ましてしまったため、みなさんもネットで情報を得る際は自分の記述しているLaravelのバージョンとネットに記述してあるLaravelのバージョンを比較して、正しい情報を得れるように心がけていきましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel Excelの結合セルに罫線を引きたい

laravelにspreadsheetをインストール
/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader

Html.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);

                        }


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel × Docker AlpineでER図を自動生成する

こんにちは。むらってぃーです。
Laravel Advent Calendar 2020の3日目を担当させていただきます。

皆さんはER図使っていますか?
エンティティ同士の関係が一目でわかるドキュメントであるため、手元にあれば非常に便利なものとして効力を発揮します。
一方で、DBスキーマを更新するたびにER図を書き換える必要があり、メンテナンスコストが少々高めです。

今回はLaravelでアプリケーションを開発する際、Eloquent ModelからER図を自動生成するツールを紹介します。
DockerのAlpineイメージでLaravelを動かし、その上で生成します。
そのため、チームで運用する際にも導入しやすいです。

なお、今回生成するER図はこちらです。
スクリーンショット 2020-11-22 15.56.19.png

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 -fv

composer.json

Alpineイメージに入り込み、下記コマンドでLaravel ER Diagram Generatorを入れます。

composer require beyondcode/laravel-er-diagram-generator --dev

ServiceProvider

ローカルでのみこのライブラリが読み込まれるようにします。

AppServiceProvider.php
class 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 migrate

Modelファイル用意

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というファイルが出来上がります。

スクリーンショット 2020-11-22 15.56.19.png

Userは、Laravelで最初から組み込まれているものです。
このようにBookとAuthorの関係がER図として出力されています。

BelongsToの矢印が色付けされているのが個人的には嬉しいです。

最後に

今回はLaravel ER Diagram Generatorを使い、Larvel × Docker AlpineでER図を自動生成する方法を紹介しました。
一度このように土台を作れば、migrationファイルとModelファイルの管理のみでER図を自動生成できます。
メンテナンスコストがほとんどかからないのが嬉しいですね。
ER図生成のコマンドはCIに組み込むなりして、常に最新に保つことができればさらに幸せになれそうですね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Web エンジニアが始める Server-side Swift "Vapor"

はじめに

image.png
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 は今のところ Mac と Linux で利用可能です。Mac だと Homebrew、Linux では yum や apt などでパッケージが提供されています。あくまでも実行環境ということなので docker や Homestead を使えば Windows でも開発は可能です。

image.png

余談ですが、Swift 製の Web アプリケーションフレームワークと言えば当初は IBM が開発する Kitura が有名でしたが、調べたら2020年1月に(IBMとして)開発を終了しているようでした。

サーバサイドを 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: Mongo

Fluent を使用すると言うことはその後ろにデータベースがあるわけですので、次でデータベース製品を選択します。PostgreSQL が Recomended なので、1 を入力。

Would you like to use Leaf? (--leaf/--no-leaf)
y/n>

次は Leaf の追加。こちらは Laravel で言うところの Blade テンプレートシステムです。もちろん y を入力。

すると完了メッセージと共に下記のようなアスキーアートが表示されます。

image.png

アスキーアートと言えば、昔アドベントカレンダーで書いた自分の記事を思い出しました。全然関係ないけど。

Xcode の起動

プロジェクトディレクトリに移動して下記コマンドで Xcode が起動します。

$ cd cd ApiTest
$ vapor xcode

初回起動時は依存パッケージがダウンロードされるのでしばらく待って、左上のビルドボタン(再生ボタン)がアクティブになるまで待ってください。
image.png

依存バッケージのダウンロードが完了したら、ビルドを実行してみましょう。ショートカットキー「⌘ + 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 したような感じですね。ではブラウザでアクセスしましょう。

image.png

軽く構成を眺める

初期構成のファイル構成を軽く見てみましょう。
image.png
既視感があると言うかなんと言うか、もうフォルダ名のファイル名で大体わかっちゃいますよね。主要なファイルをさくっと紹介。

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 は今年もアドベントカレンダーに参加しています!
応援よろしくお願いします!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.php

AdminテーブルのMigrationファイルの編集

Schema::create部分をUserモデル参考に変更します。

2020_11_29_015635_create_admins_table.php
        Schema::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 migrate

Seederの準備

■テストユーザー作成のためのシーダーを準備

$ 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:seed

Modelの準備

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/DashboardController
User/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. ログイン未:制限ページへのアクセス
  2. ログイン済:ログインページへのアクセス
  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.php
    public 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.php
    public 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>
@endsection
resources/views/user/index.blade.php
@extends('layouts.default')
@section('content')
<h1>ユーザーダッシュボード</h1>
@endsection
resources/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>
@endsection
resources/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> 
@endsection
resources/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>
@endsection
resources/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/DashboardController
app/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.php
    public 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>
@endsection
resources/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>
@endsection
resources/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>

@endsection
resources/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>
@endsection
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

参考にさせて頂いたページ。

Laravel6でマルチ認証を実装する(UserとAdminの階層を分ける)
https://qiita.com/namizatork/items/5d56d96d4c255a0e3a87

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[PHP]標準入力で与えられる値を取得する

PHPで標準入力で与えられる値を取得する

PHP
<?php
$input = fgets(STDIN);
echo $input;
?>

Rubyの場合

Ruby
input = gets
print input

PHPはRubyと違いコード量が多くなる。 <?php ?>タグで囲む事でPHPのコードとして認識される

以上!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.

参考

補足: 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秒

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.

参考

補足: 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();
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.

参考

補足: 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秒

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む