Skip to content

[Doctrine bridge] Support custom ID-types in EntityType / ORMQueryBuilderLoader #35165

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Devristo opened this issue Jan 2, 2020 · 4 comments

Comments

@Devristo
Copy link
Contributor

Devristo commented Jan 2, 2020

Related issues and comments: #14583 #23853 #23808

Description
The ORMQueryBuilderLoader tries to guess the identifier type to perform filter out invalid values for integral or UUID types by matching a hardcoded list of Doctrine type names. However there are cases that guessing is not working properly and it causes invalid queries to be constructed.

In my case I have separate UUID types for each entity, such that I have strong typing in PHP for these IDs. Lets say BlogId (doctrine name blog_id) and PostId (doctrine name post_id).

When using an EntityType, internally constructing a ORMQueryBuilderLoader, it fails to filter out empty strings. This creates a query Postgres chokes on with error: ERROR: invalid input syntax for uuid.

It would be nice if there was an extension point such that this guessing logic can be altered for custom types.

Example

Maybe we could creating a factory service which can be decorated for the ORMQueryBuilderLoader? Such that a custom implementation can be provided?

@HeahDude
Copy link
Contributor

HeahDude commented Jan 8, 2020

Hello @Devristo, thank you for opening this issue.

There is already an extension point to provide a custom object loader, but it requires to implement a new form type extending the abstract DoctrineType (or the EntityType using classic PHP inheritance, not the getParent() method from the interface) and implement the protected getLoader() method, which is exactly what the EntityType is doing by the way, also normalizing the query builder option value.

However, note that using a pure extends, the custom type won't inherit any registered EntityType extension if any.

@Devristo
Copy link
Contributor Author

Devristo commented Jan 8, 2020

Thanks, didn't think of that. Since I needed it to work with vendor-types that use the EntityType I have created a FormTypeExtension instead of a new type. For now I used the following ugly extension to be able to use my own loader. Probably will swap the ugly anonymous class out with a copy of the choiceLoader-anonymous function created by the EntityType. But for now I am happy. Thanks again @HeahDude.

<?php

namespace App\Service;


use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectManager;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;


class EntityTypeExtension extends EntityType implements FormTypeExtensionInterface
{
    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        // This is a nasty way to get the choiceLoader callable defined by
        // the EntityType.        
        $myResolver = new class extends OptionsResolver {
            private $choiceLoader;

            public function setDefault($option, $value)
            {
                if ($option === 'choice_loader'){
                    $this->choiceLoader = $value;
                }

                parent::setDefault($option, $value);
            }

            public function getChoiceLoader()
            {
                return $this->choiceLoader;
            }
        };

        // Please be warned that the options are now configured twice, here and by Symfony.
        parent::configureOptions($myResolver);

        // This is magic: now the callable is bound to EntityTypeExtension
        // context instead of the EntityType context so we can override 
        // the getLoader() method-below
        $choiceLoader = $myResolver->getChoiceLoader();
        $resolver->setDefault('choice_loader', $choiceLoader);
    }

    /**
     * Return the default loader object.
     *
     * @param QueryBuilder $queryBuilder
     * @param string       $class
     *
     * @return ORMQueryBuilderLoader
     */
    public function getLoader(ObjectManager $manager, $queryBuilder, $class)
    {
        if (!$queryBuilder instanceof QueryBuilder) {
            throw new \TypeError(sprintf('Expected an instance of %s, but got %s.', QueryBuilder::class, \is_object($queryBuilder) ? \get_class($queryBuilder) : \gettype($queryBuilder)));
        }

        return new MyORMQueryBuilderLoader($queryBuilder);
    }

    public static function getExtendedTypes()
    {
        return [EntityType::class];
    }

    /**
     * @inheritDoc
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    }

    /**
     * @inheritDoc
     */
    public function buildView(
        FormView $view,
        FormInterface $form,
        array $options
    ) {
    }

    /**
     * @inheritDoc
     */
    public function finishView(
        FormView $view,
        FormInterface $form,
        array $options
    ) {
    }

    /**
     * @inheritDoc
     */
    public function getExtendedType()
    {
        return [EntityType::class];
    }
}

@Devristo Devristo closed this as completed Jan 8, 2020
@HeahDude
Copy link
Contributor

HeahDude commented Jan 9, 2020

Ok this is tricky :D. Maybe you could use something simpler like:

<?php

namespace App\Service;

use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectManager;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;

class EntityTypeExtension extends EntityType implements FormTypeExtensionInterface
{
    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefault('choice_loader', function (Options $options, $choiceLoader) {
            return $choiceLoader instanceof \Closure ? \Closure::bind($choiceLoader, $this) : $choiceLoader;
        });
    }
    // ...
}

@Devristo
Copy link
Contributor Author

Devristo commented Jan 9, 2020

This unfortunately does not work as the function is already called at this point and returned a DoctrineChoiceLoader instance.

Thanks for thinking with me. Will come back to this and try some different stuff when I get bored ;)

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

No branches or pull requests

3 participants