はじめに
タイトルのとおりではあるのですが、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問題も回避できるのではないかと思います。
もし、よければ使ってみてください。
ではでは。