diff --git a/docs/reference/filter_field_definition.rst b/docs/reference/filter_field_definition.rst
index ca2d1f737..27486849a 100644
--- a/docs/reference/filter_field_definition.rst
+++ b/docs/reference/filter_field_definition.rst
@@ -44,6 +44,7 @@ For now, only `Doctrine ORM` filters are available:
* ``Sonata\DoctrineORMAdminBundle\Filter\DateTimeFilter``: depends on the ``Sonata\AdminBundle\Form\Type\Filter\DateTimeType`` Form Type, renders a datetime field,
* ``Sonata\DoctrineORMAdminBundle\Filter\DateTimeRangeFilter``: depends on the ``Sonata\AdminBundle\Form\Type\Filter\DateTimeRangeType`` Form Type, renders a 2 datetime fields,
* ``Sonata\DoctrineORMAdminBundle\Filter\ClassFilter``: depends on the ``Sonata\AdminBundle\Form\Type\Filter\DefaultType`` Form type, renders a choice list field.
+* ``Sonata\DoctrineORMAdminBundle\Filter\EmptyFieldFilter``: depends on the ``Sonata\AdminBundle\Form\Type\Filter\DefaultType`` Form type, renders a choice list field.
Example
-------
@@ -162,6 +163,26 @@ ClassFilter
}
}
+Empty
+-----
+
+``Sonata\DoctrineORMAdminBundle\Filter\EmptyFieldFilter`` supports filtering for empty (null) entity fields::
+
+ namespace Sonata\NewsBundle\Admin;
+
+ use Sonata\AdminBundle\Admin\AbstractAdmin;
+ use Sonata\AdminBundle\Datagrid\DatagridMapper;
+ use Sonata\AdminBundle\Filter\EmptyFieldFilter;
+
+ final class PostAdmin extends AbstractAdmin
+ {
+ protected function configureDatagridFilters(DatagridMapper $datagridMapper)
+ {
+ $datagridMapper
+ ->add('deleted', EmptyFieldFilter::class, ['field_name' => 'deletedAt']);
+ }
+ }
+
Advanced usage
--------------
diff --git a/src/Filter/EmptyFieldFilter.php b/src/Filter/EmptyFieldFilter.php
new file mode 100644
index 000000000..6ff42ae27
--- /dev/null
+++ b/src/Filter/EmptyFieldFilter.php
@@ -0,0 +1,97 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Sonata\DoctrineORMAdminBundle\Filter;
+
+use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
+use Sonata\AdminBundle\Form\Type\Filter\DefaultType;
+use Sonata\Form\Type\BooleanType;
+use Symfony\Component\Form\Extension\Core\Type\HiddenType;
+
+final class EmptyFieldFilter extends Filter
+{
+ /**
+ * @param string $alias
+ * @param string $field
+ * @param mixed[]|null $data
+ */
+ public function filter(ProxyQueryInterface $queryBuilder, $alias, $field, $data): void
+ {
+ if (null === $data || !\is_array($data) || !\array_key_exists('value', $data)) {
+ return;
+ }
+
+ if (BooleanType::TYPE_NO === (int) $data['value']) {
+ $this->applyWhere(
+ $queryBuilder,
+ $queryBuilder
+ ->expr()
+ ->isNull(sprintf('%s.%s', $alias, $field))
+ );
+ } elseif (BooleanType::TYPE_YES === (int) $data['value']) {
+ $this->applyWhere(
+ $queryBuilder,
+ $queryBuilder
+ ->expr()
+ ->isNotNull(sprintf('%s.%s', $alias, $field))
+ );
+ }
+ }
+
+ public function getDefaultOptions()
+ {
+ return [
+ 'field_type' => BooleanType::class,
+ 'operator_type' => HiddenType::class,
+ 'operator_options' => [],
+ ];
+ }
+
+ public function getRenderSettings()
+ {
+ return [DefaultType::class, [
+ 'field_type' => $this->getFieldType(),
+ 'field_options' => $this->getFieldOptions(),
+ 'operator_type' => $this->getOption('operator_type'),
+ 'operator_options' => $this->getOption('operator_options'),
+ 'label' => $this->getLabel(),
+ ]];
+ }
+
+ public function getParentAssociationMappings()
+ {
+ $mappings = $this->getOption('parent_association_mappings', []);
+
+ $fields = explode('.', $this->getFieldName(), -1);
+
+ foreach ($fields as $field) {
+ $mappings[] = ['fieldName' => $field];
+ }
+
+ return $mappings;
+ }
+
+ /**
+ * @param mixed|null $data
+ *
+ * @return string[]
+ */
+ protected function association(ProxyQueryInterface $queryBuilder, $data): array
+ {
+ $alias = $queryBuilder->entityJoin($this->getParentAssociationMappings());
+ $part = strrchr('.'.$this->getFieldName(), '.');
+ $fieldName = substr(false === $part ? $this->getFieldType() : $part, 1);
+
+ return [$alias, $fieldName];
+ }
+}
diff --git a/src/Resources/config/doctrine_orm_filter_types.xml b/src/Resources/config/doctrine_orm_filter_types.xml
index e921938cf..a26de8139 100644
--- a/src/Resources/config/doctrine_orm_filter_types.xml
+++ b/src/Resources/config/doctrine_orm_filter_types.xml
@@ -40,5 +40,8 @@
+
+
+
diff --git a/tests/Filter/EmptyFieldFilterTest.php b/tests/Filter/EmptyFieldFilterTest.php
new file mode 100644
index 000000000..3ccc2a21a
--- /dev/null
+++ b/tests/Filter/EmptyFieldFilterTest.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Sonata\DoctrineORMAdminBundle\Tests\Filter;
+
+use PHPUnit\Framework\TestCase;
+use Sonata\DoctrineORMAdminBundle\Filter\EmptyFieldFilter;
+use Sonata\Form\Type\BooleanType;
+use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
+
+final class EmptyFieldFilterTest extends TestCase
+{
+ public function testRenderSettings(): void
+ {
+ $filter = new EmptyFieldFilter();
+ $filter->initialize('field_name', [
+ 'field_options' => ['class' => 'FooBar'],
+ ]);
+ $options = $filter->getRenderSettings()[1];
+
+ $this->assertSame(BooleanType::class, $options['field_type']);
+ }
+}