Repository utili con Eloquent?

La scorsa settimana, ho scritto un articolo sull'inutilità del template Repository per entità Eloquent, tuttavia ho promesso di spiegare come utilizzarlo parzialmente in modo utile. Per questo cercherò di analizzare come di solito si utilizza questo template nei progetti. Il set minimo necessario di metodi per il 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, di solito vengono aggiunti metodi per le selezioni 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 scopes di Eloquent, ma sovraccaricare le classi delle entità con compiti di selezione non è mai una buona idea, e trasferire questa responsabilità nelle classi repository sembra logico. È così? Ho separato visivamente questo interfaccia in due parti. La prima parte dei metodi sarà utilizzata nelle operazioni di scrittura.

Le operazioni di scrittura standard sono:

  • la costruzione di un nuovo oggetto e la chiamata PostRepository::save
  • PostRepository::getById, manipolazioni con l'entità e la chiamata PostRepository::save
  • la chiamata PostRepository::delete

Nelle operazioni di scrittura non si utilizzano metodi di selezione. Nelle operazioni di lettura, invece, si utilizzano solo i metodi get*. Se leggi riguardo al Principio di Separazione delle Interfacce (la lettera I in SOLID), diventa chiaro che la nostra interfaccia è risultata troppo grande e svolge almeno due diverse responsabilità. È tempo di dividerla in due. Il metodo getById è necessario in entrambe, ma con il complicarsi dell'applicazione, le sue implementazioni saranno diverse. Questo lo vedremo più avanti. Ho scritto della parte di scrittura inutile nel mio articolo precedente, quindi in questo mi dimenticherò semplicemente di essa.

La parte di lettura, tuttavia, mi sembra non così inutile, poiché anche per Eloquent qui potrebbero esserci diverse implementazioni. Come chiamare la classe? Potremmo chiamarla ReadPostRepository, ma rispetto al template Repository ha già poco a che fare. Potremmo semplicemente chiamarla PostQueries:

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

La sua implementazione tramite Eloquent è piuttosto semplice:

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 collegata a un'implementazione, per esempio in AppServiceProvider:

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

Questa classe è già utile. Essa sviluppa la propria responsabilità, alleggerendo così i controller o la classe entità. Nel controller può essere utilizzata in questo modo:

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

Sanitizer.replaceElementWithChildren() PostsController::lastPosts richiede semplicemente qualche implementazione PostsQueries e lavora con essa. Nel provider abbiamo collegato PostQueries alla classe EloquentPostQueries e nel controller verrà inserita questa classe.

Immaginiamo che la nostra applicazione sia diventata molto popolare. Migliaia di utenti al minuto aprono la pagina con gli ultimi post. Anche i post più popolari vengono letti molto frequentemente. I database non gestiscono bene questi carichi, quindi viene utilizzata una soluzione standard: la cache. Oltre al database, un certo campione di dati viene memorizzato in uno storage ottimizzato per determinate operazioni - memcached o redis.

La logica di caching non è di solito così complessa, ma implementarla in EloquentPostQueries non è propriamente corretto (almeno per il Single Responsibility Principle). È molto più naturale utilizzare il pattern Decoratore e implementare la cache come decorazione dell'azione 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();
            });
    }

    // altri metodi sono praticamente gli stessi
}

Non prestare attenzione all'interfaccia Repository nel costruttore. Per qualche ragione inspiegabile, hanno deciso di chiamare l'interfaccia per la cache in Laravel.

Classe CachedPostQueries implementa solo la memorizzazione nella cache. $this->cache->remember controlla se c'è già un record nella cache e, se non c'è, chiama il callback e memorizza nella cache il valore restituito. Resta solo da integrare questa classe nell'applicazione. È necessario che tutte le classi che nell'applicazione richiedono l'implementazione dell'interfaccia PostQueries comincino a ricevere un'istanza della classe CachedPostQueries. Tuttavia, lo stesso CachedPostQueries come parametro nel costruttore deve ricevere la classe EloquentPostQueries, poiché non può funzionare senza una vera implementazione. Cambiamo AppServiceProvider:

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

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

Tutti i miei desideri sono descritti piuttosto naturalmente nel provider. In questo modo, abbiamo implementato la memorizzazione nella cache per le nostre query semplicemente scrivendo una classe e cambiando la configurazione del contenitore. Il codice del resto dell'applicazione non è cambiato.

Naturalmente, per una completa implementazione della memorizzazione nella cache, è necessario implementare anche l'invalidazione, in modo che un articolo rimosso non rimanga sul sito per un po' di tempo, ma venga eliminato immediatamente. Ma questo è un dettaglio.

Risultato: abbiamo utilizzato non uno, ma ben due modelli. Il modello Command Query Responsibility Segregation (CQRS) suggerisce di separare completamente le operazioni di lettura e scrittura a livello di interfacce. Ci sono arrivato tramite Principio di Separazione delle Interfacce, il che dimostra che manovro abilmente modelli e principi e deduco uno dall'altro come un teorema 🙂 Naturalmente, non ogni progetto ha bisogno di tale astrazione per le entità di selezione, ma condividerò con voi un trucco. Nella fase iniziale dello sviluppo dell'applicazione, si può semplicemente creare una classe PostQueries con una normale implementazione tramite Eloquent:

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

    // altri metodi
}

Quando sorgerà la necessità di memorizzare nella cache, è facile creare un'interfaccia (o una classe astratta) al posto di questa classe PostQueries, copiare la sua implementazione nella classe EloquentPostQueries e passare allo schema che ho descritto in precedenza. Non è necessario cambiare il resto del codice dell'applicazione.

Tutti questi trucchi con classi, interfacce, Dependency Injection e CQRS sono descritti in dettaglio nel mio libro "Architettura di applicazioni web complesse". Qui si trova la spiegazione del mistero del perché tutte le mie classi negli esempi di questo articolo sono contrassegnate come final.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster