diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f0aef9f..14cb15d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for the analytics component - Function builder +- Query builder - Solarium\Component\FacetSet::setMatches() - Solarium\Component\FacetSet::setExcludeTerms() - Solarium\Component\Facet\Field::setMatches() diff --git a/docs/queries/query-helper/query-builder.md b/docs/queries/query-helper/query-builder.md new file mode 100644 index 000000000..ef1f87300 --- /dev/null +++ b/docs/queries/query-helper/query-builder.md @@ -0,0 +1,84 @@ +Query builder +------------- + +The query builder is a simple helper class to help writing and maintaining (filter) queries using Solr's query language. +While the query will only accept a single (composite) expression, the addition of filter queries can consist of multiple expressions. + +Query example +------------- +```php +createSelect(); + +$expr = QueryBuilder::expr(); +$builder = QueryBuilder::create() + ->where($expr->andX( + $expr->eq('foo', 'bar'), + $expr->eq('baz', 'qux') + )) +; + +$query->setQueryFromQueryBuilder($builder); + +// which would be equal to +$query->setQuery('foo:"bar" AND baz:"qux"'); +``` + +Filter Query example +------------- +```php +createSelect(); + +$expr = QueryBuilder::expr(); +$builder = QueryBuilder::create() + ->where($expr->eq('foo', 'bar')), + ->andWhere($expr->neq('baz', 'qux') +); + +$query->addFilterQueriesFromQueryBuilder($builder); + +// which would be equal to +$value = 'foo:"bar"'; +$query->addFilterQuery(['key' => sha1($value), 'query' => $value]); +$value = '-baz:"qux"'; +$query->addFilterQuery(['key' => sha1($value), 'query' => $value]); +``` + +Complex filter queries +---------------------- +While the ``addFilterQueriesFromQueryBuilder`` method only provides in setting the facet query key and actual query, the ``QueryBuilder`` can be used in the construction of more complex facet queries. +If one, for example, need to add a tag to the filter query the following method could be used. +```php +createSelect(); + +$expr = QueryBuilder::expr(); +$visitor = new QueryExpressionVisitor(); + +$builder = QueryBuilder::create() + ->where($expr->eq('foo', 'bar')) +); + +$query->addFilterQuery([ + 'key' => 'my-key, + 'query' => $visitor->dispatch($builder->getExpression()[0]), + 'local_tag' => 'my-tag', +]); +``` \ No newline at end of file diff --git a/src/Builder/AbstractExpressionVisitor.php b/src/Builder/AbstractExpressionVisitor.php index 400dadb27..d649521e6 100644 --- a/src/Builder/AbstractExpressionVisitor.php +++ b/src/Builder/AbstractExpressionVisitor.php @@ -16,8 +16,6 @@ /** * Expression Visitor. * - * @codeCoverageIgnore - * * @author wicliff */ abstract class AbstractExpressionVisitor diff --git a/src/Builder/Comparison.php b/src/Builder/Comparison.php index 90dafb8ec..1c46f03f5 100644 --- a/src/Builder/Comparison.php +++ b/src/Builder/Comparison.php @@ -14,8 +14,6 @@ /** * Comparison. * - * @codeCoverageIgnore - * * @author wicliff */ class Comparison implements ExpressionInterface @@ -80,6 +78,11 @@ class Comparison implements ExpressionInterface */ public const MATCH = 'MATCH'; + /** + * Empty. + */ + public const EMPTY = 'EMPTY'; + /** * @var string */ diff --git a/src/Builder/CompositeComparison.php b/src/Builder/CompositeComparison.php index af3721594..e4b668258 100644 --- a/src/Builder/CompositeComparison.php +++ b/src/Builder/CompositeComparison.php @@ -16,8 +16,6 @@ /** * Composite Expression. * - * @codeCoverageIgnore - * * @author wicliff */ class CompositeComparison implements ExpressionInterface diff --git a/src/Builder/Select/ExpressionBuilder.php b/src/Builder/Select/ExpressionBuilder.php new file mode 100644 index 000000000..f6cb84b65 --- /dev/null +++ b/src/Builder/Select/ExpressionBuilder.php @@ -0,0 +1,189 @@ + + */ +class ExpressionBuilder +{ + /** + * @param null $x + * + * @throws \Solarium\Exception\RuntimeException + * + * @return \Solarium\Builder\CompositeComparison + */ + public function andX($x = null): CompositeComparison + { + return new CompositeComparison(CompositeComparison::TYPE_AND, \func_get_args()); + } + + /** + * @param null $x + * + * @throws \Solarium\Exception\RuntimeException + * + * @return \Solarium\Builder\CompositeComparison + */ + public function orX($x = null): CompositeComparison + { + return new CompositeComparison(CompositeComparison::TYPE_OR, \func_get_args()); + } + + /** + * @param string $field + * @param mixed $value + * + * @return \Solarium\Builder\Comparison + */ + public function eq(string $field, $value): Comparison + { + return new Comparison($field, Comparison::EQ, $value); + } + + /** + * @param string $field + * @param mixed $value + * + * @return \Solarium\Builder\Comparison + */ + public function neq(string $field, $value): Comparison + { + return new Comparison($field, Comparison::NEQ, $value); + } + + /** + * @param string $field + * @param mixed $value + * + * @return \Solarium\Builder\Comparison + */ + public function lt(string $field, $value): Comparison + { + return new Comparison($field, Comparison::LT, $value); + } + + /** + * @param string $field + * @param mixed $value + * + * @return \Solarium\Builder\Comparison + */ + public function gt(string $field, $value): Comparison + { + return new Comparison($field, Comparison::GT, $value); + } + + /** + * @param string $field + * @param mixed $value + * + * @return \Solarium\Builder\Comparison + */ + public function lte(string $field, $value): Comparison + { + return new Comparison($field, Comparison::LTE, $value); + } + + /** + * @param string $field + * @param mixed $value + * + * @return \Solarium\Builder\Comparison + */ + public function gte(string $field, $value): Comparison + { + return new Comparison($field, Comparison::GTE, $value); + } + + /** + * @param string $field + * @param mixed $value + * + * @return \Solarium\Builder\Comparison + */ + public function in(string $field, $value): Comparison + { + return new Comparison($field, Comparison::IN, $value); + } + + /** + * @param string $field + * @param mixed $value + * + * @return \Solarium\Builder\Comparison + */ + public function notIn(string $field, $value): Comparison + { + return new Comparison($field, Comparison::NIN, $value); + } + + /** + * @param string $field + * @param mixed $value + * + * @return \Solarium\Builder\Comparison + */ + public function range(string $field, $value): Comparison + { + return new Comparison($field, Comparison::RANGE, $value); + } + + /** + * @param string $field + * @param mixed $value + * + * @return \Solarium\Builder\Comparison + */ + public function regexp(string $field, $value): Comparison + { + return new Comparison($field, Comparison::REGEXP, $value); + } + + /** + * @param string $field + * @param mixed $value + * + * @return \Solarium\Builder\Comparison + */ + public function like(string $field, $value): Comparison + { + return new Comparison($field, Comparison::LIKE, $value); + } + + /** + * @param string $field + * @param mixed $value + * + * @return \Solarium\Builder\Comparison + */ + public function match(string $field, $value): Comparison + { + return new Comparison($field, Comparison::MATCH, $value); + } + + /** + * @param string $field + * + * @return \Solarium\Builder\Comparison + */ + public function empty(string $field): Comparison + { + return new Comparison($field, Comparison::EMPTY, null); + } +} diff --git a/src/Builder/Select/QueryBuilder.php b/src/Builder/Select/QueryBuilder.php new file mode 100644 index 000000000..3cf53915d --- /dev/null +++ b/src/Builder/Select/QueryBuilder.php @@ -0,0 +1,84 @@ + + */ +class QueryBuilder +{ + /** + * @var \Solarium\Builder\ExpressionInterface[] + */ + private $expressions = []; + + /** + * @var \Solarium\Builder\Select\ExpressionBuilder + */ + private static $expressionBuilder; + + /** + * @return static + */ + public static function create(): self + { + return new self(); + } + + /** + * @return \Solarium\Builder\Select\ExpressionBuilder + */ + public static function expr(): ExpressionBuilder + { + if (null === self::$expressionBuilder) { + self::$expressionBuilder = new ExpressionBuilder(); + } + + return self::$expressionBuilder; + } + + /** + * @param \Solarium\Builder\ExpressionInterface $comparison + * + * @return $this + */ + public function where(ExpressionInterface $comparison): self + { + $this->expressions[] = $comparison; + + return $this; + } + + /** + * Convenience method for readability. + * + * @param \Solarium\Builder\ExpressionInterface $comparison + * + * @return $this + */ + public function andWhere(ExpressionInterface $comparison): self + { + return $this->where($comparison); + } + + /** + * @return \Solarium\Builder\ExpressionInterface[] + */ + public function getExpressions(): array + { + return $this->expressions; + } +} diff --git a/src/Builder/Select/QueryExpressionVisitor.php b/src/Builder/Select/QueryExpressionVisitor.php new file mode 100644 index 000000000..8f5e2dcfb --- /dev/null +++ b/src/Builder/Select/QueryExpressionVisitor.php @@ -0,0 +1,199 @@ + + */ +class QueryExpressionVisitor extends AbstractExpressionVisitor +{ + /** + * @var Helper + */ + private $helper; + + /** + * Constructor. + */ + public function __construct() + { + $this->helper = new Helper(); + } + + /** + * @param \Solarium\Builder\ExpressionInterface $expression + * + * @throws \Solarium\Exception\RuntimeException + * + * @return mixed|string + */ + public function walkExpression(ExpressionInterface $expression) + { + $field = $expression->getField(); + $value = $expression->getValue()->getValue(); + + switch ($expression->getOperator()) { + case Comparison::EQ: + case Comparison::NEQ: + $strValue = $this->valueToString($value, ',', '"'); + + if ($value instanceof \DateTime) { + $strValue = sprintf('[%1$s TO %1$s]', $strValue); + } + + $not = (Comparison::NEQ === $expression->getOperator()) ? '-' : ''; + + return sprintf('%s%s:%s', $not, $field, $strValue); + case Comparison::GT: + return sprintf('%s:{%s TO *]', $field, $this->valueToString($value)); + case Comparison::GTE: + return sprintf('%s:[%s TO *]', $field, $this->valueToString($value)); + case Comparison::LT: + return sprintf('%s:[* TO %s}', $field, $this->valueToString($value)); + case Comparison::LTE: + return sprintf('%s:[* TO %s]', $field, $this->valueToString($value)); + case Comparison::RANGE: + if (\is_array($value)) { + if (2 === \count($value)) { + return sprintf('%s:[%s TO %s]', $field, $this->valueToString($value[0]), $this->valueToString($value[1])); + } + + if (1 === \count($value)) { + return sprintf('%s:[%s TO *]', $field, $this->valueToString($value[0])); + } + } + + throw new RuntimeException(sprintf('Invalid range value: %s', $value)); + case Comparison::IN: + if (\is_array($value)) { + return sprintf('%s:(%s)', $field, $this->valueToString($value, ' OR ', '"')); + } + + return sprintf('%s:%s', $field, $this->valueToString($value, ',', '"')); + case Comparison::LIKE: + case Comparison::MATCH: + if (\is_array($value)) { + return sprintf('%s:(%s)', $field, $this->valueToString($value, ' OR ', '', false)); + } + + return sprintf('%s:%s', $field, $this->valueToString($value, ',', '', false)); + case Comparison::NIN: + if (\is_array($value)) { + return sprintf('-%s:(%s)', $field, $this->valueToString($value, ' OR ', '"')); + } + + return sprintf('-%s:%s', $field, $this->valueToString($value, ',', '"')); + case Comparison::REGEXP: + if ('/' !== $value[0]) { + $value = sprintf('/%s/', $value); + } + + return sprintf('%s:%s', $field, $this->valueToString($value, ',', '', false)); + case Comparison::EMPTY: + return sprintf('(*:* NOT %s:*)', $field); + default: + throw new RuntimeException(sprintf('Unknown comparison operator: %s', $expression->getOperator())); + } + } + + /** + * {@inheritdoc} + */ + public function walkValue(Value $value) + { + return $value->getValue(); + } + + /** + * {@inheritdoc} + * + * @throws \Solarium\Exception\RuntimeException + */ + public function walkCompositeExpression(ExpressionInterface $expr) + { + $comparisons = []; + + foreach ($expr->getComparisons() as $child) { + $comparisons[] = $this->dispatch($child); + } + + switch ($expr->getType()) { + case CompositeComparison::TYPE_AND: + return implode(' AND ', $comparisons); + case CompositeComparison::TYPE_OR: + return implode(' OR ', $comparisons); + default: + throw new RuntimeException(sprintf('Unknown composite %s', $expr->getType())); + } + } + + /** + * @param mixed $value + * @param string $separator + * @param string $quote + * @param bool $escape + * + * @return string + */ + private function valueToString($value, string $separator = ',', string $quote = '', bool $escape = true): string + { + if (\is_array($value)) { + $ret = []; + + foreach ($value as $v) { + $ret[] = $this->typedValueToString($v, $quote, $escape); + } + + return implode($separator, $ret); + } + + return $this->typedValueToString($value, $quote, $escape); + } + + /** + * @param mixed $value + * @param string $quote + * @param bool $escape + * + * @return string + */ + private function typedValueToString($value, string $quote = '', $escape = true): string + { + if (null === $value) { + return '[* TO *]'; + } + + if ($value instanceof \DateTime) { + return $this->helper->formatDate($value); + } + + if (true === $escape && \is_string($value)) { + $value = $this->helper->escapeTerm($value); + } + + if (\is_string($value)) { + $value = sprintf('%1$s%2$s%1$s', $quote, $value); + } + + return (string) $value; + } +} diff --git a/src/Builder/Value.php b/src/Builder/Value.php index a6a9eea3b..3eb658f27 100644 --- a/src/Builder/Value.php +++ b/src/Builder/Value.php @@ -14,8 +14,6 @@ /** * Value. * - * @codeCoverageIgnore - * * @author wicliff */ class Value implements ExpressionInterface diff --git a/src/Component/QueryTrait.php b/src/Component/QueryTrait.php index f0680845a..1be135e02 100644 --- a/src/Component/QueryTrait.php +++ b/src/Component/QueryTrait.php @@ -9,6 +9,10 @@ namespace Solarium\Component; +use Solarium\Builder\Select\QueryBuilder; +use Solarium\Builder\Select\QueryExpressionVisitor; +use Solarium\Exception\RuntimeException; + /** * Query Trait. */ @@ -34,6 +38,22 @@ public function setQuery(string $query, array $bind = null): QueryInterface return $this->setOption('query', trim($query)); } + /** + * @param \Solarium\Builder\Select\QueryBuilder $builder + * + * @throws \Solarium\Exception\RuntimeException + * + * @return \Solarium\Component\QueryInterface + */ + public function setQueryFromQueryBuilder(QueryBuilder $builder): QueryInterface + { + if (1 !== \count($builder->getExpressions())) { + throw new RuntimeException('The QueryBuilder can only contain one expression when setting the query. Use ExpressionBuilder::andX or ExpressionBuilder::orX to combine expressions.'); + } + + return $this->setOption('query', (new QueryExpressionVisitor())->dispatch($builder->getExpressions()[0])); + } + /** * Get query option. * diff --git a/src/QueryType/Select/Query/Query.php b/src/QueryType/Select/Query/Query.php index c1ad44568..1142a481c 100644 --- a/src/QueryType/Select/Query/Query.php +++ b/src/QueryType/Select/Query/Query.php @@ -9,6 +9,8 @@ namespace Solarium\QueryType\Select\Query; +use Solarium\Builder\Select\QueryBuilder; +use Solarium\Builder\Select\QueryExpressionVisitor; use Solarium\Component\Analytics\Analytics; use Solarium\Component\ComponentAwareQueryInterface; use Solarium\Component\ComponentAwareQueryTrait; @@ -591,6 +593,28 @@ public function addFilterQueries(array $filterQueries): self return $this; } + /** + * Add multiple filter queries from the QueryBuilder. + * + * @param \Solarium\Builder\Select\QueryBuilder $builder + * + * @throws \Solarium\Exception\RuntimeException + * + * @return $this + */ + public function addFilterQueriesFromQueryBuilder(QueryBuilder $builder): self + { + $visitor = new QueryExpressionVisitor(); + + foreach ($builder->getExpressions() as $expression) { + $value = $visitor->dispatch($expression); + + $this->addFilterQuery(new FilterQuery(['key' => sha1($value), 'query' => $value])); + } + + return $this; + } + /** * Get a filterquery. * diff --git a/tests/Builder/Select/QueryBuilderTest.php b/tests/Builder/Select/QueryBuilderTest.php new file mode 100644 index 000000000..5a7a8e9dc --- /dev/null +++ b/tests/Builder/Select/QueryBuilderTest.php @@ -0,0 +1,369 @@ + + */ +class QueryBuilderTest extends TestCase +{ + /** + * @var \Solarium\Builder\Select\QueryExpressionVisitor + */ + private $visitor; + + /** + * Set up. + */ + public function setUp(): void + { + $this->visitor = new QueryExpressionVisitor(); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testEquals(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->eq('foo', 'bar')); + + $this->assertSame('foo:"bar"', $this->visitor->dispatch($filter->getExpressions()[0])); + + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->eq('foo', date_create('2020-01-01', new \DateTimeZone('UTC')))); + + $this->assertSame('foo:[2020-01-01T00:00:00Z TO 2020-01-01T00:00:00Z]', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testNullValue(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->eq('foo', null)); + + $this->assertSame('foo:[* TO *]', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testDoesNotEqual(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->neq('foo', 'bar')); + + $this->assertSame('-foo:"bar"', $this->visitor->dispatch($filter->getExpressions()[0])); + + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->neq('foo', date_create('2020-01-01', new \DateTimeZone('UTC')))); + + $this->assertSame('-foo:[2020-01-01T00:00:00Z TO 2020-01-01T00:00:00Z]', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testGreaterThan(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->gt('foo', 2)); + + $this->assertSame('foo:{2 TO *]', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testGreaterThanEqual(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->gte('foo', 2)); + + $this->assertSame('foo:[2 TO *]', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testLowerThan(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->lt('foo', 2)); + + $this->assertSame('foo:[* TO 2}', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testLowerThanEqual(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->lte('foo', 2)); + + $this->assertSame('foo:[* TO 2]', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testRange(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->range('foo', [2])); + + $this->assertSame('foo:[2 TO *]', $this->visitor->dispatch($filter->getExpressions()[0])); + + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->range('foo', [2, 5])); + + $this->assertSame('foo:[2 TO 5]', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \Solarium\Exception\RuntimeException + */ + public function testRangeInvalidValue(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->range('foo', 'bar')); + + $this->expectException(RuntimeException::class); + + $this->visitor->dispatch($filter->getExpressions()[0]); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testIn(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->in('foo', [2, 5])); + + $this->assertSame('foo:(2 OR 5)', $this->visitor->dispatch($filter->getExpressions()[0])); + + $filter = QueryBuilder::create() + ->andWhere(QueryBuilder::expr()->in('foo', 'bar')); + + $this->assertSame('foo:"bar"', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testNotIn(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->notIn('foo', [2, 5])); + + $this->assertSame('-foo:(2 OR 5)', $this->visitor->dispatch($filter->getExpressions()[0])); + + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->notIn('foo', 'bar')); + + $this->assertSame('-foo:"bar"', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testLike(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->like('title', ['*foo', 'bar*'])); + + $this->assertSame('title:(*foo OR bar*)', $this->visitor->dispatch($filter->getExpressions()[0])); + + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->like('title', 'foo*')); + + $this->assertSame('title:foo*', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testRegularExpression(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->regexp('title', '[0-9]{5}')); + + $this->assertSame('title:/[0-9]{5}/', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testMatch(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->match('title', 'foo*')); + + $this->assertSame('title:foo*', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testEmpty(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->empty('title')); + + $this->assertSame('(*:* NOT title:*)', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testCompositeAnd(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->andX( + QueryBuilder::expr()->eq('title', 'foo'), + QueryBuilder::expr()->in('description', ['bar', 'baz']) + )); + + $this->assertSame('title:"foo" AND description:("bar" OR "baz")', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testCompositeOr(): void + { + $filter = QueryBuilder::create() + ->where(QueryBuilder::expr()->orX( + QueryBuilder::expr()->eq('title', 'foo'), + QueryBuilder::expr()->in('description', ['bar', 'baz']) + )); + + $this->assertSame('title:"foo" OR description:("bar" OR "baz")', $this->visitor->dispatch($filter->getExpressions()[0])); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testVisitExpressions(): void + { + $expression = QueryBuilder::expr()->eq('title', 'foo'); + + $this->assertSame('title:"foo"', $expression->visit($this->visitor)); + + $compositeExpression = QueryBuilder::expr()->andX( + QueryBuilder::expr()->eq('title', 'foo'), + QueryBuilder::expr()->in('description', ['bar', 'baz']) + ); + + $this->assertSame('title:"foo" AND description:("bar" OR "baz")', $compositeExpression->visit($this->visitor)); + + $value = new Value('foo'); + $this->assertSame('foo', $value->visit($this->visitor)); + $this->assertSame('foo', $this->visitor->dispatch($value)); + } + + /** + * @throws \Solarium\Exception\RuntimeException + */ + public function testInvalidCompositeExpressionWithValue(): void + { + $this->expectException(RuntimeException::class); + + new CompositeComparison(CompositeComparison::TYPE_OR, [new Value('foo')]); + } + + /** + * @throws \Solarium\Exception\RuntimeException + */ + public function testInvalidCompositeExpressionWithObject(): void + { + $this->expectException(RuntimeException::class); + + new CompositeComparison(CompositeComparison::TYPE_AND, [new \DateTime()]); + } + + /** + * @throws \Solarium\Exception\RuntimeException + */ + public function testUnknownExpression(): void + { + $this->expectException(RuntimeException::class); + + $this->visitor->dispatch(new ExpressionDummy()); + } + + /** + * @throws \Solarium\Exception\RuntimeException + */ + public function testUnknownCompositeComparison(): void + { + $comparison = new CompositeComparison('TO', [QueryBuilder::expr()->eq('title', 'foo')]); + + $this->expectException(RuntimeException::class); + + $this->visitor->walkCompositeExpression($comparison); + } + + /** + * @throws \Solarium\Exception\RuntimeException + */ + public function testUnknownComparison(): void + { + $comparison = new Comparison('title', 'FOO', 'bar'); + + $this->expectException(RuntimeException::class); + + $this->visitor->walkExpression($comparison); + } +} + +/** + * ExpressionDummy. + * + * @author wicliff + */ +class ExpressionDummy implements ExpressionInterface +{ + /** + * {@inheritdoc} + */ + public function visit(AbstractExpressionVisitor $visitor) + { + return $visitor->walkExpression($this); + } +} diff --git a/tests/QueryType/Select/Query/AbstractQueryTest.php b/tests/QueryType/Select/Query/AbstractQueryTest.php index fd153330e..0f43e245f 100644 --- a/tests/QueryType/Select/Query/AbstractQueryTest.php +++ b/tests/QueryType/Select/Query/AbstractQueryTest.php @@ -3,11 +3,14 @@ namespace Solarium\Tests\QueryType\Select\Query; use PHPUnit\Framework\TestCase; +use Solarium\Builder\Select\QueryBuilder; +use Solarium\Builder\Select\QueryExpressionVisitor; use Solarium\Component\Analytics\Analytics; use Solarium\Component\MoreLikeThis; use Solarium\Core\Client\Client; use Solarium\Exception\InvalidArgumentException; use Solarium\Exception\OutOfBoundsException; +use Solarium\Exception\RuntimeException; use Solarium\QueryType\Select\Query\FilterQuery; use Solarium\QueryType\Select\Query\Query; @@ -749,4 +752,57 @@ public function testSetAndGetSplitOnWhitespace() $this->query->setSplitOnWhitespace(false); $this->assertFalse($this->query->getSplitOnWhitespace()); } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testSetQueryFromQueryBuilder(): void + { + $visitor = new QueryExpressionVisitor(); + $builder = QueryBuilder::create() + ->where(QueryBuilder::expr()->eq('foo', 'bar')); + + $this->query->setQueryFromQueryBuilder($builder); + + self::assertSame($visitor->dispatch($builder->getExpressions()[0]), $this->query->getQuery()); + } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testSetCompositeQueryFromQueryBuilder(): void + { + $expr = QueryBuilder::expr(); + $visitor = new QueryExpressionVisitor(); + + $builder = QueryBuilder::create() + ->where($expr->andX( + $expr->eq('foo', 'bar'), + $expr->eq('baz', 'qux') + )) + ; + + $this->query->setQueryFromQueryBuilder($builder); + + self::assertSame($visitor->dispatch($builder->getExpressions()[0]), $this->query->getQuery()); + } + + /** + * @throws \Solarium\Exception\RuntimeException + */ + public function testSetQueryFromQueryBuilderException(): void + { + $this->expectException(RuntimeException::class); + + $expr = QueryBuilder::expr(); + + $builder = QueryBuilder::create() + ->where($expr->eq('foo', 'bar')) + ->andWhere($expr->eq('baz', 'qux')) + ; + + $this->query->setQueryFromQueryBuilder($builder); + } } diff --git a/tests/QueryType/Select/Query/QueryTest.php b/tests/QueryType/Select/Query/QueryTest.php index 9356ddbb2..982a60697 100644 --- a/tests/QueryType/Select/Query/QueryTest.php +++ b/tests/QueryType/Select/Query/QueryTest.php @@ -2,6 +2,8 @@ namespace Solarium\Tests\QueryType\Select\Query; +use Solarium\Builder\Select\QueryBuilder; +use Solarium\Builder\Select\QueryExpressionVisitor; use Solarium\QueryType\Select\Query\Query; class QueryTest extends AbstractQueryTest @@ -10,4 +12,46 @@ public function setUp(): void { $this->query = new Query(); } + + /** + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testSetFacetQueryFromQueryBuilder(): void + { + $visitor = new QueryExpressionVisitor(); + $builder = QueryBuilder::create() + ->where(QueryBuilder::expr()->eq('foo', 'bar')); + + $this->query->addFilterQueriesFromQueryBuilder($builder); + + $value = $visitor->dispatch($builder->getExpressions()[0]); + $filterQuery = $this->query->getFilterQuery(sha1($value)); + + self::assertSame($value, $filterQuery->getQuery()); + } + + /** + * @throws \PHPUnit\Framework\Exception + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \Solarium\Exception\RuntimeException + */ + public function testSetMultipleFilterQueriesFromQueryBuilder(): void + { + $visitor = new QueryExpressionVisitor(); + $expr = QueryBuilder::expr(); + + $builder = QueryBuilder::create() + ->where($expr->eq('foo', 'bar')) + ->andWhere($expr->eq('baz', 'qux')) + ; + + $this->query->addFilterQueriesFromQueryBuilder($builder); + + $first = $visitor->dispatch($builder->getExpressions()[0]); + $second = $visitor->dispatch($builder->getExpressions()[1]); + + self::assertArrayHasKey(sha1($first), $this->query->getFilterQueries()); + self::assertArrayHasKey(sha1($second), $this->query->getFilterQueries()); + } }