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で実行されることは普通ない。

連想配列への代入は一括でやった方がいいと思う

2020-03-26追記

もっと単純に、一つの操作を複数に分けるな、って話でよかったね。


<?php

$assoc1['key1'] = 'val1';
$assoc1['key2'] = 'val2';
$assoc1['key3'] = 'val3';

$assoc2 = [
    'key1' => 'val1',
    'key2' => 'val2',
    'key3' => 'val3',
];

上記のような場合に、$assoc1の方法より$assoc2の方法がいいと思う。まあ昔からずっと思っていたのだが最近$assoc1の方法で書かれたコードを読んで書き直してとすることがあって再度思って、でもそれはなぜかと聞かれても即答できなそうだったのでちょっと考えた。

まあ単純な話で、

  1. $assoc1の場合は、変数が3回出現しており、その分変数名に間違えがないか(1つ$assoclがあったらどうする)、追加で確認しなければならない。
  2. $assoc1の場合は、代入演算子も3回出現しており、その分よく見ると別の演算子(+=とか)だったりしないか、追加で確認しなければならない。

ということで、あらゆる繰り返しと同様、同じことなのに別々に書くと別々に確認しなければならないので、まとめた方がいいというそれだけの話。もちろん確認だけではなく、変更のときもその分変更が増えるよね。

これで今後$assoc1の書き方をしている人がいたら、ちゃんと根拠を持って注意できる。

ちなみにあまりに当たり前で普通のことだからか、リーダブルコードにもCode Completeにもこの辺に関する説明はなかったと思う。