Užitečné repozitáře s Eloquent?

Minulý týden jsem psal článek o zbytečnosti šablony Repository pro entity Eloquent, nicméně slíbil, že mi řekne, jak to částečně využít ve svůj prospěch. Za tímto účelem se pokusím analyzovat, jak se tato šablona obvykle používá v projektech. Minimální požadovaná sada metod pro úložiště:

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

Pokud se však ve skutečných projektech rozhodlo použít úložiště, často se k nim přidávají metody pro získávání záznamů:

<?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);
}

Tyto metody by mohly být implementovány prostřednictvím oborů Eloquent, ale přetěžování tříd entit odpovědností za načítání samy není nejlepší nápad a přesunutí této odpovědnosti na třídy úložiště se zdá logické. Je to tak? Toto rozhraní jsem konkrétně vizuálně rozdělil na dvě části. První část metod bude použita při operacích zápisu.

Standardní operace zápisu je:

  • výstavba nového objektu a výzva PostRepository::save
  • PostRepository::getById, manipulace a vyvolávání entit PostRepository::save
  • výzvu PostRepository::delete

Operace zápisu nepoužívají metody načítání. V operacích čtení se používají pouze metody get*. Pokud čtete o Princip segregace rozhraní (dopis I в SOLID), pak bude jasné, že naše rozhraní je příliš velké a plní minimálně dvě různé odpovědnosti. Je čas to vydělit dvěma. Metoda getById je nezbytný v obou, ale jak se aplikace stává složitější, její implementace se budou lišit. To uvidíme o něco později. O zbytečnosti psaní jsem psal v předchozím článku, takže v tomto na to prostě zapomenu.

Část Read mi nepřipadá tak zbytečná, protože i pro Eloquent zde může být několik implementací. Jak třídu pojmenovat? Umět ReadPostRepository, ale k šabloně sklad už má malý význam. Můžeš jen PostQueries:

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

Jeho implementace pomocí Eloquent je poměrně jednoduchá:

<?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();
    }
}

Rozhraní musí být spojeno s implementací, například v AppServiceProvider:

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

Tato třída je již užitečná. Svou odpovědnost si uvědomí tím, že uvolní buď kontrolory, nebo třídu entit. V ovladači to lze použít takto:

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

metoda PostsController::lastPosts jen žádám o nějakou implementaci PostsQueries a pracuje s ním. V poskytovateli, kterého jsme propojili PostQueries s třídou Výmluvné PostQueries a tato třída bude nahrazena do ovladače.

Představme si, že se naše aplikace stala velmi populární. Stránku s nejnovějšími publikacemi otevírají tisíce uživatelů za minutu. Velmi často se čtou i nejoblíbenější publikace. Databáze takové zátěže příliš nezvládají, proto používají standardní řešení – cache. Kromě databáze je v úložišti optimalizovaném pro určité operace uložen určitý snímek dat - memcached nebo redis.

Logika ukládání do mezipaměti obvykle není tak složitá, ale její implementace v EloquentPostQueries není příliš správná (už jen proto, Princip jediné odpovědnosti). Mnohem přirozenější je použít šablonu Dekoratér a implementujte ukládání do mezipaměti jako ozdobu pro hlavní akci:

<?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();
            });
    }

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

Ignorujte rozhraní sklad v konstruktoru. Z neznámého důvodu se rozhodli takto pojmenovat rozhraní pro ukládání do mezipaměti v Laravelu.

Třída CachedPostQueries implementuje pouze ukládání do mezipaměti. $this->cache->pamatovat zkontroluje, zda je tento záznam v mezipaměti, a pokud ne, zavolá zpětné volání a zapíše vrácenou hodnotu do mezipaměti. Zbývá pouze implementovat tuto třídu do aplikace. Potřebujeme všechny třídy, které v aplikaci vyžadují implementaci rozhraní PostQueries začal přijímat instanci třídy CachedPostQueries. Nicméně on sám CachedPostQueries konstruktor musí obdržet třídu jako parametr Výmluvné PostQueriesprotože to nemůže fungovat bez "skutečné" implementace. Měníme se AppServiceProvider:

<?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);
    }
}

Všechna moje přání jsou zcela přirozeně popsána v poskytovateli. Implementovali jsme tedy ukládání do mezipaměti pro naše požadavky pouze napsáním jedné třídy a změnou konfigurace kontejneru. Zbytek kódu aplikace se nezměnil.

Pro plnou implementaci cachování je samozřejmě nutné implementovat i zneplatnění, aby smazaný článek na webu nějakou dobu nevisel, ale byl okamžitě smazán. Ale to jsou drobnosti.

Sečteno a podtrženo: použili jsme ne jednu, ale dvě šablony. Vzorek Segregace odpovědnosti za příkazový dotaz (CQRS) navrhuje zcela oddělit operace čtení a zápisu na úrovni rozhraní. Přišel jsem k němu skrz Princip segregace rozhraní, což naznačuje, že obratně manipuluji se vzory a principy a odvozuji jeden od druhého jako větu :) Samozřejmě ne každý projekt potřebuje takovou abstrakci pro výběr entit, ale o trik se s vámi podělím.V počáteční fázi aplikace vývoj, můžete jednoduše vytvořit třídu PostQueries s obvyklou implementací přes Eloquent:

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

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

Když vznikne potřeba ukládání do mezipaměti, můžete jednoduchým přesunem vytvořit rozhraní (nebo abstraktní třídu) místo této třídy PostQueries, zkopírujte jeho implementaci do třídy Výmluvné PostQueries a přejděte na schéma, které jsem popsal dříve. Zbytek kódu aplikace není třeba měnit.

Všechny tyto triky s třídami, rozhraními, Vstřikování závislosti и CQRS podrobně popsáno v moje kniha „Architektura komplexních webových aplikací“. Existuje také řešení hádanky, proč jsou všechny mé třídy v příkladech k tomuto článku označeny jako konečné.

Zdroj: www.habr.com

Přidat komentář