なずなログ

ただのSIer系SEが思ったことや色々書く感じのアレです

【Laravel】サブディレクトリで2サイト運用時に片方でしかログイン継続できない #Laravel相談ログ

友人からLaravel周りの相談を受けることが度々あるので、備忘録として残してみる。

相談内容

  • 2つのLaravelベースのサイトを作成している。

  • それぞれドキュメントルートのサブディレクトリとしてデプロイしている。

    [イメージ]

    • hogehoge/app1

    • hogehoge/app2

  • それぞれLaravel標準の認証機能を使っている。

  • 片方のサイトでログインした状態でもう片方のサイトにログインすると、先にログインしていた方がログアウトされる。

    [イメージ]

    1. app1にログイン
    2. app2にアクセス
    3. app2にログイン
    4. app1にアクセス ← ログアウトされてる!

聞かれたこと

なぜ発生するの?

⇒ ログイン情報をブラウザが保持するのにcookieを使っていて、同じcookie名だから後からログインしたサイトの方で上書きされる。

WEBサーバ(nginx)のせい?

⇒ No. Cookieを設定するのはあくまでアプリケーション。
Cookieはブラウザでドメイン単位で管理される。サブディレクトリ運用=同一ドメインなので、サブディレクトリのアプリそれぞれが同じ場所にCookieを保存する。
そのため、同名のCookieは衝突してしまい、後から更新したほうで上書きされてしまう。

解決方法

.envcookieプレフィックスを指定することでサイト間でセッションIDのcookieが同じキーで重複しないようにできる。

SESSION_COOKIE=app1

【Laravel】Blade記法のincludeとslotの違い

友人からLaravel周りの相談を受けることが度々あるので、備忘録として残してみる。

相談内容

Blade記法のincludeslotの違いって何?

includeとは

bladeテンプレートを読み込む方法。
@includeディレクティブを使用する。 一回作ったbladeテンプレートを再利用したいときに使う。
表示する内容を動的にするには、変数を使用する。

slotとは

includeと似ているが、変数だけじゃなくHTML自体を差し込みたいときに使う。
@componentディレクティブを使用する。

使い分け方

include

  • ページヘッダーやページフッターなど、固定のもの

  • 変数だけで済むような場合

slot

  • 途中にHTMLを差し込みたい場合

slotを使うと何がうれしいの?

HTMLをわざわざ変数に入れて渡す、という手間がいらなくなる。
また、HTMLを変数で渡してそのまま表示したいときには{!! !!}文を使用してエスケープをしないようにする必要性があるが、そんなことを気にする必要もなくなる。

【Laravel】普通のRequestを受け取った後にFormRequestに切り替える

やりたいこと

汎用的なRequestで一旦受け取って、あとからFormRequestを作成するようにすることで、無駄にコントローラやメソッドを生やさなくて済むようにしたい。 例えばAPIなどで、サービスが複数存在しているときに各パラメータを若干カスタマイズしたいときなどに使える。

routes/api.php
Route::post('/{serviceName}/apply', 'APIController@somethingAction')->name('apply');

解決方法

APIController.php
    /** @var array サービス名とバリデーションに使用するFormRequestの関連付け */
    protected $formRequests = [
        'hoge' => 'App\Http\Requests\HogeRequest',
        'huge' => 'App\Http\Requests\HugaRequest'
    ];

    /**
     * なんかのアクション
     * 
     * @param  Illuminate\Http\Request $request
     * @param  string $serviceName
     */
    public function somethingAction(Request $request, string $serviceName)
    {
        if (empty($serviceName) || isset($this->formRequests[$serviceName]))
        {
            // サービスを特定できない場合はNot Foundエラー
            abort(404);
        }
        
        // バリデーション実行のため、FormRequestを作成する
        $formRequest = app()->make($this->formRequests[$serviceName]);

        // do something
    }

参考にしたやつ Use form request manually · Issue #7995 · laravel/framework

解説

FormRequestをDIコンテナ経由で生成すると、自動的にvalidateが走るから。

/Illuminate/Foundation/Providers/FormRequestServiceProvider.php

Illuminate\Foundation\Providers\FormRequestServiceProvider
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app->afterResolving(ValidatesWhenResolved::class, function ($resolved) {
            $resolved->validateResolved();
        });
        $this->app->resolving(FormRequest::class, function ($request, $app) {
            $request = FormRequest::createFrom($app['request'], $request);
            $request->setContainer($app)->setRedirector($app->make(Redirector::class));
        });
    }

大まかな流れはこんな感じ。

  • $this->app->afterResolving(ValidatesWhenResolved::class, function ($resolved){});で依存解決後の挙動を定義
  • Illuminate\Foundation\Http\FormRequestIlluminate\Contracts\Validation\ValidatesWhenResolvedをインターフェースとして実装しているため、FormRequestServiceProviderで定義されたとおりにvalidateResolvedが呼び出される
  • Illuminate\Validation\ValidatesWhenResolvedTraitvalidateResolvedメソッドでバリデーションを実行

そのため、インスタンスを生成するだけでバリデーションが実行される。

【docker】docker-compose PHP + MySQL + Nginxでnginxが`host not found in upstream`というエラーを吐いた時の対処法

やろうとしたこと

docker-compose.yml
version: '2'

services:
  web:
    image: nginx
    ports:
      - 8080:80
    volumes:
      - ./src:/src
      - ./web/default.conf:/etc/nginx/conf.d/default.conf
  app:
    depends_on:
      - db
    build: ./app
    volumes:
      - ./src:/src
    environment:
      MYSQL_USER: homestead
      MYSQL_PASSWORD: secret
  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: october
      MYSQL_USER: homestead
      MYSQL_PASSWORD: secret
    ports:
      - 13306:3306
    volumes:
      - ./db/mysql_init:/docker-entrypoint-initdb.d
      - ./db/mysql_data:/var/lib/mysql
    command: --innodb-use-native-aio=0

事象

Nginxがhost not found in upstream appというエラーを吐いてうまく起動しないで終了してしまう。

原因

appにdepends_on: dbが存在するから。 depends_onでappが起動待機中にnginxが起動するからエラーになる。 (要するに起動順序の指定が甘かった)

なお、depends_onはあくまで起動順序のみを見てくれるやつで、ちゃんと立ち上がったかどうかまでは確認してくれない。

解決方法

暫定対処

depends_onを消す(起動順は気にせずに、たぶん起動するだろう論)

本格対処

dockerizeを使用してコンテナの起動確認をしてからentrypointを叩くようにする。 dockerizeは以下の記事が詳しい。 dockerize を使って他のコンテナ内サービス起動を待つ

ホワイトリストの.gitignoreで管理対象にしたいファイルが追加されないときの解決方法

やろうとしたこと

LaravelでAPIを作っていて、そのAPI仕様をLaravel API Spec Generator(kotamat/laravel-apispec-generator)でいい感じに残したかった。 このライブラリはPHPUnitを動かしたときにいい感じにRFC 7230ベースでの記述でAPI Specを記録してくれる。

詳しくはこちら。 SwaggerでAPI仕様書に消耗しているなら.restを使うといい。特にLaravelなら

例えば

http://localhost/api/test

というエンドポイントのテストを動かすと、

laravel/storage/app/http/localhost/api/test/GET.http

というファイル名でAPI仕様を吐き出してくれる。

インストールOK!テストもOK!あとはAPI Specをコミットしておけばいいよね! (作者様の意図だとgit汚染しないためにデフォルトでgit管理対象外のstorage/app/配下に出力するようにしてたらしいけど、フロントエンドが別担当だったので敢えてgit管理することにした。)

しかしgitにしばし悩まされることに…。

事象

  • 既に管理対象外だったのでホワイトリスト形式の.gitignoreに追加した。
  • しかしgit管理上では管理対象外のままで、追加できない。
  • git rm --cached .も効果がない。(めちゃくちゃ差分出てきて一瞬びっくりするだけだった)
laravel/storage/app/.gitignore
*
!http/
!http/*

解決策

1つだけの.gitignoreで済まそうとしたのが悪かったのかもしれないと思い、git管理対象外のディレクトリ内にも.gitignoreを配置した。

laravel/storage/app/http/.gitignore
!*

一度上位の.gitignoreで管理対象に含め、下位の.gitignoreを新規に作成してすべてを管理対象にすればOKってことなんですね…。

【Laravel】自作Artisanコマンドがコマンドラインからは正常に動くのにテストコードでInvalidArgumentExceptionが発生したときの話

Qiitaからの転載。

事象

  • Artisanコマンドを作成した
  • コマンドラインからは正常に動くのを確認
  • よーしテストコードも書いちゃうぞ~っ
  • テストの時だけ何故かエラーが発生する…

環境

$ php --version
PHP 7.3.0 (cli) (built: Dec 21 2018 01:48:02) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.0-dev, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.3.0, Copyright (c) 1999-2018, by Zend Technologies
    with Xdebug v2.7.2, Copyright (c) 2002-2019, by Derick Rethans

$ php artisan --version
Laravel Framework 5.7.28

Artisanコマンド

<?php

namespace App\Console\Commands;

use App\User;
use App\DripEmailer;
use Illuminate\Console\Command;

class SendEmails extends Command
{
    /**
     * コンソールコマンドの名前と引数、オプション
     *
     * @var string
     */
    protected $signature = 'email:send {user?}';

    /**
     * コンソールコマンドの説明
     *
     * @var string
     */
    protected $description = 'Send drip e-mails to a user';

    /**
     * 新しいコマンドインスタンスの生成
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * コンソールコマンドの実行
     *
     * @param  \App\DripEmailer  $drip
     * @return mixed
     */
    public function handle(DripEmailer $drip)
    {
        $drip->send(User::find($this->argument('user')));
    }
}

テストコード

<?php

namespace Tests\Unit\app\Console\Commands;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class SendEmailsTest extends TestCase
{
    public function setUp()
    {
        parent::setUp();
        // 何らかのセットアップ
    }

   /**
     * @test
     */
    public function 引数なしの場合に正常に終了すること()
    {
        $this->artisan('email:send')
             ->assertExitCode(0);
    }

   /**
     * @test
     */
    public function 引数ありの場合に正常に終了すること()
    {
        $this->artisan('email:send', ['user' => 'hogehoge'])  // ← ここでエラーが発生
             ->assertExitCode(0);
    }
}

発生したエラー

Fatal error: Uncaught Symfony\Component\Console\Exception\InvalidArgumentException: The "user" argument does not exist. in /project/vendor/symfony/console/Input/ArrayInput.php

原因

今回のケースでは、なんらかのキャッシュが残っておりargumentsが古い状態で保有されていたため、「"user"なんて引数ないよー!」って怒られている。 いろいろ試行錯誤してる最中に試しに実行したりすると起きる可能性がある。

protected $signature = $signature = 'email:send {user}';
protected $signature = $signature = 'email:send {user?}';
protected $signature = $signature = 'email:send';

解決策

キャッシュを削除することで解決する。

$ php artisan optimize:clear
Compiled views cleared!
Application cache cleared!
Route cache cleared!
Configuration cache cleared!
Compiled services and packages files removed!
Caches cleared successfully!

最後に

キャッシュの可能性をすっかり忘れていて、無駄に4時間くらい時間を潰してしまった…。 原因を探しているときにキューワーカー(queue:work)で同じエラーに遭遇している人が見つけて、「ああね、変更が反映されないやつね」と思ったが、ふと「あれ、キャッシュのせいで変更が反映されてないってのもあるのでは?」と思ったらビンゴだった。

Possible bug Artisan::queue() with queue:work "The option does not exist" · Issue #17487 · laravel/framework

開発環境では反映されないときに問答無用でまずはキャッシュを消すくらいの感じがいいのかもしれない。