Eloquent で役立つリポジトリ?

先週書きました Eloquent エンティティのリポジトリ テンプレートの役に立たないことに関する記事しかし、彼はそれを部分的に有利に利用する方法を私に教えると約束しました。 これを行うために、このテンプレートがプロジェクトで通常どのように使用されるかを分析してみます。 リポジトリに最低限必要なメソッドのセットは次のとおりです。

<?php
interface PostRepository
{
    public function getById($id): Post;
    public function save(Post $post);
    public function delete($id);
}

ただし、実際のプロジェクトでは、リポジトリを使用することが決定された場合、レコードを取得するためのメソッドがリポジトリに追加されることがよくあります。

<?php
interface PostRepository
{
    public function getById($id): Post;
    public function save(Post $post);
    public function delete($id);

    public function getLastPosts();
    public function getTopPosts();
    public function getUserPosts($userId);
}

これらのメソッドは Eloquent スコープを通じて実装できますが、エンティティ クラスに自身をフェッチする責任を負わせてオーバーロードするのは最良のアイデアではなく、この責任をリポジトリ クラスに移すのは論理的だと思われます。 そうですか? 特に、このインターフェイスを視覚的に XNUMX つの部分に分割しました。 メソッドの最初の部分は書き込み操作で使用されます。

標準的な書き込み操作は次のとおりです。

  • 新しいオブジェクトの構築と挑戦 PostRepository::save
  • PostRepository::getById、エンティティの操作と召喚 PostRepository::save
  • 挑戦 PostRepository::削除

書き込み操作ではフェッチ メソッドは使用されません。 読み取り操作では、get* メソッドのみが使用されます。 について読んだら インターフェース分離原理 (手紙 I в SOLID) の場合、インターフェースが大きすぎて、少なくとも XNUMX つの異なる役割を実行していることが明らかになります。 XNUMXで割る時が来ました。 方法 getById はどちらでも必要ですが、アプリケーションが複雑になるにつれて、その実装は異なります。 これについては少し後で説明します。 以前の記事で書き込み部分の無駄について書きましたので、今回は単純に忘れます。

Read 部分は、Eloquent でもいくつかの実装がある可能性があるため、それほど役に立たないように思えます。 クラスに名前を付けるにはどうすればよいですか? できる ReadPostリポジトリ、ただしテンプレートに 倉庫 彼にはすでにほとんど関連性がありません。 できるのは、 ポストクエリ:

<?php
interface PostQueries
{
    public function getById($id): Post;
    public function getLastPosts();
    public function getTopPosts();
    public function getUserPosts($userId);
}

Eloquent での実装は非常に簡単です。

<?php
final class EloquentPostQueries implements PostQueries
{
    public function getById($id): Post
    {
        return Post::findOrFail($id);
    }

    /**
    * @return Post[] | Collection
    */
    public function getLastPosts()
    {
        return Post::orderBy('created_at', 'desc')
            ->limit(/*some limit*/)
            ->get();
    }
    /**
    * @return Post[] | Collection
    */
    public function getTopPosts()
    {
        return Post::orderBy('rating', 'desc')
            ->limit(/*some limit*/)
            ->get();
    }

    /**
    * @param int $userId
    * @return Post[] | Collection
    */
    public function getUserPosts($userId)
    {
        return Post::whereUserId($userId)
            ->orderBy('created_at', 'desc')
            ->get();
    }
}

インターフェイスは実装に関連付ける必要があります。たとえば、 アプリサービスプロバイダー:

<?php
final class AppServiceProvider extends ServiceProvider 
{
    public function register()
    {
        $this->app->bind(PostQueries::class, 
            EloquentPostQueries::class);
    }
}

このクラスはすでに役に立ちます。 彼は、コントローラーまたはエンティティ クラスのいずれかを解放することで自分の責任を認識します。 コントローラーでは次のように使用できます。

<?php
final class PostsController extends Controller
{
    public function lastPosts(PostQueries $postQueries)
    {
        return view('posts.last', [
            'posts' => $postQueries->getLastPosts(),
        ]);
    }
} 

方法 PostsController::lastPosts 実装を求めているだけです 投稿クエリ そしてそれを使って作業します。 リンクしたプロバイダーでは ポストクエリ クラスと一緒に EloquentPostQuery このクラスはコントローラーに置き換えられます。

私たちのアプリケーションが非常に人気になったと想像してみましょう。 毎分数千人のユーザーが最新の出版物のページを開きます。 最も人気のある出版物は、非常に頻繁に読まれます。 データベースはこのような負荷をうまく処理できないため、標準的なソリューションであるキャッシュを使用します。 データベースに加えて、特定のデータ スナップショットが特定の操作に最適化されたストレージに保存されます。 memcached または 赤目.

キャッシュ ロジックは通常、それほど複雑ではありませんが、EloquentPostQueries での実装はあまり正しくありません (理由は次のとおりです) 単一責任の原則)。 テンプレートを使用する方がはるかに自然です デコレータ そして、メインアクションの装飾としてキャッシュを実装します。

<?php
use IlluminateContractsCacheRepository;

final class CachedPostQueries implements PostQueries
{
    const LASTS_DURATION = 10;

    /** @var PostQueries */
    private $base;

    /** @var Repository */
    private $cache;

    public function __construct(
        PostQueries $base, Repository $cache) 
    {
        $this->base = $base;
        $this->cache = $cache;
    }

    /**
    * @return Post[] | Collection
    */
    public function getLastPosts()
    {
        return $this->cache->remember('last_posts', 
            self::LASTS_DURATION, 
            function(){
                return $this->base->getLastPosts();
            });
    }

    // другие методы практически такие же
}

インターフェースを無視する 倉庫 コンストラクター内で。 何らかの理由で、Laravel のキャッシュ用のインターフェイスにこのような名前を付けることにしました。

クラス キャッシュされたポストクエリ キャッシュのみを実装します。 $this->キャッシュ->記憶する このエントリがキャッシュにあるかどうかを確認し、存在しない場合はコールバックを呼び出し、戻り値をキャッシュに書き込みます。 残っているのは、このクラスをアプリケーションに実装することだけです。 インターフェイスの実装を要求するには、アプリケーション内のすべてのクラスが必要です ポストクエリ クラスのインスタンスの受信を開始しました キャッシュされたポストクエリ。 しかしながら、彼自身は、 キャッシュされたポストクエリ コンストラクターはクラスをパラメーターとして受け取る必要があります EloquentPostQuery「実際の」実装がなければ機能しないからです。 私たちは変わります アプリサービスプロバイダー:

<?php
final class AppServiceProvider extends ServiceProvider 
{
    public function register()
    {
        $this->app->bind(PostQueries::class, 
            CachedPostQueries::class);

        $this->app->when(CachedPostQueries::class)
            ->needs(PostQueries::class)
            ->give(EloquentPostQueries::class);
    }
}

私の願いはすべてプロバイダーにごく自然に記述されています。 したがって、XNUMX つのクラスを作成し、コンテナー構成を変更するだけで、リクエストのキャッシュを実装しました。 アプリケーション コードの残りの部分は変更されていません。

もちろん、キャッシュを完全に実装するには、削除された記事がしばらくサイト上に留まらず、すぐに削除されるように無効化を実装する必要もあります。 しかし、これらは些細なことです。

結論: XNUMX つではなく XNUMX つのテンプレートを使用しました。 サンプル コマンドクエリ責任分離 (CQRS) インターフェイス レベルで読み取り操作と書き込み操作を完全に分離することを提案しています。 私は彼のところへやって来ました インターフェース分離原理これは、パターンと原則を巧みに操作し、一方から他方を定理として導出していることを示唆しています:) もちろん、すべてのプロジェクトでエンティティを選択するためにそのような抽象化が必要なわけではありませんが、そのコツを共有します。開発では、クラスを作成するだけで済みます ポストクエリ Eloquent による通常の実装では次のようになります。

<?php
final class PostQueries
{
    public function getById($id): Post
    {
        return Post::findOrFail($id);
    }

    // другие методы
}

キャッシュの必要性が生じた場合、簡単な操作でこのクラスの代わりにインターフェイス (または抽象クラス) を作成できます。 ポストクエリ、その実装をクラスにコピーします EloquentPostQuery そして、前に説明したスキームに進みます。 アプリケーション コードの残りの部分は変更する必要はありません。

クラス、インターフェイス、 依存性注入 и CQRS で詳しく説明 私の著書「複雑な Web アプリケーションのアーキテクチャ」。 この記事の例にあるすべてのクラスが最終クラスとしてマークされている理由の謎に対する解決策もあります。

出所: habr.com

コメントを追加します