Expression transformers 
Expression transformers provide a way to extend Doctrine DQL expressions before they are executed by a filter handler.
Built-in expression transformers 
TrimExpressionTransformer- wraps the expression in theTRIM()functionLowerExpressionTransformer- wraps the expression in theLOWER()functionUpperExpressionTransformer- wraps the expression in theUPPER()functionCallbackExpressionTransformer- allows transforming the expression using the callback function
The expression transformers can be passed using the expression_transformers option:
use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer;
use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\LowerExpressionTransformer;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\TrimExpressionTransformer;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
class ProductDataTableType extends AbstractDataTableType
{
    public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
    {
        $builder
            ->addFilter('name', TextFilterType::class, [
                'expression_transformers' => [
                    new LowerExpressionTransformer(),
                    new TrimExpressionTransformer(),
                ],
            ])
        ;
    }
}The transformers are called in the same order as they are passed.
For easier usage, some of the built-in transformers can be enabled using the trim, lower and upper filter options:
use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
class ProductDataTableType extends AbstractDataTableType
{
    public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
    {
        $builder
            ->addFilter('name', TextFilterType::class, [
                'trim' => true,
                'lower' => true,
                'upper' => true,
            ])
        ;
    }
}When using the trim, lower or upper options, their transformers are called before the expression_transformers ones.
Creating a custom expression transformer 
To create a custom expression transformer, create a new class that implements ExpressionTransformerInterface:
namespace App\DataTable\Filter\ExpressionTransformer;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\Expr\Comparison;
use Kreyu\Bundle\DataTableBundle\Exception\UnexpectedTypeException;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\ExpressionTransformerInterface;
class UnaccentExpressionTransformer implements ExpressionTransformerInterface
{
    public function transform(mixed $expression): mixed
    {
        if (!$expression instanceof Comparison) {
            throw new UnexpectedTypeException($expression, Comparison::class);
        }
        $leftExpr = sprintf('UNACCENT(%s)', (string) $expression->getLeftExpr());
        $rightExpr = sprintf('UNACCENT(%s)', (string) $expression->getRightExpr());
        
        // or use expression API:
        //
        // $leftExpr = new Expr\Func('UNACCENT', $expression->getLeftExpr());
        // $rightExpr = new Expr\Func('UNACCENT', $expression->getRightExpr());
        return new Comparison($leftExpr, $expression->getOperator(), $rightExpr);
    }
}If you're sure that the expression transformer requires the expression to be a comparison (it will be in most cases), you can extend the AbstractComparisonExpressionTransformer class which simplifies the definition:
namespace App\DataTable\Filter\ExpressionTransformer;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\ExpressionTransformer\AbstractComparisonExpressionTransformer;
class UnaccentExpressionTransformer extends AbstractComparisonExpressionTransformer
{
    protected function transformLeftExpr(mixed $leftExpr): mixed
    {
        return sprintf('UNACCENT(%s)', (string) $leftExpr);
        
        // or use expression API: 
        // 
        // return new Expr\Func('UNACCENT', $leftExpr);
    }
    protected function transformRightExpr(mixed $rightExpr): mixed
    {
        return sprintf('UNACCENT(%s)', (string) $rightExpr);
        
        // or use expression API: 
        //
        // return new Expr\Func('UNACCENT', $rightExpr);
    }
}The AbstractComparisonExpressionTransformer accepts two boolean arguments:
transformLeftExpr- defaults totruetransformRightExpr- defaults totrue
Thanks to that, the user can specify which side of the expression should be transformed. The transformLeftExpr() and transformRightExpr() methods are called only when necessary. For example:
$expression = new Expr\Comparison('foo', '=', 'bar');
// LOWER(foo) = LOWER(bar)
(new LowerExpressionTransformer())
    ->transform($expression);
// foo = LOWER(bar)
(new LowerExpressionTransformer(transformLeftExpr: false, transformRightExpr: true))
    ->transform($expression);
// LOWER(foo) = bar
(new LowerExpressionTransformer(transformLeftExpr: true, transformRightExpr: false))
    ->transform($expression);To use the created expression transformer, pass it as the expression_transformers filter type option:
use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer;
use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
class ProductDataTableType extends AbstractDataTableType
{
    public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
    {
        $builder
            ->addFilter('name', TextFilterType::class, [
                'expression_transformers' => [
                    new UnaccentExpressionTransformer(),
                ],
            ])
        ;
    }
}Adding an option to automatically apply transformer 
Following the above example of UnaccentExpressionTransformer, let's assume, that we want to create such definition:
use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer;
use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType;
use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\TextFilterType;
class ProductDataTableType extends AbstractDataTableType
{
    public function buildDataTable(DataTableBuilderInterface $builder, array $options): void
    {
        $builder
            ->addFilter('name', TextFilterType::class, [
                'unaccent' => true,
            ])
        ;
    }
}To achieve that, create a custom filter type extension:
use App\DataTable\Filter\ExpressionTransformer\UnaccentExpressionTransformer;
use Kreyu\Bundle\DataTableBundle\Filter\Extension\AbstractFilterTypeExtension;
use Kreyu\Bundle\DataTableBundle\Bridge\Doctrine\Orm\Filter\Type\DoctrineOrmFilterType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\Options;
class UnaccentFilterTypeExtension extends AbstractFilterTypeExtension
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver
            ->setDefault('unaccent', false)
            ->setAllowedTypes('unaccent', 'bool')
            ->addNormalizer('expression_transformers', function (Options $options, array $expressionTransformers) {
                if ($options['unaccent']) {
                    $expressionTransformers[] = new UnaccentExpressionTransformer();
                }
                
                return $expressionTransformers;
            })
        ;
    }
    
    public static function getExtendedTypes(): iterable
    {
        return [DoctrineOrmFilterType::class];
    }
}The unaccent option is now defined, and defaults to false. In addition, the options resolver normalizer will automatically push an instance of the custom expression transformer to the expression_transformers option, but only if the unaccent option equals true.
