From 984018f344b2867f1d176a6cbecc6f2a27edbe6d Mon Sep 17 00:00:00 2001 From: Krzysztof Trzos Date: Mon, 6 Nov 2017 08:12:36 +0100 Subject: [PATCH 1/8] Searching by related entities fields While we can make add columns of an related entity in the "list" actions, there is no way to search by them. ```yml easy_admin: entities: Campaign: list: fields: - property: name - property: domain.name // <--- ``` ```yml easy_admin: entities: Campaign: search: fields: - domain.name // <--- throwing error ``` These changes are making that it will now be possible. What do you think about it? --- src/Search/QueryBuilder.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Search/QueryBuilder.php b/src/Search/QueryBuilder.php index 5aede4b45c..3dad02231d 100644 --- a/src/Search/QueryBuilder.php +++ b/src/Search/QueryBuilder.php @@ -102,6 +102,13 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor $queryParameters = array(); foreach ($entityConfig['search']['fields'] as $name => $metadata) { + $fieldPrefix = 'entity'; + + if(strpos($name, '.') !== false) { + [$fieldPrefix, $name] = explode('.', $name); + $queryBuilder->join('entity.'.$fieldPrefix, $fieldPrefix); + } + $isSmallIntegerField = 'smallint' === $metadata['dataType']; $isIntegerField = 'integer' === $metadata['dataType']; $isNumericField = in_array($metadata['dataType'], array('number', 'bigint', 'decimal', 'float')); @@ -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', $fieldPrefix, $name)); // 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', $fieldPrefix, $name)); $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', $fieldPrefix, $name)); $queryParameters['fuzzy_query'] = '%'.$lowerSearchQuery.'%'; - $queryBuilder->orWhere(sprintf('LOWER(entity.%s) IN (:words_query)', $name)); + $queryBuilder->orWhere(sprintf('LOWER(%s.%s) IN (:words_query)', $fieldPrefix, $name)); $queryParameters['words_query'] = explode(' ', $lowerSearchQuery); } } From e73afa42b79c9ae229940cb81fde829e077d1d4b Mon Sep 17 00:00:00 2001 From: Maxime COLIN Date: Fri, 5 Jan 2018 15:47:56 +0100 Subject: [PATCH 2/8] Fix multiple join --- src/Search/QueryBuilder.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Search/QueryBuilder.php b/src/Search/QueryBuilder.php index 3dad02231d..f90853e9fe 100644 --- a/src/Search/QueryBuilder.php +++ b/src/Search/QueryBuilder.php @@ -88,10 +88,12 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor ->from($entityConfig['class'], 'entity') ; + $joined = array(); $isSortedByDoctrineAssociation = false !== strpos($sortField, '.'); if ($isSortedByDoctrineAssociation) { $sortFieldParts = explode('.', $sortField); $queryBuilder->leftJoin('entity.'.$sortFieldParts[0], $sortFieldParts[0]); + $joined[] = $sortFieldParts[0]; } $isSearchQueryNumeric = is_numeric($searchQuery); @@ -106,9 +108,13 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor if(strpos($name, '.') !== false) { [$fieldPrefix, $name] = explode('.', $name); - $queryBuilder->join('entity.'.$fieldPrefix, $fieldPrefix); + + if (!in_array($fieldPrefix, $joined)) { + $queryBuilder->join('entity.'.$fieldPrefix, $fieldPrefix); + $joined[] = $fieldPrefix; + } } - + $isSmallIntegerField = 'smallint' === $metadata['dataType']; $isIntegerField = 'integer' === $metadata['dataType']; $isNumericField = in_array($metadata['dataType'], array('number', 'bigint', 'decimal', 'float')); From db3b4b42be77950be770907b6ff9b582bb5f051c Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Wed, 7 Feb 2018 21:28:47 +0100 Subject: [PATCH 3/8] Code syntax fixes and tweaks --- src/Search/QueryBuilder.php | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/Search/QueryBuilder.php b/src/Search/QueryBuilder.php index f90853e9fe..00227bf13e 100644 --- a/src/Search/QueryBuilder.php +++ b/src/Search/QueryBuilder.php @@ -88,12 +88,12 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor ->from($entityConfig['class'], 'entity') ; - $joined = array(); + $entitiesAlreadyJoined = array(); $isSortedByDoctrineAssociation = false !== strpos($sortField, '.'); if ($isSortedByDoctrineAssociation) { - $sortFieldParts = explode('.', $sortField); - $queryBuilder->leftJoin('entity.'.$sortFieldParts[0], $sortFieldParts[0]); - $joined[] = $sortFieldParts[0]; + list($associatedEntityName, $associatedFieldName) = explode('.', $sortField); + $queryBuilder->leftJoin('entity.'.$associatedEntityName, $associatedFieldName); + $entitiesAlreadyJoined[] = $associatedEntityName; } $isSearchQueryNumeric = is_numeric($searchQuery); @@ -103,16 +103,17 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor $lowerSearchQuery = mb_strtolower($searchQuery); $queryParameters = array(); - foreach ($entityConfig['search']['fields'] as $name => $metadata) { - $fieldPrefix = 'entity'; - - if(strpos($name, '.') !== false) { - [$fieldPrefix, $name] = explode('.', $name); - - if (!in_array($fieldPrefix, $joined)) { - $queryBuilder->join('entity.'.$fieldPrefix, $fieldPrefix); - $joined[] = $fieldPrefix; + 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']; @@ -127,17 +128,17 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor $isIntegerField && $isSearchQueryInteger || $isNumericField && $isSearchQueryNumeric ) { - $queryBuilder->orWhere(sprintf('%s.%s = :numeric_query', $fieldPrefix, $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('%s.%s = :uuid_query', $fieldPrefix, $name)); + $queryBuilder->orWhere(sprintf('%s.%s = :uuid_query', $entityName, $fieldName)); $queryParameters['uuid_query'] = $searchQuery; } elseif ($isTextField) { - $queryBuilder->orWhere(sprintf('LOWER(%s.%s) LIKE :fuzzy_query', $fieldPrefix, $name)); + $queryBuilder->orWhere(sprintf('LOWER(%s.%s) LIKE :fuzzy_query', $entityName, $fieldName)); $queryParameters['fuzzy_query'] = '%'.$lowerSearchQuery.'%'; - $queryBuilder->orWhere(sprintf('LOWER(%s.%s) IN (:words_query)', $fieldPrefix, $name)); + $queryBuilder->orWhere(sprintf('LOWER(%s.%s) IN (:words_query)', $entityName, $fieldName)); $queryParameters['words_query'] = explode(' ', $lowerSearchQuery); } } From 6f201dd2a425b346f82c6fe2022a20c608ed8467 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Thu, 8 Feb 2018 20:52:05 +0100 Subject: [PATCH 4/8] Fixes --- src/Configuration/ViewConfigPass.php | 3 ++- src/Search/QueryBuilder.php | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Configuration/ViewConfigPass.php b/src/Configuration/ViewConfigPass.php index c1449107e7..b8b96d3f5c 100644 --- a/src/Configuration/ViewConfigPass.php +++ b/src/Configuration/ViewConfigPass.php @@ -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)); } diff --git a/src/Search/QueryBuilder.php b/src/Search/QueryBuilder.php index 00227bf13e..77e1ca8e81 100644 --- a/src/Search/QueryBuilder.php +++ b/src/Search/QueryBuilder.php @@ -88,14 +88,6 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor ->from($entityConfig['class'], 'entity') ; - $entitiesAlreadyJoined = array(); - $isSortedByDoctrineAssociation = false !== strpos($sortField, '.'); - if ($isSortedByDoctrineAssociation) { - list($associatedEntityName, $associatedFieldName) = explode('.', $sortField); - $queryBuilder->leftJoin('entity.'.$associatedEntityName, $associatedFieldName); - $entitiesAlreadyJoined[] = $associatedEntityName; - } - $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; @@ -103,10 +95,11 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor $lowerSearchQuery = mb_strtolower($searchQuery); $queryParameters = array(); + $entitiesAlreadyJoined = array(); foreach ($entityConfig['search']['fields'] as $fieldName => $metadata) { $entityName = 'entity'; if (false !== strpos($fieldName, '.')) { - list($associatedEntityName, $associatedFieldName) = explode('.', $fieldName); + list($associatedEntityName, $associatedFieldName) = explode('.', $fieldName); if (!in_array($associatedEntityName, $entitiesAlreadyJoined)) { $queryBuilder->leftJoin('entity.'.$associatedEntityName, $associatedEntityName); $entitiesAlreadyJoined[] = $associatedEntityName; @@ -151,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, $associatedFieldName); + $entitiesAlreadyJoined[] = $associatedEntityName; + } + } + if (null !== $sortField) { $queryBuilder->orderBy(sprintf('%s%s', $isSortedByDoctrineAssociation ? '' : 'entity.', $sortField), $sortDirection ?: 'DESC'); } From 6248c60bcb2f95c49ea9a3b085e8bca5295467b4 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Sat, 10 Feb 2018 10:55:12 +0100 Subject: [PATCH 5/8] Fixed tests --- .travis.yml | 1 + src/Search/QueryBuilder.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index aa35b346b5..df75faee8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,6 +66,7 @@ install: - if [[ "$ENABLE_CODE_COVERAGE" == "true" && "$TRAVIS_EVENT_TYPE" == "cron" ]]; then composer require --dev satooshi/php-coveralls; fi script: + - cat composer.lock - if [[ "$ENABLE_CODE_COVERAGE" == "true" && "$TRAVIS_EVENT_TYPE" == "cron" ]]; then vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml; else vendor/bin/phpunit; fi; - if [[ "$CHECK_PHP_SYNTAX" == "yes" ]]; then php vendor/bin/php-cs-fixer --no-interaction --dry-run --diff -v fix; fi; - if [[ "$CHECK_PHP_SYNTAX" == "yes" ]]; then mv ./.php_cs.cache $HOME/.app/cache/.php_cs.cache 2> /dev/null; fi; diff --git a/src/Search/QueryBuilder.php b/src/Search/QueryBuilder.php index 77e1ca8e81..1118937106 100644 --- a/src/Search/QueryBuilder.php +++ b/src/Search/QueryBuilder.php @@ -148,7 +148,7 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor if ($isSortedByDoctrineAssociation) { list($associatedEntityName, $associatedFieldName) = explode('.', $sortField); if (!in_array($associatedEntityName, $entitiesAlreadyJoined)) { - $queryBuilder->leftJoin('entity.'.$associatedEntityName, $associatedFieldName); + $queryBuilder->leftJoin('entity.'.$associatedEntityName, $associatedEntityName); $entitiesAlreadyJoined[] = $associatedEntityName; } } From f136e6f412aa2d274c577cc4b19ff18616a12ad2 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Sat, 10 Feb 2018 11:06:35 +0100 Subject: [PATCH 6/8] Fix tests --- .travis.yml | 1 - src/Search/QueryBuilder.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index df75faee8b..aa35b346b5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,7 +66,6 @@ install: - if [[ "$ENABLE_CODE_COVERAGE" == "true" && "$TRAVIS_EVENT_TYPE" == "cron" ]]; then composer require --dev satooshi/php-coveralls; fi script: - - cat composer.lock - if [[ "$ENABLE_CODE_COVERAGE" == "true" && "$TRAVIS_EVENT_TYPE" == "cron" ]]; then vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml; else vendor/bin/phpunit; fi; - if [[ "$CHECK_PHP_SYNTAX" == "yes" ]]; then php vendor/bin/php-cs-fixer --no-interaction --dry-run --diff -v fix; fi; - if [[ "$CHECK_PHP_SYNTAX" == "yes" ]]; then mv ./.php_cs.cache $HOME/.app/cache/.php_cs.cache 2> /dev/null; fi; diff --git a/src/Search/QueryBuilder.php b/src/Search/QueryBuilder.php index 1118937106..c715cfc900 100644 --- a/src/Search/QueryBuilder.php +++ b/src/Search/QueryBuilder.php @@ -99,7 +99,7 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor foreach ($entityConfig['search']['fields'] as $fieldName => $metadata) { $entityName = 'entity'; if (false !== strpos($fieldName, '.')) { - list($associatedEntityName, $associatedFieldName) = explode('.', $fieldName); + list($associatedEntityName, $associatedFieldName) = explode('.', $fieldName); if (!in_array($associatedEntityName, $entitiesAlreadyJoined)) { $queryBuilder->leftJoin('entity.'.$associatedEntityName, $associatedEntityName); $entitiesAlreadyJoined[] = $associatedEntityName; From 851fce9eb4c17525fe307538e9f75868f0242062 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Sat, 10 Feb 2018 11:37:12 +0100 Subject: [PATCH 7/8] Added tests for the new feature --- tests/Controller/CustomizedBackendTest.php | 15 +++++++++++++++ .../App/config/config_customized_backend.yml | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/tests/Controller/CustomizedBackendTest.php b/tests/Controller/CustomizedBackendTest.php index dedf22d0c1..3a5cf21569 100644 --- a/tests/Controller/CustomizedBackendTest.php +++ b/tests/Controller/CustomizedBackendTest.php @@ -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'); diff --git a/tests/Fixtures/App/config/config_customized_backend.yml b/tests/Fixtures/App/config/config_customized_backend.yml index dcebd9f14f..1dba2be322 100644 --- a/tests/Fixtures/App/config/config_customized_backend.yml +++ b/tests/Fixtures/App/config/config_customized_backend.yml @@ -135,8 +135,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' From 7ed99c6e9274972dc67f6077fc06c406035f8f28 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Sat, 10 Feb 2018 11:59:36 +0100 Subject: [PATCH 8/8] Added docs for the new feature --- doc/book/list-search-show-configuration.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/book/list-search-show-configuration.rst b/doc/book/list-search-show-configuration.rst index de48082409..52afb204e8 100644 --- a/doc/book/list-search-show-configuration.rst +++ b/doc/book/list-search-show-configuration.rst @@ -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