Repository-uri utile cu Eloquent?

Săptămâna trecută, am scris un articol despre inutilitatea șablonului Repository pentru entitățile Eloquent, dar am promis că voi explica cum poate fi folosit parțial cu utilitate. În acest scop, voi încerca să analizez cum este utilizat în mod obișnuit acest șablon în proiecte. Setul minim necesar de metode pentru un repository:

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

Cu toate acestea, în proiectele reale, dacă s-a decis să se folosească repository-uri, adesea sunt adăugate metode pentru selectarea înregistrărilor:

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

Aceste metode ar putea fi implementate prin intermediul Eloquent scopes, dar supraîncărcarea claselor entităților cu responsabilitățile de selectare a acestora nu este cea mai bună idee, iar transferul acestei responsabilități în clasele repository-urilor pare logic. Oare este așa? Am divizat vizual acest interface în două părți. Prima parte a metodelor va fi utilizată în operațiunile de scriere.

Operațiunile standard de scriere sunt:

  • construirea unui nou obiect și apelul PostRepository::save
  • PostRepository::getById, manipulări cu entitatea și apelul PostRepository::save
  • apelul PostRepository::delete

În operațiunile de scriere nu există utilizarea metodelor de selecție. În operațiunile de citire, sunt folosite doar metodele get*. Dacă citim despre Principiul separării interfețelor (litera I în SOLID), se va înțelege că interfața noastră a ieșit prea mare și îndeplinește cel puțin două responsabilități diferite. Este timpul să o împărțim în două. Metoda getById este necesară în ambele, însă la complicarea aplicației, implementările sale vor fi diferite. Acest lucru îl vom observa puțin mai târziu. Despre inutilitatea părții de scriere am scris în articolul trecut, așa că în acesta o voi uita pur și simplu.

Partea de citire mi se pare totuși nu atât de inutilă, deoarece chiar și pentru Eloquent aici ar putea exista mai multe implementări. Cum să numim clasa? Poate ReadPostRepository, dar la șablonul Repository are deja o relație mică. Poate pur și simplu PostQueries:

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

Implementarea sa cu Eloquent este destul de simplă:

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

Interfața trebuie să fie legată de implementare, de exemplu în AppServiceProvider:

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

Această clasă este deja utilă. Ea își îndeplinește responsabilitatea, descărcând astfel fie controlerele, fie clasa entitate. În controler poate fi utilizată astfel:

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

Metoda PostsController::lastPosts doar cere o implementare oarecare pentru sine PostsQueries și lucrează cu ea. În provider am legat-o PostQueries de clasa EloquentPostQueries și în controler va fi introdusă această clasă.

Să ne imaginăm că aplicația noastră a devenit extrem de populară. Mii de utilizatori pe minut deschid pagina cu ultimele publicații. Cele mai populare publicații sunt, de asemenea, citite foarte frecvent. Bazele de date nu fac față bine unor astfel de încărcări, de aceea se folosește soluția standard — cache. Pe lângă baza de date, o anumită copie de date este stocată într-un depozit optimizat pentru anumiți parametri de operare — memcached sau redis.

Logica de cache este de obicei nu atât de complexă, dar implementarea ei în EloquentPostQueries nu este foarte corectă (cel puțin din cauza Single Responsibility Principle). Este mult mai natural să folosim modelul Decorator și să implementăm cache-ul ca o decorare a acțiunii principale:

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

    // celelalte metode sunt practic aceleași
}

Nu acordați atenție interfeței Repository din constructor. Dintr-un motiv incomprehensibil, așa au decis să numească interfața pentru cache în Laravel.

Clasă CachedPostQueries implementează doar caching. $this->cache->remember verifică dacă acest înregistrare există în cache, iar dacă nu, apelează callback-ul și îl scrie în cache returnând valoarea obținută. Rămâne doar să integrăm această clasă în aplicație. Trebuie să ne asigurăm că toate clasele care cer implementarea interfeței PostQueries vor primi o instanță a clasei CachedPostQueries. Totuși, acesta CachedPostQueries ca parametru în constructor trebuie să primească clasa EloquentPostQueries, deoarece nu poate funcționa fără o implementare 'neapărată'. Schimbăm AppServiceProvider:

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

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

Toate dorințele mele sunt descrise destul de natural în provider. Astfel, am implementat caching pentru interogările noastre doar scriind o singură clasă și schimbând configurația containerului. Codul restului aplicației nu a fost afectat.

Desigur, pentru implementarea completă a caching-ului este necesar să implementăm și invalidarea, pentru ca articolul șters să nu rămână pe site o vreme și să fie șters imediat. Dar acestea sunt deja detalii.

În concluzie: am folosit nu unul, ci două modele. Modelul Command Query Responsibility Segregation (CQRS) propune separarea completă a operațiunilor de citire și scriere la nivel de interfețe. Am ajuns la el prin Principiul separării interfețelor, ceea ce dovedește că manipulez cu abilitate modelele și principiile și deduc unul din altul ca pe o teoremă 🙂 Desigur, nu fiecare proiect necesită o astfel de abstractizare pentru selecția entităților, dar voi împărtăși cu voi un truc. În etapa inițială a dezvoltării aplicației, se poate crea pur și simplu o clasă PostQueries cu o implementare obișnuită prin Eloquent:

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

    // alte metode
}

Când va apărea necesitatea în caching, cu o mișcare ușoară se poate crea o interfață (sau o clasă abstractă) în locul acestei clase PostQueries, implementarea sa poate fi copiată în clasa EloquentPostQueries și se poate trece la schema pe care am descris-o mai devreme. Restul codului aplicației nu trebuie schimbat.

Toate aceste trucuri cu clasele, interfețele, Dependency Injection și CQRS sunt detaliat descrise în cartea mea „Arhitectura aplicațiilor web complexe”. Acolo se află soluția enigma de ce toate clasele mele din exemplele acestui articol sunt marcate ca finale.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster