Skip to content

Instantly share code, notes, and snippets.

@teklakct
Last active July 8, 2024 08:11
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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment