Skip to content

Commit

Permalink
feature #1392 Allow to sort using Doctrine associations (javiereguiluz)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the master branch (closes #1392).

Discussion
----------

Allow to sort using Doctrine associations

This is a very early "proof of concept" to solve issues like #1337 which allow to sort entities using Doctrine associations. Example:

```yaml
entities:
    Article:
        # ...
        sort: author.name
```

---

If we support this, my intention is to only support 1 nesting level (e.g. `author.name`) and not deeper nesting (e.g. `purchase.item.product.name`).

**My question**: doing a naïve `$queryBuilder->join(...)` will work in all cases?

Commits
-------

bbe620c Allow to sort using Doctrine associations
  • Loading branch information
javiereguiluz committed Jan 21, 2017
2 parents 990392f + bbe620c commit 0cc14b1
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 8 deletions.
13 changes: 11 additions & 2 deletions Configuration/ViewConfigPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,17 @@ private function processSortingConfig(array $backendConfig)
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));
}

if (!array_key_exists($sortConfig['field'], $entityConfig['properties']) && !isset($entityConfig[$view]['fields'][$sortConfig['field']])) {
throw new \InvalidArgumentException(sprintf('The "%s" field used in the "sort" option of the "%s" view of the "%s" entity does not exist neither as a property of that entity nor as a virtual field of that view.', $sortConfig['field'], $view, $entityName));
// sort can be defined using simple properties (sort: author) or association properties (sort: author.name)
if (substr_count($sortConfig['field'], '.') > 1) {
throw new \InvalidArgumentException(sprintf('The "%s" value cannot be used as the "sort" option in the "%s" view of the "%s" entity because it defines multiple sorting levels (e.g. "aaa.bbb.ccc") but only up to one level is supported (e.g. "aaa.bbb").', $sortConfig['field'], $view, $entityName));
}

// sort field can be a Doctrine association (sort: author.name) instead of a simple property
$sortFieldParts = explode('.', $sortConfig['field']);
$sortFieldProperty = $sortFieldParts[0];

if (!array_key_exists($sortFieldProperty, $entityConfig['properties']) && !isset($entityConfig[$view]['fields'][$sortFieldProperty])) {
throw new \InvalidArgumentException(sprintf('The "%s" field used in the "sort" option of the "%s" view of the "%s" entity does not exist neither as a property of that entity nor as a virtual field of that view.', $sortFieldProperty, $view, $entityName));
}

$backendConfig['entities'][$entityName][$view]['sort'] = $sortConfig;
Expand Down
15 changes: 12 additions & 3 deletions Resources/doc/book/3-list-search-show-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,14 +417,23 @@ property using the `sort` configuration option:
# app/config/config.yml
easy_admin:
entities:
Product:
User:
# ...
list:
# if the sort order is not specified, 'DESC' is used
sort: 'updatedAt'
sort: 'createdAt'
search:
# use an array to also define the sorting direction
sort: ['updatedAt', 'ASC']
sort: ['createdAt', 'ASC']
Purchase:
# ...
# the 'sort' option supports Doctrine associations up to one level
# (e.g. 'sort: user.name' works but 'sort: user.group.name' won't work)
list:
sort: 'user.name'
search:
sort: ['user.name', 'ASC']
```
The `sort` option of each entity is only used as the default content sorting. If
Expand Down
2 changes: 1 addition & 1 deletion Resources/views/default/list.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
{% block table_head %}
<tr>
{% for field, metadata in fields %}
{% set isSortingField = metadata.property == app.request.get('sortField') %}
{% set isSortingField = metadata.property == app.request.get('sortField')|split('.')|first %}
{% set nextSortDirection = isSortingField ? (app.request.get('sortDirection') == 'DESC' ? 'ASC' : 'DESC') : 'DESC' %}
{% set _column_label = (metadata.label ?: field|humanize)|trans(_trans_parameters) %}
{% set _column_icon = isSortingField ? (nextSortDirection == 'DESC' ? 'fa-caret-up' : 'fa-caret-down') : 'fa-sort' %}
Expand Down
16 changes: 14 additions & 2 deletions Search/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,18 @@ public function createListQueryBuilder(array $entityConfig, $sortField = null, $
->from($entityConfig['class'], 'entity')
;

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

if (!empty($dqlFilter)) {
$queryBuilder->andWhere($dqlFilter);
}

if (null !== $sortField) {
$queryBuilder->orderBy('entity.'.$sortField, $sortDirection);
$queryBuilder->orderBy(sprintf('%s%s', $isSortedByDoctrineAssociation ? '' : 'entity.', $sortField), $sortDirection);
}

return $queryBuilder;
Expand Down Expand Up @@ -82,6 +88,12 @@ 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]);
}

$queryParameters = array();
foreach ($entityConfig['search']['fields'] as $name => $metadata) {
$isNumericField = in_array($metadata['dataType'], array('integer', 'number', 'smallint', 'bigint', 'decimal', 'float'));
Expand Down Expand Up @@ -116,7 +128,7 @@ public function createSearchQueryBuilder(array $entityConfig, $searchQuery, $sor
}

if (null !== $sortField) {
$queryBuilder->orderBy('entity.'.$sortField, $sortDirection ?: 'DESC');
$queryBuilder->orderBy(sprintf('%s%s', $isSortedByDoctrineAssociation ? '' : 'entity.', $sortField), $sortDirection ?: 'DESC');
}

return $queryBuilder;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# TEST
# when 'sort' uses a Doctrine association (sort: author.name) the first part
# (in this example, 'author') must refer to a valid entity property

# EXCEPTION
expected_exception:
class: InvalidArgumentException
message_string: 'The "this-does-not-exist" field used in the "sort" option of the "list" view of the "Category" entity does not exist neither as a property of that entity nor as a virtual field of that view.'

# CONFIGURATION
easy_admin:
entities:
Category:
class: AppTestBundle\Entity\UnitTests\Category
list:
sort: 'this-does-not-exist.name'
15 changes: 15 additions & 0 deletions Tests/Configuration/fixtures/exceptions/sort_multiple_levels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# TEST
# the sort field can only contain up to one nesting level (e.g. aaa.bbb)

# EXCEPTION
expected_exception:
class: InvalidArgumentException
message_string: 'The "association1.association2.property" value cannot be used as the "sort" option in the "list" view of the "Category" entity because it defines multiple sorting levels (e.g. "aaa.bbb.ccc") but only up to one level is supported (e.g. "aaa.bbb").'

# CONFIGURATION
easy_admin:
entities:
Category:
class: AppTestBundle\Entity\UnitTests\Category
list:
sort: 'association1.association2.property'
40 changes: 40 additions & 0 deletions Tests/Controller/EntitySortingByAssociationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/*
* This file is part of the EasyAdminBundle.
*
* (c) Javier Eguiluz <javier.eguiluz@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace JavierEguiluz\Bundle\EasyAdminBundle\Tests\Controller;

use JavierEguiluz\Bundle\EasyAdminBundle\Tests\Fixtures\AbstractTestCase;

class EntitySortingByAssociationTest extends AbstractTestCase
{
public function setUp()
{
parent::setUp();

$this->initClient(array('environment' => 'entity_sorting_by_association'));
}

public function testListViewSorting()
{
$crawler = $this->requestListView();

$this->assertContains('sorted', $crawler->filter('th[data-property-name="parent"]')->attr('class'));
$this->assertContains('fa-caret-down', $crawler->filter('th[data-property-name="parent"] i')->attr('class'));
}

public function testSearchViewSorting()
{
$crawler = $this->requestSearchView();

$this->assertContains('sorted', $crawler->filter('th[data-property-name="parent"]')->attr('class'));
$this->assertContains('fa-caret-up', $crawler->filter('th[data-property-name="parent"] i')->attr('class'));
}
}
11 changes: 11 additions & 0 deletions Tests/Fixtures/App/config/config_entity_sorting_by_association.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
imports:
- { resource: config.yml }

easy_admin:
entities:
Category:
class: AppTestBundle\Entity\FunctionalTests\Category
list:
sort: parent.name
search:
sort: [parent.name, ASC]

0 comments on commit 0cc14b1

Please sign in to comment.