Skip to content

Instantly share code, notes, and snippets.

@teklakct
Last active February 3, 2025 14:07
Show Gist options
  • Save teklakct/ed154ccadc18b1463a139f09d3286355 to your computer and use it in GitHub Desktop.
Save teklakct/ed154ccadc18b1463a139f09d3286355 to your computer and use it in GitHub Desktop.
Naive support for dot notation in filters

If you are having trouble with dot notation in EasyAdmin filters and cannot wait for Use filters on nested properties #4882, you can use this workaround.

Of course, this code is not optimal and probably does not support all comparison cases, but it can still work as a foundation for your temporary solution.

Example usage

Tested on EasyAdmin v4.9.4

Entities

For the sake of clarity in the example, all mapping and other methods are omitted.

class Location 
{
  public string $id;
  
  public string $name;
  
  public function getNiceName(): string
  {
    return \sprint('#%d - %s', $this->id, $this->name);    
  }
}

class Offer 
{
  public string $id;
  
  #[ORM\ManyToOne(targetEntity: Location::class))]
  private Location $location
}

class Contract 
{
  public string $id;
  
  #[ORM\ManyToOne(targetEntity: Offer::class))]
  private Location $offer
}

class Invoice 
{
  public string $id;
  
  #[ORM\ManyToOne(targetEntity: Contract::class))]
  private Location $contract
}

Filters

Filter by location, where Location::class is assosiated with Offer::class (offer.location), and to get Offer::class we need to have Contract::class (contract.offer.location).

class InvoiceCrudController extends AbstractCrudController
{
    public static function getEntityFqcn(): string
    {
        return Invoice::class;
    }
  
    public function configureFilters(Filters $filters): Filters
    {
        return $filters
            ->add(AssociatedEntityFilter::new('contract.offer.location', 'Location')
                ->setFormTypeOptions([
                    'value_type_options.class' => Location::class,
                    'value_type_options.choice_label' => 'getNiceName',
                ])
            );
    }
}

Or filter in Contract::class crud

class ContractCrudController extends AbstractCrudController
{
    public static function getEntityFqcn(): string
    {
        return Contract::class;
    }
  
    public function configureFilters(Filters $filters): Filters
    {
        return $filters
            ->add(AssociatedEntityFilter::new('offer.location', 'Location')
                ->setFormTypeOptions([
                    'value_type_options.class' => Location::class,
                    'value_type_options.choice_label' => 'getNiceName',
                ])
            );
    }
}

Also works as EntityFilter

class OfferCrudController extends AbstractCrudController
{
    public static function getEntityFqcn(): string
    {
        return Offer::class;
    }
  
    public function configureFilters(Filters $filters): Filters
    {
        return $filters
            ->add(AssociatedEntityFilter::new('location', 'Location')
                ->setFormTypeOptions([
                    'value_type_options.class' => Location::class,
                    'value_type_options.choice_label' => 'getNiceName',
                ])
            );
    }
}
<?php
declare(strict_types=1);
namespace Acme\Filter;
use Acme\Form\Filter\Type\AssociatedEntityFilterType;
use Doctrine\ORM\QueryBuilder;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Filter\FilterInterface;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\FilterDataDto;
use EasyCorp\Bundle\EasyAdminBundle\Filter\FilterTrait;
class AssociatedEntityFilter implements FilterInterface
{
use FilterTrait;
use FilterQueryTrait;
public static function new(string $pathToProperty, ?string $label = null): FilterInterface
{
$propertyName = self::stripPropertyName($pathToProperty);
return (new self())
->setFilterFqcn(__CLASS__)
->setProperty($propertyName)
->setLabel($label)
->setFormType(AssociatedEntityFilterType::class)
->setFormTypeOptions([
'association_path' => $pathToProperty
]);
}
public function apply(QueryBuilder $queryBuilder, FilterDataDto $filterDataDto, ?FieldDto $fieldDto, EntityDto $entityDto): void
{
$associationPath = $filterDataDto->getFormTypeOption('association_path');
$associations = ($associationPath && \strlen($associationPath) > 0) ? explode('.', $associationPath) : [];
$property = array_pop($associations) ?? $filterDataDto->getProperty();
$alias = $filterDataDto->getEntityAlias();
$comparison = $filterDataDto->getComparison();
$parameterName = $filterDataDto->getParameterName();
$value = $filterDataDto->getValue();
foreach ($associations as $associatedProperty) {
$assocAlias = 'ea_' . $associatedProperty;
if (!$this->alreadyJoined($queryBuilder, $assocAlias)) {
$queryBuilder->leftJoin(\sprintf('%s.%s', $alias, $associatedProperty), $assocAlias);
}
$alias = $assocAlias;
}
$queryBuilder
->andWhere(\sprintf('%s.%s %s (:%s)', $alias, $property, $comparison, $parameterName))
->setParameter($parameterName, $value)
;
}
private static function stripPropertyName(string $propertyName): string
{
$lastDot = strrchr($propertyName, '.');
return $lastDot === false ? $propertyName : substr($lastDot, 1);
}
}
<?php
declare(strict_types=1);
namespace Acme\Form\Filter\Type;
use EasyCorp\Bundle\EasyAdminBundle\Form\Filter\Type\EntityFilterType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AssociatedEntityFilterType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'association_path' => null,
]);
$resolver->setAllowedTypes('association_path', ['null', 'string']);
}
public function getParent(): string
{
return EntityFilterType::class;
}
}
<?php
declare(strict_types=1);
namespace Acme\Filter;
use Doctrine\ORM\QueryBuilder;
trait FilterQueryTrait
{
public function alreadyJoined(QueryBuilder $queryBuilder, string $alias): bool
{
return \in_array($alias, $queryBuilder->getAllAliases(), true);
}
}
@Crease29
Copy link

Crease29 commented Aug 1, 2024

Thanks for sharing! Is the FilterQueryTrait missing in this Gist? I can't seem to find it as part of EasyAdmin.

@teklakct
Copy link
Author

teklakct commented Aug 1, 2024

Sorry @Crease29 , I forgot to add that. Already updated the gist

@johndodev
Copy link

Suggestion : not sure about the trait, as it's used only one time. I replaced it with a private method to reduce complexity.

Error on my case : I added 2 filters :

  • one classic EntityFilter on a property named "foo".
  • one of your AssiociationFilter for an association named "bar.foo"

As you are removing the "bar." of the property name on the "new" method (line 22), the property name is also "foo" instead of "bar.foo", and there is an EasyAdmin Exception triggered because you can't define multiple filters for one property".
Suggestion : Keep the "pathToProperty" as propery name for the DTO, and retrive the real one on the apply method.
I did that here, it's working. Actually you have to replace the dots with a specific placeholder, otherwise there is another error related to an array..

ex :

return (new self())
            ->setFilterFqcn(__CLASS__)
            ->setProperty(str_replace('.', 'DOT', $pathToProperty))

and in the apply method :

$property = str_replace('DOT', '.', $filterDataDto->getProperty());
$property = self::stripPropertyName($property);

Anyway, very usefull, thanks.

@teklakct
Copy link
Author

teklakct commented Feb 3, 2025

Thank you a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment