Skip to content

Commit

Permalink
feature #2045 Allow to search by related entities fields (ktrzos, max…
Browse files Browse the repository at this point in the history
…imecolin, javiereguiluz)

This PR was merged into the 1.17.x-dev branch.

Discussion
----------

Allow to search by related entities fields

This finishes #1991.

Commits
-------

7ed99c6 Added docs for the new feature
851fce9 Added tests for the new feature
f136e6f Fix tests
6248c60 Fixed tests
6f201dd Fixes
db3b4b4 Code syntax fixes and tweaks
b60b986 Merge pull request #1 from maximecolin/patch-3
e73afa4 Fix multiple join
984018f Searching by related entities fields
  • Loading branch information
javiereguiluz committed Feb 10, 2018
2 parents ab91172 + 7ed99c6 commit 39e9442
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 12 deletions.
11 changes: 11 additions & 0 deletions doc/book/list-search-show-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,17 @@ Use the ``fields`` option to explicitly set the properties to display:
class: AppBundle\Entity\Customer
list:
fields: ['id', 'firstName', 'lastName', 'phone', 'email']
# if the field name contains a dot, it's the property of a Doctrine association
list:
# this config displays the 'email' and 'phone' properties of the
# Doctrine entity associated via the 'user' property of 'Customer'
fields: ['id', 'name', 'age', 'user.email', 'user.phone']
# Doctrine associations are also supported in the 'search' view. This config looks
# for data in the 'email' and 'phone' properties of the associated 'user' entity
search:
fields: ['name', 'user.email', 'user.phone']
# ...
This option is also useful to reorder the properties, because by default they
Expand Down
3 changes: 2 additions & 1 deletion src/Configuration/ViewConfigPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ private function processSortingConfig(array $backendConfig)
throw new \InvalidArgumentException(sprintf('If defined, the second value of the "sort" option of the "%s" view of the "%s" entity can only be "ASC" or "DESC".', $view, $entityName));
}

if (isset($entityConfig[$view]['fields'][$sortConfig['field']]) && true === $entityConfig[$view]['fields'][$sortConfig['field']]['virtual']) {
$isSortedByDoctrineAssociation = false !== strpos($sortConfig['field'], '.');
if (!$isSortedByDoctrineAssociation && (isset($entityConfig[$view]['fields'][$sortConfig['field']]) && true === $entityConfig[$view]['fields'][$sortConfig['field']]['virtual'])) {
throw new \InvalidArgumentException(sprintf('The "%s" field cannot be used in the "sort" option of the "%s" view of the "%s" entity because it\'s a virtual property that is not persisted in the database.', $sortConfig['field'], $view, $entityName));
}

Expand Down
38 changes: 27 additions & 11 deletions src/Search/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,27 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor
->from($entityConfig['class'], 'entity')
;

$isSortedByDoctrineAssociation = false !== strpos($sortField, '.');
if ($isSortedByDoctrineAssociation) {
$sortFieldParts = explode('.', $sortField);
$queryBuilder->leftJoin('entity.'.$sortFieldParts[0], $sortFieldParts[0]);
}

$isSearchQueryNumeric = is_numeric($searchQuery);
$isSearchQuerySmallInteger = (is_int($searchQuery) || ctype_digit($searchQuery)) && $searchQuery >= -32768 && $searchQuery <= 32767;
$isSearchQueryInteger = (is_int($searchQuery) || ctype_digit($searchQuery)) && $searchQuery >= -2147483648 && $searchQuery <= 2147483647;
$isSearchQueryUuid = 1 === preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $searchQuery);
$lowerSearchQuery = mb_strtolower($searchQuery);

$queryParameters = array();
foreach ($entityConfig['search']['fields'] as $name => $metadata) {
$entitiesAlreadyJoined = array();
foreach ($entityConfig['search']['fields'] as $fieldName => $metadata) {
$entityName = 'entity';
if (false !== strpos($fieldName, '.')) {
list($associatedEntityName, $associatedFieldName) = explode('.', $fieldName);
if (!in_array($associatedEntityName, $entitiesAlreadyJoined)) {
$queryBuilder->leftJoin('entity.'.$associatedEntityName, $associatedEntityName);
$entitiesAlreadyJoined[] = $associatedEntityName;
}

$entityName = $associatedEntityName;
$fieldName = $associatedFieldName;
}

$isSmallIntegerField = 'smallint' === $metadata['dataType'];
$isIntegerField = 'integer' === $metadata['dataType'];
$isNumericField = in_array($metadata['dataType'], array('number', 'bigint', 'decimal', 'float'));
Expand All @@ -114,17 +121,17 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor
$isIntegerField && $isSearchQueryInteger ||
$isNumericField && $isSearchQueryNumeric
) {
$queryBuilder->orWhere(sprintf('entity.%s = :numeric_query', $name));
$queryBuilder->orWhere(sprintf('%s.%s = :numeric_query', $entityName, $fieldName));
// adding '0' turns the string into a numeric value
$queryParameters['numeric_query'] = 0 + $searchQuery;
} elseif ($isGuidField && $isSearchQueryUuid) {
$queryBuilder->orWhere(sprintf('entity.%s = :uuid_query', $name));
$queryBuilder->orWhere(sprintf('%s.%s = :uuid_query', $entityName, $fieldName));
$queryParameters['uuid_query'] = $searchQuery;
} elseif ($isTextField) {
$queryBuilder->orWhere(sprintf('LOWER(entity.%s) LIKE :fuzzy_query', $name));
$queryBuilder->orWhere(sprintf('LOWER(%s.%s) LIKE :fuzzy_query', $entityName, $fieldName));
$queryParameters['fuzzy_query'] = '%'.$lowerSearchQuery.'%';

$queryBuilder->orWhere(sprintf('LOWER(entity.%s) IN (:words_query)', $name));
$queryBuilder->orWhere(sprintf('LOWER(%s.%s) IN (:words_query)', $entityName, $fieldName));
$queryParameters['words_query'] = explode(' ', $lowerSearchQuery);
}
}
Expand All @@ -137,6 +144,15 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor
$queryBuilder->andWhere($dqlFilter);
}

$isSortedByDoctrineAssociation = false !== strpos($sortField, '.');
if ($isSortedByDoctrineAssociation) {
list($associatedEntityName, $associatedFieldName) = explode('.', $sortField);
if (!in_array($associatedEntityName, $entitiesAlreadyJoined)) {
$queryBuilder->leftJoin('entity.'.$associatedEntityName, $associatedEntityName);
$entitiesAlreadyJoined[] = $associatedEntityName;
}
}

if (null !== $sortField) {
$queryBuilder->orderBy(sprintf('%s%s', $isSortedByDoctrineAssociation ? '' : 'entity.', $sortField), $sortDirection ?: 'DESC');
}
Expand Down
15 changes: 15 additions & 0 deletions tests/Controller/CustomizedBackendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,21 @@ public function testSearchViewShowActionReferer()
$this->assertSame($parameters, $refererParameters);
}

public function testSearchUsingAssociations()
{
$parameters = array(
'action' => 'search',
'entity' => 'Purchase',
'page' => '1',
'query' => 'user9@example',
);

$crawler = $this->getBackendPage($parameters);

$this->assertSame('user9', trim($crawler->filter('.table tbody tr td[data-label="Buyer"]')->eq(0)->text()));
$this->assertContains('sorted', $crawler->filter('.table th[data-property-name="buyer"]')->eq(0)->attr('class'));
}

public function testListViewVirtualFields()
{
$crawler = $this->requestListView('Product');
Expand Down
5 changes: 5 additions & 0 deletions tests/Fixtures/App/config/config_customized_backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,13 @@ easy_admin:
list:
fields:
- id
- buyer
- billingAddress
- { property: 'deliveryDate', format: 'Ymd' }
- { property: 'deliveryHour', format: 'H:i' }
search:
fields: ['guid', 'billingAddress', 'buyer.email']
sort: ['buyer.email', ASC]
PurchaseItem:
class: AppTestBundle\Entity\FunctionalTests\PurchaseItem
label: 'Purchase Items'
Expand Down

0 comments on commit 39e9442

Please sign in to comment.