【Laravel】 認証でユーザを取得するとき，特定のスコープに限定する

# 問題

```php
class User extends Model implements Authenticatable
{
    use SoftDeletes;
}
```

```php
Auth::user() // 論理削除ユーザは除外される
```

[`SoftDeletes`] を使用している [`Authenticatable`] の場合，グローバルスコープで論理削除を除外するものが付与されるので，認証時にも除外される。ところが論理削除とは違う「有効アカウント」「無効アカウント」といった表現を **`status={0,1}`** のように行っている場合，素の状態では対応できない。ここでは，できるだけ Laravel 標準の [Auth](https://github.com/illuminate/auth) に乗っかる形での実装を目指して対応してみる。


(備考)[【Laravel】 認証や認可に関する補足資料 - Qiita](https://qiita.com/mpyw/items/8c5413b99b8e299f7002)

[`SoftDeletes`]: https://github.com/illuminate/database/blob/7efcf43d91094c2b4aebb274b7652ea75e706867/Eloquent/SoftDeletes.php
[`Authenticatable`]: https://github.com/illuminate/contracts/blob/6b0122dc740d6db5ab8b1187af313c6e65afeb55/Auth/Authenticatable.php

# 実装

## `AuthScopable` インタフェースの作成

モデルごとに実装は異なるので，共通して使えるようにインタフェースを作成する。命名や引数・返り値の方は Laravel の標準的なスコープの規約に従っている。

```php
<?php

namespace App\Auth;

use Illuminate\Database\Eloquent\Builder;

interface AuthScopable
{
    /**
     * Add a scope for authentication.
     *
     * @param  \Illuminate\Database\Eloquent\Builder $query
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeForAuthentication(Builder $query): Builder;
}
```

[`EloquentUserProvider`]: https://github.com/illuminate/auth/blob/7b2297b6cd5e7000b31caca40399c33832237649/EloquentUserProvider.php

## `ScopedEloquentUserProvider` の作成

[`EloquentUserProvider`] は Laravel の標準実装にあり，既定ではこれが使用されている。今回はここの一部を継承して，認証でクエリが発行されるときに `AuthScopable` で示されるスコープを適用できるようにしてみる。

```php
<?php

namespace App\Auth;

use Illuminate\Auth\EloquentUserProvider;

class ScopedEloquentUserProvider extends EloquentUserProvider
{
    /**
     * @param  null|\Illuminate\Database\Eloquent\Model $model
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function newModelQuery($model = null)
    {
        $query = parent::newModelQuery($model);

        $instance = $query->getModel();

        if ($instance instanceof AuthScopable) {
            $query = $instance->scopeForAuthentication($query);
        }

        return $query;
    }
}
```

## `AuthServiceProvider` での登録

```php
<?php

namespace App\Providers;

use App;
use App\Auth\ScopedEloquentUserProvider;
use App\Policies;
use App\User;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        App\User::class => Policies\UserPolicy::class,
    ];

    /**
     * Register any authentication / authorization services.
     */
    public function boot(): void
    {
        $this->registerPolicies();

        // EloquentUserProvider をオーバーライド
        Auth::provider('eloquent', function (Container $app, array $config) {
            return $app->make(ScopedEloquentUserProvider::class, [
                'model' => $config['model'],
            ]);
        });
    }
}
```

## モデルへの適用

あとは書くだけ！

```php
class User extends Model implements Authenticatable, AuthScopable
{
    public function scopeForAuthentication(Builder $query): Builder
    {
        return $query->where('status', 1);
    }
}
```

```php
Auth::user() // ステータスが 0 のユーザは除外される
```

## おまけ

標準的なスコープ命名規則に従っているので，一応こんな使い方もできます…
（多分そんなに使わないと思うけど）

```php
User::where('email', 'xxx@example.com')->forAuthentication()->firstOrFail()
```

```php
User::where('email', 'xxx@example.com')->scopes(['forAuthentication'])->firstOrFail()
```

# 追記

OSS化しました

[mpyw/scoped-auth: Apply specific scope to for user authentication.](https://github.com/mpyw/scoped-auth)