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.
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
`
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.
Most helpful comment
@pomporov an example using form extension: