Nyttiga repositorier med Eloquent?

Förra veckan skrev jag en artikel om onödigheten av mönstret Repository för Eloquent-entiteter, men jag lovade att berÀtta hur man delvis kan anvÀnda det med fördel. För detta kommer jag att försöka analysera hur detta mönster vanligtvis anvÀnds i projekt. MinimiuppsÀttningen av metoder för ett repository:

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

Men i verkliga projekt, om det verkligen beslutas att anvÀnda repository, lÀggs det ofta till metoder för att vÀlja 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);
}

Dessa metoder skulle kunna implementeras genom Eloquent-scopes, men att överbelasta entitetsklasser med uppgifter för att hĂ€mta sig sjĂ€lva Ă€r ingen bra idĂ© och att flytta detta ansvar till repository-klasser verkar logiskt. Är det sĂ„? Jag har avsiktligt visuellt delat denna grĂ€nssnitt i tvĂ„ delar. Den första delen av metoder kommer att anvĂ€ndas i skrivoperationer.

Standard skrivoperationer Àr:

  • konstruera ett nytt objekt och anropa PostRepository::save
  • PostRepository::getById, manipulera med entiteten och anropa PostRepository::save
  • anropa PostRepository::delete

I skrivoperationer anvÀnds inga urvalsmetoder. I lÀsoperationer anvÀnds dÀremot endast metoder med get*. Om man lÀser om Interface Segregation Principle (bokstav I pÄ SOLID), sÄ blir det tydligt att vÄrt grÀnssnitt har blivit för stort och utför Ätminstone tvÄ olika uppgifter. Det Àr dags att dela upp det i tvÄ. Metoden getById Àr nödvÀndig i bÄda, men nÀr applikationen blir mer komplex kommer deras implementeringar att bli olika. Det kommer vi att se snart. Om onödigheten av skrivdelen skrev jag i den förra artikeln, sÄ i denna kommer jag helt enkelt att glömma den.

LÀsdelen verkar mig inte sÄ onödig, eftersom Àven för Eloquent kan det finnas flera implementeringar hÀr. Vad ska klassen heta? Man kan ReadPostRepository, men i förhÄllande till mönstret Repository har den redan lite att göra. Man kan helt enkelt PostQueries:

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

Dess implementering med hjÀlp av Eloquent Àr ganska 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();
    }
}

GrÀnssnittet bör kopplas till implementeringen, till exempel i AppServiceProvider:

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

Den hÀr klassen Àr redan anvÀndbar. Den utför sitt ansvar och avlastar antingen kontroller eller entitetsklassen. I kontrollern kan den anvÀndas sÄ hÀr:

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

Metod PostsController::lastPosts ber bara om en implementering PostsQueries och arbetar med den. I leverantören kopplade vi PostQueries till klassen EloquentPostQueries och i kontrollern kommer denna klass att anvÀndas.

LĂ„t oss förestĂ€lla oss att vĂ„r applikation har blivit mycket populĂ€r. Tusentals anvĂ€ndare öppnar sidan med de senaste inlĂ€ggen per minut. De mest populĂ€ra inlĂ€ggen lĂ€ses ocksĂ„ mycket ofta. Databaser klarar inte alltid av sĂ„dan belastning, sĂ„ en standardlösning anvĂ€nds — cache. Utöver databasen lagras en kopia av datan i ett magasin som Ă€r optimerat för vissa operationer — memcached eller redis.

Cachens logik Àr vanligtvis inte sÄ komplicerad, men det Àr inte helt korrekt att implementera den i EloquentPostQueries (Ätminstone pÄ grund av Single Responsibility Principle). Det kÀnns mycket mer naturligt att anvÀnda mönstret Decorator och implementera caching som dekorering av huvudÄtgÀrden:

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

    // andra metoder Àr praktiskt taget desamma
}

Ignorera grÀnssnittet Repository i konstruktören. Av nÄgon oförklarlig anledning beslutades det att kalla grÀnssnittet för caching i Laravel.

Klassen CachedPostQueries implementerar endast caching. $this->cache->remember kontrollerar om det finns en post i cacheminnet och om inte, sĂ„ anropas callback och vĂ€rdet som returneras skrivs till cachen. Det kvarstĂ„r bara att integrera denna klass i applikationen. Vi mĂ„ste se till att alla klasser som i applikationen begĂ€r en implementation av grĂ€nssnittet PostQueries börjar ta emot ett instans av klassen CachedPostQueries. Men sjĂ€lva CachedPostQueries som parameter i konstruktorn mĂ„ste fĂ„ klassen EloquentPostQueries, eftersom den inte kan fungera utan en "riktig" implementation. Ändra AppServiceProvider:

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

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

Alla mina önskemÄl beskrivs ganska naturligt i providern. DÀrmed har vi implementerat caching för vÄra förfrÄgningar genom att bara skriva en klass och Àndra konfigurationen av containern. Koden för resten av applikationen har inte Àndrats.

SjÀlvklart, för en fullstÀndig implementation av caching Àr det ocksÄ nödvÀndigt att implementera invalidation, sÄ att den borttagna artikeln inte hÀnger kvar pÄ webbplatsen en stund utan tas bort direkt. Men det Àr redan smÄsaker.

Sammanfattning: vi anvĂ€nde inte en utan hela tvĂ„ mönster. Mönstret Command Query Responsibility Segregation (CQRS) erbjuder att helt separera lĂ€s- och skrivoperationer pĂ„ grĂ€nssnittens nivĂ„. Jag kom till det via Interface Segregation Principle, vilket indikerar att jag kunnigt manipulerar mönster och principer och hĂ€rleder ett ur ett annat som en teorem 🙂 SjĂ€lvklart Ă€r inte varje projekt i behov av en sĂ„dan abstraktion för entitetsförfrĂ„gningar, men jag kommer att dela med mig av ett trick. I det inledande skedet av applikationsutvecklingen kan man helt enkelt skapa en klass PostQueries med en vanlig implementation genom Eloquent:

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

    // andra metoder
}

NÀr behovet av caching uppstÄr kan man enkelt skapa ett grÀnssnitt (eller abstrakt klass) istÀllet för denna klass PostQueries, kopiera dess implementation till klassen EloquentPostQueries och övergÄ till schemat som jag beskrev tidigare. Den övriga koden i applikationen behöver inte Àndras.

Alla dessa trick med klasser, grÀnssnitt, Beroendeinjektion och CQRS beskrivs i detalj i min bok "Arkitektur för komplexa webbapplikationer". DÀr finns gÄtan till varför alla mina klasser i exemplen till den hÀr artikeln Àr mÀrkta som final.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster