For many years I repeated the same code over and over (with evolution), without finding any method that would be clean and efficient, an abstraction.
A template is the basic method find [Type] s in my service layers, which abstracts the choice of creating a request for a single point in the service, but supports the ability to quickly create easier-to-use proxy methods (see the PostServivce :: getPostById () example below) )
Unfortunately, so far I have not been able to fulfill these goals:
- Reduce the likelihood of errors resulting from a separate re-implementation.
- Provide valid / invalid parameter options for IDE for autocomplete
- Follow the DRY Principle
My last implementation usually looks something like this: A method accepts many conditions, and an array of parameters creates and executes Doctrine_Query from them (I basically rewrote it here today, so there may be some typo / syntax errors, this is not a direct cut and paste).
class PostService
{
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 = '=';
if(false !== ($spacePos = strpos($conditions, ' '))) {
$operator = substr($condition, $spacePost+1);
$conditionStr = substr($condition, 0, $spacePos);
if(substr($operatorStr, 0, 4) == 'NOT ') {
$not = true;
$operatorStr = substr($operatorStr, 4);
}
if($operatorStr == 'IN') {
$in = true;
} elseif($operatorStr == 'NOT') {
$not = true;
} else {
$operator = $operatorStr;
}
}
switch($condition) {
case 'Author.role':
case 'Author.id':
$options['includeAuthor'] = true;
case 'id':
case 'title':
case 'body':
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'");
}
}
$includeAuthor = $includeCategories = $paginate = false;
foreach(array_merge_recursivce($detaultOptions, $options) as $option => $value) {
switch($option) {
case 'includeAuthor':
case 'includeCategories':
case 'paginate':
$$option = (bool)$value;
break;
case 'limit':
case 'offset':
case 'orderBy':
$q->$option($value);
break;
case 'hydrate':
break;
default:
throw new Exception("Invalid option '$option'");
}
}
if($includeAuthor) {
$q->leftJoin('p.Authors a')
->addSelect('a.*');
}
if($paginate) {
return $paginator;
}
return $q->execute(array(), $hydration);
}
}
Phewf
The advantages of this basic function:
- it allows me to quickly maintain new conditions and parameters as the scheme develops.
- it allows me to quickly implement global conditions for the request (for example, by adding the 'excludeDisabled' option with the default value true and filtering out all disconnected = 0 models if the caller does not say differently).
- it allows me to quickly create new, easier to use methods that the proxy calls the findPosts method. For instance:
class PostService
{
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;
}
public function getPostById(int $id, $conditions = array(), $options()) {
$conditions['id'] = $id;
return $this->getPost($conditions, $options);
}
public function getPostsByAuthorId(int $id, $conditions = array(), $options()) {
$conditions['Author.id'] = $id;
return $this->getPosts($conditions, $options);
}
}
MAJOR disadvantages with this approach:
- find [Model] s ' , .
- AND/OR conditon. AND.
- API (, orderBy, ).
- .
- IDE, .
OO- , , , .
, , - ( Doctrine2 fyi, )...
namespace Foo\Service;
use Foo\Service\PostService\FindConditions;
use Foo\FindConditions\Mapper\Dql as DqlConditionsMapper;
use Foo\Service\PostService\FindOptions;
use Foo\FindOptions\Mapper\Dql as DqlOptionsMapper;
use \Doctrine\ORM\QueryBuilder;
class PostService
{
public function findUsers(FindConditions $conditions = null, FindOptions $options = null) {
$mapper = new DqlConditionsMapper();
$q = $mapper
->setQuery($q)
->setConditions($conditions)
->map();
$optionsMapper = new DqlOptionsMapper($q);
$q = $optionsMapper->map($options);
if($conditionsMapper->hasUnmappedConditions()) {
}
if($optionsMapper->hasUnmappedConditions()) {
}
if($conditions->paginate) {
return new Some_Doctrine2_Zend_Paginator_Adapter($q);
} else {
return $q->execute();
}
}
}
, 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',
);
}
Foo\Options\FindConditions Foo\Options\FindOptions , , , Foo\Options. , , DqlOptionsMapper .
, , . , OR - , Foo\Options\FindConditions\Comparison, FindConditions ($conditions->setCondition('Foo', new Comparison('NOT LIKE', 'bar'));).
- , , , , .
, , .
, Stack Overflowers:
- , , , ?