Skip to content
This repository

[2.2] [Form] Added form type "entity_identifier" (#1946) #1951

Closed
wants to merge 8 commits into from
Grégoire Passault

(You can have a look at the issue #1946)

The "entity_id" type allow you to do things such:

        $builder->add('city', 'entity_id', array(
            'query_builder' => function($repository, $id) {
                return $repository->createQueryBuilder('c')
                    ->where('c.id = :id AND c.available = 1')
                    ->setParameter('id', $id);
            },  
            'class' => 'Gregwar\TestBundle\Entity\City',
            'required' => false,
            'hidden' => true
        )); 

So, with some JS logics and UI, you can fill the field directly using the entity id.

If you pass hidden => false, a text input will be rendered, which can allow an user to provide the id manually.

If you don't pass a query_builder closure, ->find() will be used.

Eric Clemmons

Have you tried using a FormExtension to add a prototype option, where you can define hidden, text, integer, etc.? I ask since entity performs most of this work & translation already: the issue is more along the lines of how to render it.

Grégoire Passault

No, the issue is not about the way it's rendered.

As mentionned in the issue #1946, the problem with the entity type is that it will always fetch all the records from the database and then act like a choice type.

What if you want your user to choose among thousands of cities ?

And, even if you don't have many entries in your database, do you think that's normal to fetch everything in PHP and then look if the id is part of the results ?

src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((39 lines not shown))
  39
+        $this->class = $class;
  40
+        $this->queryBuilder = $queryBuilder;
  41
+    }
  42
+
  43
+    /**
  44
+     * Fetch the id of the entity to populate the form
  45
+     */
  46
+    public function transform($data)
  47
+    {
  48
+        if (null === $data)
  49
+            return null;
  50
+
  51
+        $meta = $this->em->getClassMetadata($this->class);
  52
+
  53
+        if (!$meta->getReflectionClass()->isInstance($data))
  54
+            throw new TransformationFailedException('Invalid data, must be an instance of '.$this->class);
1
Christophe Coevoet Collaborator
stof added a note

you need to add the curly braces here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((16 lines not shown))
  16
+use Symfony\Component\Form\FormError;
  17
+use Symfony\Component\Form\Exception\TransformationFailedException;
  18
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
  19
+
  20
+use Doctrine\ORM\EntityManager;
  21
+use Doctrine\ORM\NoResultException;
  22
+
  23
+class OneEntityToIdTransformer implements DataTransformerInterface
  24
+{
  25
+    private $em;
  26
+    private $class;
  27
+    private $queryBuilder;
  28
+
  29
+    public function __construct(EntityManager $em, $class, $queryBuilder)
  30
+    {
  31
+        if (!(null === $queryBuilder || $queryBuilder instanceof \Closure)) {
2
Christophe Coevoet Collaborator
stof added a note

you should write it if (null !== $queryBuilder && ! $queryBuilder instanceof \Closure) instead.

Also, what about allowing any callable instead of forcing to use a closure ?

Grégoire Passault
Gregwar added a note

This is almost the same as in "../ChoiceList/EntityChoiceList.php" which is written so and just accepts closure, I must confess I don't know what it would not be any callable

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((21 lines not shown))
  21
+use Doctrine\ORM\NoResultException;
  22
+
  23
+class OneEntityToIdTransformer implements DataTransformerInterface
  24
+{
  25
+    private $em;
  26
+    private $class;
  27
+    private $queryBuilder;
  28
+
  29
+    public function __construct(EntityManager $em, $class, $queryBuilder)
  30
+    {
  31
+        if (!(null === $queryBuilder || $queryBuilder instanceof \Closure)) {
  32
+            throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure');
  33
+        } 
  34
+
  35
+        if (null == $class)
  36
+            throw new UnexpectedTypeException($class, 'string');
1
Christophe Coevoet Collaborator
stof added a note

you need to add curly braces

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((20 lines not shown))
  20
+use Doctrine\ORM\EntityManager;
  21
+use Doctrine\ORM\NoResultException;
  22
+
  23
+class OneEntityToIdTransformer implements DataTransformerInterface
  24
+{
  25
+    private $em;
  26
+    private $class;
  27
+    private $queryBuilder;
  28
+
  29
+    public function __construct(EntityManager $em, $class, $queryBuilder)
  30
+    {
  31
+        if (!(null === $queryBuilder || $queryBuilder instanceof \Closure)) {
  32
+            throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure');
  33
+        } 
  34
+
  35
+        if (null == $class)
1
Christophe Coevoet Collaborator
stof added a note

you should use ===

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((34 lines not shown))
  34
+
  35
+        if (null == $class)
  36
+            throw new UnexpectedTypeException($class, 'string');
  37
+
  38
+        $this->em = $em;
  39
+        $this->class = $class;
  40
+        $this->queryBuilder = $queryBuilder;
  41
+    }
  42
+
  43
+    /**
  44
+     * Fetch the id of the entity to populate the form
  45
+     */
  46
+    public function transform($data)
  47
+    {
  48
+        if (null === $data)
  49
+            return null;
1
Christophe Coevoet Collaborator
stof added a note

you need to add curly braces here too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((57 lines not shown))
  57
+        $id = $meta->getReflectionProperty($identifierField)->getValue($data);
  58
+
  59
+        return $id;
  60
+    }
  61
+
  62
+    /**
  63
+     * Try to fetch the entity from its id in the database
  64
+     */
  65
+    public function reverseTransform($data)
  66
+    {
  67
+        if (!$data) {
  68
+            return null;
  69
+        }
  70
+
  71
+        $em = $this->em;
  72
+        $class = $this->class;
1
Christophe Coevoet Collaborator
stof added a note

why doing this ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((77 lines not shown))
  77
+            if ($qb instanceof \Closure) {
  78
+                $qb = $qb($repository, $data);
  79
+            }
  80
+
  81
+            try {
  82
+                $result = $qb->getQuery()->getSingleResult();
  83
+            } catch (NoResultException $e) {
  84
+                $result = null;
  85
+            }
  86
+        } else {
  87
+            // Defaults to find()
  88
+            $result = $repository->find($data);
  89
+        }
  90
+
  91
+        if (!$result)
  92
+            throw new TransformationFailedException('Can not find entity');
1
Christophe Coevoet Collaborator
stof added a note

curly braces are missing here too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Bridge/Doctrine/Form/Type/EntityIdType.php
((10 lines not shown))
  10
+ */
  11
+
  12
+namespace Symfony\Bridge\Doctrine\Form\Type;
  13
+
  14
+use Symfony\Component\Form\FormBuilder;
  15
+use Symfony\Bridge\Doctrine\RegistryInterface;
  16
+use Symfony\Bridge\Doctrine\Form\DataTransformer\OneEntityToIdTransformer;
  17
+use Symfony\Component\Form\AbstractType;
  18
+
  19
+class EntityIdType extends AbstractType
  20
+{
  21
+    protected $em;
  22
+
  23
+    public function __construct(RegistryInterface $registry)
  24
+    {
  25
+        $this->em = $registry->getEntityManager();
1
Christophe Coevoet Collaborator
stof added a note

this is wrong. You should keep the reference to the registry instead and then retrieve the good entity manager according to the name passed as option, like in the EntityType.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((45 lines not shown))
  45
+     * Fetch the id of the entity to populate the form
  46
+     */
  47
+    public function transform($data)
  48
+    {
  49
+        if (null === $data) {
  50
+            return null;
  51
+        }
  52
+
  53
+        $meta = $this->em->getClassMetadata($this->class);
  54
+
  55
+        if (!$meta->getReflectionClass()->isInstance($data)) {
  56
+            throw new TransformationFailedException('Invalid data, must be an instance of '.$this->class);
  57
+        }
  58
+
  59
+        $identifierField = $meta->getSingleIdentifierFieldName();
  60
+        $id = $meta->getReflectionProperty($identifierField)->getValue($data);
3
Marc Weistroff
marcw added a note

Using Reflection here is actually a problem because we lose the ability of the Proxy to load the missing properties of the entity.

Grégoire Passault
Gregwar added a note

Should I use "Form\Util\PropertyPath" to get the id value then ?

Marc Weistroff
marcw added a note

I sent you a PR on your Bundle which is inspired of the original EntityToIdTransformer.
btw, I don't think it's a good idea to use the Form PropertyPath.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Grégoire Passault

I added the support of "property" options which you can use to search entries using an alternative field instead of the real database id.

Example of use

Imagine you want to implement a simple "message" entity which users can send to others, you want to add a recipient field in which they can type the login of the other users, you simply do:

$builder->add('recipient', 'entity_id', array(
    'class' => 'Acme\DemoBundle\Entity\User',
    'property' => 'login',
    'hidden' => false
));

And entity_id will do all the job !

You can of course put autocompletion and everything you want for your UI, and still override the query_builder to fetch the entity, but using the property instead of the id to search it.

Christophe Coevoet stof commented on the diff
src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((92 lines not shown))
  92
+            try {
  93
+                $result = $qb->getQuery()->getSingleResult();
  94
+            } catch (NoResultException $e) {
  95
+                $result = null;
  96
+            }
  97
+        } else {
  98
+            // Defaults to find()
  99
+            if ($this->property) {
  100
+                $result = $repository->findOneBy(array($this->property => $data));
  101
+            } else {
  102
+                $result = $repository->find($data);
  103
+            }
  104
+        }
  105
+
  106
+        if (!$result) {
  107
+            throw new TransformationFailedException('Can not find entity');
1
Christophe Coevoet Collaborator
stof added a note

TransformationFailedException are turned into errors by the Form class. This is the way to add errors in transformers (see the doc of the interface)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Grégoire Passault

I just updated my FormBundle repo if you want to use entity_id :
https://github.com/Gregwar/FormBundle

src/Symfony/Bridge/Doctrine/Form/Type/EntityIdType.php
((37 lines not shown))
  37
+
  38
+    public function getDefaultOptions(array $options)
  39
+    {
  40
+        $defaultOptions = array(
  41
+            'required'          => true,
  42
+            'em'                => null,
  43
+            'class'             => null,
  44
+            'query_builder'     => null,
  45
+            'property'          => null,
  46
+            'hidden'            => true
  47
+        );
  48
+
  49
+        $options = array_replace($defaultOptions, $options);
  50
+
  51
+        if (null === $options['class']) {
  52
+            throw new \RunTimeException('You must provide a class option for the entity_id field');
1
Christophe Coevoet Collaborator
stof added a note

it should be a FormException instead

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Philip Dahlstrøm

+1

Alexandre Bacco

Massive +1 to this PR.

Shouldn't you rename it to entity_single or something like that ? Entity_id is obsolete since you added the property option.

Grégoire Passault

@winzou, that's right, I'm still not sure about the name...

Note that If you read "entity identifer", you understand well what the field does in any case; it is an information that identifies an entity. Maybe entity_identifier ?

The problem with entity_single is that the entity type can also be used to choose a single entity so it would be a little confusing

Any feedbacks about that ?

Philip Dahlstrøm

entity_identifier sounds fine to me.

Jordan Stout
j commented

So I'm needing something similar to this. My current use case uses elastic search and javascript autopopulate on an input field. Upon clicking, I of course set that id to the hidden field, but I also need to populate a select options with relational data to what was clicked... So I get the argument error on both the hidden type as well as the choice type.

I'd rather not have to render another hidden type for an onchange event of the relational choice type... so perhaps having the option to specify what kind of "empty" field you would like to render?

And a name like "empty_entity" makes more sense to me

Grégoire Passault

@jstout24, I think that would imply moving the "text" and "hidden" form types services from FrameworkBundle to the DoctrineBundle, and I'm not sure that's a good idea because of code coupling concerns.

And I'm not sure I understand why do you want this to be empty ?

Jordan Stout
j commented

@Gregwar, I don't quite understand. I just mean that most use cases would need just a hidden field, but what about if you're using javascript to populate a select.

ie:

There are Users > Zipcode (100,000+ records) > SexOffenders (about 20 per zip)

Users need a location in a zipcode database (very large, as you were saying.. an autocomplete box passing ID to a hidden entity type would suffice for this.)

But let's say after they enter their zipcode, I want to populate a regular ...

The select should automatically bind to entity SexOffender.. whereas using this FormType, I'd have to also write an onchange event to the SexOffender select to input that id to the hidden field.

Maybe it doesn't need to be within this type unless it was renamed to "empty_entity" or something..... This current implementation is for only

However, if 'choices' => array() could ensure that no data is selected:

$builder->add('sexOffenders', 'entity', array(
    'class'   => 'Acme\DemoBundle\Entity\SexOffender',
    'choices' => array()
));

Or just create an empty option for the EntityType:

$builder->add('sexOffenders', 'entity', array(
    'class' => 'Acme\DemoBundle\Entity\SexOffender',
    'empty' => true
));

Also, another thing I wanted to do was have a hidden DateType but was limited by the documentation (I'm not sure if it's possible)

Thoughts?

Jordan Stout
j commented

What's the status of this PR?

Grégoire Passault

@jstout24, I'm waiting for feedbacks from @fabpot or someone else

Chris Duell

I'm desperately needing something like this too.

Grégoire Passault

You can use this bundle while waiting for news on this : https://github.com/Gregwar/FormBundle

Jonathan N

Any update in this PR? I am hitting the same wall...

Jordan Stout
j commented

^^

Jordan Stout
j commented

I still think there are other ways of doing this / name it. To me, it should be something like, "empty_entity" and possibly have a way to choose the form type (whether it's a hidden input or an empty choice field to be populated perhaps?). Or what if someone is using models instead of entities and want to just tie it to a class instead of a doctrine entity? Most use cases are directly for things like auto complete input boxes and auto population of sub selects where you don't need to select everything within a collection, but the children of another entity, etc. Regardless, this type of functionality is really needed asap.

@fabpot @bschussek ?

Henrik Bjørnskov

@jstout24 "Or what if someone is using models instead of entities and want to just tie it to a class instead of a doctrine entity? " is invalid as the original EntityType is in the Doctrine bridge.

Jeremy Mikola

I realize it's short notice, but is anyone available to speak for this in this week's developer meeting (5pm CET in #symfony-dev on Freenode)? I recall @Gregwar said he would be unavailable in his email yesterday.

Alexander

I think there is one other issue with this PR. The property option is inconsistent with the property option of the 'entity' type. This is confusing?

More information here:
Gregwar/FormBundle#3

Grégoire Passault

(cf response on Gregwar/FormBundle#3)

Jordan Stout
j commented

I think this issue needs to be addressed by @fabpot or @bschussek

Alexander

Any insights from @fabpot or @bschussek?

I think this is a nice to have for forms interacting with js? Also have a look at the discussion here:
Gregwar/FormBundle#3

Tobias Schultze
Collaborator

Hey guys, I need something similar. I've got a text field for a person which is filled with autocomplete in the format:
firstName lastName [ID]. This way the form could also work when javascript is disabled by letting the user name the person.
The DataTransformer then should transform the text to an entity but I also need to consider another field in the form.
I guess I need to write my own DataTransformer and apply it to the form and not to the field, correct?
Another problem is that I can raise a validation error with TransformationFailedException but this is just a generic error. I would like to specify a real error message like "Your input is ambiguos" (when the user left out the ID and there are several people with the same name). How to achieve this? Thanks

src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((78 lines not shown))
  78
+    {
  79
+        if (!$data) {
  80
+            return null;
  81
+        }
  82
+
  83
+        $em = $this->em;
  84
+        $repository = $em->getRepository($this->class);
  85
+
  86
+        if ($qb = $this->queryBuilder) {
  87
+            // If a closure was passed, call id with the repository and the id
  88
+            if ($qb instanceof \Closure) {
  89
+                $qb = $qb($repository, $data);
  90
+            }
  91
+
  92
+            try {
  93
+                $result = $qb->getQuery()->getSingleResult();
2
Christophe Coevoet Collaborator
stof added a note

this does not make sense in the case where a QueryBuilder is passed directly as it does not receive the data at all

Christophe Coevoet Collaborator
stof added a note

ah sorry, looking at the constructor, the queryBuilder property is always a closure (the naming is bad then) which means that the previous check is useless and that a check is missing to be sure you receive a query builder

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((22 lines not shown))
  22
+use Doctrine\ORM\EntityManager;
  23
+use Doctrine\ORM\NoResultException;
  24
+
  25
+class OneEntityToIdTransformer implements DataTransformerInterface
  26
+{
  27
+    private $em;
  28
+    private $class;
  29
+    private $property;
  30
+    private $queryBuilder;
  31
+
  32
+    private $unitOfWork;
  33
+
  34
+    public function __construct(EntityManager $em, $class, $property, $queryBuilder)
  35
+    {
  36
+        if (null !== $queryBuilder && ! $queryBuilder instanceof \Closure) {
  37
+            throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure');
1
Christophe Coevoet Collaborator
stof added a note

this is wrong. The check forbids giving a QueryBuilder instance so the error message should not say it is allowed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Christophe Coevoet stof commented on the diff
src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((56 lines not shown))
  56
+     */
  57
+    public function transform($data)
  58
+    {
  59
+        if (null === $data) {
  60
+            return null;
  61
+        }
  62
+        if (!$this->unitOfWork->isInIdentityMap($data)) {
  63
+            throw new FormException('Entities passed to the choice field must be managed');
  64
+        }
  65
+
  66
+        if ($this->property) {
  67
+            $propertyPath = new PropertyPath($this->property);
  68
+            return $propertyPath->getValue($data);
  69
+        }
  70
+
  71
+        return current($this->unitOfWork->getEntityIdentifier($data));
2
Christophe Coevoet Collaborator
stof added a note

this is wrong IMO as you are loosing part of the data in case of a composite key without saying anything to the user

Grégoire Passault
Gregwar added a note

How would you handle that ?
Throwing an exception if the array contains is bigger that 1 element ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Grégoire Passault

@Stof, just took your comments in account about the $queryBuilder, thanks

David ALLIX

+1 for this PR

Cédric Lahouste

Hello,

Why using an other type? I think it makes confusion.
I did this with EntityType and with a theme displaying 2 inputs instead of one select. One with id, one with the text + autocomplete updating the first input with ID.

There is already something similar with DateType and single_text property.
We could add a property "double_text" to the original EntityType.
We could also provide the choices in "data-prototype"-like property...

What do you think?

Regards,

Grégoire Passault

@RaptOR, IMO adding this behaviour in some options in the entity field type would be far more confusing since it would become a "big type" with a very large set of options and people would be lost, but it's just an opinion

I think that generating the field to do the autocompletion is not a good idea, autocompletion is related to javascript logics and Symfony2 does not include any JS stub AFAIK

But all of this is just a matter of architecture, and there's only @fabpot and some others core developpers that can bring some point of views

Maybe talking about this in the next IRC meeting could be nice...

I'm doing the second project on symfony2 and this is the second time i need this feature. Please approve it asap.

Jonathan N

I don't really know why this feature is taking so long to be implemented. This feature is needed and is useful.

Eric Clemmons

FYI, I could have used something similar as well today, except that I'm using a different property that's unique (e.g. slug) rather than id.

Jordan Stout
j commented

Agreed... I've needed this functionality multiple times.

Tobias Schultze
Collaborator

I also don't understand why this pull is beeing delayed. It deals with a common requirement and is pretty usefull.
Anyway I didnt't use it yet but implemented something similar with custom logic. I solved it with a form listener (instead of DataTransformer) because it allows me to add specific form errors. See https://github.com/Tobion/Tropaion/blob/master/src/Tobion/TropaionBundle/Form/EventListener/TransformAthletesListener.php

Grégoire Passault

@ericclemmons you can use another property with this feature using the 'property' option (se above)

I'm also wondering why this is so delayed (poke @fabpot)

Craige Leeder

+1 from me too. I'm not sure why this pull request hasn't been addressed yet. The validation on the EntityType doesn't make sense to me. It's secure against injection, but it hinders development.

Restricting the values to the values pulled from the database isn't a logical action in many cases. This would solve that issue.

Cédric Lahouste

@Gregwar ; I quickly dev something on a branch to explain me:
https://github.com/RapotOR/symfony/blob/entity-no-array/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php

public function buildForm(FormBuilder $builder, array $options)
{
    if($options['widget'] === 'text') {
        $builder
            ->add($options['property'], 'text')
            ->add('id', 'hidden')
            ->prependClientTransformer(new EntityToArrayTransformer(
                $options['choice_list'], $options['property']
            ))
        ;
    } else {
        if ($options['multiple']) {
            $builder
                ->addEventSubscriber(new MergeCollectionListener())
                ->prependClientTransformer(new EntitiesToArrayTransformer($options['choice_list']))
            ;
        } else {
            $builder->prependClientTransformer(new EntityToIdTransformer($options['choice_list']));
        }
    }
}
...
public function getParent(array $options)
{
    return $options['widget'] === 'text' ? 'form' : 'choice';
}

I don't think it is really confusing... it is used for DateType where it is much more complicated.

The entity is retrieved from the the hidden field contained in the entitype (which is form and not choice anymore). The feature will allow javascript script.
We could also try to retrieve the entity from the text field (property field) if the ID is null; if we keep in mind that the no-javascript way should work also.

For me, having a new type is not a nice way.

Grégoire Passault

@RapotOR, you're approach is nice, but there's a (small) problem if the property is id (if you want your users to directly enter ids. Moreover, I think you should support the case where there is only an hidden field.

Don't you think we really need to catch @fabpot attention on this ? Maybe opening a new pull request will be the way...

I'll send (another) email on the developpers mailing list

Cédric Lahouste

@Gregwar, Thanks. I did that really quickly just to justify my approach without developing deeply. Regarding the id as proprety for example; We could have property optional if the id field is the only one needed.
I will create a pull request only if the way is the right one. Some comments from core developers will help a lot.

This feature is definitely needed, you're right.

ping @beberlei

Jordan Stout
j commented

@RapotOR That is exactly what I was going to push before I ever saw this post. I prefer your approach over having an entirely new "entity_id" type.

Jordan Stout
j commented

And yes, I think @beberlei should take attention to this... I'm sure him and @fabpot have a ridiculous amount of github notifications.

@RapotOR perhaps make another PR with your changes and reference this chain so a discussion can be made. I mean, the issue about making the parent field non nested is being discussed heavily and I feel that this is MUCH MORE IMPORTANT.

Jeremy Mikola

@Gregwar: it would probably be helpful to add some unit tests, if only for the transformer. We shouldn't get into the habit of introducing new functionality without test coverage.

Jordan Stout
j commented

I feel that most types should have the ability to be rendered as hidden... as if every type should have an option like, 'hidden' => true/false... or if there's a way to have a hidden field but register a transformer to it... but that would get tricky with entitytype, etc... which is why i think making a hidden option a neat feature to types that may need it (DateType, DateTimeType, EntityType, etc, etc.)

Cédric Lahouste

@jstout24 Yep! For that, the widget=>'hidden' could become general for every type... but that's another discussion.

I continued my implementation on my repository and added some tests. I will propose a PR referencing this one as soon as possible.

Grégoire Passault

@jstout24, @RapotOR maybe we should open an issue about hidden fields. The first Purpose of this PR was dealing with hidden entities ids.

Bernhard Schussek
Collaborator

I agree that it would be better to merge this functionality into the existing EntityType. Creating a second type doing basically the same thing except for lazy loading and a different rendering will create confusion IMHO.

@RapotOR's solution looks good to me, but still has an issue with loading all entities upon form binding, I think.

I will look into this later today.

Jordan Stout
j commented

@jmikola @bschussek @RapotOR @Gregwar -- I added an issue that I feel is better than all the proposed solutions above regarding a hidden option here: #2926

Benjamin Eberlei
Collaborator

hm i dont think adding this to the existing entity type makes sense. The existing type requires a choice list. A dedicated type makes sense, but this implementation seems strange, because its called entity_identifier, but it actually allows for properties as well so the name is wrong.

Cédric Lahouste

@beberlei The current implementation requires a choice_list to populate the list (because based on Choice).
I think the first goal of the EntityType is to map the user input to an entity; right? Keeping this idea, retrieving an entity from id or a defined property sounds the same for me.

The proposition is to have an optional choice_list because no choice is necessary in this case.
To argue this explanation, we could have a look at DateType (https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Core/Type/DateType.php).
We have years, months, days options. There are a kind of choice_list for widget==='choice'. If the widget==='single_text' is defined, these options are not used because the field doesn't contain any list.

Marcus Stöhr

+1 from me also.

Eric Clemmons

@RapotOR I've been following this thread (as most have) for quite a while and your comparison to the DateType and your PR (pending recommendations and approval) fits well, IMO.

Jordan Stout
j commented

+1 for @RapotOR's PR over this one: #3095

Bernhard Schussek
Collaborator

I would like to put this ticket on hold. I dislike adding a very specialized type to the core that should probably be abstracted into a a generic type. Postponed for 2.2.

Henrik Bjørnskov

Couldnt this be fixed by a validator alone?

Bernhard Schussek
Collaborator

@henrikbjorn This is a solution, of course. But I like the idea of a generic "choice" field without loading a full collection of choices too.

Henrik Bjørnskov

Isnt that using a ChoiceType to do validation through its transformer? The choice type should only be responsible to load the choices for the view, then a validator should be used to validate that choice. Or does that sound wrong?

Bernhard Schussek
Collaborator

@henrikbjorn In theory this sounds nice, but in practice this doesn't work. Choices need to be converted between the actual choice and the value displayed and submitted in the form. When the form is bound, the submitted value needs to be converted back into the original choice. If this isn't possible, transformation fails and no data will ever reach your model.

Jordan Stout
j commented

@bschussek you da mannnn.

Alexander

No chance for a fix for this in 2.1? Quite some people participating in this issue, so I guess there is some demand?

Rafael Dohms

So any solutions to this, its a very common use case.
Any other ways around it?

Konstantin.Myakshin
Koc commented

any news on this PR?

Bernhard Schussek
Collaborator

Not yet.

Andrea
avaldi commented

Do you know any way or workaround to address this problem in Symfony2 master (2.1.x)?

Fabien Potencier
Owner
fabpot commented

This is indeed really useful. I've used it to manage coupon codes on Symfony Live. So, this is an example where there is no JS and the input is not hidden. The user enters its coupon code and it is automatically converted to the right Coupon object in the main Order object.

Jordan Stout
j commented

Is it possible to call it doctrine_identifier or something so that it can be used with ORM and ODM? I'm not sure if there's a common interface in Doctrine/Common

Christophe Coevoet
Collaborator
stof commented

@jstout24 it would need to be done the same way than the current entity type: an abstract class extended for each Doctrine project to change the name (we cannot register 2 types with the same name), the deps injected in it (once the ORM registry and once the ODM registry) and eventually some other method needing to be changed (to support query builders for instance)

Jordan Stout
j commented
Bernhard Schussek
Collaborator

I'd prefer putting this into ChoiceType. Basically it is the same as ChoiceType but with a different widget and lazy loading.

Jordan Stout
j commented

@bschussek yeah it's a choice (kind of) if one was to have an input with some sort of javascript autocomplete. It's not much of a ChoiceType if it's not used in this manner.

For example, from a recent problem I ran into, I had a Form with a collection... I added javascript to reposition the items on the DOM and do an ajax post. After the first post, all is great. If I reposition without reloading the form and rebinding the data, I get crazy weird results. I'm assuming this is from the 0-indexed behaviour the ArrayCollection? This may be a problem with the collection type.

I used to do it like so:

<input type="hidden" name="children[0][id]" value="9" />
<input type="hidden" name="children[0][position]" value="1" />
<input type="hidden" name="children[1][id]" value="3" />
<input type="hidden" name="children[1][position]" value="2" />

I could open up an issue on the above if it makes sense to.

Anyway, back to this topic.

This is why I was in favor of being able to associate a hidden field to a model and make it easier for other ORMs/etc to adapt to.

If it's always going to be up to the method of setting data of the form to associate models, and the hidden entity type is always going to be for "choice" functionality ChoiceType makes more sense imo.

Mario

+1 to this request

Jonathan N
jnonon commented

Any updates?

Christophe Coevoet
Collaborator
stof commented

As you could see in the title and the milestone, this has been scheduled for 2.2 as 2.1 is now frozen as it is in beta

Jonathan N
jnonon commented

I did not noticed. Thanks for replying.

Lee M

Any idea when this will land?

Lee M lmcd commented on the diff
src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((48 lines not shown))
  48
+
  49
+        if ($property) {
  50
+            $this->property = $property;
  51
+        }
  52
+    }
  53
+
  54
+    /**
  55
+     * Fetch the id of the entity to populate the form
  56
+     */
  57
+    public function transform($data)
  58
+    {
  59
+        if (null === $data) {
  60
+            return null;
  61
+        }
  62
+        if (!$this->unitOfWork->isInIdentityMap($data)) {
  63
+            throw new FormException('Entities passed to the choice field must be managed');
1
Lee M
lmcd added a note

Does the entity have to be managed if a property is provided? In my case, I'm providing an unserialized entity from the session that's not managed by the entity manager. It has an ID, so there's no reason for it not to work.

Can we move this check after if ($this->property) {}?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Bilal Amarni bamarni referenced this pull request in genemu/GenemuFormBundle
Open

Lazy choice list (Autocompletion cities) #122

Miha Vrhovnik

I'm also wondering when this one will land

Paweł Mikołajczuk ahilles107 commented on the diff
src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
((75 lines not shown))
  75
+     * Try to fetch the entity from its id in the database
  76
+     */
  77
+    public function reverseTransform($data)
  78
+    {
  79
+        if (!$data) {
  80
+            return null;
  81
+        }
  82
+
  83
+        $em = $this->em;
  84
+        $repository = $em->getRepository($this->class);
  85
+
  86
+        if ($qb = $this->queryBuilder) {
  87
+            // Call the closure with the repository and the id
  88
+            $qb = $qb($repository, $data);
  89
+
  90
+            try {
1

Why not use getOneOrNullResult?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Grégoire Passault

Yes, this PR is going too far IMO, more than one year and very much comments... @fabpot, can you please do something about it?

Côme

+1 to this request

mkleins

faster, stronger. I like this PR.

Florent VIEL

+1 for ths PR

Bilal Amarni

Well this will probably not be merged as @bschussek stated he preferred having this done in entity type or maybe abstract it into choice type.

Markus Bachmann Baachi commented on the diff
src/Symfony/Bridge/Doctrine/Form/Type/EntityIdentifierType.php
((24 lines not shown))
  24
+    public function __construct(RegistryInterface $registry)
  25
+    {
  26
+        $this->registry = $registry;
  27
+    }
  28
+
  29
+    public function buildForm(FormBuilder $builder, array $options)
  30
+    {
  31
+        $builder->prependClientTransformer(new OneEntityToIdTransformer(
  32
+            $this->registry->getEntityManager($options['em']),
  33
+            $options['class'], 
  34
+            $options['property'],
  35
+            $options['query_builder']
  36
+        ));
  37
+    }
  38
+
  39
+    public function getDefaultOptions(array $options)
2
Markus Bachmann
Baachi added a note

The method is deprecated.

You should have a look to https://github.com/Gregwar/FormBundle/blob/master/Type/EntityIdType.php#L40
The component is up to date there ! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Markus Bachmann Baachi commented on the diff
src/Symfony/Bridge/Doctrine/Form/Type/EntityIdentifierType.php
((44 lines not shown))
  44
+            'class'             => null,
  45
+            'query_builder'     => null,
  46
+            'property'          => null,
  47
+            'hidden'            => true
  48
+        );
  49
+
  50
+        $options = array_replace($defaultOptions, $options);
  51
+
  52
+        if (null === $options['class']) {
  53
+            throw new FormException('You must provide a class option for the entity_identifier field');
  54
+        }  
  55
+
  56
+        return $defaultOptions;
  57
+    }
  58
+
  59
+    public function getParent(array $options)
1
Markus Bachmann
Baachi added a note

the method is not compatible with AbstractType :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Benjamin Jeanjean

+1 for this PR ! What about it @fabpot, @stof ?!

Michaël Perrin

This form type could avoid the need to create data transformers in some cases, making code easier to read & write.

+1 !

Bernhard Schussek
Collaborator

Closed in favor of #6602.

Bernhard Schussek webmozart closed this
zazzou

hi,
i have this error:
FatalErrorException: Compile Error: Declaration of Symfony\Bridge\Doctrine\Form\Type\EntityIdType::buildForm() must be compatible with Symfony\Component\Form\FormTypeInterface::buildForm(Symfony\Component\Form\FormBuilderInterface $builder, array $options) in C:\wamp\www\Symfony\vendor\symfony\symfony\src\Symfony\Bridge\Doctrine\Form\Type\EntityIdType.php line 59

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
111  src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
... ...
@@ -0,0 +1,111 @@
  1
+<?php
  2
+
  3
+/*
  4
+ * This file is part of the Symfony package.
  5
+ *
  6
+ * (c) Fabien Potencier <fabien@symfony.com>
  7
+ *
  8
+ * For the full copyright and license information, please view the LICENSE
  9
+ * file that was distributed with this source code.
  10
+ */
  11
+
  12
+namespace Symfony\Bridge\Doctrine\Form\DataTransformer;
  13
+
  14
+use Symfony\Component\Form\FormInterface;
  15
+use Symfony\Component\Form\DataTransformerInterface;
  16
+use Symfony\Component\Form\FormError;
  17
+use Symfony\Component\Form\Exception\TransformationFailedException;
  18
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
  19
+use Symfony\Component\Form\Exception\FormException;
  20
+use Symfony\Component\Form\Util\PropertyPath;
  21
+
  22
+use Doctrine\ORM\EntityManager;
  23
+use Doctrine\ORM\NoResultException;
  24
+
  25
+class OneEntityToIdTransformer implements DataTransformerInterface
  26
+{
  27
+    private $em;
  28
+    private $class;
  29
+    private $property;
  30
+    private $queryBuilder;
  31
+
  32
+    private $unitOfWork;
  33
+
  34
+    public function __construct(EntityManager $em, $class, $property, $queryBuilder)
  35
+    {
  36
+        if (null !== $queryBuilder && ! $queryBuilder instanceof \Closure) {
  37
+            throw new UnexpectedTypeException($queryBuilder, '\Closure');
  38
+        } 
  39
+
  40
+        if (null === $class) {
  41
+            throw new UnexpectedTypeException($class, 'string');
  42
+        }
  43
+
  44
+        $this->em = $em;
  45
+        $this->unitOfWork = $em->getUnitOfWork();
  46
+        $this->class = $class;
  47
+        $this->queryBuilder = $queryBuilder;
  48
+
  49
+        if ($property) {
  50
+            $this->property = $property;
  51
+        }
  52
+    }
  53
+
  54
+    /**
  55
+     * Fetch the id of the entity to populate the form
  56
+     */
  57
+    public function transform($data)
  58
+    {
  59
+        if (null === $data) {
  60
+            return null;
  61
+        }
  62
+        if (!$this->unitOfWork->isInIdentityMap($data)) {
  63
+            throw new FormException('Entities passed to the choice field must be managed');
  64
+        }
  65
+
  66
+        if ($this->property) {
  67
+            $propertyPath = new PropertyPath($this->property);
  68
+            return $propertyPath->getValue($data);
  69
+        }
  70
+
  71
+        return current($this->unitOfWork->getEntityIdentifier($data));
  72
+    }
  73
+
  74
+    /**
  75
+     * Try to fetch the entity from its id in the database
  76
+     */
  77
+    public function reverseTransform($data)
  78
+    {
  79
+        if (!$data) {
  80
+            return null;
  81
+        }
  82
+
  83
+        $em = $this->em;
  84
+        $repository = $em->getRepository($this->class);
  85
+
  86
+        if ($qb = $this->queryBuilder) {
  87
+            // Call the closure with the repository and the id
  88
+            $qb = $qb($repository, $data);
  89
+
  90
+            try {
  91
+                $result = $qb->getQuery()->getSingleResult();
  92
+            } catch (NoResultException $e) {
  93
+                $result = null;
  94
+            }
  95
+        } else {
  96
+            // Defaults to find()
  97
+            if ($this->property) {
  98
+                $result = $repository->findOneBy(array($this->property => $data));
  99
+            } else {
  100
+                $result = $repository->find($data);
  101
+            }
  102
+        }
  103
+
  104
+        if (!$result) {
  105
+            throw new TransformationFailedException('Can not find entity');
  106
+        }
  107
+
  108
+        return $result;
  109
+    }
  110
+}
  111
+
68  src/Symfony/Bridge/Doctrine/Form/Type/EntityIdentifierType.php
... ...
@@ -0,0 +1,68 @@
  1
+<?php
  2
+
  3
+/*
  4
+ * This file is part of the Symfony package.
  5
+ *
  6
+ * (c) Fabien Potencier <fabien@symfony.com>
  7
+ *
  8
+ * For the full copyright and license information, please view the LICENSE
  9
+ * file that was distributed with this source code.
  10
+ */
  11
+
  12
+namespace Symfony\Bridge\Doctrine\Form\Type;
  13
+
  14
+use Symfony\Component\Form\FormBuilder;
  15
+use Symfony\Bridge\Doctrine\RegistryInterface;
  16
+use Symfony\Bridge\Doctrine\Form\DataTransformer\OneEntityToIdTransformer;
  17
+use Symfony\Component\Form\AbstractType;
  18
+use Symfony\Component\Form\Exception\FormException;
  19
+
  20
+class EntityIdentifierType extends AbstractType
  21
+{
  22
+    protected $registry;
  23
+
  24
+    public function __construct(RegistryInterface $registry)
  25
+    {
  26
+        $this->registry = $registry;
  27
+    }
  28
+
  29
+    public function buildForm(FormBuilder $builder, array $options)
  30
+    {
  31
+        $builder->prependClientTransformer(new OneEntityToIdTransformer(
  32
+            $this->registry->getEntityManager($options['em']),
  33
+            $options['class'], 
  34
+            $options['property'],
  35
+            $options['query_builder']
  36
+        ));
  37
+    }
  38
+
  39
+    public function getDefaultOptions(array $options)
  40
+    {
  41
+        $defaultOptions = array(
  42
+            'required'          => true,
  43
+            'em'                => null,
  44
+            'class'             => null,
  45
+            'query_builder'     => null,
  46
+            'property'          => null,
  47
+            'hidden'            => true
  48
+        );
  49
+
  50
+        $options = array_replace($defaultOptions, $options);
  51
+
  52
+        if (null === $options['class']) {
  53
+            throw new FormException('You must provide a class option for the entity_identifier field');
  54
+        }  
  55
+
  56
+        return $defaultOptions;
  57
+    }
  58
+
  59
+    public function getParent(array $options)
  60
+    {
  61
+        return $options['hidden'] ? 'hidden' : 'field';
  62
+    }
  63
+
  64
+    public function getName()
  65
+    {
  66
+        return 'entity_identifier';
  67
+    }
  68
+}
5  src/Symfony/Bundle/DoctrineBundle/Resources/config/orm.xml
@@ -58,6 +58,11 @@
58 58
             <argument type="service" id="doctrine" />
59 59
         </service>
60 60
 
  61
+        <service id="form.type.entity_identifier" class="Symfony\Bridge\Doctrine\Form\Type\EntityIdentifierType">
  62
+            <tag name="form.type" alias="entity_identifier" />
  63
+            <argument type="service" id="doctrine" />
  64
+        </service>
  65
+
61 66
         <service id="doctrine.orm.configuration" class="%doctrine.orm.configuration.class%" abstract="true" public="false" />
62 67
 
63 68
         <service id="doctrine.orm.entity_manager.abstract" class="%doctrine.orm.entity_manager.class%" factory-class="%doctrine.orm.entity_manager.class%" factory-method="create" abstract="true" />
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.