Easyadminbundle: implement dynamic query_builder with parameters.

Created on 10 May 2016  路  25Comments  路  Source: EasyCorp/EasyAdminBundle

Hello

i'm trying to implement a dynamic query_builder with parameters like this:

function(EntityRepository $r) use($options) {
return $r->createQueryBuilder('c');
}

i try to pass it by the type_options configuration but it fail as its a string :

The option "query_builder" with value "XXXXX" is expected to be of type "null" or "callable" or "Doctrine\ORM\QueryBuilder", but is of type "string".

witch is normal.

I could use a static call like
query_builder : ['Repository','method'] the it will work but i will not be able to pass any param to it and it wont be aware of the current entity .

i see where the injection take place in EasyAdminFormType but i have no idea how to pass the current entity or any other variable from EasyAdminFormType context.

i could patch here but its nasty. EasyAdminFormType: $builder->add($name, $formFieldType, $formFieldOptions);

is there any elegand way to fetch options from custom query_builder ? As this is very common need for example if you have a select list and you want to exclude the current entity from it .

I hope i'm clear its a bit funky issue.

Most helpful comment

@pomporov an example using form extension:

class EntityTypeExtension extends AbstractTypeExtension
{
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'query_builder_options' => array(), // <-- custom option for this feature.
        ));

        $normalizer = function (Options $options, $queryBuilder) {
            if (is_callable($queryBuilder)) {
                $queryBuilder = call_user_func(
                    $queryBuilder,
                    $options['em']->getRepository($options['class']),
                    $options // <-- type_options
                );
            }

            return $queryBuilder;
        };

        $resolver->setNormalizer('query_builder', $normalizer);
    }

    public function getExtendedType()
    {
        return EntityType::class;
    }
}
services:
    app.form.extension.entity:
        class: AppBundle\Form\Extension\EntityTypeExtension
        tags:
            - { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }

All 25 comments

Its a complemnt to https://github.com/javiereguiluz/EasyAdminBundle/issues/947
but with parameters

I don't see an easy way to do this, what you can do is create your own EntityType and add the options parameter to the function call_user_func when query_builder option is being normalized (Source).

$queryBuilderNormalizer = function (Options $options, $queryBuilder) {
    if (is_callable($queryBuilder)) {
        $queryBuilder = call_user_func(
            $queryBuilder, $options['em']->getRepository($options['class']), $options
        );

        if (null !== $queryBuilder && !$queryBuilder instanceof QueryBuilder) {
            throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder');
        }
    }

    return $queryBuilder;
};

EDIT:
Later, your static function looks like this:

Class Foo
{
    public static function bar(EntityRepository $repository, Options $options)
    {
        // return a QueryBuilder instance.
    }
}

Cheers.

this is rather dirty no ?
I think its a very common need for the easyadmin dont you think ?

@pomporov I want to improve this ... but I need more time to do it.

i was thinking make an query_builder_dynamic: ['Path\To\Repository\Class','methodName']
then in : EasyAdminFormType hardcode a query_builder injection with options

so it does not break back compatibility and allow the parameters with minimal footprint .

I don't get why you say this doesn't work:

function(EntityRepository $r) use($options) {
    // $options should be accessible here
    return $r->createQueryBuilder('c');
}

What do you mean by:

i try to pass it by the type_options configuration but it fail as its a string

?

i mean it can be:
1/ type_options:[query_builder: "function(EntityRepository $r) use($options) {return $r->createQueryBuilder('c');}"]
or
2/ type_options:[query_builder: ['repositoryClass','staticMethod']]

2 will work but no way to inject any parameter to staticMethod
1 wont work as its a string and it expect a callable

@pomporov well, maybe you can create a custom EasyAdmin TypeConfigurator for you app:

class MyEntityTypeConfigurator implements TypeConfiguratorInterface
{
    public function configure($name, array $options, array $metadata, FormConfigInterface $parentConfig)
    {
        if (isset($options['query_builder']) && is_callable($options['query_builder'])) {
            $options['query_builder'] = call_user_func(
                $options['query_builder'], 
                $options['em']->getRepository($options['class']), 
                $options // <---- type_options
            );
        }

        return $options;
    }

    public function supports($type, array $options, array $metadata)
    {
        return 'entity' === $type && 'association' === $metadata['type'];
    }
}

This class will need to be registered as service with easyadmin.form.type.configurator tag and priority less than -20.

how about this :
added in "EasyAdminFormType"

 if (isset($formFieldOptions['query_builder_dynamic']) && is_array($formFieldOptions['query_builder_dynamic']) && count($formFieldOptions['query_builder_dynamic']) == 3) {
$respositoryClassName = $formFieldOptions['query_builder_dynamic'][0];
$entityClassName = $formFieldOptions['query_builder_dynamic'][1];
$methodName = $formFieldOptions['query_builder_dynamic'][2];
$repository = $this->em->getRepository($entityClassName);
$formFieldOptions['query_builder'] = call_user_func_array(array($respositoryClassName, $methodName), [$repository, $options]);
unset($formFieldOptions['query_builder_dynamic']);
            }

then in the config :

                    - { property: 'members', type_options: { by_reference: false, query_builder_dynamic: ['My\Corp\Repository\EntityRepository','My\Corp\Entity\Entity', 'staticMethod'] } }

and then

 public static function staticMethod(EntityRepository $repository, $options)
    {
        return $repository->createQueryBuilder('c');
    }

sorry something wrong in formating

this allow with minimal modificaiton of the EasyAdminFormType (and injection of doctrine)
to have dynamic querybuilder with full acces to the repository in arbitrary location

@yceruto solutions are a lot nicer (beside the formatting ;) and far more flexible in user land code imo. No need to tweak the bundle, a form extension could hold his first example's logic if you need it.

i dont fully understand how to get it to work as i'm a bit lost in the injections.
i got the idea but i need a bit of help to figure it out .
for example it need doctrine to be injected how it works with the extension ?

@pomporov an example using form extension:

class EntityTypeExtension extends AbstractTypeExtension
{
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'query_builder_options' => array(), // <-- custom option for this feature.
        ));

        $normalizer = function (Options $options, $queryBuilder) {
            if (is_callable($queryBuilder)) {
                $queryBuilder = call_user_func(
                    $queryBuilder,
                    $options['em']->getRepository($options['class']),
                    $options // <-- type_options
                );
            }

            return $queryBuilder;
        };

        $resolver->setNormalizer('query_builder', $normalizer);
    }

    public function getExtendedType()
    {
        return EntityType::class;
    }
}
services:
    app.form.extension.entity:
        class: AppBundle\Form\Extension\EntityTypeExtension
        tags:
            - { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }

I was linking the doc for it, nice post! And here's the link: http://symfony.com/doc/current/cookbook/form/create_form_type_extension.html

thanks alot i understand now

class EntityTypeExtension extends AbstractTypeExtension
{
    public function configureOptions(OptionsResolver $resolver)
    {
        parent::configureOptions($resolver);

        // Invoke the query builder closure so that we can cache choice lists
        // for equal query builders
        $queryBuilderNormalizer = function (Options $options, $queryBuilder) {

            if (is_callable($queryBuilder)) {
                $queryBuilder = call_user_func($queryBuilder, $options['em']->getRepository($options['class']), [$entity]); //<< how to get the entity here 
                if (null !== $queryBuilder && !$queryBuilder instanceof QueryBuilder) {
                    throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder');
                }
            }

            return $queryBuilder;
        };
        $resolver->setNormalizer('query_builder', $queryBuilderNormalizer);
        $resolver->setAllowedTypes('query_builder', array('null', 'callable', 'Doctrine\ORM\QueryBuilder'));
    }

    public function getExtendedType()
    {
        return EntityType::class;
    }
}

allmost working , still EntityTypeExtension is not aware of selected entity
how to make it aware without patching the main form ?
because in EasyAdminForm:

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $entity = $options['entity'];
        $view = $options['view'];
        $entityConfig = $this->configurator->getEntityConfig($entity);
        $entityProperties = $entityConfig[$view]['fields'];

        foreach ($entityProperties as $name => $metadata) {
            $formFieldOptions = $metadata['type_options'];

            // Configure options using the list of registered type configurators:
            foreach ($this->configurators as $configurator) {
                if ($configurator->supports($metadata['fieldType'], $formFieldOptions, $metadata)) {
                    $formFieldOptions = $configurator->configure($name, $formFieldOptions, $metadata, $builder);
                }
            }

            $formFieldType = LegacyFormHelper::getType($metadata['fieldType']);

            $builder->add($name, $formFieldType, $formFieldOptions);
        }
    }

formFieldOptions has no way to get to the entity.

Thanks again, i'm learning alot with this simple task.

Hi,

i maid it work but i still have a minor modification to the EasyAdminForm in order to pass the options down to the extended Entity Type. I understand its dirty and want to find a clean way to do it .

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $entity = $options['entity'];
        $view = $options['view'];
        $entityConfig = $this->configurator->getEntityConfig($entity);
        $entityProperties = $entityConfig[$view]['fields'];

        foreach ($entityProperties as $name => $metadata) {
            $formFieldOptions = $metadata['type_options'];

            // Configure options using the list of registered type configurators:
            foreach ($this->configurators as $configurator) {
                if ($configurator->supports($metadata['fieldType'], $formFieldOptions, $metadata)) {
                    $formFieldOptions = $configurator->configure($name, $formFieldOptions, $metadata, $builder);
                }
            }

            if (isset($formFieldOptions['easyadmin_config'])) {
                $formFieldOptions['easyadmin_config'] = $options;
            }

            $formFieldType = LegacyFormHelper::getType($metadata['fieldType']);
            $builder->add($name, $formFieldType, $formFieldOptions);
        }
    }

in the config
`

  • { property: 'members', type_options: { by_reference: false, easyadmin_config: "true", query_builder: ['App\Repository\EntityRepository', 'staticMethod'] } }
    `
    the easyadmin_config trigger the custom behaviour .
    is there maybe a way to extend somehow the EasyAdminForm ??

an idea would be to use %%entity_id%% in configuration (same as for Title for example) so i can fetch the entity using the id in the extended type but i'm not sure its the way to go . i think %%entity_id%% works only in title ?

allmost working , still EntityTypeExtension is not aware of selected entity

formFieldOptions has no way to get to the entity

What do you mean by fetch the entity? Because actually when you define a form type buildForm has no access to the underlying data as it's not set yet. You need an event listener on POST_SET_DATA event.
See http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html#customizing-your-form-based-on-the-underlying-data.

I mean i want to fetch

    $entity = $options['entity'];

or whole "options" from the extended Entity Type
so in EasyAdminForm buildForm is aware of the entity, i want to pass it down
i'm not sure i'm clear

@pomporov you can use query_builder_options to pass all static values that you need in you query builder method https://github.com/javiereguiluz/EasyAdminBundle/issues/1158#issuecomment-218208259, because in form type extensions there isn't access to parent form.

hello

i cannot i think because the value i need is "$options" , how can i feed it to the variable form the admin config ?

i can pass any "string" or array , but i need options so its dynamic

regards

I think a simple addition of something like this will allow alot of dynamic operations

   - { property: 'members', type_options: { by_reference: false, myVariable: "%%options%%", query_builder: ['AppBundle\Repository\EntityRepository', 'staticMethod'] } }

then with minimal addition to buildForm we inject $options incase of %%options%%
in the same way it works for names.

it allow to push a whole new level of customisation to selects and make it fully aware of context.

I did not found any other way as it appears impossible to override the buildForm from userspace.

it looks like this

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $entity = $options['entity'];
        $view = $options['view'];
        $entityConfig = $this->configurator->getEntityConfig($entity);
        $entityProperties = $entityConfig[$view]['fields'];

        foreach ($entityProperties as $name => $metadata) {
            $formFieldOptions = $metadata['type_options'];

            // Configure options using the list of registered type configurators:
            foreach ($this->configurators as $configurator) {
                if ($configurator->supports($metadata['fieldType'], $formFieldOptions, $metadata)) {
                    $formFieldOptions = $configurator->configure($name, $formFieldOptions, $metadata, $builder);
                }
            }

            foreach ($formFieldOptions as $key => $value) {
                if ($value == '%options%') {
                    $value = $options;
                }
                $formFieldOptions[$key] = $value;
            }

            $formFieldType = LegacyFormHelper::getType($metadata['fieldType']);
            $builder->add($name, $formFieldType, $formFieldOptions);
        }
    }

I'm sorry but I'm closing this old issue as "won't fix". The reason is that this is one of those occasions were the YAML simplicity is a problem. The solution should be to move this logic to PHP as explained by @yceruto. Thanks for understanding it.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

seb-jean picture seb-jean  路  3Comments

Ealenn picture Ealenn  路  3Comments

Wait4Code picture Wait4Code  路  3Comments

nickicool picture nickicool  路  4Comments

ghost picture ghost  路  3Comments