Il modello di progettazione del repository

Il modello di progettazione del repository, definito da Eric Evens nel suo libro Domain Driven Design, è uno dei modelli di progettazione più utili e più ampiamente applicabili mai inventati. Qualsiasi applicazione deve funzionare con persistenza e con una sorta di elenco di elementi. Questi possono essere utenti, prodotti, reti, dischi o qualsiasi altra cosa riguardi la tua applicazione. Se si dispone di un blog, ad esempio, è necessario gestire elenchi di post di blog e elenchi di commenti. Il problema che tutte queste logiche di gestione delle liste hanno in comune è come collegare la logica aziendale, le fabbriche e la persistenza.


Il modello di progettazione di fabbrica

Come accennato nel paragrafo introduttivo, un repository collegherà le fabbriche con i gateway (persistenza). Questi sono anche modelli di design e se non si ha familiarità con loro, questo paragrafo farà luce sull'argomento.

Una fabbrica è un modello di progettazione semplice che definisce un modo conveniente per creare oggetti. È una classe o un insieme di classi responsabili della creazione degli oggetti di cui la nostra logica aziendale ha bisogno. Una fabbrica ha tradizionalmente un metodo chiamato "rendere()" e saprà come prendere tutte le informazioni necessarie per costruire un oggetto e fare l'edificio stesso e restituire un oggetto pronto all'uso alla logica del business.

Ecco un po 'di più sul Pattern di fabbrica in un vecchio tutorial di Nettuts +: una guida per principianti per progettare modelli. Se preferisci una vista più approfondita su Pattern di fabbrica, controlla il primo pattern di progettazione nel corso Agile Design Patterns che abbiamo su Tuts+.


Il modello gateway

Conosciuto anche come "Table Data Gateway" è un modello semplice che offre la connessione tra la business logic e il database stesso. La sua responsabilità principale è quella di eseguire le query sul database e fornire i dati recuperati in una struttura dati tipica per il linguaggio di programmazione (come un array in PHP). Questi dati vengono quindi generalmente filtrati e modificati nel codice PHP in modo che possiamo ottenere le informazioni e le variabili necessarie per crare i nostri oggetti. Questa informazione deve quindi essere passata alle fabbriche.

Il modello di Gateway Design è spiegato ed esemplificato in modo molto dettagliato in un tutorial di Nettuts + sull'evoluzione verso uno strato di persistenza. Inoltre, nello stesso corso Agile Design Patterns, la seconda lezione sul modello di progettazione riguarda questo argomento.


I problemi che dobbiamo risolvere

Duplicazione mediante gestione dei dati

Potrebbe non essere ovvio a prima vista, ma collegare i gateway alle fabbriche può portare a molte duplicazioni. Qualsiasi software di dimensioni considerevoli deve creare gli stessi oggetti da luoghi diversi. In ogni luogo sarà necessario utilizzare il gateway per recuperare un set di dati grezzi, filtrarli e elaborarli per essere pronti per essere inviati alle fabbriche. Da tutti questi posti, chiamerai le stesse fabbriche con le stesse strutture dati ma ovviamente con dati diversi. I tuoi oggetti saranno creati e forniti dalle fabbriche. Ciò comporterà inevitabilmente molte duplicazioni nel tempo. E la duplicazione sarà diffusa in classi o moduli distanti e sarà difficile da notare e risolvere.

Duplicazione mediante reimplementazione della logica di recupero dei dati

Un altro problema che abbiamo è come esprimere le domande che dobbiamo fare con l'aiuto dei gateway. Ogni volta che abbiamo bisogno di alcune informazioni dal Gateway, dobbiamo pensare a cosa abbiamo esattamente bisogno? Abbiamo bisogno di tutti i dati su un singolo argomento? Abbiamo bisogno solo di alcune informazioni specifiche? Vogliamo recuperare un gruppo specifico dal database e fare l'ordinamento o il filtraggio raffinato nel nostro linguaggio di programmazione? Tutte queste domande devono essere affrontate ogni volta che recuperiamo informazioni dal livello di persistenza attraverso il nostro Gateway. Ogni volta che lo facciamo, dovremo trovare una soluzione. Col tempo, con l'aumentare della nostra applicazione, ci troveremo ad affrontare gli stessi dilemmi in diversi punti della nostra applicazione. Inavvertitamente troveremo soluzioni leggermente diverse per gli stessi problemi. Ciò richiede non solo tempo e sforzi supplementari, ma porta anche a una duplicazione sottile, per lo più molto difficile da riconoscere. Questo è il tipo più pericoloso di duplicazione.

Duplicazione mediante reimplementazione della logica di persistenza dei dati

Nei due paragrafi precedenti abbiamo parlato solo del recupero dei dati. Ma il Gateway è bidirezionale. La nostra logica di business è bidirezionale. Dobbiamo in qualche modo persistere i nostri oggetti. Questo di nuovo porta a molte ripetizioni se vogliamo implementare questa logica, se necessario, attraverso diversi moduli e classi della nostra applicazione.


I principali concetti

Repository per il recupero dei dati

Un repository può funzionare in due modi: recupero dei dati e persistenza dei dati.


Quando viene utilizzato per recuperare oggetti dalla persistenza, verrà chiamato un repository con una query personalizzata. Questa query può essere un metodo specifico per nome o un metodo più comune con parametri. Il repository è responsabile di fornire e implementare questi metodi di query. Quando viene chiamato un tale metodo, il repository contatterà il gateway per recuperare i dati non elaborati dalla persistenza. Il gateway fornirà dati di oggetti grezzi (come un array con valori). Quindi il Repository prenderà questi dati, eseguirà le trasformazioni necessarie e chiamerà i metodi Factory appropriati. Le Fabbriche forniranno gli oggetti costruiti con i dati forniti dal Repository. Il Repository raccoglierà questi oggetti e li restituirà come un insieme di oggetti (come una matrice di oggetti o un oggetto di raccolta come definito nella lezione Pattern Composito nel corso Agile Design Patterns).

Repository per la persistenza dei dati

Il secondo modo in cui un repository può funzionare è fornire la logica necessaria per estrarre le informazioni da un oggetto e persistere. Questo può essere semplice come serializzare l'oggetto e inviare i dati serializzati al Gateway per mantenerlo o altrettanto sofisticati come creare matrici di informazioni con tutti i campi e lo stato di un oggetto.


Quando viene utilizzato per persistere informazioni, la classe cliente è quella che comunica direttamente con la fabbrica. Immagina uno scenario quando un nuovo commento viene pubblicato su un post del blog. Un oggetto Commento viene creato dalla nostra logica aziendale (la classe Client) e quindi inviato al Repository per essere mantenuto. Il repository manterrà gli oggetti utilizzando il Gateway e, facoltativamente, li memorizzerà in un elenco locale in memoria. I dati devono essere trasformati perché ci sono solo rari casi in cui gli oggetti reali possono essere salvati direttamente in un sistema di persistenza.


Collegare i punti

L'immagine sotto è una vista di livello superiore su come integrare il Repository tra le Fabbriche, il Gateway e il Cliente.


Al centro dello schema c'è il nostro repository. A sinistra, c'è un'interfaccia per il gateway, un'implementazione e la persistenza stessa. A destra, c'è un'interfaccia per le fabbriche e un'implementazione di fabbrica. Infine, in cima c'è la classe cliente.

Come si può osservare dalla direzione delle frecce, le dipendenze sono invertite. Il repository dipende solo dalle interfacce astratte per fabbriche e gateway. Il gateway dipende dalla sua interfaccia e dalla persistenza che offre. La fabbrica dipende solo dalla sua interfaccia. Il client dipende dal repository, che è accettabile perché il deposito tende ad essere meno concreto del client.


Metti in prospettiva, il paragrafo precedente rispetta la nostra architettura di alto livello e la direzione delle dipendenze che vogliamo raggiungere.


Gestione dei commenti ai post del blog con un repository

Ora che abbiamo visto la teoria, è il momento per un esempio pratico. Immagina di avere un blog in cui abbiamo oggetti Post e oggetti Comment. I commenti appartengono ai Post e dobbiamo trovare un modo per persisterli e recuperarli.

Il commento

Inizieremo con un test che ci costringerà a riflettere su ciò che l'oggetto Comment dovrebbe contenere.

class RepositoryTest estende PHPUnit_Framework_TestCase function testACommentHasAllItsComposingParts () $ postId = 1; $ commentAutore = "Joe"; $ commentAutoreEmail = "[email protected]"; $ commentSubject = "Joe ha un'opinione sul pattern del repository"; $ commentBody = "Penso che sia una buona idea usare il modello di repository per mantenere e recuperare gli oggetti."; $ comment = new Comment ($ postId, $ commentAutore, $ commentAuthorEmail, $ commentSubject, $ commentBody); 

A prima vista, un commento sarà solo un oggetto dati. Potrebbe non avere alcuna funzionalità, ma questo dipende dal contesto della nostra applicazione. Per questo esempio, supponiamo che sia un semplice oggetto dati. Costruito con un insieme di variabili.

classe Commento 

Solo creando una classe vuota e richiedendola nel test, passa.

require_once '... /Comment.php'; class RepositoryTest estende PHPUnit_Framework_TestCase [...]

Ma questo è tutt'altro che perfetto. Il nostro test non prova ancora nulla. Costringiamoci a scrivere tutti i getter della classe Comment.

function testACommentsHasAllItsComposingParts () $ postId = 1; $ commentAutore = "Joe"; $ commentAutoreEmail = "[email protected]"; $ commentSubject = "Joe ha un'opinione sul pattern del repository"; $ commentBody = "Penso che sia una buona idea usare il modello di repository per mantenere e recuperare gli oggetti."; $ comment = new Comment ($ postId, $ commentAutore, $ commentAuthorEmail, $ commentSubject, $ commentBody); $ this-> assertEquals ($ postId, $ comment-> getPostId ()); $ this-> assertEquals ($ commentAutore, $ comment-> getAuthor ()); $ this-> assertEquals ($ commentAuthorEmail, $ comment-> getAuthorEmail ()); $ this-> assertEquals ($ commentSubject, $ comment-> getSubject ()); $ this-> assertEquals ($ commentBody, $ comment-> getBody ()); 

Per controllare la durata del tutorial, ho scritto tutte le asserzioni contemporaneamente e le implementeremo contemporaneamente. Nella vita reale, prendili uno per uno.

 commento di classe private $ postId; autore privato $; private $ authorEmail; soggetto privato $; corpo privato $; function __construct ($ postId, $ author, $ authorEmail, $ subject, $ body) $ this-> postId = $ postId; $ this-> author = $ author; $ this-> authorEmail = $ authorEmail; $ this-> subject = $ subject; $ this-> body = $ body;  public function getPostId () return $ this-> postId;  public function getAuthor () return $ this-> author;  public function getAuthorEmail () return $ this-> authorEmail;  public function getSubject () return $ this-> subject;  funzione pubblica getBody () return $ this-> body; 

Tranne l'elenco delle variabili private, il resto del codice è stato generato dal mio IDE, NetBeans, quindi testare il codice generato automaticamente potrebbe essere un po 'di overhead alcune volte. Se non stai scrivendo queste righe da solo, sentiti libero di farlo direttamente e non preoccuparti dei test per setter e costruttori. Tuttavia, il test ci ha aiutato a esporre meglio le nostre idee e a documentare meglio ciò che la nostra classe Comment conterrà.

Possiamo anche considerare questi metodi di test e classi di test come le nostre classi "Client" dagli schemi.


La nostra porta verso la persistenza

Per mantenere questo esempio il più semplice possibile, implementeremo solo InMemoryPersistence in modo da non complicare la nostra esistenza con i file system o i database.

require_once '... /InMemoryPersistence.php'; class InMemoryPersistenceTest estende PHPUnit_Framework_TestCase function testItCanPerisistAndRetrieveASingleDataArray () $ data = array ('data'); $ persistence = new InMemoryPersistence (); $ Persistence-> persistere ($ dati); $ this-> assertEquals ($ data, $ persistence-> retrieve (0)); 

Come al solito, iniziamo con il test più semplice che potrebbe fallire e ci costringe anche a scrivere del codice. Questo test crea un nuovo InMemoryPersistence oggetto e cerca di persistere e recuperare un array chiamato dati.

require_once __DIR__. '/Persistence.php'; class InMemoryPersistence implementa Persistence private $ data = array (); funzione persist ($ data) $ this-> data = $ data;  function retrieve ($ id) return $ this-> data; 

Il codice più semplice per farlo passare è solo per mantenere l'arrivo $ data in una variabile privata e restituirlo in recuperare metodo. Il codice com'è adesso non si preoccupa dell'invio $ id variabile. È la cosa più semplice che potrebbe far passare il test. Ci siamo anche presi la libertà di introdurre e implementare un'interfaccia chiamata Persistenza.

persistenza di interfaccia funzione persist ($ data); funzione recupera ($ id); 

Questa interfaccia definisce i due metodi che ogni Gateway deve implementare. Persistere e recuperare. Come probabilmente avete già intuito, il nostro Gateway è il nostro InMemoryPersistence la classe e la nostra persistenza fisica è la variabile privata che tiene i nostri dati nella memoria. Ma torniamo all'implementazione di questo nella persistenza della memoria.

function testItCanPerisistSeveralElementsAndRetrieveAnyOfThem () $ data1 = array ('data1'); $ data2 = array ('data2'); $ persistence = new InMemoryPersistence (); $ Persistence-> persistono ($ data1); $ Persistence-> persistono ($ data2); $ this-> assertEquals ($ data1, $ persistence-> retrieve (0)); $ this-> assertEquals ($ data2, $ persistence-> retrieve (1)); 

Abbiamo aggiunto un altro test. In questo si mantengono due diversi array di dati. Ci aspettiamo di essere in grado di recuperarli individualmente.

require_once __DIR__. '/Persistence.php'; class InMemoryPersistence implementa Persistence private $ data = array (); funzione persist ($ data) $ this-> data [] = $ data;  function retrieve ($ id) return $ this-> data [$ id]; 

Il test ci ha costretti a modificare leggermente il nostro codice. Ora dobbiamo aggiungere dati al nostro array, non solo sostituirlo con quello inviato a persiste (). Dobbiamo anche considerare il $ id parametro e restituisce l'elemento in quell'indice.

Questo è abbastanza per il nostro InMemoryPersistence. Se necessario, possiamo modificarlo in seguito.


La nostra fabbrica

Abbiamo un cliente (i nostri test), una persistenza con un gateway e oggetti di commento da persiste. La prossima cosa che manca è la nostra fabbrica.

Abbiamo iniziato la nostra codifica con a RepositoryTest file. Questo test, tuttavia, ha effettivamente creato un Commento oggetto. Ora abbiamo bisogno di creare test per verificare se la nostra Factory sarà in grado di creare Commento oggetti. Sembra che abbiamo avuto un errore di giudizio e il nostro test è più probabilmente un test per la nostra prossima fabbrica che per il nostro deposito. Possiamo spostarlo in un altro file di test, CommentFactoryTest.

require_once '... /Comment.php'; class CommentFactoryTest estende PHPUnit_Framework_TestCase function testACommentsHasAllItsComposingParts () $ postId = 1; $ commentAutore = "Joe"; $ commentAutoreEmail = "[email protected]"; $ commentSubject = "Joe ha un'opinione sul pattern del repository"; $ commentBody = "Penso che sia una buona idea usare il modello di repository per mantenere e recuperare gli oggetti."; $ comment = new Comment ($ postId, $ commentAutore, $ commentAuthorEmail, $ commentSubject, $ commentBody); $ this-> assertEquals ($ postId, $ comment-> getPostId ()); $ this-> assertEquals ($ commentAutore, $ comment-> getAuthor ()); $ this-> assertEquals ($ commentAuthorEmail, $ comment-> getAuthorEmail ()); $ this-> assertEquals ($ commentSubject, $ comment-> getSubject ()); $ this-> assertEquals ($ commentBody, $ comment-> getBody ()); 

Ora, questo test ovviamente passa. E mentre è un test corretto, dovremmo considerare di modificarlo. Vogliamo creare un Fabbrica oggetto, passare un array e chiedergli di creare un Commento per noi.

require_once '... /CommentFactory.php'; class CommentFactoryTest estende PHPUnit_Framework_TestCase function testACommentsHasAllItsComposingParts () $ postId = 1; $ commentAutore = "Joe"; $ commentAutoreEmail = "[email protected]"; $ commentSubject = "Joe ha un'opinione sul pattern del repository"; $ commentBody = "Penso che sia una buona idea usare il modello di repository per mantenere e recuperare gli oggetti."; $ commentData = array ($ postId, $ commentAutore, $ commentAuthorEmail, $ commentSubject, $ commentBody); $ comment = (nuovo CommentFactory ()) -> make ($ commentData); $ this-> assertEquals ($ postId, $ comment-> getPostId ()); $ this-> assertEquals ($ commentAutore, $ comment-> getAuthor ()); $ this-> assertEquals ($ commentAuthorEmail, $ comment-> getAuthorEmail ()); $ this-> assertEquals ($ commentSubject, $ comment-> getSubject ()); $ this-> assertEquals ($ commentBody, $ comment-> getBody ()); 

Non dovremmo mai nominare le nostre classi sulla base del modello di progettazione che implementano, ma Fabbrica e deposito rappresentano più del semplice modello di design stesso. Personalmente non ho nulla contro l'includere queste due parole nei nomi della nostra classe. Tuttavia continuo a raccomandare e rispettare il concetto di non nominare le nostre classi dopo i modelli di progettazione che usiamo per il resto dei modelli.

Questo test è leggermente diverso da quello precedente, ma fallisce. Prova a creare un CommentFactory oggetto. Quella classe non esiste ancora. Cerchiamo anche di chiamare a rendere() metodo su di esso con una matrice contenente tutte le informazioni di un commento come una matrice. Questo metodo è definito nel Fabbrica interfaccia.

interfaccia Factory function make ($ data); 

Questo è molto comune Fabbrica interfaccia. Definiva l'unico metodo richiesto per una fabbrica, il metodo che crea effettivamente gli oggetti che vogliamo.

require_once __DIR__. '/Factory.php'; require_once __DIR__. '/Comment.php'; classe CommentFactory implementa Factory function make ($ components) return new Comment ($ components [0], $ components [1], $ components [2], $ components [3], $ components [4]); 

E CommentFactory implementa il Fabbrica interfacciare con successo prendendo il $ componenti parametro nel suo rendere() metodo, crea e restituisce un nuovo Commento oggetto con le informazioni da lì.

Manterremo la nostra persistenza e logica di creazione dell'oggetto il più semplice possibile. Per questo tutorial, possiamo tranquillamente ignorare qualsiasi gestione degli errori, convalida e lancio di eccezioni. Ci fermeremo qui con la persistenza e l'implementazione della creazione di oggetti.


Utilizzo di un repository per i commenti persistenti

Come abbiamo visto sopra, possiamo usare un repository in due modi. Per recuperare informazioni dalla persistenza e anche per mantenere informazioni sul livello di persistenza. Usando TDD è, per la maggior parte del tempo, più facile iniziare con la parte di salvataggio (persistente) della logica e quindi usare quella implementazione esistente per testare il recupero dei dati.

require_once '... / ... / ... /vendor/autoload.php'; require_once '... /CommentRepository.php'; require_once '... /CommentFactory.php'; class RepositoryTest estende PHPUnit_Framework_TestCase funzione protetta tearDown () \ Mockery :: close ();  function testItCallsThePersistenceWhenAddingAComment () $ persistanceGateway = \ Mockery :: mock ('Persistenza'); $ commentRepository = new CommentRepository ($ persistanceGateway); $ commentData = array (1, 'x', 'x', 'x', 'x'); $ comment = (nuovo CommentFactory ()) -> make ($ commentData); $ PersistanceGateway-> shouldReceive ( 'persistono') -> una volta () -> con ($ commentData); $ CommentRepository-> add ($ commento); 

Usiamo Mockery per deridere la nostra persistenza e iniettare quell'oggetto deriso sul Repository. Quindi chiamiamo Inserisci() sul repository. Questo metodo ha un parametro di tipo Commento. Ci aspettiamo che la persistenza venga chiamata con una matrice di dati simile a $ commentData.

require_once __DIR__. '/InMemoryPersistence.php'; class CommentRepository private $ persistence; function __construct (Persistenza $ persistence = null) $ this-> persistence = $ persistence? : nuovo InMemoryPersistence ();  funzione add (commento $ commento) $ this-> persistence-> persist (array ($ comment-> getPostId (), $ comment-> getAuthor (), $ comment-> getAuthorEmail (), $ comment-> getSubject ( ), $ comment-> getBody ())); 

Come puoi vedere, il Inserisci() il metodo è abbastanza intelligente. Incapsula le conoscenze su come trasformare un oggetto PHP in un semplice array utilizzabile dalla persistenza. Ricorda, il nostro gateway di persistenza di solito è un oggetto generale per tutti i nostri dati. Può e persisterà tutti i dati della nostra applicazione, quindi inviarlo ad oggetti lo farebbe fare troppo: sia la conversione che la persistenza effettiva.

Quando hai un InMemoryPersistence classe come facciamo, è molto veloce. Possiamo usarlo come alternativa al beffardo del gateway.

function testAPersistedCommentCanBeRetrievedFromTheGateway () $ persistanceGateway = new InMemoryPersistence (); $ commentRepository = new CommentRepository ($ persistanceGateway); $ commentData = array (1, 'x', 'x', 'x', 'x'); $ comment = (nuovo CommentFactory ()) -> make ($ commentData); $ CommentRepository-> add ($ commento); $ this-> assertEquals ($ commentData, $ persistanceGateway-> retrieve (0)); 

Naturalmente se non hai un'implementazione in-memory della tua persistenza, il mocking è l'unico modo ragionevole per andare. Altrimenti il ​​test sarà troppo lento per essere pratico.

function testItCanAddMultipleCommentsAtOnce () $ persistanceGateway = \ Mockery :: mock ('Persistenza'); $ commentRepository = new CommentRepository ($ persistanceGateway); $ commentData1 = array (1, 'x', 'x', 'x', 'x'); $ comment1 = (new CommentFactory ()) -> make ($ commentData1); $ commentData2 = array (2, 'y', 'y', 'y', 'y'); $ comment2 = (nuovo CommentFactory ()) -> make ($ commentData2); $ PersistanceGateway-> shouldReceive ( 'persistono') -> una volta () -> con ($ commentData1); $ PersistanceGateway-> shouldReceive ( 'persistono') -> una volta () -> con ($ commentData2); $ commentRepository-> add (array ($ comment1, $ comment2)); 

Il nostro prossimo passo logico è implementare un modo per aggiungere più commenti contemporaneamente. Il tuo progetto potrebbe non richiedere questa funzionalità e non è qualcosa richiesto dal modello. In effetti, il modello di repository dice solo che fornirà una query personalizzata e un linguaggio di persistenza per la nostra logica aziendale. Quindi, se la nostra logica di bushiness sente la necessità di aggiungere più commenti contemporaneamente, il repository è il luogo in cui deve risiedere la logica.

function add ($ commentData) if (is_array ($ commentData)) foreach ($ commentData as $ comment) $ this-> persistence-> persist (array ($ comment-> getPostId (), $ comment-> getAuthor (), $ comment-> getAuthorEmail (), $ comment-> getSubject (), $ comment-> getBody ())); altrimenti $ this-> persistence-> persist (array ($ commentData-> getPostId (), $ commentData-> getAuthor (), $ commentData-> getAuthorEmail (), $ commentData-> getSubject (), $ commentData-> getBody ( ))); 

E il modo più semplice per eseguire il test pass è solo verificare se il parametro che stiamo ottenendo è un array o meno. Se è un array, scorreremo ciclicamente ogni elemento e chiameremo la persistenza con l'array che generiamo da un singolo elemento Commento oggetto. E mentre questo codice è sintatticamente corretto e fa passare il test, introduce una leggera duplicazione che possiamo eliminare abbastanza facilmente.

function add ($ commentData) if (is_array ($ commentData)) foreach ($ commentData as $ comment) $ this-> addOne ($ comment); altrimenti $ this-> addOne ($ commentData);  funzione privata addOne (commento $ commento) $ this-> persistence-> persist (array ($ comment-> getPostId (), $ comment-> getAuthor (), $ comment-> getAuthorEmail (), $ comment-> getSubject (), $ comment-> getBody ())); 

Quando tutti i test sono verdi, è sempre tempo di refactoring prima di continuare con il prossimo test fallimentare. E lo abbiamo fatto con il Inserisci() metodo. Abbiamo estratto l'aggiunta di un singolo commento in un metodo privato e l'abbiamo chiamato da due diversi posti nel nostro pubblico Inserisci() metodo. Ciò non solo ha ridotto la duplicazione ma ha anche aperto la possibilità di creare il Aggiungi uno() metodo pubblico e lasciando che la logica aziendale decida se vuole aggiungere uno o più commenti alla volta. Ciò porterebbe a una diversa implementazione del nostro repository, con un Aggiungi uno() e un altro addMany () metodi. Sarebbe un'implementazione perfettamente legittima del modello di deposito.


Recupero di commenti con il nostro deposito

Un repository fornisce un linguaggio di query personalizzato per la logica aziendale. Pertanto, i nomi e le funzionalità dei metodi di query di un repository sono ampiamente conformi ai requisiti della business logic. Costruisci il tuo repository mentre sviluppi la tua logica di business, poiché hai bisogno di un altro metodo di query personalizzato. Tuttavia, ci sono almeno uno o due metodi che troverai in quasi tutti i repository.

function testItCanFindAllComments () $ repository = new CommentRepository (); $ commentData1 = array (1, 'x', 'x', 'x', 'x'); $ comment1 = (new CommentFactory ()) -> make ($ commentData1); $ commentData2 = array (2, 'y', 'y', 'y', 'y'); $ comment2 = (nuovo CommentFactory ()) -> make ($ commentData2); $ Repository-> aggiungere ($ comment1); $ Repository-> aggiungere ($ comment2); $ this-> assertEquals (array ($ comment1, $ comment2), $ repository-> findAll ()); 

Il primo metodo di questo tipo è chiamato trova tutto(). Questo dovrebbe restituire tutti gli oggetti di cui è responsabile il repository, nel nostro caso Commenti. Il test è semplice, aggiungiamo un commento, poi un altro e alla fine vogliamo chiamare trova tutto() e ottieni una lista contenente entrambi i commenti. Questo tuttavia non è possibile con il nostro InMemoryPersistence come è a questo punto. È richiesto un piccolo aggiornamento.

function retrieveAll () return $ this-> data; 

Questo è tutto. Abbiamo aggiunto un RetrieveAll () metodo che restituisce il tutto $ data array dalla classe. Semplice ed efficace. È tempo di implementare trova tutto() sul CommentRepository adesso.

function findAll () $ allCommentsData = $ this-> persistence-> retrieveAll (); $ commenti = array (); foreach ($ allCommentsData as $ commentData) $ comments [] = $ this-> commentFactory-> make ($ commentData); restituire $ commenti; 

trova tutto() chiamerà il RetrieveAll () metodo sulla nostra persistenza. Questo metodo fornisce una matrice di dati grezza. trova tutto() scorrerà ciclicamente ogni elemento e utilizzerà i dati necessari per essere passati alla fabbrica. La fabbrica fornirà uno Commento un tempo. Un array con questi commenti sarà costruito e restituito alla fine di trova tutto(). Semplice ed efficace.

Un altro metodo comune che si trova sui repository è cercare un oggetto o un gruppo di oggetti specifici in base alla loro chiave caratteristica. Ad esempio, tutti i nostri commenti sono collegati a un post sul blog da un $ postId variabile interna. Posso immaginare che nella logica di business del nostro blog vorremmo quasi sempre trovare tutti i commenti relativi a un post sul blog quando viene visualizzato quel post. Quindi un metodo chiamato findByPostId ($ id) mi sembra ragionevole.

function testItCanFindCommentsByBlogPostId () $ repository = new CommentRepository (); $ commentData1 = array (1, 'x', 'x', 'x', 'x'); $ comment1 = (new CommentFactory ()) -> make ($ commentData1); $ commentData2 = array (1, 'y', 'y', 'y', 'y'); $ comment2 = (nuovo CommentFactory ()) -> make ($ commentData2); $ commentData3 = array (3, 'y', 'y', 'y', 'y'); $ comment3 = (nuovo CommentFactory ()) -> make ($ commentData3); $ repository-> add (array ($ comment1, $ comment2)); $ Repository-> aggiungere ($ comment3); $ this-> assertEquals (array ($ comment1, $ comment2), $ repository-> findByPostId (1)); 

Creiamo solo tre semplici commenti. I primi due hanno lo stesso $ postId = 1, il terzo ha $ postID = 3. Li aggiungiamo tutti al repository e quindi ci aspettiamo un array con i primi due quando facciamo a findByPostId () per il $ postId = 1.

function findByPostId ($ postId) return array_filter ($ this-> findAll (), function ($ comment) usa ($ postId) return $ comment-> getPostId () == $ postId;); 

L'implementazione non potrebbe essere più semplice. Troviamo tutti i commenti usando i nostri già implementati trova tutto() metodo e filtriamo la matrice. Non abbiamo modo di chiedere la persistenza di fare il filtro per noi, quindi lo faremo qui. Il codice interrogherà ciascuno Commento oggetto e confrontarlo $ postId con quello che abbiamo inviato come parametro. Grande. Il test passa. Ma sento che ci siamo persi qualcosa.

function testItCanFindCommentsByBlogPostId () $ repository = new CommentRepository (); $ commentData1 = array (1, 'x', 'x', 'x', 'x'); $ comment1 = (new CommentFactory ()) -> make ($ commentData1); $ commentData2 = array (1, 'y', 'y', 'y', 'y'); $ comment2 = (nuovo CommentFactory ()) -> make ($ commentData2); $ commentData3 = array (3, 'y', 'y', 'y', 'y'); $ comment3 = (nuovo CommentFactory ()) -> make ($ commentData3); $ repository-> add (array ($ comment1, $ comment2)); $ Repository-> aggiungere ($ comment3); $ this-> assertEquals (array ($ comment1, $ comment2), $ repository-> findByPostId (1)); $ this-> assertEquals (array ($ comment3), $ repository-> findByPostId (3)); 

Aggiunta di una seconda asserzione per ottenere il terzo commento con il findByPostID () il metodo rivela il nostro errore. Ogni volta che puoi facilmente testare percorsi o casi extra, come nel nostro caso con una semplice asserzione aggiuntiva, dovresti. Queste semplici asserzioni o metodi di test possono rivelare problemi nascosti. Come nel nostro caso, array_filter () non reindicizza la matrice risultante. E mentre abbiamo una matrice con gli elementi corretti, gli indici sono incasinati.

1) RepositoryTest :: testItCanFindCommentsByBlogPostId Impossibile affermare che due array sono uguali. --- Previsto +++ Attuale @@ @@ Matrice (- 0 => Oggetto commento (...) + 2 => Oggetto commento (...))

Ora, potresti considerare questo un difetto di PHPUnit o una mancanza della tua logica aziendale. Tendo ad essere rigoroso con gli indici di array perché ho