В течение многих лет я переопределял один и тот же код снова и снова (с развитием), не находя какой-то метод чистого и эффективного его абстрагирования.
Шаблон - это базовый 'find [Тип] '' в моих сервисных слоях, который абстрагирует создание запроса на выборку от одной точки в сервисе, но поддерживает возможность быстрого создания более простых в использовании прокси-методов (см. пример метода PostServivce :: getPostById () ниже).
К сожалению, до сих пор мне не удавалось достичь этих целей:
- Уменьшить вероятность ошибок, вызванных отдельной повторной реализацией
- Предоставить допустимые / недействительные параметры параметровв IDE для автозаполнения
- Следуйте принципу DRY
Моя последняя реализация обычно выглядит примерно так, как показано в следующем примере.Метод принимает массив условий и массив опций, и из них создает и выполняет Doctrine_Query (сегодня я в основном переписал это здесь, поэтому могут быть некоторые ошибки опечаток / синтаксиса, это не прямое вырезание и вставка).
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
Преимущества этой базовой функции:
- , что позволяет мне быстро поддерживать новые условия и параметры, каксхема развивается
- , что позволяет мне быстро реализовывать глобальные условия в запросе (например, добавив опцию 'excludeDisabled' со значением по умолчанию true и отфильтровав все модели disabled = 0, если только вызывающая сторона явно не указана)говорит по-другому).
- это позволяет мне быстро создавать новые, более простые в использовании методы, которые прокси вызывает обратно к методу findPosts.Например:
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 ... */
}
Недостатки MAJOR при таком подходе:
- Та же самая монолитная 'находка [Метод Model] s создается в каждой службе, обращающейся к модели, при этом в основном меняются только конструкция переключателя условия и имена базовых таблиц.
- Нет простого способа выполнения операций И / ИЛИ.Все условия в явном виде ANDed.
- Вводит множество возможностей для ошибок опечаток
- Вводит множество возможностей для разрывов в API, основанном на соглашениях (например, для более поздней службы может потребоваться реализация другого соглашения о синтаксисе для указанияопция orderBy, которая становится утомительной для обратного переноса на все предыдущие службы).
- Нарушение принципов DRY.
- Допустимые условия и параметры скрыты для анализаторов автозаполнения IDE и параметров параметров и условийтребуется длинное объяснение блока документации для отслеживания разрешенных опций.
В течение последних нескольких дней я пытался разработать более OO-решение этой проблемы, но мне показалось, что я разрабатываю слишком сложное решениекоторый будет слишком жестким и ограничительным для использования.
Идея, над которой я работал, была чем-то вроде следующего (текущий проект будет Doctrine2 fyi, так что небольшие изменения там) ...
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 ... */
}
И, наконец, образец Foo \ SerВайс \ 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 и Foo \ Options \ FindOptions действительно очень похожи, поэтому, по крайней мере, на данный момент они оба расширяют общий родительский класс Foo \ Options.Этот родительский класс обрабатывает инициализацию разрешенных переменных и значений по умолчанию, получает доступ к заданным параметрам, ограничивает доступ только к определенным параметрам и предоставляет интерфейс итератора для DqlOptionsMapper для циклического перебора параметров.
К сожалению, после взлома этого дляЧерез несколько дней я чувствую разочарование по поводу сложности этой системы.Как и раньше, в этой группе до сих пор нет поддержки групп условий и условий ИЛИ, и возможность указать альтернативные операторы сравнения условий была полным затруднением при создании класса Foo \ Options \ FindConditions \ Comparison, обернутого вокруг значения при указании FindConditions.значение ($conditions->setCondition('Foo', new Comparison('NOT LIKE', 'bar'));
).
Я бы предпочел использовать чье-то решение, если бы оно существовало, но мне еще не приходилось сталкиваться с тем, что делает то, что я ищу.
Я бы хотел выйти за рамки этого процесса ивернемся к фактическому созданию проекта, над которым я работаю, но я даже не вижу конца.
Итак, Stack Overflowers: - Есть ли лучший способ, который обеспечит преимущества, которые я определил безвключая недостатки?