From a259cb7806342631a41f52d7e86549a1e230f133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Parafi=C5=84ski?= Date: Thu, 22 Feb 2018 16:23:15 +0100 Subject: [PATCH] EZP-28783: As a Developer I want to search using REST FieldCriterion (#2239) * [REST] Implemented Field Criterion * Allowed empty FieldCriterion values as long as key exists * [EZP-28683] Basic functionality for content query logic query * [EZP-28683] Fix and tests for queries containing ezkeyword fieldtype * [EZP-28683] Changing payload structure, Functional test refactoring * [EZP-28783] Allow logicalOperator as xml array --- .../Tests/Functional/SearchViewTest.php | 277 ++++++++++++++++++ .../Server/Input/Parser/Criterion/Field.php | 57 +++- .../Input/Parser/Criterion/LogicalAnd.php | 4 + .../Input/Parser/Criterion/LogicalOr.php | 4 + .../Common/Gateway/CriterionHandler/Field.php | 1 - 5 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 eZ/Bundle/EzPublishRestBundle/Tests/Functional/SearchViewTest.php diff --git a/eZ/Bundle/EzPublishRestBundle/Tests/Functional/SearchViewTest.php b/eZ/Bundle/EzPublishRestBundle/Tests/Functional/SearchViewTest.php new file mode 100644 index 00000000000..76cc9e9f5ef --- /dev/null +++ b/eZ/Bundle/EzPublishRestBundle/Tests/Functional/SearchViewTest.php @@ -0,0 +1,277 @@ +contentTypeHref = $this->createTestContentType(); + $this->contentHrefList[] = $this->createTestContentWithTags('test-name', ['foo', 'bar']); + $this->contentHrefList[] = $this->createTestContentWithTags('fancy-name', ['baz', 'foobaz']); + $this->contentHrefList[] = $this->createTestContentWithTags('even-fancier', ['bar', 'bazfoo']); + } + + protected function tearDown() + { + parent::tearDown(); + array_map([$this, 'deleteContent'], $this->contentHrefList); + $this->deleteContent($this->contentTypeHref); + } + + /** + * @dataProvider xmlProvider + * Covers POST with ContentQuery Logic on /api/ezp/v2/views. + */ + public function testSimpleContentQuery(string $xmlQueryBody, int $expectedCount) + { + $request = $this->createHttpRequest('POST', '/api/ezp/v2/views', 'ViewInput+xml; version=1.1', 'ContentInfo+json'); + $body = <<< XML + + +your-query-id +false + + + $xmlQueryBody + + 10 + 0 + + +XML; + + $request->setContent($body); + + $response = $this->sendHttpRequest($request); + self::assertHttpResponseCodeEquals($response, 200); + $jsonResponse = json_decode($response->getContent()); + self::assertEquals($expectedCount, $jsonResponse->View->Result->count); + } + + private function createTestContentType(): string + { + $body = <<< XML + + + tags-test + + testContentQueryWithTags + + testContentQueryWithTags + <title> + <title> + true + eng-GB + true + PATH + ASC + + + title + ezstring + content + 1 + true + true + false + New Title + true + + Title + + + This is the title + + + + tags + ezkeyword + content + 2 + true + true + false + true + + Tags + + + Those are searchable tags + + + + +XML; + + $request = $this->createHttpRequest( + 'POST', + '/api/ezp/v2/content/typegroups/1/types?publish=true', + 'ContentTypeCreate+xml', + 'ContentType+json' + ); + $request->setContent($body); + $response = $this->sendHttpRequest($request); + + return $response->getHeader('Location'); + } + + private function createTestContentWithTags(string $name, array $tags): string + { + $request = $this->createHttpRequest('POST', '/api/ezp/v2/content/objects', 'ContentCreate+xml', 'ContentInfo+json'); + $tagsString = implode(',', $tags); + $body = <<< XML + + + + eng-GB + + + 0 + false + PATH + ASC + +
+ true + $name + + 2018-01-30T18:30:00 + + + title + eng-GB + $name + + + tags + eng-GB + $tagsString + + + +XML; + $request->setContent($body); + + $response = $this->sendHttpRequest($request); + $href = $response->getHeader('Location'); + $this->sendHttpRequest( + $this->createHttpRequest('PUBLISH', "$href/versions/1") + ); + + return $href; + } + + private function deleteContent($href) + { + $this->sendHttpRequest( + $this->createHttpRequest('DELETE', $href) + ); + } + + public function xmlProvider() + { + $fooTag = $this->buildFieldXml('tags', Operator::CONTAINS, 'foo'); + $barTag = $this->buildFieldXml('tags', Operator::CONTAINS, 'bar'); + $bazTag = $this->buildFieldXml('tags', Operator::CONTAINS, 'baz'); + $foobazTag = $this->buildFieldXml('tags', Operator::CONTAINS, 'foobaz'); + + return [ + [ + $this->getXmlString( + $this->wrapIn('AND', [$fooTag, $barTag]) + ), + 1, + ], + [ + $this->getXmlString( + $this->wrapIn('OR', [ + $this->wrapIn('AND', [$fooTag, $barTag]), + $this->wrapIn('AND', [$bazTag, $foobazTag]), + ]) + ), + 2, + ], + [ + $this->getXmlString( + $this->wrapIn('AND', [ + $this->wrapIn('NOT', [$fooTag]), + $barTag, + ]) + ), + 1, + ], + ]; + } + + /** + * @param string $name + * @param string $operator + * @param string|string[] $value + * @return \DomElement + */ + private function buildFieldXml(string $name, string $operator, $value): \DomElement + { + $xml = new \DOMDocument(); + $element = $xml->createElement('Field'); + $element->appendChild(new \DOMElement('name', $name)); + $element->appendChild(new \DOMElement('operator', $operator)); + if (is_array($value)) { + $valueWrapper = $xml->createElement('value'); + foreach ($value as $key => $singleValue) { + $valueWrapper->appendChild(new \DOMElement('value', $singleValue)); + } + $element->appendChild($valueWrapper); + + return $element; + } + + $element->appendChild(new \DOMElement('value', $value)); + + return $element; + } + + /** + * @param string $logicalOperator + * @param \DomElement|\DomElement[] $toWrap + * @return \DomElement + */ + private function wrapIn(string $logicalOperator, array $toWrap): \DomElement + { + $xml = new \DOMDocument(); + $wrapper = $xml->createElement($logicalOperator); + + foreach ($toWrap as $key => $field) { + $innerWrapper = $xml->createElement($logicalOperator); + $innerWrapper->appendChild($xml->importNode($field, true)); + $wrapper->appendChild($innerWrapper); + } + + return $wrapper; + } + + private function getXmlString(\DomElement $simpleXMLElement): string + { + return $simpleXMLElement->ownerDocument->saveXML($simpleXMLElement); + } +} diff --git a/eZ/Publish/Core/REST/Server/Input/Parser/Criterion/Field.php b/eZ/Publish/Core/REST/Server/Input/Parser/Criterion/Field.php index 1f2c5ea02eb..4c6867d2b11 100644 --- a/eZ/Publish/Core/REST/Server/Input/Parser/Criterion/Field.php +++ b/eZ/Publish/Core/REST/Server/Input/Parser/Criterion/Field.php @@ -8,14 +8,29 @@ */ namespace eZ\Publish\Core\REST\Server\Input\Parser\Criterion; +use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Field as FieldCriterion; +use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator; use eZ\Publish\Core\REST\Common\Input\BaseParser; use eZ\Publish\Core\REST\Common\Input\ParsingDispatcher; +use eZ\Publish\Core\REST\Common\Exceptions; /** * Parser for Field Criterion. */ class Field extends BaseParser { + const OPERATORS = [ + 'IN' => Operator::IN, + 'EQ' => Operator::EQ, + 'GT' => Operator::GT, + 'GTE' => Operator::GTE, + 'LT' => Operator::LT, + 'LTE' => Operator::LTE, + 'LIKE' => Operator::LIKE, + 'BETWEEN' => Operator::BETWEEN, + 'CONTAINS' => Operator::CONTAINS, + ]; + /** * Parses input structure to a Criterion object. * @@ -28,6 +43,46 @@ class Field extends BaseParser */ public function parse(array $data, ParsingDispatcher $parsingDispatcher) { - throw new \Exception('@todo implement'); + if (!array_key_exists('Field', $data)) { + throw new Exceptions\Parser('Invalid format'); + } + + $fieldData = $data['Field']; + if (empty($fieldData['name']) || empty($fieldData['operator']) || !array_key_exists('value', $fieldData)) { + throw new Exceptions\Parser(' format expects name, operator and value keys'); + } + + $operator = $this->getOperator($fieldData['operator']); + + return new FieldCriterion( + $fieldData['name'], + $operator, + $fieldData['value'] + ); + } + + /** + * Get operator for the given literal name. + * + * For the full list of supported operators: + * @see \eZ\Publish\Core\REST\Server\Input\Parser\Criterion\Field::OPERATORS + * + * @param string $operatorName operator literal operator name + * + * @return string + */ + private function getOperator($operatorName) + { + $operatorName = strtoupper($operatorName); + if (!isset(self::OPERATORS[$operatorName])) { + throw new Exceptions\Parser( + sprintf( + 'Unexpected Field operator, expected one of the following: %s', + implode(', ', array_keys(self::OPERATORS)) + ) + ); + } + + return self::OPERATORS[$operatorName]; } } diff --git a/eZ/Publish/Core/REST/Server/Input/Parser/Criterion/LogicalAnd.php b/eZ/Publish/Core/REST/Server/Input/Parser/Criterion/LogicalAnd.php index 640236f264f..cf590e44d4d 100644 --- a/eZ/Publish/Core/REST/Server/Input/Parser/Criterion/LogicalAnd.php +++ b/eZ/Publish/Core/REST/Server/Input/Parser/Criterion/LogicalAnd.php @@ -36,6 +36,10 @@ public function parse(array $data, ParsingDispatcher $parsingDispatcher) $criteria = array(); foreach ($data['AND'] as $criterionName => $criterionData) { + if (is_array($criterionData) && !array_key_exists(0, $criterionData)) { + $criterionName = key($criterionData); + $criterionData = current($criterionData); + } $criteria[] = $this->dispatchCriterion($criterionName, $criterionData, $parsingDispatcher); } diff --git a/eZ/Publish/Core/REST/Server/Input/Parser/Criterion/LogicalOr.php b/eZ/Publish/Core/REST/Server/Input/Parser/Criterion/LogicalOr.php index a3249e8d7bb..452eeeeb27b 100644 --- a/eZ/Publish/Core/REST/Server/Input/Parser/Criterion/LogicalOr.php +++ b/eZ/Publish/Core/REST/Server/Input/Parser/Criterion/LogicalOr.php @@ -36,6 +36,10 @@ public function parse(array $data, ParsingDispatcher $parsingDispatcher) $criteria = array(); foreach ($data['OR'] as $criterionName => $criterionData) { + if (is_array($criterionData) && !array_key_exists(0, $criterionData)) { + $criterionName = key($criterionData); + $criterionData = current($criterionData); + } $criteria[] = $this->dispatchCriterion($criterionName, $criterionData, $parsingDispatcher); } diff --git a/eZ/Publish/Core/Search/Legacy/Content/Common/Gateway/CriterionHandler/Field.php b/eZ/Publish/Core/Search/Legacy/Content/Common/Gateway/CriterionHandler/Field.php index 6db1af127c3..b44eedaf606 100644 --- a/eZ/Publish/Core/Search/Legacy/Content/Common/Gateway/CriterionHandler/Field.php +++ b/eZ/Publish/Core/Search/Legacy/Content/Common/Gateway/CriterionHandler/Field.php @@ -14,7 +14,6 @@ use eZ\Publish\Core\Persistence\Database\DatabaseHandler; use eZ\Publish\API\Repository\Values\Content\Query\Criterion; use eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry as Registry; -use eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\Converter; use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException; use eZ\Publish\Core\Persistence\TransformationProcessor; use eZ\Publish\Core\Persistence\Database\SelectQuery;