はじめに
タイトルのとおりではあるのですが、laravel-database-queries というパッケージを作ってみました。
ソースコードはこちらです。
composer require imunew/laravel-database-queries
で導入できます。
動機
Laravel
には、Global Scopes という仕組みがあり、検索条件をひとつのクラスにまとめることができます。
Scope
クラスは、Illuminate\Database\Eloquent\Scope
インターフェイスをimplements
し、apply
メソッドを実装するだけのシンプルなクラスです。
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 にて実装して、SoftDelete
みたいにパラメータが不要なクエリはGlobal Scope
(Scope
クラス)にまとめます。
Local Scopes
を複数呼び出して、一つの検索条件としてまとめたい
Local Scopes
を外部から呼び出そうとすると、型定義的に、特定のModel
が必要となる。
前述のapply
関数の第2引数であるModel $model
でModel
インスタンス自体は参照できるが、型定義的には不十分で、PHP Storm
のようなインテリセンス機能とは相性が悪くなる。
例えば、Userモデルに対するクエリの場合、
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
クラスを生成し、
$ php artisan make:database-queries User/SameName --model=User
SameName
クラスのbuildQuery
関数を以下のように実装します。
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
を(再)利用することができます。
use App\Database\Queries\User\SameName;
function findByName(string $name) {
$query = new SameName(['name' => $name]);
return $query->build()->get();
}
Chain
クラスを使うと、複数のQuery
クラスをつなげることもできます。
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
問題も回避できるのではないかと思います。
もし、よければ使ってみてください。
ではでは。