Užitočné repozitáre s Eloquentom?

Minulý týždeň som napísal článok o zbytočnosti vzoru Repository pre Eloquent entity, no sľúbil som, že poviem, ako ho je možné čiastočne využiť. Na to sa pokúsim analyzovať, ako sa tento vzor obvykle používa v projektoch. Minimálny potrebný súbor metód pre repozitár:

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

Avšak v reálnych projektoch, ak sa rozhodne repozitáre používať, sa do nich často pridávajú metódy na získavanie záznamov:

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

Tieto metódy by sa dali realizovať cez Eloquent scopes, ale preťažovať triedy entít povinnosťami na získanie samých seba — nie je najlepší nápad a presun tejto povinnosti do tried repozitárov sa zdá logický. Je to tak? Úmyselne som vizuálne rozdelil tento rozhranie na dve časti. Prvá časť metód bude použitá v operáciách zápisu.

Štandardná operácia zápisu je:

  • konštruovanie nového objektu a volanie PostRepository::save
  • PostRepository::getById, manipulácie s entitou a volanie PostRepository::save
  • volanie PostRepository::delete

V operáciách zápisu sa nepoužívajú metódy na získavanie. V operáciách čítania sa používajú iba metódy get*. Ak si prečítate o Interface Segregation Principle (písmeno I v SOLID), bude jasné, že naše rozhranie sa ukázalo ako príliš veľké a plní minimálne dve rôzne povinnosti. Je čas ho rozdeliť na dve. Metóda getById je potrebná v oboch, avšak pri zložitosti aplikácie sa jej realizácie budú líšiť. To uvidíme o chvíľu. O zbytočnosti write-časti som písal v predchádzajúcom článku, preto na ňu v tomto článku jednoducho zabudnem.

Časť Read mi však nepripadá taká zbytočná, pretože aj pre Eloquent môžu existovať viaceré realizácie. Ako nazvať triedu? Môžeme ReadPostRepository, ale k vzoru Repository má už len málo spoločného. Môžeme jednoducho PostQueries:

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

Jeho realizácia pomocou Eloquent je pomerne 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();
    }
}

Rozhranie by malo byť prepojené s implementáciou, napríklad v AppServiceProvider:

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

Trieda je už užitočná. Realizuje svoju zodpovednosť, čím uvoľňuje buď kontroléry, alebo triedu entity. V kontroléri môže byť použitá takto:

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

Metóda PostsController::lastPosts iba žiada akúkoľvek realizáciu PostsQueries a pracuje s ňou. V poskytovateľovi sme prepojili PostQueries s triedou EloquentPostQueries a v kontroléri bude daná táto trieda.

Predstavme si, že naša aplikácia sa stala veľmi populárnou. Tisíce používateľov za minútu otvárajú stránku s poslednými publikáciami. Najpopulárnejšie publikácie sa tiež často čítajú. Databázy sa s takými záťažami veľmi dobre nevyrovnávajú, preto používajú štandardné riešenie — cache. Okrem databázy sa určitý zlomek dát uchováva v úložisku optimalizovanom pre určité operácie — memcached alebo redis.

Logika cacheovania zvyčajne nie je taká zložitá, ale implementovať ju v EloquentPostQueries nie je veľmi správne (aspoň kvôli Single Responsibility Principle). Omnoho prirodzenejšie je použiť návrhový vzor Dekorátor a implementovať cacheovanie ako dekoráciu hlavnej akcie:

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

    // ostatné metódy sú takmer rovnaké
}

Nehajte si robiť starosti s rozhraním Repository v konštruktore. Z nejasných dôvodov sa tak rozhodlo nazývať rozhranie pre cacheovanie v Laravel.

Trieda CachedPostQueries realizuje len cacheovanie. $this->cache->remember kontroluje, či daný záznam nie je v cache, a ak nie je, vyvolá callback a uloží do cache vrátenú hodnotu. Už len zaviesť túto triedu do aplikácie. Potrebujeme, aby všetky triedy, ktoré v aplikácii žiadajú implementáciu rozhrania PostQueries začali dostávať inštanciu triedy CachedPostQueries. Avšak samotný CachedPostQueries ako parameter v konštruktore musí prijať triedu EloquentPostQueries, pretože nemôže fungovať bez „skutočnej“ implementácie. Meníme AppServiceProvider:

app->bind(PostQueries::class, 
            CachedPostQueries::class);

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

Všetky moje priania sú dosť prirodzene popísané v provideri. Takto sme implementovali cacheovanie pre naše dotazy len napísaním jednej triedy a zmenou konfigurácie kontajnera. Kód zvyšku aplikácie sa nezmenil.

Samozrejme, pre plnohodnotnú implementáciu cacheovania je ešte potrebné implementovať invalidáciu, aby odstránený článok nezostal na stránke nejaký čas, ale okamžite sa vymazal. Ale to sú už maličkosti.

Výsledok: použili sme nie jeden, ale celé dva vzory. Vzor Command Query Responsibility Segregation (CQRS) ponúka úplne oddeliť operácie čítania a zápisu na úrovni rozhraní. K tomuto som dospel cez Interface Segregation Principle, čo hovorí o tom, že zručne manipuluje vzormi a princípmi a odvádza jeden z druhého ako tézu 🙂 Samozrejme, nie každému projektu je potrebná taká abstrakcia na výbery entít, ale podelím sa s vami o trik. V počiatočnej fáze vývoja aplikácie môžete jednoducho vytvoriť triedu PostQueries s bežnou implementáciou cez Eloquent:

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

    // iné metódy
}

Keď vznikne potreba v cacheovaní, ľahkým pohybom môžete vytvoriť rozhranie (alebo abstraktnú triedu) na mieste tejto triedy PostQueries, jej implementáciu skopírovať do triedy EloquentPostQueries a prejsť na schému, ktorú som popísal skôr. Zvyšok kódu aplikácie sa meniť nemusí.

Všetky tieto triky s triedami, rozhraniami, Dependency Injection a CQRS sú podrobne opísané v mojej knihe „Architektúra komplexných webových aplikácií“. Tam je aj rozuzlenie záhady, prečo sú všetky moje triedy v príkladoch k tomuto článku označené ako final.

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster