Nyttige lagre med Eloquent?

I sidste uge skrev jeg en artikel om nytten af Repository-mønsteret for Eloquent-entity'er, men jeg lovede at fortælle, hvordan man delvist kan bruge det med fordel. Til dette vil jeg forsøge at analysere, hvordan dette mønster sædvanligvis bruges i projekter. Det minimale sæt metoder til et repository er:

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

Men i virkelige projekter, hvis det er besluttet at bruge repositories, tilføjes der ofte metoder til at vælge poster:

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

Disse metoder kunne implementeres via Eloquent scopes, men det er ikke den bedste idé at overbelaste enheds-klasser med ansvar for at vælge sig selv — så det virker logisk at flytte dette ansvar til repository-klasserne. Er det korrekt? Jeg har bevidst opdelt dette interface visuelt i to dele. Den første del af metoderne vil blive brugt i skriveoperationer.

Standard skriveoperationer er:

  • konstruere et nyt objekt og kalde PostRepository::save
  • PostRepository::getById, manipulere med entiteten og kalde PostRepository::save
  • kalde PostRepository::delete

I skriveoperationer bruges der ikke metoder til valg. I læseoperationer bruges kun metoderne get*. Hvis man læser om Interface Segregation Principle (forkortelse I i SOLID), bliver det klart, at vores interface er blevet for stort og udfører mindst to forskellige ansvarsområder. Det er tid til at dele det op i to. Metoden getById er nødvendig i begge, men ved kompleksiteten i applikationen vil deres implementeringer være forskellige. Det vil vi se lidt senere. Om nytten ved skrive-delen skrev jeg i den tidligere artikel, så i denne glemmer jeg den blot.

Læsedelen synes ikke at være så værdiløs, da der selv for Eloquent kan være flere implementeringer. Hvordan skal klassen hedde? Man kan ReadPostRepository, men den har allerede lidt at gøre med mønsteret Repository . Man kan simpelt hen kalde den PostQueries:

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

Implementeringen ved hjælp af Eloquent er ret enkel:

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

Interface skal være knyttet til implementeringen, for eksempel i AppServiceProvider:

app->bind(PostQueries::class, 
            EloquentPostQueries::class);
    }
}

Denne klasse er allerede nyttig. Den implementerer sit ansvar, hvilket aflaster enten controllere eller entitetsklassen. I controllere kan den bruges sådan her:

$postQueries->getLastPosts(),
        ]);
    }
} 

Metoden PostsController::lastPosts beder bare om en hvilken som helst implementering PostsQueries og arbejder med den. I provideren har vi knyttet PostQueries til klassen EloquentPostQueries og denne klasse vil blive indsat i controllere.

Lad os forestille os, at vores applikation er blevet meget populær. Tusindvis af brugere åbner i minuttet siden med de seneste indlæg. De mest populære indlæg læses også meget ofte. Databaser håndterer normalt ikke sådanne belastninger særlig godt, derfor anvendes den standard løsningsmetode — cache. Udover databasen opbevares en slags snapshot af dataene i et lager, der er optimeret til bestemte operationer — memcached eller redis.

Logikken bag caching er normalt ikke så kompleks, men det er ikke korrekt at implementere det i EloquentPostQueries (blandt andet på grund af Single Responsibility Principle). Det er meget mere naturligt at bruge mønsteret Decorator og implementere caching som dekoration af den primære handling:

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

    // andre metoder er praktisk talt de samme
}

Ignorer ikke grænsefladen Repository i konstruktøren. Af en eller anden grund besluttede man at kalde grænsefladen for caching i Laravel.

Klasse CachedPostQueries implementerer kun caching. $this->cache->remember tjekker, om denne post findes i cachen, og hvis ikke, kalder den callback og gemmer den returnerede værdi i cachen. Nu skal vi bare integrere denne klasse i applikationen. Vi har brug for, at alle klasser, der beder om implementering af grænsefladen PostQueries begynder at modtage en instans af klassen CachedPostQueries. Men selve CachedPostQueries som parameter i konstruktøren skal få klassen EloquentPostQueries, da den ikke kan fungere uden en 'ægte' implementering. Vi ændrer AppServiceProvider:

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

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

Alle mine ønsker beskrives ganske naturligt i udbyderen. På denne måde har vi implementeret caching for vores forespørgsler ved kun at skrive én klasse og ændre konfigurationen af containeren. Koden i resten af applikationen er ikke ændret.

Selvfølgelig er det nødvendigt for en fuldgyldig implementering af caching også at implementere invalidation, så den slettede artikel ikke hænger på websitet i et stykke tid, men fjernes straks. Men det er bare småting.

Konklusion: vi brugte ikke én, men hele to mønstre. Mønsteret Command Query Responsibility Segregation (CQRS) foreslår helt at adskille læse- og skriveoperationer på interface-niveau. Jeg kom til det gennem Interface Segregation Principle, hvilket viser, at jeg dygtigt manipulerer med mønstre og principper og udleder den ene fra den anden som en sætning 🙂 Selvfølgelig er en sådan abstraktion på forespørgsler ikke nødvendigvis nødvendig for hvert projekt, men jeg vil dele en trick med dig. I starten af udviklingen af applikationen kan man blot oprette en klasse PostQueries med en almindelig implementation via Eloquent:

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

    // andre metoder
}

Når behovet for caching opstår, kan man nemt oprette et interface (eller abstrakt klasse) i stedet for denne klasse PostQueries, kopiere dens implementering ind i klassen EloquentPostQueries og gå videre til den plan, jeg beskrev tidligere. Den øvrige kode i applikationen skal ikke ændres.

Alle disse tricks med klasser, interfaces, Dependency Injection og CQRS er beskrevet i detaljer i min bog 'Arkitektur af komplekse webapplikationer'. Her er løsningen på mysteriet om, hvorfor alle mine klasser i eksemplerne til denne artikel er markeret som final.

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster