Handige repository's met Eloquent?

Vorige week schreef ik artikel over de nutteloosheid van de Repository-sjabloon voor Eloquent-entiteitenHij beloofde me echter te vertellen hoe ik het gedeeltelijk in zijn voordeel kon gebruiken. Om dit te doen, zal ik proberen te analyseren hoe deze sjabloon gewoonlijk in projecten wordt gebruikt. De minimaal vereiste set methoden voor een repository:

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

Als er in echte projecten echter wordt besloten om repository's te gebruiken, worden er vaak methoden voor het ophalen van records aan toegevoegd:

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

Deze methoden zouden kunnen worden geïmplementeerd via Eloquent scopes, maar het overbelasten van entiteitsklassen met de verantwoordelijkheid om zichzelf op te halen is niet het beste idee, en het verplaatsen van deze verantwoordelijkheid naar repositoryklassen lijkt logisch. Is dat zo? Ik heb deze interface specifiek visueel in twee delen verdeeld. Het eerste deel van de methoden zal worden gebruikt bij schrijfbewerkingen.

De standaard schrijfbewerking is:

  • bouw van een nieuw object en uitdaging PostRepository::opslaan
  • PostRepository::getById, manipulatie en oproeping van entiteiten PostRepository::opslaan
  • de uitdaging PostRepository::verwijderen

Bij schrijfbewerkingen worden geen ophaalmethoden gebruikt. Bij leesbewerkingen worden alleen get*-methoden gebruikt. Als je erover leest Principe van interface-segregatie (brief I в SOLID), dan zal het duidelijk worden dat onze interface te groot is en minstens twee verschillende verantwoordelijkheden vervult. Het is tijd om het door twee te delen. Methode getById is in beide noodzakelijk, maar naarmate de toepassing complexer wordt, zullen de implementaties ervan verschillen. We zullen dit iets later zien. Ik schreef over de nutteloosheid van het schrijfgedeelte in een vorig artikel, dus in dit artikel zal ik het gewoon vergeten.

Het Read-gedeelte lijkt mij niet zo nutteloos, aangezien er zelfs voor Eloquent hier verschillende implementaties kunnen zijn. Hoe moet de klas genoemd worden? Kan LeesPostRepository, maar naar de sjabloon bewaarplaats hij heeft al weinig relevantie. Je kunt gewoon Postquery's:

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

Het implementeren ervan met Eloquent is vrij eenvoudig:

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

De interface moet gekoppeld zijn aan de implementatie, bijvoorbeeld in AppServiceProvider:

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

Deze klasse is al nuttig. Hij realiseert zich zijn verantwoordelijkheid door de controleurs of de entiteitenklasse te ontlasten. In een controller kan het als volgt worden gebruikt:

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

werkwijze BerichtenController::laatste berichten vraag gewoon om wat implementatie BerichtenQueries en werkt ermee. In de provider die we hebben gekoppeld Postquery's met klasse Welsprekende PostQueries en deze klasse zal worden vervangen door de controller.

Laten we ons voorstellen dat onze applicatie erg populair is geworden. Duizenden gebruikers per minuut openen de pagina met de nieuwste publicaties. Ook de populairste publicaties worden heel vaak gelezen. Databases kunnen dergelijke belastingen niet zo goed aan, dus gebruiken ze een standaardoplossing: een cache. Naast de database wordt een bepaalde momentopname van gegevens opgeslagen in een opslag die is geoptimaliseerd voor bepaalde bewerkingen - memcached of redis.

Cachinglogica is meestal niet zo ingewikkeld, maar de implementatie ervan in EloquentPostQueries is niet erg correct (al was het maar omdat Principe van een enkele verantwoordelijkheid). Het is veel natuurlijker om een ​​sjabloon te gebruiken Decorateur en implementeer caching als decoratie voor de hoofdactie:

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

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

Negeer de interface bewaarplaats in de constructeur. Om een ​​onbekende reden besloten ze de interface voor caching in Laravel op deze manier te noemen.

Klasse In cache geplaatste postquery's implementeert alleen caching. $dit->cache->onthouden controleert of deze vermelding zich in de cache bevindt en zo niet, roept dan callback aan en schrijft de geretourneerde waarde naar de cache. Het enige dat overblijft is om deze klasse in de applicatie te implementeren. We hebben alle klassen nodig die in de applicatie aanwezig zijn om een ​​implementatie van de interface aan te vragen Postquery's begon een exemplaar van de klasse te ontvangen In cache geplaatste postquery's. Hijzelf echter In cache geplaatste postquery's de constructor moet een klasse als parameter ontvangen Welsprekende PostQueriesomdat het niet kan werken zonder een "echte" implementatie. We veranderen 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);
    }
}

Al mijn wensen worden heel natuurlijk beschreven in de aanbieder. Daarom hebben we caching voor onze verzoeken alleen geïmplementeerd door één klasse te schrijven en de containerconfiguratie te wijzigen. De rest van de applicatiecode is niet veranderd.

Om caching volledig te implementeren, is het natuurlijk ook noodzakelijk om invalidatie te implementeren, zodat het verwijderde artikel niet enige tijd op de site blijft hangen, maar onmiddellijk wordt verwijderd. Maar dit zijn kleine dingen.

Kortom: we gebruikten niet één, maar twee sjablonen. Steekproef Segregatie van opdrachtqueryverantwoordelijkheid (CQRS) stelt voor om lees- en schrijfbewerkingen op interfaceniveau volledig te scheiden. Via via kwam ik bij hem terecht Principe van interface-segregatie, wat suggereert dat ik vakkundig patronen en principes manipuleer en de een uit de ander afleid als een stelling :) Natuurlijk heeft niet elk project zo'n abstractie nodig voor het selecteren van entiteiten, maar ik zal de truc met je delen. ontwikkeling, kunt u eenvoudig een klasse maken Postquery's met de gebruikelijke implementatie via Eloquent:

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

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

Wanneer de behoefte aan caching ontstaat, kunt u met een eenvoudige beweging een interface (of abstracte klasse) maken in plaats van deze klasse Postquery's, kopieer de implementatie ervan naar de klasse Welsprekende PostQueries en ga naar het schema dat ik eerder heb beschreven. De rest van de applicatiecode hoeft niet te worden gewijzigd.

Al deze trucs met klassen, interfaces, Afhankelijkheid injectie и CQRS in detail beschreven in mijn boek “Architectuur van complexe webapplicaties”. Er is ook een oplossing voor het raadsel waarom al mijn lessen in de voorbeelden voor dit artikel als definitief zijn gemarkeerd.

Bron: www.habr.com

Voeg een reactie