データベースの検索条件をまとめる Database Query クラスを生成する Laravel パッケージ作った

## はじめに
タイトルのとおりではあるのですが、[laravel-database-queries](https://packagist.org/packages/imunew/laravel-database-queries) というパッケージを作ってみました。

ソースコードは[こちら](https://github.com/imunew/laravel-database-queries)です。
`composer require imunew/laravel-database-queries`で導入できます。

## 動機
`Laravel`には、[Global Scopes](https://laravel.com/docs/8.x/eloquent#global-scopes) という仕組みがあり、検索条件をひとつのクラスにまとめることができます。
`Scope`クラスは、`Illuminate\Database\Eloquent\Scope`インターフェイスを`implements`し、`apply`メソッドを実装するだけのシンプルなクラスです。

```php
namespace App\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class AncientScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('created_at', '<', now()->subYears(2000));
    }
}
```

### `Scope`クラスの`apply`関数は、パラメーターを受け取るようにはできてない
別関数を定義するか、コンストラクタなどでパラメータを受け取りメンバ変数で保持する必要があります。
通常、パラメータ付きのクエリは、[Local Scopes](https://laravel.com/docs/8.x/eloquent#local-scopes) にて実装して、`SoftDelete`みたいにパラメータが不要なクエリは`Global Scope`（`Scope`クラス）にまとめます。

### `Local Scopes`を複数呼び出して、一つの検索条件としてまとめたい
`Local Scopes`を外部から呼び出そうとすると、型定義的に、特定の`Model`が必要となる。
前述の`apply`関数の第２引数である`Model $model`で`Model`インスタンス自体は参照できるが、型定義的には不十分で、`PHP Storm`のようなインテリセンス機能とは相性が悪くなる。

例えば、Userモデルに対するクエリの場合、

```php
    public function apply(Builder $builder, Model $model)
    {
        $model->withdrawn();
    }
```

`User`モデルに実装された`Local Scopes`（上記の`withdrawn`メソッド）は候補に出てこないし、未定義扱いになる。

### 結果、`Scope`クラスは検索条件をまとめるのに使えないので、新たに定義することに
`Local Scopes`で全てをカバーしようとすると、`Model`クラスに`Local Scopes`がたくさん実装されてしまうか、`Local Scopes`を呼び出す外部のクラスに検索条件が散らばってしまうかのどちらかになりがちです。
よく使う検索条件をまとめておければ、そういった課題は解決できると考えました。

## `imunew/laravel-database-queries`の使い方
例えば、`User`モデルを`name`で検索する`Query`クラスを実装してみます。

まずは、以下のコマンドで`Query`クラスを生成し、

```bash
$ php artisan make:database-queries User/SameName --model=User
```

`SameName`クラスの`buildQuery`関数を以下のように実装します。

```php
namespace App\Database\Queries\User;

use App\Models\User;
use Imunew\Laravel\Database\Queries\Query;

/**
 * Class SameName
 * @package App\Database\Queries\User
 *
 * @mixin User
 */
class SameName extends Query
{
    /**
     * SameName constructor.
     * @param array $parameters
     * @param array $with
     */
    public function __construct(array $parameters, array $with = [])
    {
        parent::__construct(User::class, $parameters, $with);
    }

    /**
     * {@inheritdoc}
     */
    protected function validateParameters(array $parameters, ?string &$errorMessage)
    {
        if (!array_key_exists('name', $parameters)) {
            $errorMessage = 'The parameter \'name\' must not be empty.';
            return false;
        }
        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function buildQuery(array $parameters)
    {
        $this->whereName($parameters['name']);
        return $this;
    }
}
```

すると、以下のように`SameName`を（再）利用することができます。

```php
use App\Database\Queries\User\SameName;

function findByName(string $name) {
    $query = new SameName(['name' => $name]);
    return $query->build()->get();
}
```

`Chain`クラスを使うと、複数の`Query`クラスをつなげることもできます。

```php
use App\Database\Queries\User\SameName;
use App\Database\Queries\User\SameEmail;
use Imunew\Laravel\Database\Queries\Chain;

function firstByNameAndEmail(string $name, string $email) {
    $chain = Chain::all([
        new SameName(['name' => $name]), 
        new SameEmail(['email' => $email])
    ]);
    return $chain->build()->first();
}
```

## おわりに
`Local Scopes`には、シンプルな検索条件を定義しておいて、その組み合わせは、`Query`クラスにまとめておくと、`Laravel`の持つ密結合の強さも出しつつ、いわゆる`Fat Model`問題も回避できるのではないかと思います。

もし、よければ使ってみてください。
ではでは。