Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

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

Closed
wants to merge 8 commits into from
@Gregwar

(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.

@ericclemmons

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.

@Gregwar

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 ?

...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((39 lines not shown))
+ $this->class = $class;
+ $this->queryBuilder = $queryBuilder;
+ }
+
+ /**
+ * Fetch the id of the entity to populate the form
+ */
+ public function transform($data)
+ {
+ if (null === $data)
+ return null;
+
+ $meta = $this->em->getClassMetadata($this->class);
+
+ if (!$meta->getReflectionClass()->isInstance($data))
+ throw new TransformationFailedException('Invalid data, must be an instance of '.$this->class);
@stof 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
...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((16 lines not shown))
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+
+use Doctrine\ORM\EntityManager;
+use Doctrine\ORM\NoResultException;
+
+class OneEntityToIdTransformer implements DataTransformerInterface
+{
+ private $em;
+ private $class;
+ private $queryBuilder;
+
+ public function __construct(EntityManager $em, $class, $queryBuilder)
+ {
+ if (!(null === $queryBuilder || $queryBuilder instanceof \Closure)) {
@stof 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 ?

@Gregwar
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
...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((21 lines not shown))
+use Doctrine\ORM\NoResultException;
+
+class OneEntityToIdTransformer implements DataTransformerInterface
+{
+ private $em;
+ private $class;
+ private $queryBuilder;
+
+ public function __construct(EntityManager $em, $class, $queryBuilder)
+ {
+ if (!(null === $queryBuilder || $queryBuilder instanceof \Closure)) {
+ throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure');
+ }
+
+ if (null == $class)
+ throw new UnexpectedTypeException($class, 'string');
@stof 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
...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((20 lines not shown))
+use Doctrine\ORM\EntityManager;
+use Doctrine\ORM\NoResultException;
+
+class OneEntityToIdTransformer implements DataTransformerInterface
+{
+ private $em;
+ private $class;
+ private $queryBuilder;
+
+ public function __construct(EntityManager $em, $class, $queryBuilder)
+ {
+ if (!(null === $queryBuilder || $queryBuilder instanceof \Closure)) {
+ throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure');
+ }
+
+ if (null == $class)
@stof 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
...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((34 lines not shown))
+
+ if (null == $class)
+ throw new UnexpectedTypeException($class, 'string');
+
+ $this->em = $em;
+ $this->class = $class;
+ $this->queryBuilder = $queryBuilder;
+ }
+
+ /**
+ * Fetch the id of the entity to populate the form
+ */
+ public function transform($data)
+ {
+ if (null === $data)
+ return null;
@stof 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
...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((57 lines not shown))
+ $id = $meta->getReflectionProperty($identifierField)->getValue($data);
+
+ return $id;
+ }
+
+ /**
+ * Try to fetch the entity from its id in the database
+ */
+ public function reverseTransform($data)
+ {
+ if (!$data) {
+ return null;
+ }
+
+ $em = $this->em;
+ $class = $this->class;
@stof 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
...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((77 lines not shown))
+ if ($qb instanceof \Closure) {
+ $qb = $qb($repository, $data);
+ }
+
+ try {
+ $result = $qb->getQuery()->getSingleResult();
+ } catch (NoResultException $e) {
+ $result = null;
+ }
+ } else {
+ // Defaults to find()
+ $result = $repository->find($data);
+ }
+
+ if (!$result)
+ throw new TransformationFailedException('Can not find entity');
@stof 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))
+ */
+
+namespace Symfony\Bridge\Doctrine\Form\Type;
+
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Bridge\Doctrine\RegistryInterface;
+use Symfony\Bridge\Doctrine\Form\DataTransformer\OneEntityToIdTransformer;
+use Symfony\Component\Form\AbstractType;
+
+class EntityIdType extends AbstractType
+{
+ protected $em;
+
+ public function __construct(RegistryInterface $registry)
+ {
+ $this->em = $registry->getEntityManager();
@stof 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
...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((45 lines not shown))
+ * Fetch the id of the entity to populate the form
+ */
+ public function transform($data)
+ {
+ if (null === $data) {
+ return null;
+ }
+
+ $meta = $this->em->getClassMetadata($this->class);
+
+ if (!$meta->getReflectionClass()->isInstance($data)) {
+ throw new TransformationFailedException('Invalid data, must be an instance of '.$this->class);
+ }
+
+ $identifierField = $meta->getSingleIdentifierFieldName();
+ $id = $meta->getReflectionProperty($identifierField)->getValue($data);
@marcw
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.

@Gregwar
Gregwar added a note

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

@marcw
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
@Gregwar

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.

@stof stof commented on the diff
...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((92 lines not shown))
+ try {
+ $result = $qb->getQuery()->getSingleResult();
+ } catch (NoResultException $e) {
+ $result = null;
+ }
+ } else {
+ // Defaults to find()
+ if ($this->property) {
+ $result = $repository->findOneBy(array($this->property => $data));
+ } else {
+ $result = $repository->find($data);
+ }
+ }
+
+ if (!$result) {
+ throw new TransformationFailedException('Can not find entity');
@stof 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
@Gregwar

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))
+
+ public function getDefaultOptions(array $options)
+ {
+ $defaultOptions = array(
+ 'required' => true,
+ 'em' => null,
+ 'class' => null,
+ 'query_builder' => null,
+ 'property' => null,
+ 'hidden' => true
+ );
+
+ $options = array_replace($defaultOptions, $options);
+
+ if (null === $options['class']) {
+ throw new \RunTimeException('You must provide a class option for the entity_id field');
@stof 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
@phidah

+1

@winzou

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.

@Gregwar

@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 ?

@phidah

entity_identifier sounds fine to me.

@j
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

@Gregwar

@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 ?

@j
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?

@j
j commented

What's the status of this PR?

@Gregwar

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

@duellsy

I'm desperately needing something like this too.

@Gregwar

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

@jnonon

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

@j
j commented

^^

@j
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 ?

@henrikbjorn

@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.

@jmikola

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.

@asm89

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

@Gregwar

(cf response on Gregwar/FormBundle#3)

@j
j commented

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

@asm89

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

@Tobion
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

...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((78 lines not shown))
+ {
+ if (!$data) {
+ return null;
+ }
+
+ $em = $this->em;
+ $repository = $em->getRepository($this->class);
+
+ if ($qb = $this->queryBuilder) {
+ // If a closure was passed, call id with the repository and the id
+ if ($qb instanceof \Closure) {
+ $qb = $qb($repository, $data);
+ }
+
+ try {
+ $result = $qb->getQuery()->getSingleResult();
@stof 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

@stof 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
...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((22 lines not shown))
+use Doctrine\ORM\EntityManager;
+use Doctrine\ORM\NoResultException;
+
+class OneEntityToIdTransformer implements DataTransformerInterface
+{
+ private $em;
+ private $class;
+ private $property;
+ private $queryBuilder;
+
+ private $unitOfWork;
+
+ public function __construct(EntityManager $em, $class, $property, $queryBuilder)
+ {
+ if (null !== $queryBuilder && ! $queryBuilder instanceof \Closure) {
+ throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder or \Closure');
@stof 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
@stof stof commented on the diff
...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((56 lines not shown))
+ */
+ public function transform($data)
+ {
+ if (null === $data) {
+ return null;
+ }
+ if (!$this->unitOfWork->isInIdentityMap($data)) {
+ throw new FormException('Entities passed to the choice field must be managed');
+ }
+
+ if ($this->property) {
+ $propertyPath = new PropertyPath($this->property);
+ return $propertyPath->getValue($data);
+ }
+
+ return current($this->unitOfWork->getEntityIdentifier($data));
@stof 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

@Gregwar
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
@Gregwar

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

@webda2l

+1 for this PR

@RapotOR

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,

@Gregwar

@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...

@chetzof

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

@jnonon

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

@ericclemmons

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.

@j
j commented

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

@Tobion
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

@Gregwar

@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

+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.

@RapotOR

@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.

@Gregwar

@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

@RapotOR

@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

@j
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.

@j
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.

@jmikola

@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.

@j
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.)

@RapotOR

@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.

@Gregwar

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

@webmozart
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.

@j j referenced this pull request
Closed

[Form] Adding option 'hidden' #2926

@j
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

@beberlei

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.

@RapotOR

@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.

@daFish

+1 from me also.

@ericclemmons

@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.

@j
j commented

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

@webmozart
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.

@henrikbjorn

Couldnt this be fixed by a validator alone?

@webmozart
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.

@henrikbjorn

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?

@webmozart
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.

@j
j commented

@bschussek you da mannnn.

@asm89

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

@rdohms

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

@Koc
Koc commented

any news on this PR?

@webmozart
Collaborator

Not yet.

@avaldi

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

@fabpot
Owner

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.

@j
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

@stof
Collaborator

@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)

@j
j commented
@webmozart
Collaborator

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

@j
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.

@thalionath

+1 to this request

@jnonon

Any updates?

@stof
Collaborator

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

@jnonon

I did not noticed. Thanks for replying.

@lmcd

Any idea when this will land?

@lmcd lmcd commented on the diff
...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((48 lines not shown))
+
+ if ($property) {
+ $this->property = $property;
+ }
+ }
+
+ /**
+ * Fetch the id of the entity to populate the form
+ */
+ public function transform($data)
+ {
+ if (null === $data) {
+ return null;
+ }
+ if (!$this->unitOfWork->isInIdentityMap($data)) {
+ throw new FormException('Entities passed to the choice field must be managed');
@lmcd
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
@bamarni bamarni referenced this pull request in genemu/GenemuFormBundle
Open

Lazy choice list (Autocompletion cities) #122

@mvrhov

I'm also wondering when this one will land

@ahilles107 ahilles107 commented on the diff
...ine/Form/DataTransformer/OneEntityToIdTransformer.php
((75 lines not shown))
+ * Try to fetch the entity from its id in the database
+ */
+ public function reverseTransform($data)
+ {
+ if (!$data) {
+ return null;
+ }
+
+ $em = $this->em;
+ $repository = $em->getRepository($this->class);
+
+ if ($qb = $this->queryBuilder) {
+ // Call the closure with the repository and the id
+ $qb = $qb($repository, $data);
+
+ try {

Why not use getOneOrNullResult?

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

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

@come

+1 to this request

@mkleins

faster, stronger. I like this PR.

@luxifer

+1 for ths PR

@Burgov Burgov referenced this pull request in SamsonIT/AutocompleteBundle
Open

Extend the EntityIdentifierType if it ever gets pulled to symfony master #3

@bamarni

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.

@Baachi Baachi commented on the diff
...ny/Bridge/Doctrine/Form/Type/EntityIdentifierType.php
((24 lines not shown))
+ public function __construct(RegistryInterface $registry)
+ {
+ $this->registry = $registry;
+ }
+
+ public function buildForm(FormBuilder $builder, array $options)
+ {
+ $builder->prependClientTransformer(new OneEntityToIdTransformer(
+ $this->registry->getEntityManager($options['em']),
+ $options['class'],
+ $options['property'],
+ $options['query_builder']
+ ));
+ }
+
+ public function getDefaultOptions(array $options)
@Baachi
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
@Baachi Baachi commented on the diff
...ny/Bridge/Doctrine/Form/Type/EntityIdentifierType.php
((44 lines not shown))
+ 'class' => null,
+ 'query_builder' => null,
+ 'property' => null,
+ 'hidden' => true
+ );
+
+ $options = array_replace($defaultOptions, $options);
+
+ if (null === $options['class']) {
+ throw new FormException('You must provide a class option for the entity_identifier field');
+ }
+
+ return $defaultOptions;
+ }
+
+ public function getParent(array $options)
@Baachi
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
@apsylone

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

@nurikabe

+1

@michaelperrin

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

+1 !

@webmozart
Collaborator

Closed in favor of #6602.

@webmozart 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.
View
111 src/Symfony/Bridge/Doctrine/Form/DataTransformer/OneEntityToIdTransformer.php
@@ -0,0 +1,111 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\Doctrine\Form\DataTransformer;
+
+use Symfony\Component\Form\FormInterface;
+use Symfony\Component\Form\DataTransformerInterface;
+use Symfony\Component\Form\FormError;
+use Symfony\Component\Form\Exception\TransformationFailedException;
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\Form\Exception\FormException;
+use Symfony\Component\Form\Util\PropertyPath;
+
+use Doctrine\ORM\EntityManager;
+use Doctrine\ORM\NoResultException;
+
+class OneEntityToIdTransformer implements DataTransformerInterface
+{
+ private $em;
+ private $class;
+ private $property;
+ private $queryBuilder;
+
+ private $unitOfWork;
+
+ public function __construct(EntityManager $em, $class, $property, $queryBuilder)
+ {
+ if (null !== $queryBuilder && ! $queryBuilder instanceof \Closure) {
+ throw new UnexpectedTypeException($queryBuilder, '\Closure');
+ }
+
+ if (null === $class) {
+ throw new UnexpectedTypeException($class, 'string');
+ }
+
+ $this->em = $em;
+ $this->unitOfWork = $em->getUnitOfWork();
+ $this->class = $class;
+ $this->queryBuilder = $queryBuilder;
+
+ if ($property) {
+ $this->property = $property;
+ }
+ }
+
+ /**
+ * Fetch the id of the entity to populate the form
+ */
+ public function transform($data)
+ {
+ if (null === $data) {
+ return null;
+ }
+ if (!$this->unitOfWork->isInIdentityMap($data)) {
+ throw new FormException('Entities passed to the choice field must be managed');
@lmcd
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
+ }
+
+ if ($this->property) {
+ $propertyPath = new PropertyPath($this->property);
+ return $propertyPath->getValue($data);
+ }
+
+ return current($this->unitOfWork->getEntityIdentifier($data));
@stof 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

@Gregwar
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
+ }
+
+ /**
+ * Try to fetch the entity from its id in the database
+ */
+ public function reverseTransform($data)
+ {
+ if (!$data) {
+ return null;
+ }
+
+ $em = $this->em;
+ $repository = $em->getRepository($this->class);
+
+ if ($qb = $this->queryBuilder) {
+ // Call the closure with the repository and the id
+ $qb = $qb($repository, $data);
+
+ try {

Why not use getOneOrNullResult?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ $result = $qb->getQuery()->getSingleResult();
+ } catch (NoResultException $e) {
+ $result = null;
+ }
+ } else {
+ // Defaults to find()
+ if ($this->property) {
+ $result = $repository->findOneBy(array($this->property => $data));
+ } else {
+ $result = $repository->find($data);
+ }
+ }
+
+ if (!$result) {
+ throw new TransformationFailedException('Can not find entity');
@stof 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
+ }
+
+ return $result;
+ }
+}
+
View
68 src/Symfony/Bridge/Doctrine/Form/Type/EntityIdentifierType.php
@@ -0,0 +1,68 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\Doctrine\Form\Type;
+
+use Symfony\Component\Form\FormBuilder;
+use Symfony\Bridge\Doctrine\RegistryInterface;
+use Symfony\Bridge\Doctrine\Form\DataTransformer\OneEntityToIdTransformer;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Exception\FormException;
+
+class EntityIdentifierType extends AbstractType
+{
+ protected $registry;
+
+ public function __construct(RegistryInterface $registry)
+ {
+ $this->registry = $registry;
+ }
+
+ public function buildForm(FormBuilder $builder, array $options)
+ {
+ $builder->prependClientTransformer(new OneEntityToIdTransformer(
+ $this->registry->getEntityManager($options['em']),
+ $options['class'],
+ $options['property'],
+ $options['query_builder']
+ ));
+ }
+
+ public function getDefaultOptions(array $options)
@Baachi
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
+ {
+ $defaultOptions = array(
+ 'required' => true,
+ 'em' => null,
+ 'class' => null,
+ 'query_builder' => null,
+ 'property' => null,
+ 'hidden' => true
+ );
+
+ $options = array_replace($defaultOptions, $options);
+
+ if (null === $options['class']) {
+ throw new FormException('You must provide a class option for the entity_identifier field');
+ }
+
+ return $defaultOptions;
+ }
+
+ public function getParent(array $options)
@Baachi
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
+ {
+ return $options['hidden'] ? 'hidden' : 'field';
+ }
+
+ public function getName()
+ {
+ return 'entity_identifier';
+ }
+}
View
5 src/Symfony/Bundle/DoctrineBundle/Resources/config/orm.xml
@@ -58,6 +58,11 @@
<argument type="service" id="doctrine" />
</service>
+ <service id="form.type.entity_identifier" class="Symfony\Bridge\Doctrine\Form\Type\EntityIdentifierType">
+ <tag name="form.type" alias="entity_identifier" />
+ <argument type="service" id="doctrine" />
+ </service>
+
<service id="doctrine.orm.configuration" class="%doctrine.orm.configuration.class%" abstract="true" public="false" />
<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" />
Something went wrong with that request. Please try again.