Progettazione corretta del modello di repository in PHP?


291

Prefazione: sto tentando di utilizzare il modello di repository in un'architettura MVC con database relazionali.

Di recente ho iniziato a studiare TDD in PHP e sto realizzando che il mio database è accoppiato troppo strettamente al resto della mia applicazione. Ho letto sui repository e sull'utilizzo di un contenitore IoC per "iniettarlo" nei miei controller. Roba molto bella. Ma ora ho alcune domande pratiche sulla progettazione dei repository. Considera il seguente esempio.

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

Problema n. 1: troppi campi

Tutti questi metodi di ricerca utilizzano un SELECT *approccio seleziona tutti i campi ( ). Tuttavia, nelle mie app, cerco sempre di limitare il numero di campi che ottengo, poiché questo spesso aggiunge sovraccarico e rallenta le cose. Per coloro che usano questo schema, come lo gestisci?

Problema n. 2: troppi metodi

Mentre questa classe sembra carina in questo momento, so che in un'app del mondo reale ho bisogno di molti più metodi. Per esempio:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • Eccetera.

Come puoi vedere, potrebbe esserci un elenco molto lungo di possibili metodi. E quindi se aggiungi il problema di selezione dei campi sopra, il problema peggiora. In passato normalmente inserivo tutta questa logica nel mio controller:

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')
            ->byCountry('Canada')->orderBy('name')->rows();

        return View::make('users', array('users' => $users));
    }
}

Con il mio approccio al repository, non voglio finire con questo:

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

Problema n. 3: impossibile abbinare un'interfaccia

Vedo i vantaggi dell'utilizzo delle interfacce per i repository, quindi posso scambiare la mia implementazione (a scopo di test o altro). La mia comprensione delle interfacce è che definiscono un contratto che un'implementazione deve seguire. Questo è fantastico fino a quando non inizi ad aggiungere metodi aggiuntivi ai tuoi repository come findAllInCountry(). Ora ho bisogno di aggiornare la mia interfaccia per avere anche questo metodo, altrimenti, altre implementazioni potrebbero non averlo e questo potrebbe interrompere la mia applicazione. Da questo sembra folle ... un caso della coda che scuote il cane.

Modello di specifica?

Questo mi porta a credere che la repository dovrebbe avere solo un numero fisso di metodi (come save(), remove(), find(), findAll(), ecc). Ma allora come eseguo ricerche specifiche? Ho sentito parlare del modello di specifica , ma mi sembra che questo riduca solo un intero set di record (via IsSatisfiedBy()), che ha chiaramente grossi problemi di prestazioni se si estrae da un database.

Aiuto?

Chiaramente, ho bisogno di ripensare un po 'le cose quando lavoro con i repository. Qualcuno può illuminare su come questo è meglio gestito?

Risposte:


208

Ho pensato di fare una pausa nel rispondere alla mia domanda. Quello che segue è solo un modo per risolvere i problemi 1-3 nella mia domanda originale.

Disclaimer: potrei non usare sempre i termini giusti per descrivere schemi o tecniche. Scusa per quello.

Gli obiettivi:

  • Crea un esempio completo di controller di base per la visualizzazione e la modifica Users .
  • Tutto il codice deve essere completamente testabile e beffardo.
  • Il controller non dovrebbe avere idea di dove siano archiviati i dati (nel senso che possono essere modificati).
  • Esempio per mostrare un'implementazione SQL (più comune).
  • Per ottenere le massime prestazioni, i controller dovrebbero ricevere solo i dati di cui hanno bisogno, senza campi aggiuntivi.
  • L'implementazione dovrebbe sfruttare un qualche tipo di mappatore di dati per facilitare lo sviluppo.
  • L'implementazione dovrebbe avere la capacità di eseguire ricerche di dati complessi.

La soluzione

Sto dividendo la mia interazione persistente di archiviazione (database) in due categorie: R (Leggi) e CUD (Crea, Aggiorna, Elimina). La mia esperienza è stata che le letture sono realmente ciò che provoca il rallentamento di un'applicazione. E mentre la manipolazione dei dati (CUD) è in realtà più lenta, accade molto meno frequentemente ed è quindi molto meno preoccupante.

CUD (Crea, Aggiorna, Elimina) è facile. Ciò comporterà il lavoro con modelli reali , che vengono quindi passati al mioRepositories per persistenza. Nota, i miei repository forniranno comunque un metodo di lettura, ma semplicemente per la creazione di oggetti, non per la visualizzazione. Ne parleremo più avanti.

R (Leggi) non è così facile. Nessun modello qui, solo oggetti di valore . Usa le matrici se preferisci . Questi oggetti possono rappresentare un singolo modello o una miscela di molti modelli, qualsiasi cosa in realtà. Questi non sono molto interessanti da soli, ma come sono generati. Sto usando quello che sto chiamando Query Objects.

Il codice:

Modello utente

Cominciamo semplice con il nostro modello utente di base. Si noti che non esiste alcuna estensione ORM o elementi del database. Solo pura gloria di modello. Aggiungi i tuoi getter, setter, validazione, qualunque cosa.

class User
{
    public $id;
    public $first_name;
    public $last_name;
    public $gender;
    public $email;
    public $password;
}

Interfaccia del repository

Prima di creare il repository utente, desidero creare l'interfaccia del repository. Ciò definirà il "contratto" che i repository devono seguire per poter essere utilizzato dal mio controller. Ricorda, il mio controller non saprà dove sono effettivamente memorizzati i dati.

Nota che i miei repository conterranno solo questi tre metodi. Il save()metodo è responsabile sia della creazione che dell'aggiornamento degli utenti, semplicemente a seconda che l'oggetto utente abbia o meno un ID impostato.

interface UserRepositoryInterface
{
    public function find($id);
    public function save(User $user);
    public function remove(User $user);
}

Implementazione del repository SQL

Ora per creare la mia implementazione dell'interfaccia. Come accennato, il mio esempio sarebbe stato con un database SQL. Si noti l'uso di un mappatore di dati per evitare di dover scrivere query SQL ripetitive.

class SQLUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function find($id)
    {
        // Find a record with the id = $id
        // from the 'users' table
        // and return it as a User object
        return $this->db->find($id, 'users', 'User');
    }

    public function save(User $user)
    {
        // Insert or update the $user
        // in the 'users' table
        $this->db->save($user, 'users');
    }

    public function remove(User $user)
    {
        // Remove the $user
        // from the 'users' table
        $this->db->remove($user, 'users');
    }
}

Interfaccia oggetto query

Ora con CUD (Crea, Aggiorna, Elimina) curato dal nostro repository, possiamo concentrarci sulla R (Leggi). Gli oggetti query sono semplicemente un incapsulamento di un tipo di logica di ricerca dei dati. Sono Non generatori di query. Astrattandolo come il nostro repository, possiamo cambiarne l'implementazione e testarlo più facilmente. Un esempio di un oggetto query potrebbe essere un AllUsersQueryo AllActiveUsersQuery, o addirittura MostCommonUserFirstNames.

Potresti pensare "non posso semplicemente creare metodi nei miei repository per quelle query?" Sì, ma ecco perché non lo sto facendo:

  • I miei repository sono pensati per lavorare con oggetti modello. In un'app del mondo reale, perché dovrei mai avere il passwordcampo se sto cercando di elencare tutti i miei utenti?
  • I repository sono spesso specifici del modello, ma le query spesso coinvolgono più di un modello. Quindi in quale repository metti il ​​tuo metodo?
  • Ciò rende i miei repository molto semplici, non una classe di metodi gonfiati.
  • Tutte le query sono ora organizzate in proprie classi.
  • Davvero, a questo punto, esistono dei repository semplicemente per astrarre il mio livello di database.

Per il mio esempio creerò un oggetto query per cercare "AllUsers". Ecco l'interfaccia:

interface AllUsersQueryInterface
{
    public function fetch($fields);
}

Implementazione dell'oggetto query

Qui è possibile utilizzare nuovamente un mapper di dati per accelerare lo sviluppo. Si noti che sto consentendo una modifica al set di dati restituito: i campi. Questo è quanto voglio manipolare la query eseguita. Ricorda, i miei oggetti query non sono generatori di query. Eseguono semplicemente una query specifica. Tuttavia, poiché so che probabilmente lo userò molto, in diverse situazioni, mi sto dando la possibilità di specificare i campi. Non voglio mai restituire campi che non mi servono!

class AllUsersQuery implements AllUsersQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch($fields)
    {
        return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
    }
}

Prima di passare al controller, voglio mostrare un altro esempio per illustrare quanto sia potente. Forse ho un motore di segnalazione e devo creare un rapporto per AllOverdueAccounts. Questo potrebbe essere complicato con il mio mappatore di dati e potrei voler scrivere un po 'reale SQLin questa situazione. Nessun problema, ecco come potrebbe apparire questo oggetto query:

class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch()
    {
        return $this->db->query($this->sql())->rows();
    }

    public function sql()
    {
        return "SELECT...";
    }
}

Questo mantiene bene tutta la mia logica per questo rapporto in una classe ed è facile da testare. Posso deriderlo sul contenuto del mio cuore, o persino utilizzare una diversa implementazione del tutto.

Il controller

Ora la parte divertente: riunire tutti i pezzi. Si noti che sto usando l'iniezione di dipendenza. In genere le dipendenze vengono iniettate nel costruttore, ma in realtà preferisco iniettarle direttamente nei metodi del mio controller (route). Ciò riduce al minimo il grafico degli oggetti del controller e in realtà lo trovo più leggibile. Nota, se non ti piace questo approccio, usa semplicemente il metodo di costruzione tradizionale.

class UsersController
{
    public function index(AllUsersQueryInterface $query)
    {
        // Fetch user data
        $users = $query->fetch(['first_name', 'last_name', 'email']);

        // Return view
        return Response::view('all_users.php', ['users' => $users]);
    }

    public function add()
    {
        return Response::view('add_user.php');
    }

    public function insert(UserRepositoryInterface $repository)
    {
        // Create new user model
        $user = new User;
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the new user
        $repository->save($user);

        // Return the id
        return Response::json(['id' => $user->id]);
    }

    public function view(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('view_user.php', ['user' => $user]);
    }

    public function edit(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('edit_user.php', ['user' => $user]);
    }

    public function update(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Update the user
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the user
        $repository->save($user);

        // Return success
        return true;
    }

    public function delete(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Delete the user
        $repository->delete($user);

        // Return success
        return true;
    }
}

Pensieri finali:

Le cose importanti da notare qui sono che quando sto modificando (creando, aggiornando o eliminando) entità, sto lavorando con oggetti modello reali ed eseguendo la persistenza attraverso i miei repository.

Tuttavia, quando sto visualizzando (selezionando i dati e inviandoli alle viste) non sto lavorando con oggetti modello, ma piuttosto oggetti di valore vecchio. Seleziono solo i campi di cui ho bisogno ed è progettato in modo da poter massimizzare le prestazioni di ricerca dei miei dati.

I miei repository rimangono molto puliti, e invece questo "pasticcio" è organizzato nelle mie query modello.

Uso un mapper di dati per aiutare con lo sviluppo, in quanto è ridicolo scrivere SQL ripetitivi per attività comuni. Tuttavia, puoi assolutamente scrivere SQL dove necessario (query complicate, rapporti, ecc.). E quando lo fai, è ben nascosto in una classe ben chiamata.

Mi piacerebbe sentire la tua opinione sul mio approccio!


Aggiornamento luglio 2015:

Mi è stato chiesto nei commenti dove ho finito con tutto questo. Beh, in realtà non molto lontano. Sinceramente, non mi piacciono ancora i repository. Li trovo esagerati per le ricerche di base (specialmente se stai già utilizzando un ORM) e disordinati quando si lavora con query più complicate.

In genere lavoro con un ORM in stile ActiveRecord, quindi molto spesso farò semplicemente riferimento a quei modelli direttamente nella mia applicazione. Tuttavia, nelle situazioni in cui ho query più complesse, userò gli oggetti query per renderli più riutilizzabili. Dovrei anche notare che ho sempre iniettato i miei modelli nei miei metodi, rendendoli più facili da deridere nei miei test.


4
@PeeHaa Ancora una volta, è stato per mantenere semplici gli esempi. È molto comune lasciare pezzi di codice da un esempio se non riguardano specificamente l'argomento in questione. In realtà, vorrei passare le mie dipendenze.
Jonathan,

4
Interessante che tu abbia diviso il tuo Crea, Aggiorna ed Elimina dalla tua Lettura. Ho pensato che varrebbe la pena menzionare la segregazione di responsabilità delle query di comando (CQRS) che formalmente fa proprio questo. martinfowler.com/bliki/CQRS.html
Adam,

2
@Jonathan È passato un anno e mezzo da quando hai risposto alla tua domanda. Mi chiedevo se sei ancora soddisfatto della tua risposta e se questa è la tua soluzione principale ora per la maggior parte dei tuoi progetti? Nelle ultime settimane ho letto le risorse sui repository e ho visto che molte persone hanno una propria interpretazione di come dovrebbe essere implementata. Lo chiami oggetti query, ma questo è un modello esistente giusto? Penso di averlo visto essere utilizzato in altre lingue.
Boedy,

1
@Jonathan: Come gestisci le query che dovrebbero definire un utente non essere "ID", ma ad esempio tramite "nome utente" o query ancora più complicate con più di una condizione?
Gizzmo,

1
@Gizzmo Usando gli oggetti query, puoi passare parametri aggiuntivi per aiutarti con le tue domande più complicate. Ad esempio, è possibile farlo nel costruttore: new Query\ComplexUserLookup($username, $anotherCondition). Oppure, farlo tramite metodi setter $query->setUsername($username);. Puoi davvero progettarlo, tuttavia ha senso per la tua particolare applicazione e penso che gli oggetti query lascino molta flessibilità qui.
Jonathan,

48

Sulla base della mia esperienza, ecco alcune risposte alle tue domande:

D: Come gestiamo il ripristino dei campi di cui non abbiamo bisogno?

A: Dalla mia esperienza questo si riduce davvero alla gestione di entità complete rispetto a query ad hoc.

Un'entità completa è qualcosa come un User oggetto. Ha proprietà e metodi, ecc. È un cittadino di prima classe nella tua base di codice.

Una query ad hoc restituisce alcuni dati, ma non sappiamo nulla al di là di questo. Man mano che i dati vengono trasferiti nell'applicazione, viene eseguito senza contesto. È un User? A Usercon alcune Orderinformazioni allegate? Non lo sappiamo davvero.

Preferisco lavorare con entità complete.

Hai ragione a riportare spesso i dati che non utilizzerai, ma puoi affrontarli in vari modi:

  1. Memorizza in modo aggressivo le entità in modo da pagare il prezzo di lettura una sola volta dal database.
  2. Trascorri più tempo a modellare le tue entità in modo che abbiano buone distinzioni tra di loro. (Considera di dividere un'entità grande in due entità più piccole, ecc.)
  3. Valuta di avere più versioni di entità. Puoi avere un Userper il back-end e forse un UserSmallper le chiamate AJAX. Uno potrebbe avere 10 proprietà e uno ha 3 proprietà.

Gli svantaggi di lavorare con query ad hoc:

  1. Si finisce con essenzialmente gli stessi dati su molte query. Ad esempio, con un User, finirai per scrivere essenzialmente lo stesso select *per molte chiamate. Una chiamata riceverà 8 campi su 10, uno riceverà 5 su 10, uno riceverà 7 su 10. Perché non sostituire tutti con una chiamata che ottiene 10 su 10? Il motivo per cui questo è negativo è che è un omicidio ricodificare / testare / deridere.
  2. Diventa molto difficile ragionare ad alto livello sul tuo codice nel tempo. Invece di dichiarazioni come "Why is theUser così lento?" finisci per rintracciare le query una tantum e quindi le correzioni di bug tendono ad essere piccole e localizzate.
  3. È davvero difficile sostituire la tecnologia sottostante. Se memorizzi tutto in MySQL ora e vuoi passare a MongoDB, è molto più difficile sostituire 100 chiamate ad hoc di quanto non siano poche entità.

D: Avrò troppi metodi nel mio repository.

A: Non ho davvero visto nulla di diverso da quello di consolidare le chiamate. Le chiamate al metodo nel tuo repository si associano realmente alle funzionalità dell'applicazione. Più funzionalità, più chiamate specifiche per i dati. Puoi rimandare le funzioni e provare a unire chiamate simili in una sola.

La complessità alla fine della giornata deve esistere da qualche parte. Con un modello di repository lo abbiamo inserito nell'interfaccia del repository invece di creare un mucchio di procedure memorizzate.

A volte devo dire a me stesso: "Beh, doveva dare da qualche parte! Non ci sono proiettili d'argento".


Grazie per la risposta molto approfondita. Mi hai fatto pensare ora. La mia grande preoccupazione qui è che tutto ciò che ho letto dice di no SELECT *, piuttosto selezionare solo i campi richiesti. Ad esempio, vedi questa domanda . Per quanto riguarda tutte quelle query di hocking di cui parli, certamente capisco da dove vieni. Ho un'app molto grande in questo momento che ha molti di loro. Quello era il mio "Beh, doveva dare da qualche parte!" momento, ho optato per le massime prestazioni. Tuttavia, ora ho a che fare con MOLTE domande diverse.
Jonathan,

1
Un pensiero di follow-up. Ho visto una raccomandazione di usare un approccio R — CUD. Poiché readsspesso si verificano problemi di prestazioni, è possibile utilizzare un approccio di query più personalizzato per loro, che non si traduce in oggetti aziendali reali. Poi, per create, updatee delete, utilizzare un ORM, che lavora con oggetti interi. Qualche idea su questo approccio?
Jonathan,

1
Come nota per l'utilizzo di "seleziona *". L'ho fatto in passato e ha funzionato bene - fino a quando non abbiamo colpito i campi varchar (max). Quelli hanno ucciso le nostre domande. Quindi se hai tabelle con inte, piccoli campi di testo, ecc. Non è poi così male. Sembra innaturale, ma il software va così. Ciò che è stato male è improvvisamente buono e viceversa.
ryan1234,

1
L'approccio R-CUD è in realtà CQRS
MikeSW,

2
@ ryan1234 "La complessità alla fine della giornata deve esistere da qualche parte." Grazie per questo. Mi fa sentire meglio.
johnny,

20

Uso le seguenti interfacce:

  • Repository - carica, inserisce, aggiorna ed elimina entità
  • Selector - trova entità basate su filtri, in un repository
  • Filter - incapsula la logica di filtraggio

My Repositoryis database agnostic; infatti non specifica alcuna persistenza; potrebbe essere qualsiasi cosa: database SQL, file xml, servizio remoto, un alieno dallo spazio esterno ecc. Per capacità di ricerca, Repositorycostruisce un oggetto Selectorche può essere filtrato, modificato LIMIT, ordinato e contato. Alla fine, il selettore recupera uno o più Entitiesdalla persistenza.

Ecco un po 'di codice di esempio:

<?php
interface Repository
{
    public function addEntity(Entity $entity);

    public function updateEntity(Entity $entity);

    public function removeEntity(Entity $entity);

    /**
     * @return Entity
     */
    public function loadEntity($entityId);

    public function factoryEntitySelector():Selector
}


interface Selector extends \Countable
{
    public function count();

    /**
     * @return Entity[]
     */
    public function fetchEntities();

    /**
     * @return Entity
     */
    public function fetchEntity();
    public function limit(...$limit);
    public function filter(Filter $filter);
    public function orderBy($column, $ascending = true);
    public function removeFilter($filterName);
}

interface Filter
{
    public function getFilterName();
}

Quindi, un'implementazione:

class SqlEntityRepository
{
    ...
    public function factoryEntitySelector()
    {
        return new SqlSelector($this);
    }
    ...
}

class SqlSelector implements Selector
{
    ...
    private function adaptFilter(Filter $filter):SqlQueryFilter
    {
         return (new SqlSelectorFilterAdapter())->adaptFilter($filter);
    }
    ...
}
class SqlSelectorFilterAdapter
{
    public function adaptFilter(Filter $filter):SqlQueryFilter
    {
        $concreteClass = (new StringRebaser(
            'Filter\\', 'SqlQueryFilter\\'))
            ->rebase(get_class($filter));

        return new $concreteClass($filter);
    }
}

L'ideea è che il generico Selectorusa Filterma l'implementazione SqlSelectorusa SqlFilter; l' SqlSelectorFilterAdapteradatta un generico Filterad un concreto SqlFilter.

Il codice client crea Filteroggetti (che sono filtri generici) ma nell'implementazione concreta del selettore tali filtri vengono trasformati in filtri SQL.

Altre implementazioni di selettori, come InMemorySelector, si trasformano Filterin InMemoryFilterusando il loro specifico InMemorySelectorFilterAdapter; quindi, ogni implementazione del selettore viene fornita con il proprio adattatore filtro.

Usando questa strategia il mio codice client (nel livello bussines) non si preoccupa di un repository specifico o di un'implementazione del selettore.

/** @var Repository $repository*/
$selector = $repository->factoryEntitySelector();
$selector->filter(new AttributeEquals('activated', 1))->limit(2)->orderBy('username');
$activatedUserCount = $selector->count(); // evaluates to 100, ignores the limit()
$activatedUsers = $selector->fetchEntities();

PS Questa è una semplificazione del mio vero codice


"Repository - carica, inserisce, aggiorna ed elimina entità" questo è ciò che può fare un "livello di servizio", "DAO", "BLL"
Yousha Aleayoub

5

Aggiungerò un po 'di questo mentre attualmente sto cercando di capire tutto da solo.

# 1 e 2

Questo è un posto perfetto per il tuo ORM per eseguire il sollevamento di carichi pesanti. Se stai usando un modello che implementa una sorta di ORM, puoi semplicemente usare i suoi metodi per occuparti di queste cose. Crea il tuo ordine Per funzioni che implementano i metodi eloquenti se necessario. Utilizzando Eloquent per esempio:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

Quello che sembri cercare è un ORM. Nessun motivo per cui il tuo repository non può essere basato su uno. Ciò richiederebbe l'estensione dell'utente eloquente, ma personalmente non lo vedo come un problema.

Se invece vuoi evitare un ORM, dovresti "tirare i tuoi" per ottenere quello che stai cercando.

3 #

Le interfacce non dovrebbero essere requisiti difficili e veloci. Qualcosa può implementare un'interfaccia e aggiungervi. Ciò che non può fare è non riuscire a implementare una funzione richiesta di tale interfaccia. Puoi anche estendere interfacce come le classi per mantenere le cose ASCIUTTE.

Detto questo, ho appena iniziato a capire, ma queste realizzazioni mi hanno aiutato.


1
Quello che non mi piace di questo metodo è che se tu avessi un MongoUserRepository, questo e il tuo DbUserRepository restituirebbero oggetti diversi. Db restituisce un Eloquent \ Model e Mongo qualcosa di suo. Sicuramente un'implementazione migliore è che entrambi i repository restituiscano istanze / raccolte di una classe Entity \ User separata. In questo modo non ti affidi erroneamente ai metodi DB di Eloquent \ Model quando passi all'uso di MongoRepository
danharper,

1
Sono assolutamente d'accordo con te su quello. Quello che probabilmente farei per evitare che non sia mai usare quei metodi al di fuori della classe che richiede Eloquent. Quindi la funzione get probabilmente dovrebbe essere privata e utilizzata solo all'interno della classe in quanto, come hai sottolineato, restituirebbe qualcosa che altri repository non potrebbero.
Sarà il

3

Posso solo commentare il modo in cui (nella mia azienda) ci occupiamo di questo. Prima di tutto, le prestazioni non rappresentano un grosso problema per noi, ma avere un codice pulito / adeguato lo è.

Innanzitutto definiamo modelli come quelli UserModelche utilizzano un ORM per creare UserEntityoggetti. Quando un UserEntityviene caricato da un modello vengono caricati tutti i campi. Per i campi che fanno riferimento a entità straniere utilizziamo il modello straniero appropriato per creare le rispettive entità. Per tali entità i dati verranno caricati a richiesta. Ora la tua reazione iniziale potrebbe essere ... ??? ... !!! lascia che ti dia un esempio un po 'di un esempio:

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's set
    }
}

class UserModel {
    protected $orm;

    public function findUsers(IGetOptions $options = null)
    {
        return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
    }
}

class OrderEntity extends PersistentEntity {} // user your imagination
class OrderModel
{
    public function findOrdersById(array $ids, IGetOptions $options = null)
    {
        //...
    }
}

Nel nostro caso $dbè un ORM in grado di caricare entità. Il modello indica all'ORM di caricare un set di entità di un tipo specifico. L'ORM contiene un mapping e lo utilizza per iniettare tutti i campi per quell'entità nell'entità. Per i campi stranieri, tuttavia, vengono caricati solo gli ID di tali oggetti. In questo caso OrderModelcrea OrderEntitys con solo gli ID degli ordini referenziati. Quando PersistentEntity::getFieldviene chiamato OrderEntitydall'entità indica al suo modello di caricare in modo pigro tutti i campi nella OrderEntitys. Tutti OrderEntityi messaggi associati a un UserEntity vengono considerati come un set di risultati e verranno caricati contemporaneamente.

La magia qui è che il nostro modello e ORM iniettano tutti i dati nelle entità e che le entità forniscono semplicemente funzioni wrapper per il getFieldmetodo generico fornito da PersistentEntity. Per riassumere, cariciamo sempre tutti i campi, ma i campi che fanno riferimento a un'entità straniera vengono caricati quando necessario. Caricare un sacco di campi non è davvero un problema di prestazioni. Caricare tutte le possibili entità straniere, tuttavia, sarebbe una riduzione delle prestazioni ENORME.

Passiamo ora a caricare un set specifico di utenti, basato su una clausola where. Forniamo un pacchetto di classi orientato agli oggetti che consente di specificare espressioni semplici che possono essere incollate insieme. Nel codice di esempio l'ho chiamato GetOptions. È un wrapper per tutte le opzioni possibili per una query selezionata. Contiene una raccolta di clausole where, una clausola group by e tutto il resto. Le nostre clausole where sono piuttosto complicate ma ovviamente potresti facilmente creare una versione più semplice.

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

Una versione più semplice di questo sistema sarebbe quella di passare la parte WHERE della query come stringa direttamente al modello.

Mi dispiace per questa risposta abbastanza complicata. Ho cercato di riassumere il nostro quadro nel modo più rapido e chiaro possibile. Se hai ulteriori domande, non esitare a farle e aggiornerò la mia risposta.

MODIFICA: Inoltre, se davvero non si desidera caricare subito alcuni campi, è possibile specificare un'opzione di caricamento lento nella mappatura ORM. Poiché tutti i campi vengono infine caricati tramite il getFieldmetodo, è possibile caricare alcuni campi all'ultimo minuto quando viene chiamato quel metodo. Questo non è un grosso problema in PHP, ma non lo consiglierei per altri sistemi.


3

Queste sono alcune soluzioni diverse che ho visto. Ci sono pro e contro per ciascuno di essi, ma spetta a te decidere.

Problema n. 1: troppi campi

Questo è un aspetto importante soprattutto quando si prende in considerazione le scansioni solo indice . Vedo due soluzioni per affrontare questo problema. È possibile aggiornare le funzioni per accettare un parametro di array opzionale che conterrà un elenco di colonne da restituire. Se questo parametro è vuoto, restituiresti tutte le colonne nella query. Questo può essere un po 'strano; in base al parametro è possibile recuperare un oggetto o un array. È inoltre possibile duplicare tutte le funzioni in modo da disporre di due funzioni distinte che eseguono la stessa query, ma una restituisce una matrice di colonne e l'altra restituisce un oggetto.

public function findColumnsById($id, array $columns = array()){
    if (empty($columns)) {
        // use *
    }
}

public function findById($id) {
    $data = $this->findColumnsById($id);
}

Problema n. 2: troppi metodi

Ho lavorato brevemente con Propel ORM un anno fa e questo è basato su ciò che ricordo di quell'esperienza. Propel ha la possibilità di generare la sua struttura di classe basata sullo schema del database esistente. Crea due oggetti per ogni tabella. Il primo oggetto è un lungo elenco di funzioni di accesso simili a quelle attualmente elencate; findByAttribute($attribute_value). L'oggetto successivo eredita da questo primo oggetto. È possibile aggiornare questo oggetto figlio per incorporare le funzioni getter più complesse.

Un'altra soluzione sarebbe utilizzare __call()per mappare funzioni non definite su qualcosa di utilizzabile. Il tuo __callmetodo sarebbe in grado di analizzare findById e findByName in diverse query.

public function __call($function, $arguments) {
    if (strpos($function, 'findBy') === 0) {
        $parameter = substr($function, 6, strlen($function));
        // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0]
    }
}

Spero che questo aiuti almeno un po 'cosa.



0

Concordo con @ ryan1234 sul fatto che è necessario passare oggetti completi all'interno del codice e utilizzare metodi di query generici per ottenere tali oggetti.

Model::where(['attr1' => 'val1'])->get();

Per l'utilizzo esterno / endpoint mi piace molto il metodo GraphQL.

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}

0

Problema n. 3: impossibile abbinare un'interfaccia

Vedo i vantaggi dell'utilizzo delle interfacce per i repository, quindi posso scambiare la mia implementazione (a scopo di test o altro). La mia comprensione delle interfacce è che definiscono un contratto che un'implementazione deve seguire. Questo è ottimo fino a quando non inizi ad aggiungere metodi aggiuntivi ai tuoi repository come findAllInCountry (). Ora ho bisogno di aggiornare la mia interfaccia per avere anche questo metodo, altrimenti, altre implementazioni potrebbero non averlo e questo potrebbe interrompere la mia applicazione. Da questo sembra folle ... un caso della coda che scuote il cane.

Il mio istinto mi dice che forse richiede un'interfaccia che implementa metodi ottimizzati per le query insieme a metodi generici. Le query sensibili alle prestazioni dovrebbero avere metodi mirati, mentre le query rare o leggere vengono gestite da un gestore generico, forse le spese del controller che fanno un po 'più di giocoleria.

I metodi generici consentirebbero l'implementazione di qualsiasi query e quindi impedirebbero di interrompere le modifiche durante un periodo di transizione. I metodi mirati consentono di ottimizzare una chiamata quando ha senso e può essere applicata a più fornitori di servizi.

Questo approccio sarebbe simile alle implementazioni hardware che eseguono attività ottimizzate specifiche, mentre le implementazioni software svolgono un lavoro leggero o un'implementazione flessibile.


0

Penso a graphQL sia un buon candidato in questo caso per fornire un linguaggio di query su larga scala senza aumentare la complessità dei repository di dati.

Tuttavia, c'è un'altra soluzione se non vuoi andare per il graphQL per ora. Usando un DTO cui un oggetto viene utilizzato per il carring dei dati tra i processi, in questo caso tra il servizio / controller e il repository.

Una risposta elegante è già fornita sopra, tuttavia proverò a fare un altro esempio che penso sia più semplice e possa servire da punto di partenza per un nuovo progetto.

Come mostrato nel codice, avremmo bisogno solo di 4 metodi per le operazioni CRUD. ilfind metodo verrebbe usato per elencare e leggere passando l'argomento oggetto. I servizi di back-end potrebbero creare l'oggetto query definito in base a una stringa di query URL o in base a parametri specifici.

L'oggetto query ( SomeQueryDto) potrebbe anche implementare un'interfaccia specifica se necessario. ed è facile da estendere in seguito senza aggiungere complessità.

<?php

interface SomeRepositoryInterface
{
    public function create(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function update(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function delete(int $id): void;

    public function find(SomeEnitityQueryInterface $query): array;
}

class SomeRepository implements SomeRepositoryInterface
{
    public function find(SomeQueryDto $query): array
    {
        $qb = $this->getQueryBuilder();

        foreach ($query->getSearchParameters() as $attribute) {
            $qb->where($attribute['field'], $attribute['operator'], $attribute['value']);
        }

        return $qb->get();
    }
}

/**
 * Provide query data to search for tickets.
 *
 * @method SomeQueryDto userId(int $id, string $operator = null)
 * @method SomeQueryDto categoryId(int $id, string $operator = null)
 * @method SomeQueryDto completedAt(string $date, string $operator = null)
 */
class SomeQueryDto
{
    /** @var array  */
    const QUERYABLE_FIELDS = [
        'id',
        'subject',
        'user_id',
        'category_id',
        'created_at',
    ];

    /** @var array  */
    const STRING_DB_OPERATORS = [
        'eq' => '=', // Equal to
        'gt' => '>', // Greater than
        'lt' => '<', // Less than
        'gte' => '>=', // Greater than or equal to
        'lte' => '<=', // Less than or equal to
        'ne' => '<>', // Not equal to
        'like' => 'like', // Search similar text
        'in' => 'in', // one of range of values
    ];

    /**
     * @var array
     */
    private $searchParameters = [];

    const DEFAULT_OPERATOR = 'eq';

    /**
     * Build this query object out of query string.
     * ex: id=gt:10&id=lte:20&category_id=in:1,2,3
     */
    public static function buildFromString(string $queryString): SomeQueryDto
    {
        $query = new self();
        parse_str($queryString, $queryFields);

        foreach ($queryFields as $field => $operatorAndValue) {
            [$operator, $value] = explode(':', $operatorAndValue);
            $query->addParameter($field, $operator, $value);
        }

        return $query;
    }

    public function addParameter(string $field, string $operator, $value): SomeQueryDto
    {
        if (!in_array($field, self::QUERYABLE_FIELDS)) {
            throw new \Exception("$field is invalid query field.");
        }
        if (!array_key_exists($operator, self::STRING_DB_OPERATORS)) {
            throw new \Exception("$operator is invalid query operator.");
        }
        if (!is_scalar($value)) {
            throw new \Exception("$value is invalid query value.");
        }

        array_push(
            $this->searchParameters,
            [
                'field' => $field,
                'operator' => self::STRING_DB_OPERATORS[$operator],
                'value' => $value
            ]
        );

        return $this;
    }

    public function __call($name, $arguments)
    {
        // camelCase to snake_case
        $field = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $name));

        if (in_array($field, self::QUERYABLE_FIELDS)) {
            return $this->addParameter($field, $arguments[1] ?? self::DEFAULT_OPERATOR, $arguments[0]);
        }
    }

    public function getSearchParameters()
    {
        return $this->searchParameters;
    }
}

Esempio di utilizzo:

$query = new SomeEnitityQuery();
$query->userId(1)->categoryId(2, 'ne')->createdAt('2020-03-03', 'lte');
$entities = $someRepository->find($query);

// Or by passing the HTTP query string
$query = SomeEnitityQuery::buildFromString('created_at=gte:2020-01-01&category_id=in:1,2,3');
$entities = $someRepository->find($query);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.