Skip to content

Commit

Permalink
EZP-30951: Added missing support for searching (Not)Empty Field values (
Browse files Browse the repository at this point in the history
#2821)

* Added IsFieldEmpty Criterion

* Added IsFieldEmpty Criterion support for LegacySearchEngine

* Fixed tests

* CS Fixups

* Removed IsFieldNull test

* Fixups after CR

* CS Fixups

* Fixups for FieldEmpty CriterionHandler

* Postgres fix

* Fixups after CR

* Added IsEmptyFieldType interface

* Fixups

* Renamed IsEmptyFieldType to FieldType\IsEmptyValue

* Dropped IsEmptyFieldType implementation in favor of FieldType

* Refactored Field* Criterion handlers
  • Loading branch information
mateuszbieniek authored and lserwatka committed Nov 22, 2019
1 parent 27af421 commit 31bd435
Show file tree
Hide file tree
Showing 8 changed files with 435 additions and 39 deletions.
139 changes: 138 additions & 1 deletion eZ/Publish/API/Repository/Tests/SearchServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1129,10 +1129,84 @@ public function testFindLocationsNoPerformCountException()
$searchService->findLocations($query);
}

/**
* Create movie Content with subtitle field set to null.
*
* @return \eZ\Publish\API\Repository\Values\Content\Content[]
*/
protected function createMovieContent()
{
$movies = [];

$repository = $this->getRepository();
$contentTypeService = $repository->getContentTypeService();
$contentService = $repository->getContentService();

$createStruct = $contentTypeService->newContentTypeCreateStruct('movie');
$createStruct->mainLanguageCode = 'eng-GB';
$createStruct->remoteId = 'movie-123';
$createStruct->names = ['eng-GB' => 'Movie'];
$createStruct->creatorId = 14;
$createStruct->creationDate = new \DateTime();

$fieldTitle = $contentTypeService->newFieldDefinitionCreateStruct('title', 'ezstring');
$fieldTitle->names = ['eng-GB' => 'Title'];
$fieldTitle->fieldGroup = 'main';
$fieldTitle->position = 1;
$fieldTitle->isTranslatable = false;
$fieldTitle->isSearchable = true;
$fieldTitle->isRequired = true;
$createStruct->addFieldDefinition($fieldTitle);

$fieldSubtitle = $contentTypeService->newFieldDefinitionCreateStruct('subtitle', 'ezstring');
$fieldSubtitle->names = ['eng-GB' => 'Subtitle'];
$fieldSubtitle->fieldGroup = 'main';
$fieldSubtitle->position = 2;
$fieldSubtitle->isTranslatable = false;
$fieldSubtitle->isSearchable = true;
$fieldSubtitle->isRequired = false;
$createStruct->addFieldDefinition($fieldSubtitle);

$contentTypeGroup = $contentTypeService->loadContentTypeGroupByIdentifier('Content');
$contentTypeDraft = $contentTypeService->createContentType($createStruct, [$contentTypeGroup]);
$contentTypeService->publishContentTypeDraft($contentTypeDraft);
$contentType = $contentTypeService->loadContentType($contentTypeDraft->id);

$createStructRambo = $contentService->newContentCreateStruct($contentType, 'eng-GB');
$createStructRambo->remoteId = 'movie-456';
$createStructRambo->alwaysAvailable = false;
$createStructRambo->setField('title', 'Rambo');

$ramboDraft = $contentService->createContent($createStructRambo);
$movies[] = $contentService->publishVersion($ramboDraft->getVersionInfo());
$this->refreshSearch($repository);
$createStructRobocop = $contentService->newContentCreateStruct($contentType, 'eng-GB');
$createStructRobocop->remoteId = 'movie-789';
$createStructRobocop->alwaysAvailable = false;
$createStructRobocop->setField('title', 'Robocop');
$createStructRobocop->setField('subtitle', '');

$robocopDraft = $contentService->createContent($createStructRobocop);
$movies[] = $contentService->publishVersion($robocopDraft->getVersionInfo());
$this->refreshSearch($repository);
$createStructLastHope = $contentService->newContentCreateStruct($contentType, 'eng-GB');
$createStructLastHope->remoteId = 'movie-101112';
$createStructLastHope->alwaysAvailable = false;
$createStructLastHope->setField('title', 'Star Wars');
$createStructLastHope->setField('subtitle', 'Last Hope');

$lastHopeDraft = $contentService->createContent($createStructLastHope);
$movies[] = $contentService->publishVersion($lastHopeDraft->getVersionInfo());

$this->refreshSearch($repository);

return $movies;
}

/**
* Create test Content with ezcountry field having multiple countries selected.
*
* @return Content
* @return \eZ\Publish\API\Repository\Values\Content\Content[]
*/
protected function createMultipleCountriesContent()
{
Expand Down Expand Up @@ -1178,6 +1252,69 @@ protected function createMultipleCountriesContent()
return $content;
}

/**
* Test for the findContent() method.
*
* @return \eZ\Publish\API\Repository\Values\Content\Content[]
* @see \eZ\Publish\API\Repository\SearchService::findContent()
*/
public function testFieldIsEmpty()
{
$testContents = $this->createMovieContent();

$query = new Query(
[
'query' => new Criterion\IsFieldEmpty('subtitle'),
]
);

$repository = $this->getRepository();
$searchService = $repository->getSearchService();
$result = $searchService->findContent($query, ['eng-GB']);

$this->assertEquals(2, $result->totalCount);

$this->assertEquals(
$testContents[0]->id,
$result->searchHits[0]->valueObject->id
);
$this->assertEquals(
$testContents[1]->id,
$result->searchHits[1]->valueObject->id
);

return $testContents;
}

/**
* Test for the findContent() method.
*
* @param \eZ\Publish\API\Repository\Values\Content\Content[]
* @see \eZ\Publish\API\Repository\SearchService::findContent()
* @depends \eZ\Publish\API\Repository\Tests\SearchServiceTest::testFieldIsEmpty
*/
public function testFieldIsNotEmpty(array $testContents)
{
$query = new Query(
[
'query' => new Criterion\IsFieldEmpty(
'subtitle',
false
),
]
);

$repository = $this->getRepository();
$searchService = $repository->getSearchService();
$result = $searchService->findContent($query, ['eng-GB']);

$this->assertEquals(1, $result->totalCount);
$this->assertEquals(
$testContents[2]->id,
$result->searchHits[0]->valueObject->id
);
}

/**
* Test for the findContent() method.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/**
* File containing the eZ\Publish\API\Repository\Values\Content\Query\Criterion\IsFieldEmpty class.
*
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace eZ\Publish\API\Repository\Values\Content\Query\Criterion;

use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator\Specifications;

/**
* A criterion that matches content based on if Field is empty.
*/
class IsFieldEmpty extends Criterion
{
/**
* @param string $fieldDefinitionIdentifier
* @param bool $value
* @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
*/
public function __construct(string $fieldDefinitionIdentifier, bool $value = true)
{
parent::__construct($fieldDefinitionIdentifier, null, $value);
}

public function getSpecifications()
{
return [
new Specifications(Operator::EQ, Specifications::FORMAT_SINGLE, Specifications::TYPE_BOOLEAN),
];
}
}
10 changes: 9 additions & 1 deletion eZ/Publish/Core/Persistence/FieldType.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/
namespace eZ\Publish\Core\Persistence;

use eZ\Publish\SPI\Persistence\Content\FieldValue;
use eZ\Publish\SPI\Persistence\FieldType as FieldTypeInterface;
use eZ\Publish\SPI\FieldType\FieldType as SPIFieldType;

Expand All @@ -16,7 +17,7 @@
*
* @see \eZ\Publish\SPI\FieldType\FieldType
*/
class FieldType implements FieldTypeInterface
class FieldType implements FieldTypeInterface, FieldTypeInterface\IsEmptyValue
{
/**
* Holds internal FieldType object.
Expand Down Expand Up @@ -46,4 +47,11 @@ public function getEmptyValue()
$this->internalFieldType->getEmptyValue()
);
}

public function isEmptyValue(FieldValue $fieldValue): bool
{
return $this->internalFieldType->isEmptyValue(
$this->internalFieldType->fromPersistenceValue($fieldValue)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
namespace eZ\Publish\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler;

use eZ\Publish\Core\Search\Legacy\Content\Common\Gateway\CriteriaConverter;
use eZ\Publish\API\Repository\Exceptions\NotImplementedException;
use eZ\Publish\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler\FieldValue\Converter as FieldValueConverter;
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
Expand Down Expand Up @@ -46,16 +45,6 @@ class Field extends FieldBase
*/
protected $transformationProcessor;

/**
* Construct from handler handler.
*
* @param \eZ\Publish\Core\Persistence\Database\DatabaseHandler $dbHandler
* @param \eZ\Publish\SPI\Persistence\Content\Type\Handler $contentTypeHandler
* @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $languageHandler
* @param \eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\ConverterRegistry $fieldConverterRegistry
* @param \eZ\Publish\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler\FieldValue\Converter $fieldValueConverter
* @param \eZ\Publish\Core\Persistence\TransformationProcessor $transformationProcessor
*/
public function __construct(
DatabaseHandler $dbHandler,
ContentTypeHandler $contentTypeHandler,
Expand Down Expand Up @@ -89,7 +78,7 @@ public function accept(Criterion $criterion)
* The returned information is returned as an array of the attribute
* identifier and the sort column, which should be used.
*
* @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException If no searchable fields are found for the given $fieldIdentifier.
* @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException If no searchable fields are found for the given $fieldIdentifier.
* @throws \RuntimeException if no converter is found
*
* @param string $fieldIdentifier
Expand Down Expand Up @@ -140,7 +129,7 @@ protected function getFieldsInformation($fieldIdentifier)
*
* @return \eZ\Publish\Core\Persistence\Database\Expression
*
* @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException
* @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
* @throws \eZ\Publish\Core\Persistence\Legacy\Content\FieldValue\Converter\Exception\NotFound
*/
public function handle(
Expand Down Expand Up @@ -181,30 +170,12 @@ public function handle(
);
}

if (empty($whereExpressions)) {
throw new NotImplementedException(
sprintf(
'Following fieldtypes are not searchable in the legacy search engine: %s',
implode(', ', array_keys($fieldsInformation))
)
);
}

$subSelect->where(
$subSelect->expr->lAnd(
$subSelect->expr->eq(
$this->dbHandler->quoteColumn('version', 'ezcontentobject_attribute'),
$this->dbHandler->quoteColumn('current_version', 'ezcontentobject')
),
// Join conditions with a logical OR if several conditions exist
count($whereExpressions) > 1 ? $subSelect->expr->lOr($whereExpressions) : $whereExpressions[0],
$this->getFieldCondition($subSelect, $languageSettings)
)
);

return $query->expr->in(
$this->dbHandler->quoteColumn('id', 'ezcontentobject'),
$subSelect
return $this->getInExpressionWithFieldConditions(
$query,
$subSelect,
$languageSettings,
$whereExpressions,
$fieldsInformation
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/
namespace eZ\Publish\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler;

use eZ\Publish\API\Repository\Exceptions\NotImplementedException;
use eZ\Publish\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler;
use eZ\Publish\Core\Persistence\Database\DatabaseHandler;
use eZ\Publish\SPI\Persistence\Content\Type\Handler as ContentTypeHandler;
Expand Down Expand Up @@ -137,4 +138,50 @@ protected function getFieldCondition(SelectQuery $query, array $languageSettings
$query->expr->lt($leftSide, $rightSide)
);
}

/**
* @param \eZ\Publish\Core\Persistence\Database\SelectQuery $query
* @param \eZ\Publish\Core\Persistence\Database\SelectQuery $subSelect
* @param array $languageSettings
* @param array $fieldWhereExpressions
* @param array $fieldsInformation
*
* @return string
*
* @throws \eZ\Publish\API\Repository\Exceptions\NotImplementedException
*/
protected function getInExpressionWithFieldConditions(
SelectQuery $query,
SelectQuery $subSelect,
array $languageSettings,
array $fieldWhereExpressions,
array $fieldsInformation
): string {
if (empty($fieldWhereExpressions)) {
throw new NotImplementedException(
sprintf(
'Following fieldtypes are not searchable in the legacy search engine: %s',
implode(', ', array_keys($fieldsInformation))
)
);
}

$subSelect->where(
$subSelect->expr->lAnd(
$subSelect->expr->eq(
$this->dbHandler->quoteColumn('version', 'ezcontentobject_attribute'),
$this->dbHandler->quoteColumn('current_version', 'ezcontentobject')
),
count($fieldWhereExpressions) > 1 ? $subSelect->expr->lOr(
$fieldWhereExpressions
) : $fieldWhereExpressions[0],
$this->getFieldCondition($subSelect, $languageSettings)
)
);

return $query->expr->in(
$this->dbHandler->quoteColumn('id', 'ezcontentobject'),
$subSelect
);
}
}
Loading

0 comments on commit 31bd435

Please sign in to comment.