Skip to content

Commit

Permalink
List filters form (#62)
Browse files Browse the repository at this point in the history
Adds list form filters
  • Loading branch information
alterphp committed Sep 30, 2018
1 parent 279d79f commit afa2f42
Show file tree
Hide file tree
Showing 21 changed files with 723 additions and 24 deletions.
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,83 @@ If you have defined your own admin controllers, make them extend EasyAdminExtens
Features
--------

### List filters form

Add filters on list views by configuration.

Consider following Animation entity using such [ValueListTrait](https://github.com/alterphp/components/blob/master/src/AlterPHP/Component/Behavior/ValueListTrait) :

```php
class Animation
{
use ValueListTrait;

/**
* @var string
*
* @ORM\Id
* @ORM\Column(type="guid")
*/
private $id;

/**
* @var bool
*
* @ORM\Column(type="boolean", nullable=false)
*/
private $enabled;

/**
* @var string
*
* @ORM\Column(type="string", length=31)
*/
protected $status;

/**
* @var string
*
* @ORM\Column(type="string", length=31, nullable=false)
*/
private $type;

/**
* @var Organization
*
* @ORM\ManyToOne(targetEntity="App\Entity\Organization", inversedBy="animations")
* @ORM\JoinColumn(nullable=false)
*/
private $organization;

const STATUS_DRAFT = 'draft';
const STATUS_PUBLISHED = 'published';
const STATUS_OPEN = 'open';
const STATUS_ACTIVE = 'active';
const STATUS_CLOSED = 'closed';
const STATUS_ARCHIVED = 'archived';
}
```

Define your filters under `list`.`form_filters` entity configuration. Automatic guesser set up a ChoiceType for filters mapped on boolean (NULL, true, false) and string class properties. ChoiceType for string properties requires either a `choices` label/value array in `type_options` of a `choices_static_callback` static callable that returns label/value choices list.


```yaml
easy_admin:
entities:
Animation:
class: App\Entity\Animation
list:
form_filters:
- enabled
- { property: type, type_options: { choices: { Challenge: challenge, Event: event } } }
- { property: status, type_options: { choices_static_callback: [getValuesList, [status, true]] } }
- organization
```
Let's see the result !
![Embedded list example](/doc/res/img/list-form-filters.png)
### Filter list and search on request parameters
* EasyAdmin allows filtering list with `dql_filter` configuration entry. But this is not dynamic and must be configured as an apart list in `easy_admin` configuration.*
Expand Down
Binary file added doc/res/img/list-form-filters.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
196 changes: 196 additions & 0 deletions src/Configuration/ListFormFiltersConfigPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<?php

namespace AlterPHP\EasyAdminExtensionBundle\Configuration;

use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use EasyCorp\Bundle\EasyAdminBundle\Configuration\ConfigPassInterface;
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\EasyAdminAutocompleteType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

/**
* Guess form types for list form filters.
*/
class ListFormFiltersConfigPass implements ConfigPassInterface
{
/** @var ManagerRegistry */
private $doctrine;

public function __construct(ManagerRegistry $doctrine)
{
$this->doctrine = $doctrine;
}

/**
* @param array $backendConfig
*
* @return array
*/
public function process(array $backendConfig): array
{
if (!isset($backendConfig['entities'])) {
return $backendConfig;
}

foreach ($backendConfig['entities'] as $entityName => $entityConfig) {
if (!isset($entityConfig['list']['form_filters'])) {
continue;
}

$formFilters = array();

foreach ($entityConfig['list']['form_filters'] as $i => $formFilter) {
// Detects invalid config node
if (!is_string($formFilter) && !is_array($formFilter)) {
throw new \RuntimeException(
sprintf(
'The values of the "form_filters" option for the list view of the "%s" entity can only be strings or arrays.',
$entityConfig['class']
)
);
}

// Key mapping
if (is_string($formFilter)) {
$filterConfig = array('property' => $formFilter);
} else {
if (!array_key_exists('property', $formFilter)) {
throw new \RuntimeException(
sprintf(
'One of the values of the "form_filters" option for the "list" view of the "%s" entity does not define the mandatory option "property".',
$entityConfig['class']
)
);
}

$filterConfig = $formFilter;
}

$this->configureFilter($entityConfig['class'], $filterConfig);

// If type is not configured at this steps => not guessable
if (!isset($filterConfig['type'])) {
continue;
}

$formFilters[$filterConfig['property']] = $filterConfig;
}

// set form filters config and form !
$backendConfig['entities'][$entityName]['list']['form_filters'] = $formFilters;
}

return $backendConfig;
}

private function configureFilter(string $entityClass, array &$filterConfig)
{
// No need to guess type
if (isset($filterConfig['type'])) {
return;
}

$em = $this->doctrine->getManagerForClass($entityClass);
$entityMetadata = $em->getMetadataFactory()->getMetadataFor($entityClass);

// Not able to guess type
if (
!$entityMetadata->hasField($filterConfig['property'])
&& !$entityMetadata->hasAssociation($filterConfig['property'])
) {
return;
}

if ($entityMetadata->hasField($filterConfig['property'])) {
$this->configureFieldFilter(
$entityClass, $entityMetadata->getFieldMapping($filterConfig['property']), $filterConfig
);
} elseif ($entityMetadata->hasAssociation($filterConfig['property'])) {
$this->configureAssociationFilter(
$entityClass, $entityMetadata->getAssociationMapping($filterConfig['property']), $filterConfig
);
}
}

private function configureFieldFilter(string $entityClass, array $fieldMapping, array &$filterConfig)
{
switch ($fieldMapping['type']) {
case 'boolean':
$filterConfig['type'] = ChoiceType::class;
$defaultFilterConfigTypeOptions = array(
'choices' => array(
'list_form_filters.default.boolean.true' => true,
'list_form_filters.default.boolean.false' => false,
),
'choice_translation_domain' => 'EasyAdminBundle',
);
break;
case 'string':
$filterConfig['type'] = ChoiceType::class;
$defaultFilterConfigTypeOptions = array(
'multiple' => true,
'choices' => $this->getChoiceList($entityClass, $filterConfig['property'], $filterConfig),
'attr' => array('data-widget' => 'select2'),
);
break;
default:
return;
}

// Merge default type options when defined
if (isset($defaultFilterConfigTypeOptions)) {
$filterConfig['type_options'] = array_merge(
$defaultFilterConfigTypeOptions,
isset($filterConfig['type_options']) ? $filterConfig['type_options'] : array()
);
}
}

private function configureAssociationFilter(string $entityClass, array $associationMapping, array &$filterConfig)
{
// To-One (EasyAdminAutocompleteType)
if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) {
$filterConfig['type'] = EasyAdminAutocompleteType::class;
$filterConfig['type_options'] = array_merge(
array(
'class' => $associationMapping['targetEntity'],
'multiple' => true,
'attr' => array('data-widget' => 'select2'),
),
isset($filterConfig['type_options']) ? $filterConfig['type_options'] : array()
);
}
}

private function getChoiceList(string $entityClass, string $property, array &$filterConfig)
{
if (isset($filterConfig['type_options']['choices'])) {
$choices = $filterConfig['type_options']['choices'];
unset($filterConfig['type_options']['choices']);

return $choices;
}

if (!isset($filterConfig['type_options']['choices_static_callback'])) {
throw new \RuntimeException(
sprintf(
'Choice filter field "%s" for entity "%s" must provide either a static callback method returning choice list or choices option.',
$property,
$entityClass
)
);
}

$callableParams = array();
if (is_string($filterConfig['type_options']['choices_static_callback'])) {
$callable = array($entityClass, $filterConfig['type_options']['choices_static_callback']);
} else {
$callable = array($entityClass, $filterConfig['type_options']['choices_static_callback'][0]);
$callableParams = $filterConfig['type_options']['choices_static_callback'][1];
}
unset($filterConfig['type_options']['choices_static_callback']);

return forward_static_call_array($callable, $callableParams);
}
}
41 changes: 39 additions & 2 deletions src/EventListener/PostQueryBuilderSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public function onPostListQueryBuilder(GenericEvent $event)

if ($event->hasArgument('request')) {
$this->applyRequestFilters($queryBuilder, $event->getArgument('request')->get('filters', array()));
$this->applyFormFilters($queryBuilder, $event->getArgument('request')->get('form_filters', array()));
}
}

Expand All @@ -53,7 +54,7 @@ public function onPostSearchQueryBuilder(GenericEvent $event)
}

/**
* Applies filters on queryBuilder.
* Applies request filters on queryBuilder.
*
* @param QueryBuilder $queryBuilder
* @param array $filters
Expand All @@ -72,12 +73,48 @@ protected function applyRequestFilters(QueryBuilder $queryBuilder, array $filter
continue;
}
// Sanitize parameter name
$parameter = 'filter_'.str_replace('.', '_', $field);
$parameter = 'request_filter_'.str_replace('.', '_', $field);

$this->filterQueryBuilder($queryBuilder, $field, $parameter, $value);
}
}

/**
* Applies form filters on queryBuilder.
*
* @param QueryBuilder $queryBuilder
* @param array $filters
*/
protected function applyFormFilters(QueryBuilder $queryBuilder, array $filters = array())
{
foreach ($filters as $field => $value) {
$value = $this->filterEasyadminAutocompleteValue($value);
// Empty string and numeric keys is considered as "not applied filter"
if (is_int($field) || '' === $value) {
continue;
}
// Add root entity alias if none provided
$field = false === strpos($field, '.') ? $queryBuilder->getRootAlias().'.'.$field : $field;
// Checks if filter is directly appliable on queryBuilder
if (!$this->isFilterAppliable($queryBuilder, $field)) {
continue;
}
// Sanitize parameter name
$parameter = 'form_filter_'.str_replace('.', '_', $field);

$this->filterQueryBuilder($queryBuilder, $field, $parameter, $value);
}
}

private function filterEasyadminAutocompleteValue($value)
{
if (!is_array($value) || !isset($value['autocomplete']) || 1 !== count($value)) {
return $value;
}

return $value['autocomplete'];
}

/**
* Filters queryBuilder.
*
Expand Down
Loading

0 comments on commit afa2f42

Please sign in to comment.