Ayuda a crear un método flexible 'encontrar' de base en una clase de servicio utilizando el principio DRY

urante años, he estado reimplementando el mismo código una y otra vez (con evolución) sin encontrar algún método de manera limpia y eficiente, y abstrayéndolo.

El patrón es un método base 'find [Type] s' en mis capas de servicio que abstrae la creación de consultas selectas en un solo punto en el servicio, pero admite la capacidad de crear rápidamente métodos proxy más fáciles de usar (vea el ejemplo PostServivce :: método getPostById () a continuación).

Desafortunadamente, hasta ahora, no he podido cumplir con estos objetivos:

Reduce la posibilidad de errores introducidos por distintas implementacionesExponer opciones de parámetros válidos / inválidos a IDEs para autocompletar Siga el principio DRY

Mi implementación más reciente generalmente se parece al siguiente ejemplo. El método toma una variedad de condiciones y una variedad de opciones, y a partir de ellas crea y ejecuta una Doctrine_Query (la mayoría reescribí esto aquí hoy, por lo que puede haber algunos errores tipográficos / de sintaxis, no es un corte y pegado directo). @

class PostService
{
    /* ... */

    /**
     * Return a set of Posts
     *
     * @param Array $conditions Optional. An array of conditions in the format
     *                          array('condition1' => 'value', ...)
     * @param Array $options    Optional. An array of options 
     * @return Array An array of post objects or false if no matches for conditions
     */
    public function getPosts($conditions = array(), $options = array()) {
        $defaultOptions =  = array(
            'orderBy' => array('date_created' => 'DESC'),
            'paginate' => true,
            'hydrate' => 'array',
            'includeAuthor' => false,
            'includeCategories' => false,
        );

        $q = Doctrine_Query::create()
                        ->select('p.*')
                        ->from('Posts p');

        foreach($conditions as $condition => $value) {
            $not = false;
            $in = is_array($value);
            $null = is_null($value);                

            $operator = '=';
            // This part is particularly nasty :(
            // allow for conditions operator specification like
            //   'slug LIKE' => 'foo%',
            //   'comment_count >=' => 1,
            //   'approved NOT' => null,
            //   'id NOT IN' => array(...),
            if(false !== ($spacePos = strpos($conditions, ' '))) {
                $operator = substr($condition, $spacePost+1);
                $conditionStr = substr($condition, 0, $spacePos);

                /* ... snip validate matched condition, throw exception ... */
                if(substr($operatorStr, 0, 4) == 'NOT ') {
                  $not = true;
                  $operatorStr = substr($operatorStr, 4);
                }
                if($operatorStr == 'IN') {
                    $in = true;
                } elseif($operatorStr == 'NOT') {
                    $not = true;
                } else {
                    /* ... snip validate matched condition, throw exception ... */
                    $operator = $operatorStr;
                }

            }

            switch($condition) {
                // Joined table conditions
                case 'Author.role':
                case 'Author.id':
                    // hard set the inclusion of the author table
                    $options['includeAuthor'] = true;

                    // break; intentionally omitted
                /* ... snip other similar cases with omitted breaks ... */
                    // allow the condition to fall through to logic below

                // Model specific condition fields
                case 'id': 
                case 'title':
                case 'body':
                /* ... snip various valid conditions ... */
                    if($in) {
                        if($not) {
                            $q->andWhereNotIn("p.{$condition}", $value);
                        } else {
                            $q->andWhereIn("p.{$condition}", $value);
                        }
                    } elseif ($null) {
                        $q->andWhere("p.{$condition} IS " 
                                     . ($not ? 'NOT ' : '') 
                                     . " NULL");
                    } else {
                        $q->andWhere(
                            "p.{condition} {$operator} ?" 
                                . ($operator == 'BETWEEN' ? ' AND ?' : ''),
                            $value
                        );
                    }
                    break;
                default:
                    throw new Exception("Unknown condition '$condition'");
            }
        }

        // Process options

        // init some later processing flags
        $includeAuthor = $includeCategories = $paginate = false;
        foreach(array_merge_recursivce($detaultOptions, $options) as $option => $value) {
            switch($option) {
                case 'includeAuthor':
                case 'includeCategories':
                case 'paginate':
                /* ... snip ... */
                    $$option = (bool)$value;
                    break;
                case 'limit':
                case 'offset':
                case 'orderBy':
                    $q->$option($value);
                    break;
                case 'hydrate':
                    /* ... set a doctrine hydration mode into $hydration */ 
                    break;
                default:
                    throw new Exception("Invalid option '$option'");
            }
        }

        // Manage some flags...
        if($includeAuthor) {
            $q->leftJoin('p.Authors a')
              ->addSelect('a.*');
        } 

        if($paginate) {
            /* ... wrap query in some custom Doctrine Zend_Paginator class ... */
            return $paginator;
        }

        return $q->execute(array(), $hydration);
    }

    /* ... snip ... */
}

Phewf

Los beneficios de esta función base son:

it me permite admitir rápidamente nuevas condiciones y opciones a medida que el esquema evolucionait me permite un medio para implementar rápidamente condiciones globales en la consulta (por ejemplo, agregando una opción 'excludeDisabled' con un valor predeterminado verdadero y filtrando todos los modelos desactivados = 0, a menos que la persona que llama explícitamente diga lo contrario).it me permite crear rápidamente métodos nuevos y más simples de usar que el proxy vuelve a llamar al método findPosts. Por ejemplo
class PostService
{
    /* ... snip ... */

    // A proxy to getPosts that limits results to 1 and returns just that element
    public function getPost($conditions = array(), $options()) {
        $conditions['id'] = $id;
        $options['limit'] = 1;
        $options['paginate'] = false;
        $results = $this->getPosts($conditions, $options);
        if(!empty($results) AND is_array($results)) {
            return array_shift($results);
        }
        return false;
    }

    /* ... docblock ...*/       
    public function getPostById(int $id, $conditions = array(), $options()) {
        $conditions['id'] = $id;
        return $this->getPost($conditions, $options);
    }

    /* ... docblock ...*/
    public function getPostsByAuthorId(int $id, $conditions = array(), $options()) {
        $conditions['Author.id'] = $id;
        return $this->getPosts($conditions, $options);
    }

    /* ... snip ... */
}

LosMAYOos inconvenientes de @ con este enfoque son:

El mismo método monolítico 'find [Model] s' se crea en todos los servicios de acceso a modelos, y la mayoría de las veces cambia la construcción del cambio de condición y los nombres de la tabla base.No hay una forma sencilla de realizar operaciones de condición AND / OR. Todas las condiciones AND explícitamente.Introduce muchas oportunidades para errores tipográficosIntroduce muchas oportunidades para interrupciones en la API basada en la convención (por ejemplo, un servicio posterior puede requerir la implementación de una convención de sintaxis diferente para especificar la opción orderBy, que se vuelve tediosa para hacer un back-port a todos los servicios anteriores).Viola los principios DRY.as condiciones y opciones válidas están ocultas para los analizadores de autocompletado IDE y los parámetros de opciones y condiciones requieren una larga explicación del bloque de documentos para rastrear las opciones permitida

En los últimos días he intentado desarrollar una solución más OO para este problema, pero he sentido que estoy desarrollando una solución demasiado compleja que será demasiado rígida y restrictiva de usar.

La idea en la que estaba trabajando era algo similar a lo siguiente (el proyecto actual será Doctrine2 para su información, así que un pequeño cambio allí) ...

namespace Foo\Service;

use Foo\Service\PostService\FindConditions; // extends a common \Foo\FindConditions abstract
use Foo\FindConditions\Mapper\Dql as DqlConditionsMapper;

use Foo\Service\PostService\FindOptions; // extends a common \Foo\FindOptions abstract
use Foo\FindOptions\Mapper\Dql as DqlOptionsMapper;

use \Doctrine\ORM\QueryBuilder;

class PostService
{
    /* ... snip ... */
    public function findUsers(FindConditions $conditions = null, FindOptions $options = null) {

        /* ... snip instantiate $q as a Doctrine\ORM\QueryBuilder ... */

        // Verbose
        $mapper = new DqlConditionsMapper();
        $q = $mapper
                ->setQuery($q)
                ->setConditions($conditions)
                ->map();

        // Concise
        $optionsMapper = new DqlOptionsMapper($q);        
        $q = $optionsMapper->map($options);


        if($conditionsMapper->hasUnmappedConditions()) {
            /* .. very specific condition handling ... */
        }
        if($optionsMapper->hasUnmappedConditions()) {
            /* .. very specific condition handling ... */
        }

        if($conditions->paginate) {
            return new Some_Doctrine2_Zend_Paginator_Adapter($q);
        } else {
            return $q->execute();
        }
    }

    /* ... snip ... */
}

Y, por último, una muestra de la clase Foo \ Service \ PostService \ FindConditions:

namespace Foo\Service\PostService;

use Foo\Options\FindConditions as FindConditionsAbstract;

class FindConditions extends FindConditionsAbstract {

    protected $_allowedOptions = array(
        'user_id',
        'status',
        'Credentials.credential',
    );

    /* ... snip explicit get/sets for allowed options to provide ide autocompletion help */
}

Foo \ Options \ FindConditions y Foo \ Options \ FindOptions son realmente bastante similares, por lo que, al menos por ahora, ambos extienden una clase principal común de Foo \ Options. Esta clase principal maneja la inicialización de las variables permitidas y los valores predeterminados, accede a las opciones establecidas, restringe el acceso solo a las opciones definidas y proporciona una interfaz de iterador para que DqlOptionsMapper recorra las opciones.

Desafortunadamente, después de piratear esto durante unos días, me siento frustrado con la complejidad de este sistema. Como es, todavía no hay soporte en esto para grupos de condiciones y condiciones OR, y la capacidad de especificar operadores de comparación de condiciones alternativas ha sido un completo atolladero de crear una envoltura de clase Foo \ Options \ FindConditions \ Comparison alrededor de un valor al especificar un FindConditions valor $conditions->setCondition('Foo', new Comparison('NOT LIKE', 'bar'));).

Preferiría usar la solución de otra persona si existiera, pero aún no he encontrado nada que haga lo que estoy buscando.

Me gustaría ir más allá de este proceso y volver a construir el proyecto en el que estoy trabajando, pero ni siquiera veo un final a la vista.

Entonces, Stack Overflowers: - ¿Hay alguna forma mejor de proporcionar los beneficios que he identificado sin incluir los inconvenientes?