Composerの高速化を設定した状態で2系に上げるとトラブる

https://github.com/hirak/prestissimo プラグインを入れたり、https://packagist.jp/ ミラーを使うように設定した状態で、Composerを2系にアップデートすると、composer require時等に以下のような警告・エラーが出る。

The "hirak/prestissimo" plugin was skipped because it requires a Plugin API version ("^1.0.0") that does not match your Composer installation ("2.0.0"). You may need to run composer update with the "--no-plugins" option.

これはとりあえず--no-pluginsオプションをつけて実行することでも対処できるが、多分ほかのプラグインも動かないし、アンインストールしてしまおう。

composer global remove hirak/prestissimo

Composer 2系ではprestissimoがなくても、並列ダウンロードしてくれるようでだいぶ高速になっている。

また、

[InvalidArgumentException]
Could not find a version of package livewire/livewire matching your minimum-stability (dev). Require it with an explicit version constr
aint allowing its desired stability.

あるいは、

[InvalidArgumentException]
Could not find package livewire/livewire.

このようなエラーが出る場合は、ミラーの https://packagist.jp/ が2系用のパッケージを持っていないせいのようだ。エラーメッセージもうちょっとなんとかならないのかとは思うが。

packagist.jpのウェブページに書いてある通り、

composer config -g --unset repos.packagist

すればおけ。prestissimoが多分いらないかなという状況なのに対して、ミラーはまだある程度速度に効いてそう、つまり2系Composeでも地理的なネックがあるような気はするので、もしpackagist.jpが2系に対応してくれるならこれは戻すのもありかも。

Laravelのファサードとはなにか

ずっと使っているけど十分に理解できなかったので調べた。

ファサードとはなにか

結論から言うと、ファサードは、対応したサービスのインスタンスのメソッドを、静的にコールできる仕組み、のようだ。

GoFデザインパターンのFacadeパターンとはちょっと違う。あれは複数の複雑な機能に対する一つのアクセス方法を提供するものだったと思うので。こっちはもっとシンプル。多少の例外はあるが、基本的にはサービスに委譲するだけだ。

Illuminate\Support\Facades\Requestファサード(以下Requestファサード)を例に挙げる。このファサード'request'エイリアスのサービスと対応していて、その実体はIlluminate\Http\Requestインスタンスである。

コントローラアクションで、Illuminate\Http\RequestをDIして持って来た$requestのたとえば$request->all()と、ファサードRequest::all()は同等になる。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request as HttpRequest;
use Illuminate\Support\Facades\Request;

class TestController extends Controller
{
    public function index(HttpRequest $request)
    {
        dd($request->all(), Request::all()); # 等しい
    }
}

ファサードはどう実装されているか

まずファサードはクラスである。Illuminate\Support\Facades名前空間以下にあるクラスで、Illuminate\Support\Facades\Facade抽象クラスを継承している。

ファサードクラス内にはgetFacadeAccessor()というメソッドが共通して存在していて、これが肝になっている。単純にこの戻り値によって対応するサービスが決まっている。

大抵文字列で、サービスのエイリアスが返されているが、Schemaファサードに関してはオブジェクトが直接返されていたりする。

オブジェクトの場合はそのまま使うだけだが、サービスのエイリアス等の場合は、必要に応じてresolve()相当の解決が行われている。

つまりRequestファサードで静的にメソッドを呼び出すのと、resolve('request')されたオブジェクトのメソッドを呼び出すのも同じことになる。

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Request;

class TestController extends Controller
{
    public function index()
    {
        dd(resolve('request')->all(), Request::all()); # 等しい
    }
}

関連1: クラス名のエイリアス

ファサードに関連する紛らわしい個所が2つある。1つはクラス名のエイリアスで、ファサードとは直接関係ないがほぼファサードに関連して使われている。

これはuse Requestuse Illuminate\Support\Facades\Requestと同等になるあたりの話だ。

通常エイリアスを使うことはほぼ使わないと思うが、一部の素のPHPスクリプト上では、useせず、エイリアスが直接使われていることがあるかもしれない。

php artisan ui vue --authなどとして認証機能を有効にした場合、routes/web.phpAuth::routes()が追加されるが、このときファイル上部ではuse Illuminate\Support\Facades\Authとはされていないはずだ。

これはエイリアスのおかげで動作している。

エイリアスは単純に、spl_autoload_register()を使って、動的にclass_alias()されることで設定されている。

設定されているエイリアスに関しては、Illuminate\Foundation\AliasLoader::getInstance()->getAliases()をtinker等から実行すれば確認できる。

これらのほとんどはconfig/app.phpaliasesで設定されるものだが、一部Composerでインストールされたパッケージから設定されるものがある。composer.lock(じっさいにはinstalled.jsonから読まれているが)のパッケージ設定にextra.laravel.aliasesがある場合、config/app.phpaliasesのものに追加でエイリアスが作られる。

デフォルトではLaravel 7のエラー表示パッケージであるIgnitionから、Flareクラスのエイリアスが設定されているはずだ。

// ...
        {
            "name": "facade/ignition",
            // ...
            "extra": {
                "laravel": {
                    "aliases": {
                        "Flare": "Facade\\Ignition\\Facades\\Flare"
                    }
                }
            },
        },
// ...

関連2: Arr, Str

もう1つはIlluminate\Support以下の、Arr, Strクラスだ。これらのクラスはファサードではないが、静的メソッドの使用がメインで、かつエイリアスも設定されているため、場合によってはファサードと区別がしづらいかもしれない。

とはいえ区別する必要もあまりない。クラスのソースコードを見れば、一目で裏にサービスインスタンスのない静的メソッドだけのクラスとわかる。

Let's Encryptの証明書の期限切れ通知メールをunsubscribeすると現状どうしようもないっぽい

ある日更新が不要になったLet's Encryptで取得した証明書の期限切れ通知メールを見てて、unsubscribeと書いてあるのに気付いてふとクリックしてしまった。

クリックした後の感じでは、どうもこの通知解除は、あるドメインに対してだけではなくメールアドレス単位で解除されるようだ。

それはちょっと不便なのでなんとかならないかと調べてみたが、どうしようもなさそう。

公式の情報。別のメールアドレス使ってねとある。

Expiration Emails - Let's Encrypt - Free SSL/TLS Certificates

Let's Encryptのコミュニティの情報。2016年から問題にされていて、2020年になっても十分な対策はないようだ。

Accidentally Unsubscribed - Let's Encrypt Community Support

オチはない。

Googleの検索結果一覧で狭い画面でも横スクロールバーが出ないようにする

いつの間にかmin-widthが1261pxとかになっていて、ブラウザのウィンドウは小さめ派には微妙な感じになっていたのでなんとかしてみた。

#appbar,
.rhscol,
#top_nav,
div#searchform,
#fbar {
    min-width: 0px;
}

まずmin-widthが1261pxに設定されているすべての要素のmin-widthを0pxにする。これで表示領域1168pxまでは横スクロールバーが出ないようになる。

個人的にはこれで十分だったが、もう少し小さなウィンドウでブラウジングしてる人はさらに以下のスタイルシートを当てる。

#rhs {
    display: none;
}

これが謎要素で、子を見た感じ表示する必要がそもそもなさそうなんだが表示されていて、しかも珍妙なサイズの左マージンが設定されているせいで表示に影響している。

とりあえず問題なさそうなので非表示にすることで、今度は表示領域852pxまでは横スクロールバーが表示されなくなる。

Gitであるauthorが変更したファイル一覧が欲しい

git diff --name-onlyとかで行けるかなと思ったけど、git diffはどうもauthorの指定はできないようだった。まあ仕組みを考えると妥当か。

ちょっとややこしいけど、以下のようにしてgit logでそれっぽい感じで取れた。--name-onlyじゃなくて--name-statusなのは、ファイル名とコミット概要部分を分けやすいようにしてるだけ。

git log --name-status --oneline --author='Author Name' | sort -u | grep -e '^\w\s'

Laravelのフォームリクエストのジレンマ

Laravelにはフォームリクエストという便利な機能がある。コントローラに適切なフォームリクエストをインジェクションさせると、リクエストがフォームリクエスト内のルールでバリデーションされた上でインジェクションされる。

<?php

namespace App\Http\Requests;

class UserRequest extends \Illuminate\Foundation\Http\FormRequest
{
    public function rules()
    {
        return [
            'name'  => ['string'],
            'email' => ['email'],
        ];
    }
}
<?php

namespace App\Http\Controllers;

class UserController extends Controller
{
    public function store(\App\Http\Requests\UserRequest $request)
    {
        dd($request->validated()); # バリデーション済みのデータが取得できる
    }
}

上記のコードは、コントローラ側で以下のように実装しても同等だ。

<?php

namespace App\Http\Controllers;

class UserControllerWithValidation extends Controller
{
    public function store(\Illuminate\Http\Request $request)
    {
        dd($request->validate([
            'name'  => ['string'],
            'email' => ['email'],
        ]));
    }
}

フォームリクエストを使わない方がコードは短くなる。ではなぜフォームリクエストを使うのか? それはフォームリクエストを使うことでコントローラとバリデーションの双方向の依存を、コントローラからバリデーションだけの一方向の依存にできるからだ。

ではすべてのバリデーションをフォームリクエストで行おう、となりそうだが、じつはフォームリクエストは以下のようなルールで破綻する。

<?php

namespace App\Http\Controllers;

class UserControllerWithValidation extends Controller
{
    public function update(\Illuminate\Http\Request $request, \App\User $user)
    {
        dd($request->validate([
            'name'  => ['string'],
            'email' => ['email', \Illumination\Validation\Rule::unique('users')->ignore($user->id)],
        ]));
    }
}

バリデーションルールがルートと関連したモデルに依存してしまっている。このような、よくある、ちょっと複雑な例にフォームリクエストを使うことは意味がない。

フォームリクエストの利点はコントローラに依存しなくなることだが、ユーザモデルのインスタンスを取得するためにはコントローラに依存するか、もっと酷いことにルートに直接依存することになる。

フォームリクエストは Illuminate\Http\Requests を拡張していて、同様に routeメソッド経由でルートにアクセスでき、そこからユーザモデルのインスタンスを取得できるが、コントローラ側でDIで間接的にルートから取得する方がまだマシだ。

ということで、フォームリクエストはコントローラへの依存を切り離せるので便利だが、依存を切り離すとコントローラに依存しないと用意できない複雑なルールに対応できなくなるので、じっさいにはほとんど使い所はない。

<?php

namespace App\Http\Requests;

class UserRequest extends \Illuminate\Foundation\Http\FormRequest
{
    public function rules()
    {
        $user = $this->route()->parameter('user'); # 持って来ること自体はできるのだが

        return [
            'name'  => ['string'],
            'email' => ['email', \Illumination\Validation\Rule::unique('users')->ignore($user->id)],
        ];
    }
}

バリデーションルールは、素直にコントローラ内なりに書くのがよさそうだ。

LaravelでRefreshDatabaseしているテストをどう安全に運用するか

課題

RefreshDatabaseはテストごとにデータベースをクリアしてくれる便利なトレイトで、データベースを使ったテストを書くときにはほぼ必須だが、運用をミスると消したくないデータベースを消してしまうことがある。

具体的に危険なのは、.env.testingがない場合に.envが使われることにより、.envい記載されたテスト用以外のデータベースにつないでしまい、クリアしてしまうこと。

Laravel 6.8.0以降ではphpunit.xmlSQLiteのインメモリデータベースに接続するように設定されるようになっている(参考)ため、phpunit.xmlを変更していないのであればこの問題は避けられる。

(が、LaravelはRDBMS間の差異を十分に吸収しないため、テストにSQLiteを使うのは実用的ではなく、大抵の場合はこの部分の設定を消して、.env.testingに書くことになるだろう)

解決策

本番環境では、まずそもそもPHPUnitが入っていないはずなのであまり問題ないはず。

問題は開発環境だ。.envは用意せず、.env.localを用意して、アプリケーション動作時にはAPP_ENV=localだけ別途指定する形にすれば、間違って.env.testingがない状態で自動テストを叩くようなことがあっても安全そうだ。が、まだ十分には試していない。

すべてのテストをデータベースの状態に依存させずに書ければそれがベストだと思うけど、なかなか難しいよね。

その他

RefreshDatabaseはmigrate:freshコマンドを実行することでデータベースをクリアしている。このコマンドはproduction下では実行前に確認プロンプトを出してくれるのだが、そもそもテストがproductionで実行されることは普通ない。