Design Patterns – Repository Pattern and Custom Queries/REST API Approach

design-patternsdomain-driven-designrepository

I'm in the early stages of working on an application that is using the Repository Pattern to provide a data access abstraction. This application will have some form of a simple REST API but I'm not sure how to approach this using repositories. To illustrate this, a typical (very simplified) example of a repository is something like this:

<?php
$repo = new PostRepository(); // this would be resolved from an IoC container
$post = $repo->getByEmail('foo@wherever.com');

or maybe a little less rigid, like this:

$post = $repo->getBy('first_name', 'Bob');

But the pattern doesn't seem suited to anything more complicated like, say, the query from this url:

api.example.com/posts?firstName=Bob&lastName=Doe&email=foo@example.com&with=comments

It seems like to handle stuff like that (let alone any much more complicated queries that many APIs support) you would pretty much end up having to reimplement the underlying ORM, or create a very leaky abstraction, neither of which seems ideal. Am I going about this wrong?

Best Answer

A way to find instances by variable criteria, as suggested by scriptin's anwer is the right way. However, you don't necessarily need the full weight of the frameworks mentioned.

I have recently programmed exactly what you described. The repository needs meta-information about the fields ob the object you are requesting - a config class is a good option, and a way to check whether a request is valid for an object (a way to validate criteria - based on the meta-information).

In my implementation, I unified the repository pattern with the identity-map pattern. The repository holds a collection of entities, initially empty. An incoming selectByCriteria-request will first check the collection, and, if no entity is found, will have the criteria passed to a method for transformation into a db-query and executed. Returned rows are stored as entities in the repository-collection and then returned to the caller of the method as their own 'sub-collection'. Potential future requests to those resources in the lifetime of the application will then be fulfilled from the collection held in memory.

You need a way to normalize selection-criteria. The kind of requests a REST API has to fulfill can usually easily be normalized to Disjunctive Normal Form (DNF) or Conjunctive Normal Form (CNF). For simplicity of code and logic, I chose DNF for my implementation - lightweight, as a simple depth-3 array.

A set of selection criteria would then look like this:

array(                                               
    array(                                           
        array('fieldName','comparator','fieldValue'),
        **AND**                                      
        array('fieldName','comparator','fieldValue') 
        ...                                          
    ),                                               
    **OR**                                           
    array(                                           
        array('fieldName','comparator','fieldValue'),
        **AND**                                      
        array('fieldName','comparator','fieldValue') 
        ...                                          
    )                                                
    ...                                              
)                                                    

(The comparators don't have to be strings, they can be class-constants - the values don't have to be strings either - indeed it's more transparent to use the types they are being compared to - especially if we need to construct a DB-Query later on)

A service-object first validates a set of criteria against a config-object for a data-class, by checking the structure of the array, if field-names exist, if the comparators are allowed and if the field-types can be meaningfully compared with the given values and comparators. If the criteria-array is valid, it is checked with foreach-loops against the collection. The advantage of DNF here is that a loop can terminate checking each disjunct once the first conjunct in it is found to be invalid, and it can terminate the entire outer loop (over the disjuncts) once the first disjunct is found to be valid - simplifying the code.

Selecting from a collection is implemented like this:

    $resultCollection  = $this->getEmptyCollection;

    if(!$this->criteriaValidationService->validateCriteria($dataObjectConfig,$criteria)) {
        return $resultCollection;
    }

    foreach ($this->elements as $instance) {                                                           
        foreach ($criteria as $disjunctCriteria) {                                                     
            $isValidDisjunct = TRUE;                                                                   
            foreach ($disjunctCriteria as $conjunctCriterion) {                                        
                $fieldName    = $conjunctCriterion[0];                                                 
                $comparator   = $conjunctCriterion[1];                                                 
                if(isset($conjunctCriterion[2])) {                                                          
                    $compareValue = $conjunctCriterion[2];                                             
                }                                                                                      

                if ($comparator == 'IS NULL') {                                                        
                    if(!(is_null($instance->$fieldName))) {                                            
                        $isValidDisjunct = FALSE;                                                      
                        break;                                                                         
                    }                                                                                  
                } elseif ($comparator == 'IS NOT NULL') {                                              
                    if(is_null($instance->$fieldName)) {                                               
                        $isValidDisjunct = FALSE;                                                      
                        break;                                                                         
                    }                                                                                  
                } elseif ($comparator == '=') {                                                        
                    if (!(isset($instance->$fieldName) && $instance->$fieldName == $compareValue)) {   
                        $isValidDisjunct = FALSE;                                                      
                        break;                                                                         
                    }                                                                                  
                } elseif ($comparator == '!=') {                                                       
                    if (!($instance->$fieldName != $compareValue)) {                                   
                        $isValidDisjunct = FALSE;                                                      
                        break;                                                                         
                    }                                                                                  
                } elseif ($comparator == '>') {                                                        
                    if (!(isset($instance->$fieldName) && ($instance->$fieldName > $compareValue))) {  
                        $isValidDisjunct = FALSE;                                                      
                        break;                                                                         
                    }                                                                                  
                } elseif ($comparator == '<') {                                                        
                    if (!(isset($instance->$fieldName) && ($instance->$fieldName < $compareValue))) {  
                        $isValidDisjunct = FALSE;                                                      
                        break;                                                                         
                    }                                                                                  
                } elseif ($comparator == '<=') {                                                       
                    if (!(isset($instance->$fieldName) && !($instance->$fieldName <= $compareValue))) {
                        $isValidDisjunct = FALSE;                                                      
                        break;                                                                         
                    }                                                                                  
                } elseif ($comparator == '>=') {                                                       
                    if (!(isset($instance->$fieldName) || ($instance->$fieldName >= $compareValue))) { 
                        $isValidDisjunct = FALSE;                                                      
                        break;                                                                         
                    }                                                                                  
                } elseif ($comparator == 'IN') {                                                       
                    if (!(in_array($instance->$fieldName, $compareValue))) {                           
                        $isValidDisjunct = FALSE;                                                      
                        break;                                                                         
                    }                                                                                  
                } elseif ($comparator == 'LIKE') {                                                     
                    //Replace SQL-Wildcard with fnmatch-wildcard                                     
                    $compareValue = preg_replace('/(?<!\\\\)%/', '*', $compareValue);                              
                    if (!(fnmatch($compareValue, $instance->$fieldName))) {                            
                        $isValidDisjunct = FALSE;                                                      
                        break;                                                                         
                    }                                                                                  
                }                                                                                      
            }                                                                                          
            if ($isValidDisjunct) {                                                                    
                $resultCollection->add($instance, TRUE);
                break;                                               
            }                                                                                          
        }                                                                                              
    }                                                                                                  
    return $resultCollection;