Skip to content

Commit

Permalink
Merge pull request #1744 from bolt/bugfix/clean-selectquery
Browse files Browse the repository at this point in the history
Clean up and refactor SelectQuery
  • Loading branch information
bobdenotter committed Aug 21, 2020
2 parents 9b81759 + cb447a3 commit d35e4d2
Showing 1 changed file with 104 additions and 79 deletions.
183 changes: 104 additions & 79 deletions src/Storage/SelectQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Bolt\Configuration\Config;
use Bolt\Configuration\Content\ContentType;
use Bolt\Doctrine\JsonHelper;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Query\Expr\Base;
use Doctrine\ORM\Query\ParameterTypeInferer;
use Doctrine\ORM\QueryBuilder;
Expand Down Expand Up @@ -74,6 +75,9 @@ class SelectQuery implements QueryInterface
'author',
];

/** @var string */
protected $anything = 'anything';

/** @var array */
private $referenceJoins = [];

Expand Down Expand Up @@ -184,6 +188,7 @@ public function getWhereExpression(): ?Base
return null;
}
$expr = $this->qb->expr()->andX();
$em = $this->getQueryBuilder()->getEntityManager();

$this->referenceJoins = [];
$this->taxonomyJoins = [];
Expand All @@ -192,19 +197,26 @@ public function getWhereExpression(): ?Base
foreach ($this->filters as $filter) {
if (in_array($filter->getKey(), $this->coreFields, true)) {
// For fields like `id`, `createdAt` and `status`, which are in the main `bolt_content` table
$expr = $expr->add($filter->getExpression());
$expr = $expr->add($this->getCoreFieldExpression($filter));
} elseif (in_array($filter->getKey(), $this->referenceFields, true)) {
// Special case for filtering on 'author'
$this->referenceJoins[$filter->getKey()] = $filter;
$expr = $expr->add($filter->getExpression());
$expr = $expr->add($this->getReferenceFieldExpression($filter));
} elseif (in_array($filter->getKey(), $this->getTaxonomyFields(), true)) {
// For when we're using a taxonomy type in the `where`
$this->taxonomyJoins[$filter->getKey()] = $filter;
$filterExpression = sprintf('taxonomies_%s.slug = :%s', $filter->getKey(), key($filter->getParameters()));
$expr = $expr->add($filterExpression);
$expr = $expr->add($this->getTaxonomyFieldExpression($filter));
} elseif (in_array($filter->getKey(), [$this->anything], true)) {
// build all expressions
// put them in a wrapper OR expression
$anythingExpr = $this->qb->expr()->OrX();
$core = $this->getCoreFieldExpression($filter);
$reference = $this->getReferenceFieldExpression($filter);
$taxonomy = $this->getTaxonomyFieldExpression($filter);
$regular = $this->getRegularFieldExpression($filter, $em);
$anythingExpr->addMultiple([$core, $reference, $taxonomy, $regular]);
$expr = $expr->add($anythingExpr);
} else {
// This means the name / value in the `where` is stored in the `bolt_field` table
$this->fieldJoins[$filter->getKey()] = $filter;
$expr = $expr->add($this->getRegularFieldExpression($filter, $em));
}
}

Expand Down Expand Up @@ -251,17 +263,18 @@ public function build(): QueryBuilder

$dateFields = $this->getDateFields();

if ($this->getWhereExpression()) {
$query->andWhere($this->getWhereExpression());
$whereExpression = $this->getWhereExpression();
if ($whereExpression) {
$query->andWhere($whereExpression);
}

foreach ($this->getWhereParameters() as $key => $param) {
$fieldName = current(explode('_', $key));

// Use strtotime on 'date' fields to allow selections like "today", "in 3 weeks" or "this year"
if (in_array($fieldName, $dateFields, true) && (strtotime($param) !== false)) {
$param = date('c', strtotime($param));
}

$query->setParameter($key, $param, ParameterTypeInferer::inferType($param));
}

Expand Down Expand Up @@ -325,7 +338,6 @@ protected function processFilters(): void

foreach ($this->params as $key => $value) {
$this->parser->setAlias('content');

$filter = $this->parser->getFilter($key, $value);
if ($filter) {
$this->addFilter($filter);
Expand Down Expand Up @@ -358,69 +370,13 @@ public function doTaxonomyJoins(): void
*/
public function doFieldJoins(): void
{
$em = $this->qb->getEntityManager();

foreach ($this->fieldJoins as $key => $filter) {
$index = $this->getAndIncrementIndex();
$contentAlias = 'content_' . $index;
$fieldsAlias = 'fields_' . $index;
$translationsAlias = 'translations_' . $index;
$keyParam = 'field_' . $index;

$originalLeftExpression = 'content.' . $key;
// LOWER() added to query to enable case insensitive search of JSON values. Used in conjunction with converting $params of setParameter() to lowercase.
$newLeftExpression = JsonHelper::wrapJsonFunction('LOWER(' . $translationsAlias . '.value)', null, $em->getConnection());

$where = $filter->getExpression();
$exactWhere = str_replace($originalLeftExpression, $newLeftExpression, $where);

// add containsWhere to allow searching of fields with Muiltiple JSON values (eg. Selectfield with mutiple entries).
preg_match_all('/\:([a-z]*_[0-9]+)/', $where, $matches);
$clauses = array_map(function ($m) use ($translationsAlias) {
return 'LOWER(' . $translationsAlias . '.value) LIKE :' . $m . '_JSON';
}, $matches[1]);
$containsWhere = implode(' OR ', $clauses);

// Create the subselect to filter on the value of fields
$innerQuery = $em
->createQueryBuilder()
->select($contentAlias . '.id')
->from(\Bolt\Entity\Content::class, $contentAlias)
->innerJoin($contentAlias . '.fields', $fieldsAlias)
->innerJoin($fieldsAlias . '.translations', $translationsAlias)
->andWhere($exactWhere);

if (! empty($containsWhere)) {
$innerQuery->OrWhere($containsWhere);
}

// Unless the field to which the 'where' applies is `anyColumn`, we
// Make certain it's narrowed down to that fieldname
if ($key !== 'anyField') {
$innerQuery->andWhere($fieldsAlias . '.name = :' . $keyParam);
$this->qb->setParameter($keyParam, $key);
} else {
//added to include taxonomies to be searched as part of contenttype filter at the backend and frontend if anyField param is set.
foreach ($filter->getParameters() as $value) {
$innerQuery->leftJoin($contentAlias . '.taxonomies', 'taxonomies_' . $index);
$this->qb->setParameter($key . '_1', $value);
$filterExpression = sprintf('LOWER(taxonomies_%s.slug) LIKE :%s', $index, $key . '_1');
$innerQuery->orWhere($filterExpression);
}
}

$contentAlias = 'content';
$fieldsAlias = 'fields_' . $key;
$translationsAlias = 'translations_' . $key;
$this->qb
->andWhere($this->qb->expr()->in('content.id', $innerQuery->getDQL()));

foreach ($filter->getParameters() as $key => $value) {
$value = JsonHelper::wrapJsonFunction(null, $value, $em->getConnection());
$this->qb->setParameter($key, $value);

if (! empty($containsWhere)) {
//remove % if present. Reformat JSON to work with both json enabled platforms and non json platforms.
$this->qb->setParameter($key . '_JSON', '%"' . str_replace(['["', '"]', '%'], '', $value) . '"%');
}
}
->leftJoin($contentAlias . '.fields', $fieldsAlias)
->leftJoin($fieldsAlias . '.translations', $translationsAlias);
}
}

Expand Down Expand Up @@ -472,13 +428,6 @@ public function incrementIndex(): void
$this->index++;
}

public function getAndIncrementIndex()
{
$this->incrementIndex();

return $this->getIndex();
}

public function getCoreFields(): array
{
return $this->coreFields;
Expand All @@ -488,4 +437,80 @@ public function getConfig(): Config
{
return $this->config;
}

private function getCoreFieldExpression(Filter $filter): string
{
if ($filter->getKey() !== $this->anything) {
return $filter->getExpression();
}

$original = $filter->getExpression();
$expr = $this->qb->expr()->orX();

foreach ($this->coreFields as $core) {
$expr->add(preg_replace('/^(content\.)(anything)/', '$1' . $core, $original));
}

return $expr->__toString();
}

private function getReferenceFieldExpression(Filter $filter): string
{
if ($filter->getKey() !== $this->anything) {
$this->referenceJoins[$filter->getKey()] = $filter;

return $filter->getExpression();
}

$this->referenceJoins['author'] = 'author';

$original = $filter->getExpression();
$expr = $this->qb->expr()->orX();

foreach ($this->referenceFields as $reference) {
$expr->add(preg_replace('/^(content\.)(anything)/', 'content.' . $reference, $original));
}

return $expr->__toString();
}

private function getTaxonomyFieldExpression(Filter $filter): string
{
$this->taxonomyJoins[$filter->getKey()] = $filter;

return sprintf('taxonomies_%s.slug = :%s', $filter->getKey(), key($filter->getParameters()));
}

private function getRegularFieldExpression(Filter $filter, EntityManager $em): string
{
$this->fieldJoins[$filter->getKey()] = $filter;
$expr = $this->qb->expr()->andX();

// where clause for the value of the field
$valueAlias = sprintf('translations_%s.value', $filter->getKey());

$originalLeftExpression = 'content.' . $filter->getKey();
// LOWER() added to query to enable case insensitive search of JSON values. Used in conjunction with converting $params of setParameter() to lowercase.
$newLeftExpression = JsonHelper::wrapJsonFunction('LOWER(' . $valueAlias . ')', null, $em->getConnection());
$valueWhere = $filter->getExpression();
$valueWhere = str_replace($originalLeftExpression, $newLeftExpression, $valueWhere);
$expr->add($valueWhere);

// where clause for the name of the field
if (! in_array($filter->getKey(), ['anyField', $this->anything], true)) {
// Add to DQL where clause
$nameAlias = sprintf('fields_%s.name', $filter->getKey());
$nameParam = 'field_' . $filter->getKey();
$nameExpression = sprintf('%s = :%s', $nameAlias, $nameParam);
$expr->add($nameExpression);

// Create filter to set the parameter
$nameFilter = new Filter();
$nameFilter->setKey($nameParam);
$nameFilter->setParameter($nameParam, $filter->getKey());
$this->addFilter($nameFilter);
}

return $expr->__toString();
}
}

0 comments on commit d35e4d2

Please sign in to comment.