Repository utili con Eloquent?

La settimana scorsa ho scritto articolo sull'inutilità del modello Repository per le entità Eloquent, però, mi ha promesso di dirmi come sfruttarlo parzialmente a suo vantaggio. Per fare ciò, proverò ad analizzare come questo modello viene solitamente utilizzato nei progetti. Il set minimo di metodi richiesto per un repository:

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

Tuttavia, nei progetti reali, se si decide di utilizzare i repository, spesso vengono aggiunti metodi per il recupero dei record:

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

Questi metodi potrebbero essere implementati tramite ambiti Eloquent, ma sovraccaricare le classi di entità con la responsabilità di recuperare se stesse non è l'idea migliore e spostare questa responsabilità sulle classi repository sembra logico. È così? Ho diviso visivamente specificamente questa interfaccia in due parti. La prima parte dei metodi verrà utilizzata nelle operazioni di scrittura.

L'operazione di scrittura standard è:

  • costruzione di un nuovo oggetto e sfida PostRepository::salva
  • PostRepository::getById, manipolazione ed evocazione di entità PostRepository::salva
  • la sfida PostRepository::eliminazione

Le operazioni di scrittura non utilizzano metodi di recupero. Nelle operazioni di lettura vengono utilizzati solo i metodi get*. Se leggi Principio di separazione dell'interfaccia (lettera I в SOLID), allora diventerà chiaro che la nostra interfaccia è troppo grande e svolge almeno due responsabilità diverse. È ora di dividerlo per due. Metodo getById è necessario in entrambi, ma man mano che l'applicazione diventa più complessa, le sue implementazioni differiranno. Lo vedremo un po' più tardi. Ho scritto dell’inutilità della parte di scrittura in un articolo precedente, quindi in questo semplicemente me ne dimenticherò.

La parte Leggi non mi sembra così inutile, dato che anche per Eloquent potrebbero esserci diverse implementazioni qui. Come chiamare la classe? Potere LeggiPostRepository, ma al modello Repository ha già poca rilevanza. Puoi semplicemente PostQuery:

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

Implementarlo con Eloquent è abbastanza semplice:

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

L'interfaccia deve essere associata all'implementazione, ad esempio in AppService Provider:

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

Questa lezione è già utile. Si rende conto della sua responsabilità sollevando i controllori o la classe di entità. In un controller può essere utilizzato in questo modo:

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

metodo PostController::lastPosts chiedo solo qualche implementazione PostQuery e lavora con esso. Nel provider che abbiamo collegato PostQuery con classe EloquentPostQuery e questa classe verrà sostituita nel controller.

Immaginiamo che la nostra applicazione sia diventata molto popolare. Migliaia di utenti al minuto aprono la pagina con le ultime pubblicazioni. Anche le pubblicazioni più popolari vengono lette molto spesso. I database non gestiscono molto bene tali carichi, quindi utilizzano una soluzione standard: una cache. Oltre al database, una determinata istantanea dei dati viene archiviata in uno spazio di archiviazione ottimizzato per determinate operazioni: memcached o Redis.

La logica di memorizzazione nella cache di solito non è così complicata, ma implementarla in EloquentPostQueries non è molto corretta (se non altro perché Principio unico di responsabilità). È molto più naturale utilizzare un modello Decoratore e implementare la memorizzazione nella cache come decorazione per l'azione principale:

<?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 l'interfaccia Repository nel costruttore. Per qualche ragione sconosciuta, hanno deciso di chiamare l'interfaccia per la memorizzazione nella cache di Laravel in questo modo.

classe Query post memorizzate nella cache implementa solo la memorizzazione nella cache. $questo->cache->ricorda controlla se questa voce è nella cache e, in caso negativo, richiama il callback e scrive il valore restituito nella cache. Tutto ciò che resta è implementare questa classe nell'applicazione. Abbiamo bisogno di tutte le classi presenti nell'applicazione per richiedere un'implementazione dell'interfaccia PostQuery ha iniziato a ricevere un'istanza della classe Query post memorizzate nella cache. Tuttavia, lui stesso Query post memorizzate nella cache il costruttore deve ricevere una classe come parametro EloquentPostQuerypoiché non può funzionare senza un'implementazione "reale". Noi cambiamo AppService Provider:

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

Tutti i miei desideri sono descritti in modo abbastanza naturale nel fornitore. Pertanto, abbiamo implementato la memorizzazione nella cache per le nostre richieste solo scrivendo una classe e modificando la configurazione del contenitore. Il resto del codice dell'applicazione non è cambiato.

Naturalmente, per implementare completamente la memorizzazione nella cache, è necessario implementare anche l'invalidazione in modo che l'articolo eliminato non rimanga bloccato sul sito per un po ', ma venga eliminato immediatamente. Ma queste sono cose minori.

In conclusione: abbiamo utilizzato non uno, ma due modelli. Campione Segregazione delle responsabilità delle query di comando (CQRS) propone di separare completamente le operazioni di lettura e scrittura a livello di interfaccia. Sono venuto da lui Principio di separazione dell'interfaccia, il che suggerisce che manipolo abilmente modelli e principi e che derivo l'uno dall'altro come teorema :) Naturalmente, non tutti i progetti necessitano di una tale astrazione per selezionare le entità, ma condividerò il trucco con voi. sviluppo, puoi semplicemente creare una classe PostQuery con la consueta implementazione tramite Eloquent:

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

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

Quando si presenta la necessità di caching, con una semplice mossa è possibile creare un'interfaccia (o classe astratta) al posto di questa classe PostQuery, copia la sua implementazione nella classe EloquentPostQuery e vai allo schema che ho descritto prima. Non è necessario modificare il resto del codice dell'applicazione.

Tutti questi trucchi con classi, interfacce, Iniezione di dipendenza и CQRS descritto in dettaglio in il mio libro “Architettura di applicazioni Web complesse”. C'è anche una soluzione all'enigma perché tutte le mie lezioni negli esempi di questo articolo sono contrassegnate come finali.

Fonte: habr.com

Aggiungi un commento