Skip to content

Commit 46a0e9c

Browse files
committed
fix: nested conditions with loose operators now work
1 parent e45ca1c commit 46a0e9c

4 files changed

Lines changed: 138 additions & 94 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,16 @@ $products = $manager->getRepository(Product::class)
180180
]),
181181
'stock_status' => 'instock',
182182
]);
183+
184+
// Fetch two products by their SKU and two by their ID
185+
$products = $manager->getRepository(Product::class)
186+
->findBy([
187+
new NestedCondition(NestedCondition::OPERATOR_OR, [
188+
'sku' => new Operand(['woo-tshirt', 'woo-single'], Operand::OPERATOR_IN),
189+
'id' => new Operand([19, 20], Operand::OPERATOR_IN),
190+
]),
191+
]);
192+
// count($products) === 4
183193
```
184194

185195
### EAV relationship conditions

src/Bridge/Repository/AbstractEntityRepository.php

Lines changed: 102 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Williarin\WordpressInterop\Bridge\Repository;
66

7+
use Doctrine\DBAL\Query\Expression\CompositeExpression;
78
use Doctrine\DBAL\Query\QueryBuilder;
89
use JetBrains\PhpStorm\ArrayShape;
910
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -29,7 +30,6 @@
2930

3031
abstract class AbstractEntityRepository implements EntityRepositoryInterface
3132
{
32-
use NestedCriteriaTrait;
3333
use FindByTrait;
3434
use EntityPropertiesTrait;
3535

@@ -326,7 +326,7 @@ protected function handleSpecialCriteria(
326326
}
327327

328328
if ($value instanceof NestedCondition) {
329-
$this->createNestedCriteria($queryBuilder, $criteria[$field]->getCriteria(), $value);
329+
$this->createNestedCriteria($queryBuilder, $criteria[$field]);
330330

331331
return self::IS_SPECIAL_CRITERIA;
332332
}
@@ -360,46 +360,55 @@ protected function handleRegularCriteria(
360360
string $entityClassName = null,
361361
int $aliasNumber = null,
362362
): void {
363+
$queryBuilder->andWhere($this->getWhereExpressionFromCriteriaField(
364+
$queryBuilder,
365+
$criteria,
366+
$field,
367+
$value,
368+
$entityClassName,
369+
$aliasNumber,
370+
));
371+
}
372+
373+
protected function addOrderByClause(QueryBuilder $queryBuilder, ?array $orderBy): void
374+
{
375+
if (!empty($orderBy)) {
376+
foreach ($orderBy as $field => $orientation) {
377+
$this->validateFieldName($field, static::FALLBACK_ENTITY, static::TABLE_NAME);
378+
379+
if (!in_array(strtolower($orientation), ['asc', 'desc'], true)) {
380+
throw new InvalidOrderByOrientationException($orientation);
381+
}
382+
383+
$queryBuilder->addOrderBy($field, $orientation);
384+
}
385+
}
386+
}
387+
388+
private function getWhereExpressionFromCriteriaField(
389+
QueryBuilder $queryBuilder,
390+
array $criteria,
391+
string $field,
392+
mixed $value,
393+
string $entityClassName = null,
394+
int $aliasNumber = null,
395+
): CompositeExpression {
363396
$snakeField = u($field)
364397
->snake()
365398
->toString()
366399
;
367400
$parameter = ":{$snakeField}";
368401
$operator = '=';
402+
$prefixedField = $field;
403+
$expression = $queryBuilder->expr();
369404

370405
if ($criteria[$field] instanceof Operand) {
371-
$operator = $criteria[$field]->getOperator();
372-
373-
if (
374-
in_array(
375-
$operator,
376-
[Operand::OPERATOR_IN, Operand::OPERATOR_NOT_IN, Operand::OPERATOR_IN_ALL],
377-
true
378-
)
379-
) {
380-
if (count($value) === 0) {
381-
$value[] = md5((string) time());
382-
}
383-
384-
$parameters = array_map(
385-
static fn (int $number) => "{$snakeField}_{$number}",
386-
range(0, count($value) - 1),
387-
);
388-
$parameter = sprintf('(:%s)', implode(', :', $parameters));
389-
$listValue = array_values($value);
390-
391-
foreach ($parameters as $index => $name) {
392-
$queryBuilder->setParameter($name, $listValue[$index]);
393-
}
394-
} else {
395-
$queryBuilder->setParameter($snakeField, $criteria[$field]->getOperand());
396-
}
406+
['operator' => $operator, 'parameter' => $parameter] =
407+
$this->flattenOperand($queryBuilder, $criteria[$field], $field, $value);
397408
} else {
398409
$queryBuilder->setParameter($snakeField, $value);
399410
}
400411

401-
$prefixedField = $field;
402-
403412
foreach ($this->tableAliases as $alias => $fields) {
404413
if (in_array($field, $fields, true)) {
405414
$prefixedField = sprintf('%s.%s', $alias, $field);
@@ -422,46 +431,82 @@ protected function handleRegularCriteria(
422431

423432
if ($this->isFieldMapped($field, $entityClassName)) {
424433
$exprKey = sprintf('%s.meta_key = :%s_key', $metaAlias, $snakeField);
425-
$queryBuilder->andWhere($exprKey)
426-
->setParameter(
427-
sprintf('%s_key', $snakeField),
428-
$this->getMappedMetaKey($field, $entityClassName),
429-
)
430-
;
434+
$queryBuilder->setParameter(
435+
sprintf('%s_key', $snakeField),
436+
$this->getMappedMetaKey($field, $entityClassName),
437+
);
431438
} else {
432439
$exprKey = sprintf("TRIM(LEADING '_' FROM %s.meta_key) = :%s_key", $metaAlias, $snakeField);
433-
$queryBuilder->andWhere($exprKey)
434-
->setParameter(sprintf('%s_key', $snakeField), $field)
435-
;
440+
$queryBuilder->setParameter(sprintf('%s_key', $snakeField), $field);
436441
}
437442

438443
$exprValue = sprintf('%s.meta_value %s %s', $metaAlias, $operator, $parameter);
439-
$queryBuilder->andWhere($exprValue);
440-
} else {
441-
if ($operator === Operand::OPERATOR_IN_ALL) {
442-
$operator = 'IN';
443-
$queryBuilder->andHaving(sprintf('COUNT(DISTINCT %s) = :%s_count', $prefixedField, $snakeField))
444-
->setParameter(sprintf('%s_count', $snakeField), count($value))
445-
;
446-
}
447-
$expr = sprintf('%s %s %s', $prefixedField, $operator, $parameter);
448-
$queryBuilder->andWhere($expr);
444+
445+
return $expression->and($exprKey, $exprValue);
446+
}
447+
448+
if ($operator === Operand::OPERATOR_IN_ALL) {
449+
$operator = 'IN';
450+
$queryBuilder->andHaving(sprintf('COUNT(DISTINCT %s) = :%s_count', $prefixedField, $snakeField))
451+
->setParameter(sprintf('%s_count', $snakeField), count($value))
452+
;
449453
}
454+
455+
return $expression->and(sprintf('%s %s %s', $prefixedField, $operator, $parameter));
450456
}
451457

452-
protected function addOrderByClause(QueryBuilder $queryBuilder, ?array $orderBy): void
458+
#[ArrayShape([
459+
'operator' => 'string',
460+
'parameter' => 'string',
461+
])]
462+
private function flattenOperand(QueryBuilder $queryBuilder, Operand $operand, string $field, mixed $value): array
453463
{
454-
if (!empty($orderBy)) {
455-
foreach ($orderBy as $field => $orientation) {
456-
$this->validateFieldName($field, static::FALLBACK_ENTITY, static::TABLE_NAME);
464+
$snakeField = u($field)
465+
->snake()
466+
->toString()
467+
;
468+
$parameter = ":{$snakeField}";
469+
$operator = $operand->getOperator();
457470

458-
if (!in_array(strtolower($orientation), ['asc', 'desc'], true)) {
459-
throw new InvalidOrderByOrientationException($orientation);
460-
}
471+
if (
472+
in_array($operator, [Operand::OPERATOR_IN, Operand::OPERATOR_NOT_IN, Operand::OPERATOR_IN_ALL], true)
473+
) {
474+
if (count($value) === 0) {
475+
$value[] = md5((string) time());
476+
}
461477

462-
$queryBuilder->addOrderBy($field, $orientation);
478+
$parameters = array_map(
479+
static fn (int $number) => "{$snakeField}_{$number}",
480+
range(0, count($value) - 1),
481+
);
482+
483+
$parameter = sprintf('(:%s)', implode(', :', $parameters));
484+
$listValue = array_values($value);
485+
486+
foreach ($parameters as $index => $name) {
487+
$queryBuilder->setParameter($name, $listValue[$index]);
463488
}
489+
} else {
490+
$queryBuilder->setParameter($snakeField, $operand->getOperand());
491+
}
492+
493+
return [
494+
'operator' => $operator,
495+
'parameter' => $parameter,
496+
];
497+
}
498+
499+
private function createNestedCriteria(QueryBuilder $queryBuilder, NestedCondition $condition): void
500+
{
501+
$criteria = $condition->getCriteria();
502+
$normalizedCriteria = $this->normalizeCriteria($condition->getCriteria());
503+
$expressions = [];
504+
505+
foreach ($normalizedCriteria as $field => $value) {
506+
$expressions[] = $this->getWhereExpressionFromCriteriaField($queryBuilder, $criteria, $field, $value);
464507
}
508+
509+
$queryBuilder->andWhere($queryBuilder->expr()->{$condition->getOperator()}(...$expressions));
465510
}
466511

467512
private function joinSelfMetaTable(QueryBuilder $queryBuilder, bool $incrementIfNecessary = false): string

src/Bridge/Repository/NestedCriteriaTrait.php

Lines changed: 0 additions & 37 deletions
This file was deleted.

test/Test/Bridge/Repository/ProductRepositoryTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,32 @@ public function testOperatorInWithEmptyArray(): void
277277
self::assertCount(0, $products);
278278
}
279279

280+
public function testOperandInNestedCondition(): void
281+
{
282+
$products = $this->repository->findBy([
283+
new NestedCondition(NestedCondition::OPERATOR_OR, [
284+
'sku' => new Operand(['woo-tshirt', 'woo-single'], Operand::OPERATOR_IN),
285+
'id' => new Operand([19, 20], Operand::OPERATOR_IN),
286+
]),
287+
]);
288+
self::assertIsArray($products);
289+
self::assertCount(4, $products);
290+
self::assertEquals([17, 19, 20, 27], array_column($products, 'id'));
291+
}
292+
293+
public function testNestedConditionAndOperator(): void
294+
{
295+
$products = $this->repository->findBy([
296+
new NestedCondition(NestedCondition::OPERATOR_AND, [
297+
'sku' => new Operand(['woo-tshirt', 'woo-single'], Operand::OPERATOR_IN),
298+
'id' => 17,
299+
]),
300+
]);
301+
self::assertIsArray($products);
302+
self::assertCount(1, $products);
303+
self::assertEquals([17], array_column($products, 'id'));
304+
}
305+
280306
public function testTermRelationshipConditionWithTaxonomyOnly(): void
281307
{
282308
$products = $this->repository->findBy([

0 commit comments

Comments
 (0)