Repositoris útils amb Eloquent?

La setmana passada vaig escriure article sobre la inutilitat de la plantilla Repository per a entitats eloqüents, però, va prometre dir-me com utilitzar-lo parcialment al seu avantatge. Per fer-ho, intentaré analitzar com s'utilitza habitualment aquesta plantilla en els projectes. El conjunt mínim de mètodes necessaris per a un dipòsit:

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

Tanmateix, en projectes reals, si es va decidir utilitzar repositoris, sovint s'hi afegeixen mètodes per recuperar registres:

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

Aquests mètodes es podrien implementar a través d'àmbits Eloquent, però sobrecarregar les classes d'entitats amb la responsabilitat de recuperar-se per si mateixes no és la millor idea, i sembla lògic traslladar aquesta responsabilitat a les classes de repositoris. És així? Específicament, vaig dividir visualment aquesta interfície en dues parts. La primera part dels mètodes s'utilitzarà en operacions d'escriptura.

L'operació d'escriptura estàndard és:

  • construcció d'un nou objecte i repte PostRepository::guardar
  • PostRepository::getById, manipulació d'entitats i convocatòria PostRepository::guardar
  • el repte PostRepository::delete

Les operacions d'escriptura no utilitzen mètodes d'obtenció. En les operacions de lectura, només s'utilitzen els mètodes get*. Si llegiu sobre Principi de segregació de la interfície (carta I в SÒLID), llavors quedarà clar que la nostra interfície és massa gran i realitza almenys dues responsabilitats diferents. És hora de dividir-ho per dos. Mètode getById és necessari en tots dos, però a mesura que l'aplicació es faci més complexa, les seves implementacions seran diferents. Això ho veurem una mica més endavant. Vaig escriure sobre la inutilitat de la part d'escriptura en un article anterior, així que en aquest simplement m'oblidaré d'això.

La part Read no em sembla tan inútil, ja que fins i tot per a Eloquent hi pot haver diverses implementacions aquí. Com anomenar la classe? Llauna ReadPostRepository, sinó a la plantilla Dipòsit ja té poca rellevància. Només pots PostConsultes:

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

Implementar-lo amb Eloquent és bastant senzill:

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

La interfície ha d'estar associada a la implementació, per exemple a AppServiceProvider:

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

Aquesta classe ja és útil. S'adona de la seva responsabilitat rellevant els controladors o la classe d'entitat. En un controlador es pot utilitzar així:

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

Mètode PostsController::lastPosts només demanant una mica d'aplicació PostsQueries i treballa amb ell. Al proveïdor que hem enllaçat PostConsultes amb classe EloquentPostQueries i aquesta classe es substituirà al controlador.

Imaginem que la nostra aplicació s'ha fet molt popular. Milers d'usuaris per minut obren la pàgina amb les últimes publicacions. També es llegeixen molt sovint les publicacions més populars. Les bases de dades no gestionen molt bé aquestes càrregues, de manera que utilitzen una solució estàndard: una memòria cau. A més de la base de dades, una determinada instantània de dades s'emmagatzema en un emmagatzematge optimitzat per a determinades operacions: memcached o Redis.

La lògica d'emmagatzematge en memòria cau no sol ser tan complicada, però implementar-la a EloquentPostQueries no és gaire correcta (si només sigui perquè Principi de responsabilitat única). És molt més natural utilitzar una plantilla Decorador i implementeu la memòria cau com a decoració de l'acció principal:

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

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

Ignora la interfície Dipòsit en el constructor. Per algun motiu desconegut, van decidir anomenar la interfície per a la memòria cau a Laravel d'aquesta manera.

Classe CachedPostQueries només implementa la memòria cau. $això->cache->recorda comprova si aquesta entrada es troba a la memòria cau i, si no, crida a una devolució de trucada i escriu el valor retornat a la memòria cau. Només queda implementar aquesta classe a l'aplicació. Necessitem totes les classes que a l'aplicació per sol·licitar una implementació de la interfície PostConsultes va començar a rebre una instància de la classe CachedPostQueries. Tanmateix, ell mateix CachedPostQueries el constructor ha de rebre una classe com a paràmetre EloquentPostQueriesja que no pot funcionar sense una implementació "real". Canviem 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);
    }
}

Tots els meus desitjos es descriuen de manera molt natural al proveïdor. Per tant, vam implementar la memòria cau per a les nostres sol·licituds només escrivint una classe i canviant la configuració del contenidor. La resta del codi de l'aplicació no ha canviat.

Per descomptat, per implementar completament la memòria cau, també cal implementar la invalidació perquè l'article suprimit no es pengi al lloc durant un temps, sinó que s'elimini immediatament. Però aquestes són coses menors.

Conclusió: no hem utilitzat una, sinó dues plantilles. Mostra Segregació de responsabilitat de consulta d'ordres (CQRS) proposa separar completament les operacions de lectura i escriptura a nivell d'interfície. Vaig arribar a ell a través Principi de segregació de la interfície, que suggereix que manipulo amb habilitat patrons i principis i que en derivo els uns dels altres com a teorema :) Per descomptat, no tots els projectes necessiten aquesta abstracció per seleccionar entitats, però compartiré el truc amb vosaltres. En l'etapa inicial d'aplicació desenvolupament, simplement podeu crear una classe PostConsultes amb la implementació habitual via Eloquent:

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

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

Quan sorgeixi la necessitat d'emmagatzemar a la memòria cau, amb un simple moviment podeu crear una interfície (o classe abstracta) en lloc d'aquesta classe PostConsultes, copieu la seva implementació a la classe EloquentPostQueries i aneu a l'esquema que he descrit anteriorment. La resta del codi de l'aplicació no s'ha de canviar.

Tots aquests trucs amb classes, interfícies, Injecció de dependència и CQRS descrit detalladament a el meu llibre "Arquitectura d'aplicacions web complexes". També hi ha una solució a l'enigma per què totes les meves classes als exemples d'aquest article estan marcades com a finals.

Font: www.habr.com

Afegeix comentari