Ajude a criar um método 'find' de base flexível em uma classe de serviço usando o princípio DRY

Por anos agora eu tenho reimplementado o mesmo código repetidamente (com a evolução) sem encontrar algum método de forma limpa e eficiente, abstraindo-

O padrão é o método base 'find [Type] s' em minhas camadas de serviço, que abstrai a criação de consultas selecionadas em um único ponto do serviço, mas suporta a capacidade de criar rapidamente métodos de proxy mais fáceis de usar (veja o exemplo PostServivce: método getPostById () abaixo).

Infelizmente, até agora, não consegui cumprir esses objetivos:

eduzir a possibilidade de erros introduzidos por re-implementação distintExponha opções de parâmetro válidas / inválidas para IDEs para preenchimento automático Siga o princípio DRY

Minha implementação mais recente geralmente se parece com o exemplo a seguir. O método utiliza uma variedade de condições e uma variedade de opções, e a partir disso cria e executa uma Doctrine_Query (eu a reescrevi hoje aqui hoje, para que possa haver alguns erros de digitação / sintaxe, não é um recorte e colagem direta).

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

Os benefícios desta função base são:

permite que eu suporte rapidamente novas condições e opções à medida que o esquema evoluiit me permite um meio de implementar rapidamente condições globais na consulta (por exemplo, adicionar uma opção 'excludeDisabled' com o padrão true e filtrar todos os modelos disabled = 0, a menos que o chamador diga explicitamente de maneira diferente).it me permite criar rapidamente novos métodos mais simples de usar, que o proxy chama de volta para o método findPosts. Por exemplo
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 ... */
}

OPRINCIPAs inconvenientes dessa abordagem sã

O mesmo método monolítico 'find [Model] s' é criado em todos os serviços de acesso a modelos, principalmente com a construção da troca de condição e os nomes das tabelas de base sendo alterado Nenhuma maneira simples de executar operações de condicionamento E / OU. Todas as condições explicitamente ANDed.Introduz muitas oportunidades para erros de digitaçãoIntroduz muitas oportunidades de interrupções na API baseada em convenções (por exemplo, um serviço posterior pode exigir a implementação de uma convenção de sintaxe diferente para especificar a opção orderBy, que se torna tediosa na porta traseira de todos os serviços anterioresrincípios de Violação de DR Condições e opções válidas estão ocultas nos analisadores de preenchimento automático do IDE e os parâmetros de opções e condições exigem uma explicação longa do bloco de documentos para rastrear as opções permitida

os últimos dias, tentei desenvolver uma solução mais OO para esse problema, mas senti que estou desenvolvendo uma solução muito complexa demais, que será muito rígida e restritiva de usa

A idéia em que eu estava trabalhando era algo parecido com o seguinte (o projeto atual será o Doctrine2 fyi, portanto, uma pequena mudança lá) ...

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 ... */
}

or fim, uma amostra da classe 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 e Foo \ Options \ FindOptions são realmente muito semelhantes; portanto, pelo menos por enquanto, eles estendem uma classe pai comum Foo \ Options. Essa classe pai lida com a inicialização de variáveis permitidas e valores padrão, acessando as opções definidas, restringindo o acesso apenas às opções definidas e fornecendo uma interface de iterador para o DqlOptionsMapper percorrer as opções.

Infelizmente, depois de hackear isso há alguns dias, estou me sentindo frustrado com a complexidade desse sistema. Como é, ainda não há suporte para grupos de condições e condições OR, e a capacidade de especificar operadores de comparação de condições alternativas foi um atolamento completo da criação de uma classe Foo \ Options \ FindConditions \ Comparison em torno de um valor ao especificar uma FindConditions valor $conditions->setCondition('Foo', new Comparison('NOT LIKE', 'bar'));).

Prefiro usar a solução de outra pessoa, se existir, mas ainda não encontrei nada que faça o que estou procurand

Gostaria de ir além desse processo e voltar a construir o projeto em que estou trabalhando, mas nem vejo um fim à vist

Então, Stack Overflowers: - Existe alguma maneira melhor de fornecer os benefícios que identifiquei sem incluir os inconveniente

questionAnswers(1)

yourAnswerToTheQuestion