20200426のlaravelに関する記事は14件です。

Laravelのミドルウェアをテストする方法を考えてみた

テストするときにミドルウェアを無効化する記事はあっても、ミドルウェアそのものをテストする記事があまり無かったので、書いてみます。

前提

  • Laravel 6.18.10
  • PHP 7.3.15

ミドルウェア

ここでは、リクエストに含まれるバージョンをチェックするミドルウェアを作ってみます。1 API という前提で書きますが、基本的な考え方は Web でも同じだと思います。

リクエストに含まれている versionMIN_VERSION 2 以上である場合は、許可します。

一方、そもそも version が送信されなかったり、 versionMIN_VERSION よりも小さい場合は、エラーの JSON レスポンスを返します。3

namespace App\Http\Middleware;

use Closure;

class VersionCheck
{
    /**
     * 利用可能な最低バージョン
     */
    private const MIN_VERSION = '1.0.1';

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $version = $request->get('version');

        if ($version === null || $version < self::MIN_VERSION) {
            return response()->json([
               'message' => 'このバージョンは利用できません。'
            ], 400);
        }

        return $next($request);
    }

テスト

ミドルウェアが出来たので、テストコードを作っていきます。

正常系

まずは、リクエストを作ります。

$request = app()->make('request');
$request->merge(['version' => '1.0.1']);

そして、ミドルウェアを実行します。

$middleware = new VersionCheck();
$middleware->handle($request, function () {
    $this->assertTrue(true);
});

本来、 handle() の第2引数は、バージョンチェックの次に動くミドルウェアを指定します。

このことは、ミドルウェアの雛形を見ると、よく分かるかと思います。

public function handle($request, Closure $next)
{
    // ...

    // 処理が全部終わったら $next で渡された関数を実行する。
    return $next($request);
}

そのため、 handle() の第2引数の中で assertTrue(true) と書くことで、 バージョンチェックが全て終了して次のミドルウェアへ進むこと をテストすることができます。

しかし、これではまだ不十分です。 エラーレスポンスが返ってこないこと をテストできていないからです。

バージョンチェックが正常な場合、本来ならば、ミドルウェアの後に動くコントローラがレスポンスを返します。

しかし、ここではミドルウェアしか動かしていないため、レスポンスは返ってきません。 つまり、 handle() の戻り値が null であることをチェックすれば十分ということになります。

ということで、正常系のテストコードは以下のようになります。

/**
 * @test
 */
public function 利用可能なバージョン最低バージョンと等しいである場合は正常終了すること()
{
    $request = app()->make('request');
    $request->merge(['version' => '1.0.1']);

    $middleware = new VersionCheck();
    $response = $middleware->handle($request, function () {
        $this->assertTrue(true);
    });

    // エラーレスポンスが返却されないこと
    $this->assertNull($response);
}

異常系

正常系と違って、異常系ではエラーレスポンスのテストが必要になります。

ここで問題となるのは、 $response->assertStatus() のようなメソッドが 使えない ということです。

通常、コントローラのテストコードで get()post() を記載すると、自動的に TestResponse クラスのインスタンスが返却されます。 assertStatus()TestResponse クラスのメソッドなので、特に意識することなく使うことができます。

ところが、ミドルウェアの handle()TestResponse ではなく Response クラスのインスタンスを返却します。そのため、 assertStatus() を使うことはできません。

もちろん、以下のように書くことはできます。

$this->assertSame(400, $response->getStatusCode());
$this->assertSame([
    'message' => 'このバージョンは利用できません。'
], json_decode($response->getContent(), true));

しかし、これは、かなり冗長に感じられます。

そこで、get()post() がどのように動いていくかを見ていきました。

その結果、トレイト Illuminate\Foundation\Testing\Concerns\MakesHttpRequestscreateTestResponse() を呼び出して、 ResponseTestResponse に格納し直していることが分かりました。

そして、テストクラスの親クラス Illuminate\Foundation\Testing\TestCaseMakesHttpRequestsuse しているので、テストクラスからも createTestResponse() を呼び出せることが分かりました。

こうして、テストコードはこのように書き直すことができました。

$middleware = new VersionCheck();
$originalResponse = $middleware->handle($request, function () {
     $this->assertTrue(true);
});

$response = $this->createTestResponse($originalResponse);

$this->assertNotNull($response);
$response->assertStatus(400);
$response->assertExactJson([
    'message' => 'このバージョンは利用できません。'
]);

完成形

namespace Tests\Feature\Middleware;

use Tests\TestCase;
use App\Http\Middleware\VersionCheck;

class VersionCheckTest extends TestCase
{
    /**
     * @test
     */
    public function 利用可能なバージョン最低バージョンと等しいである場合は正常終了すること()
    {
        $request = app()->make('request');
        $request->merge(['version' => '1.0.1']);

        $middleware = new VersionCheck();
        $response = $middleware->handle($request, function () {
            $this->assertTrue(true);
        });

        // エラーレスポンスが返却されないこと
        $this->assertNull($response);
    }

    /**
     * @test
     */
    public function 利用可能なバージョン最低バージョンより大きいである場合は正常終了すること()
    {
        $request = app()->make('request');
        $request->merge(['version' => '1.0.2']);

        $middleware = new VersionCheck();
        $response = $middleware->handle($request, function () {
            $this->assertTrue(true);
        });

        // エラーレスポンスが返却されないこと
        $this->assertNull($response);
    }

    /**
     * @test
     */
    public function バージョンが送信されない場合はエラーレスポンスが返却されること()
    {
        $request = app()->make('request');

        $middleware = new VersionCheck();
        $originalResponse = $middleware->handle($request, function () {
            $this->assertTrue(true);
        });

        $response = $this->createTestResponse($originalResponse);

        // エラーレスポンスが返却されること
        $this->assertNotNull($response);

        // ステータスコードが400であること
        $response->assertStatus(400);

        // JSONレスポンスが期待どおりであること
        $response->assertExactJson([
            'message' => 'このバージョンは利用できません。'
        ]);
    }

    /**
     * @test
     */
    public function 利用不可能なバージョンである場合はエラーレスポンスが返却されること()
    {
        $request = app()->make('request');
        $request->merge(['version' => '1.0.0']);

        $middleware = new VersionCheck();
        $originalResponse = $middleware->handle($request, function () {
            $this->assertTrue(true);
        });

        $response = $this->createTestResponse($originalResponse);

        // エラーレスポンスが返却されること
        $this->assertNotNull($response);

        // ステータスコードが400であること
        $response->assertStatus(400);

        // JSONレスポンスが期待どおりであること
        $response->assertExactJson([
            'message' => 'このバージョンは利用できません。'
        ]);
    }

感想

ミドルウェアは便利な反面、どのように書くべきかという「ベストプラクティス」を、あまり見つけることができませんでした。

「もっと良い方法があるよ!」という方は、こっそり教えていただければと思います。

参考


  1. ミドルウェアには、コントローラの に動くタイプと、 に動くタイプの2種類がありますが、ここでは に動くタイプを作ります。 

  2. 本来、 MIN_VERSION は DB 等から取得するべきですが、ここでは簡略化します。 

  3. ここではミドルウェアで JSON レスポンスを返していますが、これが良いのかどうか、あまり自信がありません。特に、Web の場合はエラー画面を表示することになりますが、ミドルウェアで直接ビューを表示せずに、一回リダイレクトしてからコントローラで表示すべきという Stack Overflow の 回答 もありました。 

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

Pterodactyl Panelで、2FAコードが取得できなくなってログインできなくなった時の処置

パスワードを再発行しても、2FA認証は解除されない

2FA認証が出来ない状況でもパスワードの再発行はできましたが、新しいパスワードでログインを試行しても2FAコードが要求されてしまう。

コントロールパネルには2FA設定を解除する項目が無かった

screenshot_20200426_220354.png
screenshot_20200426_220438.png

解決策

職人(artisan)に頼み込んで、強制的に2FA認証を取り消す

php artisan p:user:disable2fa --email=user@example.com

※チュートリアルページにあるように{}を付けると、

Too many arguments, expected arguments "command".

と怒られてしまう。

環境

Version Information
===================

 Panel Version     0.7.17
 Latest Version    0.7.17
 Up-to-Date        Yes
 Unique Identifier user@example.com

参考リンク

Artisan CLI - Pterodactyl community

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

MAMP環境にLarabelをインストールする方法

①MAMPのhtdocsにディレクトリ移動

ターミナルを開き、下記コマンドを入力。

$ cd /Applications/MAMP/htdocs

②Larabelプロジェクト作成

htdocs配下において、下記コマンドを入力。

公文)
$ composer create-project laravel/laravel プロジェクト名 --prefer-dist "6.0.*"
例)
$ composer create-project laravel/laravel larabel_test --prefer-dist "6.0.*"

③対象プロジェクトのディレクトリに移動

htdocs配下において、下記コマンドを入力。

公文)
$ cd プロジェクト名
例)
$ cd larabel_test

④サーバ起動

③で作成したディレクトリ配下において、下記コマンドを入力。

$ php artisan serve

⑤サーバ起動確認

ブラウザを開き、下記にアクセス。
http://127.0.0.1:8000

⑥サーバ停止

「control」 + 「c」 を同時に押下

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

Laravel7 Bladeファイルのひな形を作成するmakeコマンドを自作する

概要

前回の記事Laravel7 makeコマンドのひな形をカスタマイズするでスタブのカスタマイズは行えましたが、makeコマンドを追加したい時の例をご紹介します。

環境

  • Laravel 7.5.1

make:blade コマンド

今回は make:blade コマンドを自作します。
他のmake系コマンドはGeneratorCommandクラスを継承しているのでそれに倣って作成します。

https://laravel.com/api/7.x/Illuminate/Console/GeneratorCommand.html

app/Console/Commands/BladeMakeCommand.php ファイルに以下の内容を記述します。

app/Console/Commands/BladeMakeCommand.php
<?php declare(strict_types=1);

namespace App\Console\Commands;

use Illuminate\Console\GeneratorCommand;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Symfony\Component\Console\Input\InputOption;

class BladeMakeCommand extends GeneratorCommand
{
    /**
     * The console command name.
     *
     * @var string
     */
    protected $name = 'make:blade';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Create a new blade file';

    /**
     * Execute the console command.
     *
     * @return mixed
     * @throws FileNotFoundException
     */
    public function handle()
    {
        if (parent::handle() === false && ! $this->option('force')) {
            return false;
        }

        return true;
    }

    /**
     * Parse the class name and format according to the root namespace.
     *
     * @param  string  $name
     * @return string
     */
    protected function qualifyClass($name): string
    {
        return $name;
    }

    /**
     * Get the destination class path.
     *
     * @param  string  $name
     * @return string
     */
    protected function getPath($name): string
    {
        return $this->laravel->basePath('resources/views') . '/' . str_replace('\\', '/', $name).'.blade.php';
    }

    /**
     * Get the stub file for the generator.
     *
     * @return string
     * @throws FileNotFoundException
     */
    protected function getStub(): string
    {
        if (file_exists($customPath = $this->laravel->basePath('stubs/blade.stub'))) {
            return $customPath;
        }

        throw new FileNotFoundException('stubs/blade.stub file not found.');
    }

    /**
     * Get the console command options.
     *
     * @return array
     */
    protected function getOptions(): array
    {
        return [
            ['force', null, InputOption::VALUE_NONE, 'Create the class even if the model already exists'],
        ];
    }
}

stubs/blade.stub ファイルに以下の内容を記述します。

stubs/blade.stub
@extends('layouts.app')

@section('content')

@endsection

make:blade コマンドの使い方

$ php artisan make:blade hello
resources/views/hello.blade.php
@extends('layouts.app')

@section('content')

@endsection

/ で区切るとディレクトリ切って作成できます。

$ php artisan make:blade foo/bar
resources/views/foo/bar.blade.php
@extends('layouts.app')

@section('content')

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

Laravel7 makeコマンドのひな形をカスタマイズする

概要

Laravel7以降からStubのカスタマイズを簡単に行える機能が追加されました。
makeコマンドで使用される元のひな形ファイルを簡単にカスタマイズできて便利なのでご紹介します。

環境

  • Laravel 7.5.1

モデルのひな形を生成するコマンド

$ php artisan make:model Post
app/Post.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    //
}

makeコマンドを実行するとこのようなファイルが生成されます。
このひな形の内容を変えたい場合は次のようにします。

スタブファイルの公開

Laravel7以降では stub:publish コマンドが新たに追加されました。

$ php artisan stub:publish
Stubs published successfully.

実行すると次のファイル群が生成されます。

stubs/console.stub
stubs/controller.api.stub
stubs/controller.invokable.stub
stubs/controller.model.api.stub
stubs/controller.model.stub
stubs/controller.nested.api.stub
stubs/controller.nested.stub
stubs/controller.plain.stub
stubs/controller.stub
stubs/factory.stub
stubs/job.queued.stub
stubs/job.stub
stubs/middleware.stub
stubs/migration.create.stub
stubs/migration.stub
stubs/migration.update.stub
stubs/model.pivot.stub
stubs/model.stub
stubs/policy.plain.stub
stubs/policy.stub
stubs/request.stub
stubs/rule.stub
stubs/seeder.stub
stubs/test.stub
stubs/test.unit.stub

make:model のカスタマイズ

stubs/model.stub ファイルを見ると次のようにスタブファイルが定義されています。

stubs/model.stub
<?php

namespace {{ namespace }};

use Illuminate\Database\Eloquent\Model;

class {{ class }} extends Model
{
    //
}

このファイルを書き換えてあげると make:model コマンドに反映されます。

参考

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

Laravel7 makeコマンドのひな形ファイルをカスタマイズする

概要

Laravel7以降からStubのカスタマイズを簡単に行える機能が追加されました。
makeコマンドで使用される元のひな形ファイルを簡単にカスタマイズできて便利なのでご紹介します。

環境

  • Laravel 7.5.1

モデルのひな形を生成するコマンド

$ php artisan make:model Post
app/Post.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    //
}

makeコマンドを実行するとこのようなファイルが生成されます。
このひな形の内容を変えたい場合は次のようにします。

スタブファイルの公開

Laravel7以降では stub:publish コマンドが新たに追加されました。

$ php artisan stub:publish
Stubs published successfully.

実行すると次のファイル群が生成されます。

stubs/console.stub
stubs/controller.api.stub
stubs/controller.invokable.stub
stubs/controller.model.api.stub
stubs/controller.model.stub
stubs/controller.nested.api.stub
stubs/controller.nested.stub
stubs/controller.plain.stub
stubs/controller.stub
stubs/factory.stub
stubs/job.queued.stub
stubs/job.stub
stubs/middleware.stub
stubs/migration.create.stub
stubs/migration.stub
stubs/migration.update.stub
stubs/model.pivot.stub
stubs/model.stub
stubs/policy.plain.stub
stubs/policy.stub
stubs/request.stub
stubs/rule.stub
stubs/seeder.stub
stubs/test.stub
stubs/test.unit.stub

make:model のカスタマイズ

stubs/model.stub ファイルを見ると次のようにスタブファイルが定義されています。

stubs/model.stub
<?php

namespace {{ namespace }};

use Illuminate\Database\Eloquent\Model;

class {{ class }} extends Model
{
    //
}

このファイルを書き換えてあげると make:model コマンドに反映されます。

参考

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

Laravelで ErrorException Creating default object from empty valueというエラー

エラーの原因

色々なケースでこのエラーは起こり得ますが、Laravelでこのエラーが出た時に多いパターンのひとつに「Soft Delete = 論理削除したデータ」を取ってこようとしてnullを取ってきてしまい、エラーになっているというものがあります。

Product.php
// 実際のコード
// Productモデルにはソフトデリートが定義されている
namespace App\Models;

use Illuminate\Database\Eloquent\SoftDeletes;

class Product extends Model
{
use SoftDeletes;
}

// 加えてproductsテーブルにはdeleted_atのカラムに値が入っている
ProductController.php
// id=1 のプロダクトが論理削除されている場合
// コントローラなどで該当のデータを取って来ようとすると...
$product = Product::find(1);
$product->name;

// 実行結果
ErrorException
Creating default object from empty value

つまり $product->name や大元の $product がnullになっているのに、それらに対して何かしらの操作をしようとして上記のエラーになる場合があります。

ということでDBからデータを引っ張ってくる時は、
- 該当のテーブルは論理削除を設定していないか?
- 該当のデータが論理削除されていないか?
を確認してみて下さい。

論理削除

テーブルにdeleted_atというカラムを持たせ、ここに日時が入ってるレコードは削除されたものとみなす。違う名前で削除フラグのようなカラムを持たせる場合もあるが、Laravelのデフォルトではdeleted_atのカラムがその役割を担う。
このようなカラムがなく、データベースから完全にデータを削除する場合は物理削除と表現する。
(ご存知とは思いますが念の為。)

対策

このようなエラーを回避するには「論理削除のデータも含めて取得する」という操作が必要なので下記のように記載すれば大丈夫です。

$product = Product::withTrashed()->find(1);

データにアクセスする際 withTrashed() を一緒に書けば、論理削除されたデータも検索して引っ張ってきてくれ、ErrorException : Creating default object from empty value というエラーにはなりません。

参考URL

【Laravel5.8】Creating default object from empty valueを解決したい
stdClassをnewしたらPHP Warning: Creating default object from empty value inとなったときの対応方法
PHP Warning: Creating default object from empty valueの原因と対応
【php】 Creating default object from empty value

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

【React/laravel】componentDidMount()でsetStateする予定のデータを利用してComponentのStateを定義すると失敗する【小説印刷支援サイト】

承前

小説を簡単に同人誌の形にできる印刷できるWebサイトを作りたいと思っている。
今回はその前段階として、印刷する際のプレビュー画面を作成していたときに躓いた話。

事象

直前まで動いていたReactコンポーネントが急にエラーを吐き始めた。

Uncaught TypeError: Cannot read property 'chapterNumber' of undefined
    at RenderUi (index.js:70616)
    at renderWithHooks (index.js:48209)
    at mountIndeterminateComponent (index.js:50888)
    at beginWork (index.js:52002)
    at HTMLUnknownElement.callCallback (index.js:33594)
    at Object.invokeGuardedCallbackDev (index.js:33643)
    at invokeGuardedCallback (index.js:33698)
    at beginWork$1 (index.js:56609)
    at performUnitOfWork (index.js:55560)
    at workLoopSync (index.js:55536)

chapterNumberChapterContentのメンバ。
ChapterContentNovelWithContentsのメンバ。

調査

ChapterContentのインスタンスが空だった。
なんならNovelWithContentsのインスタンスも空だった。
('、3_ヽ)_

やりたかったことと原因

NovelWithContentsのインスタンスは、componetnDidMount()でPHPから持ってきた値を詰めていた。
Console.log()を仕込んでみたところ、componentnDidMount()を通っていないことが分かった。
よって、NovelWithContentsは空で然るべきということが分かった。

axiosで取得したテキストデータに前処理をしてReactコンポーネントに渡したかった。
その前処理の部分で、データが無いクラスのプロパティを参照してエラーを吐いていた。

    componentDidMount() {
        console.log(this.state.novelId+"sssssssssssssssssssssssss");
        axios
            .get(/*API CALL*/)
            .then(response => {
                let data: NovelWithContents = response.data;
                console.log(response.data);
                this.setState({ contentsJson: data });
            })
            .catch(() => {
                console.log("通信に失敗しました。")

            });
    }

対処

失敗した方法

constructorstate初期化後にaxios以下を移してもうまく行かなかった。
renderに移しても同様。

うまく行った方法

色々やった。

  • 前処理後の値をStateにして初期値を与えた。
    • 初回のRender時、即ちaxiosが間に合ってない時に空でエラーを吐かなくなった。
  • 初期化処理をaxiosのfinally()処理に集めた。
    • これによりデータ取得と初期化処理の順番の整合性を取りながら、非同期処理という体は崩さずに済んだ。
  • 上記を可能にするために全体的にリファクタリング
    componentDidMount() {
        axios
            .get(/*API CALL*/)
            .then(response => {
                let data: NovelWithContents = response.data;
                console.log(response.data);
                this.setState({ contentsJson: data });
            })
            .catch(() => {
                console.log("通信に失敗しました。")

            }).finally(()=>{
                let hoge="";
                //前処理。
                });

    }

結局3時間くらいハマってしまった。

反省

  • Reactへの理解度が浅い中で始めたProjectだったので、設計はあとでキチンとやりなおす。

今回実現した機能

小説のページング

小説印刷支援サイト

やや遅れ気味。7月リリースを目指したい。

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

【React/laravel】axiosによる非同期のAPI callの結果を待ってからStateに値を入れたかった【小説印刷支援サイト】

承前

小説を簡単に同人誌の形にできる印刷できるWebサイトを作りたいと思っている。
今回はその前段階として、印刷する際のプレビュー画面を作成していたときに躓いた話。

事象

直前まで動いていたReactコンポーネントが急にエラーを吐き始めた。

Uncaught TypeError: Cannot read property 'chapterNumber' of undefined
    at RenderUi (index.js:70616)
    at renderWithHooks (index.js:48209)
    at mountIndeterminateComponent (index.js:50888)
    at beginWork (index.js:52002)
    at HTMLUnknownElement.callCallback (index.js:33594)
    at Object.invokeGuardedCallbackDev (index.js:33643)
    at invokeGuardedCallback (index.js:33698)
    at beginWork$1 (index.js:56609)
    at performUnitOfWork (index.js:55560)
    at workLoopSync (index.js:55536)

chapterNumberChapterContentのメンバ。
ChapterContentNovelWithContentsのメンバ。

調査

ChapterContentのインスタンスが空だった。
なんならNovelWithContentsのインスタンスも空だった。
('、3_ヽ)_

やりたかったことと原因

やりたかったこと

NovelWithContentsがなぜ空なのか

axiosで取得したJSONモデルデータが詰まっている予定だったが、ログを出してみたら問題が分かった。
* componentDidMount()はComponent描画後に呼ばれるため、Component描画時にココで取得する予定の値を参照してしまうとエラーになる。

その後、もう一つの問題も分かった。
* axiosは非同期処理をするので、Constructorなどで事前に値を取得しようとしてもデータ取得処理が間に合うかどうか分からない(間に合わない)

    componentDidMount() {
        console.log(this.state.novelId+"sssssssssssssssssssssssss");
        axios
            .get(/*API CALL*/)
            .then(response => {
                let data: NovelWithContents = response.data;
                console.log(response.data);
                this.setState({ contentsJson: data });
            })
            .catch(() => {
                console.log("通信に失敗しました。")

            });
    }

結論

前処理の部分で、データが無いクラスのプロパティを参照してエラーを吐いていた。

対処

失敗した方法

constructorstate初期化後にaxios以下を移してもうまく行かなかった。
renderに移しても同様。

うまく行った方法

色々やった。

  • 前処理後の値をStateにして初期値を与えた。
    • 初回のRender時、即ちaxiosが間に合ってない時に空でエラーを吐かなくなった。
  • 初期化処理をaxiosのfinally()処理に集めた。
    • これによりデータ取得と初期化処理の順番の整合性を取りながら、非同期処理という体は崩さずに済んだ。
  • 上記を可能にするために全体的にリファクタリング
    componentDidMount() {
        axios
            .get(/*API CALL*/)
            .then(response => {
                let data: NovelWithContents = response.data;
                console.log(response.data);
                this.setState({ contentsJson: data });
            })
            .catch(() => {
                console.log("通信に失敗しました。")

            }).finally(()=>{
                let hoge="";
                //前処理。
                });

    }

結局3時間くらいハマってしまった。

反省

  • Reactへの理解度が浅い中で始めたProjectだったので、設計はあとでキチンとやりなおす。

今回実現した機能

小説のページング

小説印刷支援サイト

やや遅れ気味。7月リリースを目指したい。

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

Mac Laravel 500 server errorが出た話

目的

  • 環境構築直後にローカル開発環境で500 server errorが出た話をまとめる

エラー概要

  • $ php artisan serveを実行してブラウザでhttp://127.0.0.1:8000/を確認したところ500 server error(下記のもの)が出力された。

    BC3D17BD-A681-4E81-A83F-B40391C770AA.png

解決法

  1. アプリ名ディレクトリを確認すると.envが存在していないことがわかった。
  2. 下記コマンドを実行して.envを作成する。

    $ cd アプリ名ディレクトリ
    $ cp .env.example .env
    
  3. laravelアプリを再起動してブラウザを確認したところ下記の画面が表示されていた。

    • アプリケーションを暗号化するためのキーがない、もしくは設定されていないらしい。

    479CDDAB-834B-4E21-B3AA-339B422263BE.png

  4. 下記コマンドを実行して鍵を作成した。

    $ php artisan key:generate
    >Application key set successfully.
    
  5. laravelアプリを再起動してブラウザを確認したところ下記の画面が表示されており、問題は解決した。

    38C8B2E5-0A42-40B6-9A96-6C978FD4DD91.png

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

Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!その9「Kernel::handle()」

INDEX

Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!

Kernel::handle()

前回はKernel::handle()がコールされた際に渡される引数

  • Symfony\Component\Console\Input\ArgvInput
  • Symfony\Component\Console\Output\ConsoleOutput

を軽く見てみました。
では、handle()メソッドを見てみましょう。

Illuminate\Foundation\Console\Kernel::handle()
    /**
     * Run the console application.
     *
     * @param  \Symfony\Component\Console\Input\InputInterface  $input
     * @param  \Symfony\Component\Console\Output\OutputInterface|null  $output
     * @return int
     */
    public function handle($input, $output = null)
    {
        try {
            $this->bootstrap();
            return $this->getArtisan()->run($input, $output);
        } catch (Throwable $e) {
            $this->reportException($e);
            $this->renderException($output, $e);
            return 1;
        }
    }
    /**
     * Get the Artisan application instance.
     *
     * @return \Illuminate\Console\Application
     */
    protected function getArtisan()
    {
        if (is_null($this->artisan)) {
            return $this->artisan = (new Artisan($this->app, $this->events, $this->app->version()))
                                ->resolveCommands($this->commands);
        }
        return $this->artisan;
    }

Illuminate\Foundation\Console\Kernel::handle()

第一引数はSymfony\Component\Console\Input\ArgvInputインスタンスです。
第二引数はSymfony\Component\Console\Output\ConsoleOutputインスタンスです。
戻り値は整数型です。

まずbootstrap()メソッドがコールされます。ここは以前読みましたのでもう理解できています。
次にgetArtisan()メソッドがコールされます。
getArtisan()メソッドの戻り値はIlluminate\Console\Applicationインスタンスです。
getArtisan()メソッドは$this->artisannullかどうか検証します。nullでなければそれを返します。nullならば、アプリケーション、イベント、バージョンを渡しArtisanインスタンスを生成し、resolveCommands()を実行した戻り値を返します。
Artisanの実体はIlluminate\Console\Applicationです。見てみましょう。

Illuminate\Console\Application::__construct()
    /**
     * Create a new Artisan console application.
     *
     * @param  \Illuminate\Contracts\Container\Container  $laravel
     * @param  \Illuminate\Contracts\Events\Dispatcher  $events
     * @param  string  $version
     * @return void
     */
    public function __construct(Container $laravel, Dispatcher $events, $version)
    {
        parent::__construct('Laravel Framework', $version);
        $this->laravel = $laravel;
        $this->events = $events;
        $this->setAutoExit(false);
        $this->setCatchExceptions(false);
        $this->events->dispatch(new ArtisanStarting($this));
        $this->bootstrap();
    }
    /**
     * Bootstrap the console application.
     *
     * @return void
     */
    protected function bootstrap()
    {
        foreach (static::$bootstrappers as $bootstrapper) {
            $bootstrapper($this);
        }
    }
Symfony\Component\Console\Application::__construct()|関連メソッド
    public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
    {
        $this->name = $name;
        $this->version = $version;
        $this->terminal = new Terminal();
        $this->defaultCommand = 'list';
    }
    /**
     * Sets whether to automatically exit after a command execution or not.
     */
    public function setAutoExit(bool $boolean)
    {
        $this->autoExit = $boolean;
    }
    /**
     * Sets whether to catch exceptions or not during commands execution.
     */
    public function setCatchExceptions(bool $boolean)
    {
        $this->catchExceptions = $boolean;
    }

Illuminate\Console\Application::__construct()

第一引数はアプリケーションコンテナです。
第二引数はイベントディスパッチャーです。
第三引数はストリング型でバージョン番号です。
戻り値はありません。

まず、スーパークラスSymfony\Component\Console\Applicationのコンストラクタをアプリケーション名「Laravel Framework」とバージョン情報を引数にコールします。
Symfony\Component\Console\Applicationのコンストラクタは受け取ったアプリケーション名とバージョン名を変数に代入し、$this->terminalTerminalインスタンスを生成したものを代入し、デフォルトコマンドをセットします。Terminalにコンストラクタはありません。保持ファンクションの一覧を見てみましょう。

Symfony\Component\Console\Terminal変数|ファンクション一覧
    private static $width;
    private static $height;
    private static $stty;
    public function getWidth()
    public function getHeight()
    private static function initDimensions()
    private static function hasVt100Support(): bool
    private static function initDimensionsUsingStty()
    private static function getConsoleMode(): ?array
    private static function getSttyColumns(): ?string
    private static function readFromProcess(string $command): ?string

Symfony\Component\Console\Terminal

縦幅横幅の取得、Sttyが利用可能か、サイズの初期化、VT100をサポートしているか、Stty利用時のサイズ初期化、mode CONが使用可能か(コマンドプロンプトウィンドウの変更)。Sttyの行数の取得、コマンドの実行、等のメソッドを実装しているようです。端末に関することを担うクラスのようです。

Illuminate\Console\Application::__construct()に戻ります。
アプリケーションコンテナとイベントディスパッチャーを変数に代入します。
setAutoExit()メソッドをfalseを渡しコールします。
setAutoExit()メソッドは$this->autoExitをセットします。
コマンド実行後に自動的に終了するかどうかを設定するパラメータのようです。
setCatchExceptions()メソッドをfalseを渡しコールします。
setCatchExceptions()メソッドは$this->catchExceptionsをセットします。
コマンドの実行中に例外をキャッチするかどうかを設定するパラメータのようです。
次にイベントディスパッチャーにArtisanStartingを引数に$thisを渡して生成したものを登録します。
ArtisanStartingクラスは非常に小さな定義のクラスです。

Illuminate\Console\Events\ArtisanStarting
<?php
namespace Illuminate\Console\Events;
class ArtisanStarting
{
    /**
     * The Artisan application instance.
     *
     * @var \Illuminate\Console\Application
     */
    public $artisan;
    /**
     * Create a new event instance.
     *
     * @param  \Illuminate\Console\Application  $artisan
     * @return void
     */
    public function __construct($artisan)
    {
        $this->artisan = $artisan;
    }
}

コンストラクタでコンソールアプリケーションを受け取り$this->artisanに代入します。
Illuminate\Console\Application::__construct()の続きです。
bootstrap()メソッドをコールしています。
bootstrap()メソッドは、foreachstatic::$bootstrappersを回して$bootstrapper()を引数に自身を渡してコールしています。static::$bootstrappersは生成の過程で仕込まれている様子はありませんでした。おそらくKernelが初期化される工程で準備されるのでしょう。

Kernel初期化の流れの中でApplication::registerConfiguredProviders()がコールされます。そこでは$this->config['app.providers']つまり、PROJECT_ROOT/config/app.phpで定義されている配列のキーprovidersつまりプロバイダーのリストを引数にProviderRepository::load()をコールしていました。その処理の中で、アプリケーションコンテナのregister()に引数としてプロバイダーを一つずつ渡し、そこでプロバダー自体のregister()がコールされます。
$this->config['app.providers']の中にはIlluminate\Foundation\Providers\ConsoleSupportServiceProviderが含まれます。
こちらを見てみましょう。

Illuminate\Foundation\Providers\ConsoleSupportServiceProvider
<?php
namespace Illuminate\Foundation\Providers;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Database\MigrationServiceProvider;
use Illuminate\Support\AggregateServiceProvider;
class ConsoleSupportServiceProvider extends AggregateServiceProvider implements DeferrableProvider
{
    /**
     * The provider class names.
     *
     * @var array
     */
    protected $providers = [
        ArtisanServiceProvider::class,
        MigrationServiceProvider::class,
        ComposerServiceProvider::class,
    ];
}

コンストラクタもregister()も見当たりません。
$providersが定義されているだけです。その中にArtisanServiceProviderとそれっぽいクラスが記述されています。
ConsoleSupportServiceProviderクラスはIlluminate\Support\AggregateServiceProviderクラスを継承しています。こちらを見てみましょう。

Illuminate\Support\AggregateServiceProviderL::register()
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->instances = [];
        foreach ($this->providers as $provider) {
            $this->instances[] = $this->app->register($provider);
        }
    }

register()メソッドがありました。
ConsoleSupportServiceProviderでオーバーライドした$providersforeachで回してアプリケーションコンテナのregister()でプロバイダーを登録して$this->instances[]に入れいていく処理です。
ということは、ArtisanServiceProviderインスタンスがアプリケーションコンテナにプロバイダー登録され、自身のregister()メソッドがコールされるはずです。
見てみましょう。

Illuminate\Foundation\Providers\ArtisanServiceProvider::register()|関連メソッド
    protected $commands = [
        'CacheClear' => 'command.cache.clear',
        'CacheForget' => 'command.cache.forget',
        'ClearCompiled' => 'command.clear-compiled',
        'ClearResets' => 'command.auth.resets.clear',
        'ConfigCache' => 'command.config.cache',
        'ConfigClear' => 'command.config.clear',
        'DbWipe' => 'command.db.wipe',
        'Down' => 'command.down',
        'Environment' => 'command.environment',
        'EventCache' => 'command.event.cache',
        'EventClear' => 'command.event.clear',
        'EventList' => 'command.event.list',
        'KeyGenerate' => 'command.key.generate',
        'Optimize' => 'command.optimize',
        'OptimizeClear' => 'command.optimize.clear',
        'PackageDiscover' => 'command.package.discover',
        'QueueFailed' => 'command.queue.failed',
        'QueueFlush' => 'command.queue.flush',
        'QueueForget' => 'command.queue.forget',
        'QueueListen' => 'command.queue.listen',
        'QueueRestart' => 'command.queue.restart',
        'QueueRetry' => 'command.queue.retry',
        'QueueWork' => 'command.queue.work',
        'RouteCache' => 'command.route.cache',
        'RouteClear' => 'command.route.clear',
        'RouteList' => 'command.route.list',
        'Seed' => 'command.seed',
        'ScheduleFinish' => ScheduleFinishCommand::class,
        'ScheduleRun' => ScheduleRunCommand::class,
        'StorageLink' => 'command.storage.link',
        'Up' => 'command.up',
        'ViewCache' => 'command.view.cache',
        'ViewClear' => 'command.view.clear',
    ];
    /**
     * The commands to be registered.
     *
     * @var array
     */
    protected $devCommands = [
        'CacheTable' => 'command.cache.table',
        'ChannelMake' => 'command.channel.make',
        'ComponentMake' => 'command.component.make',
        'ConsoleMake' => 'command.console.make',
        'ControllerMake' => 'command.controller.make',
        'EventGenerate' => 'command.event.generate',
        'EventMake' => 'command.event.make',
        'ExceptionMake' => 'command.exception.make',
        'FactoryMake' => 'command.factory.make',
        'JobMake' => 'command.job.make',
        'ListenerMake' => 'command.listener.make',
        'MailMake' => 'command.mail.make',
        'MiddlewareMake' => 'command.middleware.make',
        'ModelMake' => 'command.model.make',
        'NotificationMake' => 'command.notification.make',
        'NotificationTable' => 'command.notification.table',
        'ObserverMake' => 'command.observer.make',
        'PolicyMake' => 'command.policy.make',
        'ProviderMake' => 'command.provider.make',
        'QueueFailedTable' => 'command.queue.failed-table',
        'QueueTable' => 'command.queue.table',
        'RequestMake' => 'command.request.make',
        'ResourceMake' => 'command.resource.make',
        'RuleMake' => 'command.rule.make',
        'SeederMake' => 'command.seeder.make',
        'SessionTable' => 'command.session.table',
        'Serve' => 'command.serve',
        'StubPublish' => 'command.stub.publish',
        'TestMake' => 'command.test.make',
        'VendorPublish' => 'command.vendor.publish',
    ];
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->registerCommands(array_merge(
            $this->commands, $this->devCommands
        ));
    }
    /**
     * Register the given commands.
     *
     * @param  array  $commands
     * @return void
     */
    protected function registerCommands(array $commands)
    {
        foreach (array_keys($commands) as $command) {
            call_user_func_array([$this, "register{$command}Command"], []);
        }
        $this->commands(array_values($commands));
    }
Illuminate\Support\ServiceProvider::commands()
    /**
     * Register the package's custom Artisan commands.
     *
     * @param  array|mixed  $commands
     * @return void
     */
    public function commands($commands)
    {
        $commands = is_array($commands) ? $commands : func_get_args();
        Artisan::starting(function ($artisan) use ($commands) {
            $artisan->resolveCommands($commands);
        });
    }

$commands$devCommandsに連想配列が沢山代入されています。
register()メソッドはregisterCommands()$commands$devCommandsarray_mergeしたものを渡してコールしています。
registerCommands()メソッドは受け取った配列をarray_keysでキーのみ配列で取り出し、それをforeachで回し、call_user_func_arrayでコールバック関数を実行します。実行するコールバックは自身に定義してある関数で、関数名はregister{$command}Commandで生成されたものです。引数は空の配列となっています。
このクラスにはクロージャーをアプリケーションコンテナにシングルトンで結合するメソッドが沢山定義されてます。
以下は例です。

定義例
    /**
     * Register the command.
     *
     * @return void
     */
    protected function registerUpCommand()
    {
        $this->app->singleton('command.up', function () {
            return new UpCommand;
        });
    }

アプリケーションコンテナにcommand.upという名前でUpCommandインスタンスを生成して返すクロージャーをシングルトン結合しています。
$commands$devCommandsに代入されている沢山のコマンドがシングルトン結合されます。
次にcommands()メソッドを$commandsarray_valuesで値のみの配列にしたものを引数にコールします。
Artisan::starting()をクロージャーを引数に渡してコールしています。ArtisanIlluminate\Console\Applicationのことです。見てみましょう。

Illuminate\Console\Application::starting()
    /**
     * Register a console "starting" bootstrapper.
     *
     * @param  \Closure  $callback
     * @return void
     */
    public static function starting(Closure $callback)
    {
        static::$bootstrappers[] = $callback;
    }

Illuminate\Console\Application::starting()

第一引数はクロージャーです。
戻り値はありません。

static::$bootstrappers[]配列に受け取ったクロージャーを登録しています。
先程疑問だった、static::$bootstrappersに格納されている中身の正体がわかりました。
static::$bootstrappers[]に登録されたクロージャー全てに$thisを渡したものをアプリケーションコンテナにシングルトン結合するという手順になります。

せっかくここまで読んだので、Artisan::starting()メソッドに引数として渡されているクロージャーも読んでみましょう。

        Artisan::starting(function ($artisan) use ($commands) {
            $artisan->resolveCommands($commands);
        });

このクロージャーをコールするArtisan::bootstrap()では引数として$thisを渡しています。つまり、$this->resolveCommands(コマンド名)が実行されるクロージャーです。resolveCommands()を見てみましょう。

Illuminate\Console\Application::resolveCommands()|関連メソッド
    /**
     * Resolve an array of commands through the application.
     *
     * @param  array|mixed  $commands
     * @return $this
     */
    public function resolveCommands($commands)
    {
        $commands = is_array($commands) ? $commands : func_get_args();
        foreach ($commands as $command) {
            $this->resolve($command);
        }
        return $this;
    }
    /**
     * Add a command, resolving through the application.
     *
     * @param  string  $command
     * @return \Symfony\Component\Console\Command\Command
     */
    public function resolve($command)
    {
        return $this->add($this->laravel->make($command));
    }
    /**
     * Add a command to the console.
     *
     * @param  \Symfony\Component\Console\Command\Command  $command
     * @return \Symfony\Component\Console\Command\Command
     */
    public function add(SymfonyCommand $command)
    {
        if ($command instanceof Command) {
            $command->setLaravel($this->laravel);
        }
        return $this->addToParent($command);
    }
    /**
     * Set the Laravel application instance.
     *
     * @param  \Illuminate\Contracts\Container\Container  $laravel
     * @return void
     */
    public function setLaravel($laravel)
    {
        $this->laravel = $laravel;
    }
    /**
     * Add the command to the parent instance.
     *
     * @param  \Symfony\Component\Console\Command\Command  $command
     * @return \Symfony\Component\Console\Command\Command
     */
    protected function addToParent(SymfonyCommand $command)
    {
        return parent::add($command);
    }

Illuminate\Console\Application::resolveCommands()

第一引数は コマンドです。
戻り値は $this です。

受け取ったコマンドをforeachで回し、resolve()メソッドを引数にコマンド名を渡してコールし、自身を返しています。
resolve()はコマンド名を受け取りそれをアプリケーションコンテナでmake()した戻り値をadd()しています。
add()メソッドでは、引数として渡されたコマンドがCommandインスタンス か検証し、そうであった場合はsetLaravel()メソッドにアプリケションコンテナを引数に渡しコールします。
そうでない場合はaddToParent()メソッドを通し、スーパークラスのadd()メソッドをコールします。
setLaravel()メソッドはコマンドの変数$laravelにアプリケーションコンテナを代入します。
スーパークラスのadd()メソッドとはSymfony\Component\Console\Application::add()の事です。
Symfony\Component\Console\Application::add()が引数として受け取るのはSymfony\Component\Console\Command\Commandインスタンスで、これはIlluminate\Console\Commandのスーパークラスです。つまり Illuminate\Console\CommandSymfony\Component\Console\Command\CommandをLaravel用に拡張したコマンドクラスなのでしょう。
Symfony\Component\Console\Applicationを読みたいところですが、少しボリュームがあるので、まずは本筋に戻ります。


Illuminate\Foundation\Console\Kernel::handle()から$this->getArtisan()がコールされその処理の途中でした。

Illuminate\Foundation\Console\Kernel::getArtisan()
    /**
     * Get the Artisan application instance.
     *
     * @return \Illuminate\Console\Application
     */
    protected function getArtisan()
    {
        if (is_null($this->artisan)) {
            return $this->artisan = (new Artisan($this->app, $this->events, $this->app->version()))
                                ->resolveCommands($this->commands);
        }
        return $this->artisan;
    }

生成したArtisanインスタンスのresolveCommands$this->commandsを引数に渡しコールします。おそらく$this->commandsは空の配列なのでただ戻り値に自身が返ってくるだけでしょう。
そのまま生成されたArtisanインスタンスが戻されます。

Illuminate\Foundation\Console\Kernel::handle()に戻ります。

Illuminate\Foundation\Console\Kernel::handle()
    public function handle($input, $output = null)
    {
        try {
            $this->bootstrap();
            return $this->getArtisan()->run($input, $output);
        } catch (Throwable $e) {
            $this->reportException($e);
            $this->renderException($output, $e);
            return 1;
        }

$this->getArtisan()の戻り値にrun()しています。引数に入力と出力を渡しています。
次はこちらを見ていきましょう。

次回

今回はartisanインスタンスの生成の流れを読んでみました。次回はConsole\Application::run()を追ってみたいと思います!

続く☆

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

tymon/jwt-auth を拡張するパッケージを公開して不満が解消した

はじめに

LaravelでAPIサーバーを開発する際、認証にJWTを使うことが増えてきました。
ライブラリには決まって、tymon/jwt-authを使うのですが、いくつか不満があって、毎回カスタマイズしていたので、拡張ライブラリを作って公開しました。

tymon/jwt-authに対する不満

クエリパラメータ、リクエストボディにあるtokenもJWTトークンだと認識してしまう

Laravelで作ったらすべて解決ではないよ?」という記事でも書いたのですが、tymon/jwt-authは、クエリパラメータ、リクエストボディにあるtokenもJWTトークンだと認識してしまいます。
正直、QueryStringInputSourceRouteParamsは使うことないのでは?と考えてます。

vendor/tymon/jwt-auth/src/Providers/AbstractServiceProvider.php
    protected function registerTokenParser()
    {
        $this->app->singleton('tymon.jwt.parser', function ($app) {
            $parser = new Parser(
                $app['request'],
                [
                    new AuthHeaders,
                    new QueryString,
                    new InputSource,
                    new RouteParams,
                    new Cookies($this->config('decrypt_cookies')),
                ]
            );

            $app->refresh('request', $parser, 'setRequest');

            return $parser;
        });
    }

'token'という名前を設定ファイルから変更できない

ParsersetKey関数で名前を変更することはできますが、できれば、設定ファイルで環境ごとに変えられるようにしたいですよね?
Add ability to provide custom cookie name from config by SiebeVE · Pull Request #1933でも、setKeyする方法が紹介されていますが、設定ファイルに切り出す気はなさそうです。

vendor/tymon/jwt-auth/src/Http/Parser/KeyTrait.php
namespace Tymon\JWTAuth\Http\Parser;

trait KeyTrait
{
    /**
     * The key.
     *
     * @var string
     */
    protected $key = 'token';

    /**
     * Set the key.
     *
     * @param  string  $key
     *
     * @return $this
     */
    public function setKey($key)
    {
        $this->key = $key;

        return $this;
    }

'tymon.jwt.parser'を拡張

以上を踏まえて、imunew/tymon-jwt-authでは、下記のように、拡張しました。
QueryStringInputSourceRouteParamsは削除。
Cookiesだけにして、AuthHeadersは任意で追加可能にしました。

src/Providers/ServiceProvider.php
    private function resetParserChain()
    {
        $this->app->extend('tymon.jwt.parser', function (Parser $parser) {
            $chain = [
                (new Cookies(config('jwt.decrypt_cookies')))
                    ->setKey(config('jwt-auth.cookie.key'))
            ];
            if (config('jwt-auth.auth-headers.enabled')) {
                $chain[] = new AuthHeaders();
            }
            return $parser->setChain($chain);
        });
    }
config/jwt-auth.php
return [
    "cookie" => [
        'key' => env('JWT_AUTH_COOKIE_KEY', 'auth-token')
    ],
    "auth-header" => [
        'enabled' => env('JWT_AUTH_AUTH_HEADER_ENABLED', false)
    ]
];

追加機能

今回、'tymon.jwt.parser'を拡張しただけではなく、新たに2つの機能を追加しました。

AuthResource

ログインAPIが返すリソースとして使えそうな、API Resourceを実装しました。
JWTトークンをCookieにセットします。

src/Resources/AuthResource.php
class AuthResource extends JsonResource
{
    /** @var string|null */
    public static $wrap = null;

    /**
     * {@inheritDoc}
     */
    public function toArray($request)
    {
        return [];
    }

    /**
     * @param Request $request
     * @return JsonResponse
     */
    public function toResponse($request)
    {
        $response = parent::toResponse($request);
        return HttpHelper::respondWithCookie($request, $response, $this->resource->accessToken);
    }
}

RefreshJwtToken

今回の目玉機能だと思っているんですが、JWTトークンの有効期限が切れたら自動でリフレッシュしてくれるミドルウェアを実装しました。
尚、このミドルウェアのアイデアは、Laravel での tymon/jwt-auth による JWT トークンの自動更新から得ました。
@yh1224 さん、ありがとうございました。

src/Middleware/RefreshJwtToken.php
class RefreshJwtToken extends BaseMiddleware
{
    /**
     * @param Request $request
     * @param Closure $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        $token = null;
        try {
            $token = $this->auth->parseToken();
            $token->authenticate();
        } catch (TokenExpiredException $e) {
            // Token expired: try refresh
            try {
                return $this->refreshJwtToken($token, $request, $next);
            } catch (JWTException $e) {
                // Refresh failed (refresh expired)
            }
        } catch (JWTException $e) {
            // Invalid token
        }
        return $next($request);
    }

    /**
     * @param JWT $jwt
     * @param Request $request
     * @param Closure $next
     * @return JsonResponse|Response
     */
    private function refreshJwtToken(JWT $jwt, Request $request, Closure $next)
    {
        $newToken = $jwt->refresh();
        $request->cookies->set(config('jwt-auth.cookie.key'), $newToken);
        $response = $next($request);
        return HttpHelper::respondWithCookie($request, $response, $newToken);
    }
}

RefreshJwtTokenを有効にするには、以下のように、Kernel.phpに設定を追加します。

app/Http/Kernel.php
    protected $middlewareGroups = [
        'web' => [
            // ...
        ],

        'api' => [
            'throttle:60,1',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \Imunew\JWTAuth\Middleware\RefreshJwtToken::class, // ここを追加
        ]
    ];

    protected $middlewarePriority = [
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \Imunew\JWTAuth\Middleware\RefreshJwtToken::class, // ここを追加
        \App\Http\Middleware\Authenticate::class, // この行よりも上に追加する
        \Illuminate\Routing\Middleware\ThrottleRequests::class,
        \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \Illuminate\Auth\Middleware\Authorize::class,
    ];

$middlewarePriority\App\Http\Middleware\Authenticate::classよりも前に、RefreshJwtTokenを追加しないと、先にAuthenticateが実行されてしまい、401になってしまうので注意してください。

おわりに

2020-03-05に、ついに、1.0.0がリリースされたtymon/jwt-authですが、githubissuesなど見ていても、Parser周りは特に不満があるかと思います。
そんなときは、imunew/tymon-jwt-authも合わせて、composer requireしていただければ幸いです。
pull-requestsまたはissuesお待ちしております。
ではでは。

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

LaravelからJavaScriptに配列変数を引き渡す方法

Laravelで、コントローラから受け渡した配列変数をJavaScriptに渡す方法。

普通に、

const data = '{{json_encode($data)}}';
console.log(data);

のように渡すとダブルコーテーションが&quotのような文字になってしまう。

調べると、

https://qiita.com/azumagoro/items/51a884eb9840b08a29ee

JSON_UNESCAPED_UNICODEを、json_encodeの第二引数に入れてunicode文字列にされることを防ぎ。
!!で囲って、&quotにされることを防げるという。

そして、Laravel5.5以降は、@json()というbladeテンプレート用の関数で簡単にできるという。

const data = @json($data);

という書き方が結論のようにも思いましたが、今後外部ファイル化した時に、このことを忘れてはまる原因になりそうでしたので、一度は基本でやるということで、

const data = {!!json_encode($data, JSON_UNESCAPED_UNICODE)!!}

で対応しました。

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

Laravelの初心に還る旅【サービスコンテナ&サービスプロバイダ】

1. サービスコンテナの概要

サービスコンテナとは

オブジェクトのインスタンス化を管理するもの。
(〜が要求されれば〜のインスタンスを提供するといったようなイメージ)

公式(Laravel7.x)

Laravelサービスコンテナは、クラスの依存関係を管理し、依存関係の注入を実行するための強力なツールです。依存性注入は、本質的にこれを意味する派手なフレーズです。クラスの依存関係は、コンストラクタまたは場合によっては「セッター」メソッドを介してクラスに「注入」されます。

依存性注入(Dependency injection、略称DI)とは

あるオブジェクトが他のサービスオブジェクトに依存している際、そのサービスオブジェクトの生成は自身で担わず、外部から生成されたオブジェクトを受け取り、使用する仕組み。
生成されたオブジェクトに対しての、「使用」と「構築」の責務を分離する。
クライアント側はオブジェクトの生成方法は知る必要がなく、使用するのみ。
これにより、コードの可読性と再利用のしやすさが向上する。

例)

依存性注入を行わない場合のコンストラクタ

Foo.php
class Foo{
    private $bar;

    public function __construct()
    {
        $this->bar = new Bar();
    }
}

Fooクラス自身がBarクラスのインスタンス化を担っており、単一責任の原則に基づいていない。
さらに、コンストラクタ内にインスタンス生成のロジックがハードコードされているため、密結合になっている。

依存性注入を行う場合のコンストラクタ

Foo.php
class Foo{
    private $bar;

    public function __construct(Bar $bar)
    {
        $this->bar = $bar;
    }
}

FooクラスはBarクラスのインスタンス化は担っておらず、どこかで生成されたインスタンスをプロパティに格納するだけ。

Laravelのサービスコンテナはこの依存性注入を便利に行ってくれる。

2. サービスプロバイダ

サービスプロバイダとは

Laravelアプリケーションの全体処理において、初期起動処理を担う。
サービスコンテナの結合や、イベントリスナ、フィルター、それにルートなど、諸々の登録処理を行う。

公式(Laravel7.x)

サービスプロバイダは、Laravelアプリケーション全体の起動処理における、初めの心臓部です。皆さんのアプリケーションと同じく、Laravelのコアサービス全部もサービスプロバイダを利用し、初期起動処理を行っています。
ところで「初期起動処理」とは何を意味しているのでしょうか? サービスコンテナの結合や、イベントリスナ、フィルター、それにルートなどを登録することを一般的に意味しています。サービスプロバイダはアプリケーション設定の中心部です。

3. サービスコンテナとサービスプロバイダの活用例

自動的な解決

デフォルトでLaravelプロジェクト内に作成されているUserモデルをもとに解説。

app/User.php
<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

以下、Userモデルに合わせた形でコントローラクラスを作成。

app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;

class UserController extends Controller
{
    /**
     * ユーザーモデル
     *
     * @var User
     */
    protected $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function getName(int $id): string
    {
        return $this->user->where('id', $id)->get('name');;
    }
}

コンストラクタに注目。

UserController.php
     /**
     * ユーザーモデル
     *
     * @var User
     */
    protected $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }

コンストラクタでUserクラスをタイプヒンティングしている。

通常サービスコンテナを利用せずにUserControllerをインスタンス化しようとすると以下のようになる。

$user_controller = new UserController(new User());

しかし、Laravelのサービスコンテナを利用する場合は以下のように書くことができる。

$user_controller = app()->make(UserController);

なぜかというとLaravelがタイプヒンティングをもとにリフレクションで自動解決するからだ。
(引数にわざわざUserモデルのインスタンスを渡さなくても要求されているオブジェクトを自分で探してインスタンス化する。)

これでUserControllerクラスのプロパティ$usersにはUserモデルが格納される。

上記の例だと自動的に解決するが、明示的にサービスコンテナの結合を定義しておくこともできる。

明示的な解決

前述のUserモデルとコントローラを使用し、Repositoryパターンを用いて解説。

Repositoryパターンに必要な諸々のファイルを作成。

app/Repositories/Interfaces/UserRepositoryInterface.php
<?php

namespace App\Repositories\Interfaces;

interface UserRepositoryInterface
{
    public function all();

    public function getUserById(int $id);
}
app/Repositories/UserRepository.php
<?php

namespace App\Repositories;

use App\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Collection;
use App\Repositories\Interfaces\UserRepositoryInterface;

class UserRepository implements UserRepositoryInterface
{
    private $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function all(): Collection
    {
        return User::all();
    }

    public function getUserById(int $id): Model
    {
        return User::where('id', $id)->first();
    }
}

コントローラは以下のように書き換えておく。

app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use App\User;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Model;
use App\Repositories\Interfaces\UserRepositoryInterface;

class UserController extends Controller
{
    /**
     * リポジトリクラス
     *
     * @var UserRepositoryInterface
     */
    protected $repository;

    public function __construct(UserRepositoryInterface $repository)
    {
        $this->repository = $repository;
    }

    public function index()
    {
        return $this->repository->all();
    }

    public function getUserName(int $id): string
    {
        $model = $this->repository->getUserById($id);
        return $model->name;
    }
}

サービスプロバイダクラスの作成。

root@582455531b89:/var/www/hogehoge# php artisan make:provider RepositoryServiceProvider
Provider created successfully.

以下のサービスプロバイダクラスが出来上がる。

app/Providers/RepositoryServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

register()内を以下のように書き換える。

app/Providers/RepositoryServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(UserRepositoryInterface::class, UserRepository::class);
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

これで、「UserRepositoryInterfaceクラスが要求された際にUserRepositoryクラスを返す」という意味になる。

最後にconfig/app.phpに作成したサービスプロバイダを追加しておく。

config/app.php
...省略
    App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
        App\Providers\RepositoryServiceProvider::class,

    ],
...省略

tinkerで実行してみる。

root@582455531b89:/var/www/hogehoge# php artisan tinker
Psy Shell v0.10.3 (PHP 7.3.9-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> $controller = app()->make('UserController');
[!] Aliasing 'UserController' to 'App\Http\Controllers\UserController' for this Tinker session.
=> App\Http\Controllers\UserController {#3009}
>>> $controller->index();
=> Illuminate\Database\Eloquent\Collection {#3054
     all: [
       App\User {#3055
         id: 1,
         name: "test1",
         email: "test1@test.com",
         email_verified_at: null,
         created_at: "2020-04-18 06:21:24",
         updated_at: "2020-04-18 06:21:24",
       },
       App\User {#3056
         id: 2,
         name: "Kaci Kunze",
         email: "acarroll@example.net",
         email_verified_at: "2020-04-18 06:43:29",
         created_at: "2020-04-18 06:43:29",
         updated_at: "2020-04-18 06:43:29",
       },
       App\User {#3057
         id: 3,
         name: "August Paucek",
         email: "oreilly.jadyn@example.org",
         email_verified_at: "2020-04-18 06:43:29",
         created_at: "2020-04-18 06:43:29",
         updated_at: "2020-04-18 06:43:29",
       },
       App\User {#3058
         id: 4,
         name: "Lizeth Reichel",
         email: "boyle.heath@example.com",
         email_verified_at: "2020-04-18 06:43:29",
         created_at: "2020-04-18 06:43:29",
         updated_at: "2020-04-18 06:43:29",
       },
       App\User {#3059
         id: 5,
         name: "Aurelio Gorczany",
         email: "polly72@example.org",
         email_verified_at: "2020-04-18 06:43:29",
         created_at: "2020-04-18 06:43:29",
         updated_at: "2020-04-18 06:43:29",
       },
       App\User {#3060
         id: 6,
         name: "Derek Boehm II",
         email: "fritz19@example.org",
         email_verified_at: "2020-04-18 06:43:29",
         created_at: "2020-04-18 06:43:29",
         updated_at: "2020-04-18 06:43:29",
       },
       App\User {#3061
         id: 7,
         name: "Nat Mertz",
         email: "amira.wisozk@example.net",
         email_verified_at: "2020-04-18 06:43:29",
         created_at: "2020-04-18 06:43:29",
         updated_at: "2020-04-18 06:43:29",
       },
       App\User {#3062
         id: 8,
         name: "Dominic Ritchie",
         email: "ybartell@example.com",
         email_verified_at: "2020-04-18 06:43:29",
         created_at: "2020-04-18 06:43:29",
         updated_at: "2020-04-18 06:43:29",
       },
       App\User {#3063
         id: 9,
         name: "Princess Cronin",
         email: "zion.weissnat@example.com",
         email_verified_at: "2020-04-18 06:43:29",
         created_at: "2020-04-18 06:43:29",
         updated_at: "2020-04-18 06:43:29",
       },
       App\User {#3064
         id: 10,
         name: "Willie Becker",
         email: "leta.medhurst@example.org",
         email_verified_at: "2020-04-18 06:43:29",
         created_at: "2020-04-18 06:43:29",
         updated_at: "2020-04-18 06:43:29",
       },
...省略

>>> $controller->getUserName(2);
=> "Kaci Kunze"

UserControllerをインスタンス化しただけで、UserRepositoryクラスがコントローラクラス内のプロパティDIされ、UserRepositoryのメソッドを使用できている。

まとめ

Laravelは偉大。

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