Choice Multiple = true, create new Entries - symfony

My Entity
/**
* Set friend
*
* #param \Frontend\ChancesBundle\Entity\UserFriends $friend
* #return ChanceRequest
*/
public function setFriend(\Frontend\ChancesBundle\Entity\UserFriends $friend = null)
{
$this->friend = $friend;
return $this;
}
My Action
$task = new ChanceRequest();
$form = $this->createFormBuilder($task)
->add('friend', 'choice', array(
'required' => true,
'expanded' => true,
'choices' => $fb_friends,
'multiple' => true,
'mapped' => true
))
->getForm();
Because setFriend is expecting a scalar, I cannot validate this or save it to db. It is an array from how many friends the user want to send a message to somebody. How can I change it?
I have seen here a post:
Symfony2 Choice : Expected argument of type "scalar", "array" given
but this don't work that I put an array in front of \Frontend or $friend. I guess because of the related table.
What do I have to do in Entity to get it work?

If friends could be found in your database (for example it is a User entity), you should declare ManyToMany relationship for these two tables. If not, make friend property to be a doctrine array type. if friends is not a valid entity class, all the rest you have is to make a custom validator and datatransformer. Otherwise you have nothing left to do. All this information you can find in the official symfony and doctrine documentation.
How to create a Custom Validation Constraint
How to use Data Transformers
Association Mapping
Working with Associations

Related

ChoiceType multiple attribute according to entity property (how to choose between returning a collection of entities or one entity)

I'm working on a quiz project with the Symfony framework (version 4.4) and Doctrine as ORM.
There is a ManyToOne relation between the Answer and the Question entities, as for the QuizQuestion and Answer entities. I use the QuizQuestion entity to make the link between a quiz, a question, and the selected answer(s).
I use a EntityType "QuizQuestionType" with the multiple attribute set to true to collect answers, and it works as expected :
$builder
->add('answers', EntityType::class, [
'class' => Answer::class,
'choices' => $this->fillAnswers($quizQuestion),
'expanded' => true,
'multiple' => true,
]);
The thing is, I want to be able to setup question as multiple or single choice. If I set the EntityType multiple attibute to false, I got the error :
Entity of type "Doctrine\ORM\PersistentCollection" passed to the
choice field must be managed. Maybe you forget to persist it in the
entity manager?
I could use two answers entities with a OneToMany and a OneToOne relations, but it seems a really poor design to me.
I wonder how it can be done, ideally with a property in the Question entity that indicates if it is a multiple or unique choice question. That will allow me to simply declare it in the backend (because technically, a multiple choice question may have only one good answer, so I can't calculate it by the number of answers).
Do you have any idea on how I can achieve this ?
Here is the conceptual data model :
CDM
The answer entity : https://pastebin.com/kiRTHnvL
The QuizQuestion entity : https://pastebin.com/wL3v9fwT
Thank you for your help,
EDIT 01/08/2020
As suggested by #victor-vasiloi, I added an event listener to the form type so I can setup the correct extensions. I was not able to add the transformer though. I found the solution here and created an extension to use a data transformer from the event listener :
QuizQuestionType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($builder){
$quizQuestion = $event->getData();
$form = $event->getForm();
if ($quizQuestion->getQuestion()->getIsMultiple()){
$form->add('answers', EntityType::class, [
'class' => Answer::class,
'choices' => $this->fillAnswers($quizQuestion),
'expanded' => true,
'multiple' => true,
]);
} else {
$form->add('answers', EntityType::class, [
'class' => Answer::class,
'choices' => $this->fillAnswers($quizQuestion),
'expanded' => true,
'multiple' => false,
'model_transformer' => new CollectionToAnswerTransformer(),
]);
}
})
;
}
ModelTransformerExtension
class ModelTransformerExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
// return FormType::class to modify (nearly) every field in the system
return [FormType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options) {
parent::buildForm($builder, $options);
if (isset($options['model_transformer'])) {
$builder->addModelTransformer($options['model_transformer']);
}
}
public function configureOptions(OptionsResolver $resolver) {
parent::configureOptions($resolver);
$resolver->setDefaults(array('model_transformer' => null));
}
}
Now the form could be loaded. When submitting though (in a case of a unique answer with radio buttons), a CollectionToArrayTranformer was giving the following error :
Expected argument of type "App\Entity\Answer", "array" given at
property path "answers".
I tried a custom CollectionToAnswerTransformer, that looks like this :
class CollectionToAnswerTransformer implements DataTransformerInterface
{
/**
* #param mixed $collection
* #return mixed|string
*/
public function transform($collection)
{
if (null === $collection){
return '';
}
else
{
foreach ($collection as $answer){
return $answer;
}
}
}
/**
* #param mixed $answer
* #return ArrayCollection|mixed
*/
public function reverseTransform($answer)
{
$collection = new ArrayCollection();
$collection->add($answer);
return $collection;
}
}
But with no better results. I get the error :
Expected argument of type "App\Entity\Answer", "instance of
Doctrine\Common\Collections\ArrayCollection" given at property path
"answers".
It looks like an issue with the reverse transformer method, but if I change it to return an entity, I got the opposite error :
Could not determine access type for property "answers" in class
"App\Entity\QuizQuestion": The property "answers" in class
"App\Entity\QuizQuestion" can be defined with the methods
"addAnswer()", "removeAnswer()" but the new value must be an array or
an instance of \Traversable, "App\Entity\Answer" given...
I think I'm almost at it, but I don't know if my transformer is the way to go or if it is easier than that...
To setup questions with single choice you could use a radio button, and checkboxes for multiple choices.
Radio button is expanded "true" and multiple "false".
Checkbox is expanded "true" and multiple "true".
Code example that display checkboxes:
$builder
->add('filter', EntityType::class, array(
'class' => 'FilterBundle:Filter',
'multiple' => true,
'expanded' => true,
'required' => true
));
Source: https://symfony.com/doc/current/reference/forms/types/choice.html#select-tag-checkboxes-or-radio-buttons
And if you want to define it for each question before displaying, there could be a field on your question entity (for example a boolean "multiple").
You can dynamically set the multiple option based on the given Question using a form event listener on the Symfony\Component\Form\FormEvents::PRE_SET_DATA event, here's where you can learn more about dynamically modifying the form and form events.
Using the same logic, when the multiple option is set to true, you can add the Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer model transformer to the answers field like this $builder->get('answers')->addModelTransformer(new CollectionToArrayTransformer()); which will ensure the transformation between the Doctrine Collection and the choices array (including single choice).

Sonata Admin => Select just the given Discriminator Map type

Subject
When I have a set of entities with a Doctrine Discriminator Map then I cannot add a filter to get just one type of all mapped entities, due SonataAdminBundle and/or SonataDoctrineORMAdminBundle are throwing an error.
Example:
Entities with a Doctrine Discriminator Map
/**
* #ORM\Table(name="activities")
* #ORM\Entity()
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({
* "joined" = "...\JoinedActivity",
* "other" = "...\OtherActivity"
* })
*/
abstract class Activity()
{
abstract public function getType();
}
/**
* #ORM\Entity()
*/
class JoinActivity extends Activity()
{
const TYPE = 'joined';
public function getType()
{
return self::type;
}
}
/**
* #ORM\Entity()
*/
class OtherActivity extends Activity()
{
const TYPE = 'other';
public function getType()
{
return self::type;
}
}
Then I add the Sonata Admin filter:
protected function configureDatagridFilters(DatagridMapper $filter)
{
$filter->add(
'type',
null,
[
'label' => 'Activity Type',
],
'choice',
[
'choices' => [
JoinActivity::TYPE => ucfirst(JoinActivity::TYPE),
OtherActivity::TYPE => ucfirst(OtherActivity::TYPE),
],
]
);
}
Expected results
Get a new filter to select just joined or other activities.
Actual results
Notice: Undefined index: type
500 Internal Server Error - ContextErrorException
Stack trace
As requested by greg0ire this is the Stack Trace returned by Symfony/Sonata:
[1] Symfony\Component\Debug\Exception\ContextErrorException: Notice: Undefined index: type
at n/a
in /path/to/symfony/project/vendor/sonata-project/doctrine-orm-admin-bundle/Guesser/FilterTypeGuesser.php line 69
at Symfony\Component\Debug\ErrorHandler->handleError('8', 'Undefined index: type', '/path/to/symfony/project/vendor/sonata-project/doctrine-orm-admin-bundle/Guesser/FilterTypeGuesser.php', '69', array('class' => 'AppBundle\EntityBundle\Entity\Activity', 'property' => 'type', 'modelManager' => object(ModelManager), 'ret' => array(object(ClassMetadata), 'type', array()), 'options' => array('field_type' => null, 'field_options' => array(), 'options' => array(), 'parent_association_mappings' => array()), 'metadata' => object(ClassMetadata), 'propertyName' => 'type', 'parentAssociationMappings' => array()))
in /path/to/symfony/project/vendor/sonata-project/doctrine-orm-admin-bundle/Guesser/FilterTypeGuesser.php line 69
at Sonata\DoctrineORMAdminBundle\Guesser\FilterTypeGuesser->guessType('AppBundle\EntityBundle\Entity\Activity', 'type', object(ModelManager))
in /path/to/symfony/project/app/cache/dev/classes.php line 15104
at Sonata\AdminBundle\Guesser\TypeGuesserChain->Sonata\AdminBundle\Guesser\{closure}(object(FilterTypeGuesser))
in /path/to/symfony/project/app/cache/dev/classes.php line 15111
at Sonata\AdminBundle\Guesser\TypeGuesserChain->guess(object(Closure))
in /path/to/symfony/project/app/cache/dev/classes.php line 15105
at Sonata\AdminBundle\Guesser\TypeGuesserChain->guessType('AppBundle\EntityBundle\Entity\Activity', 'type', object(ModelManager))
in /path/to/symfony/project/vendor/sonata-project/doctrine-orm-admin-bundle/Builder/DatagridBuilder.php line 105
at Sonata\DoctrineORMAdminBundle\Builder\DatagridBuilder->addFilter(object(Datagrid), null, object(FieldDescription), object(ActivityAdmin))
in /path/to/symfony/project/app/cache/dev/classes.php line 13069
at Sonata\AdminBundle\Datagrid\DatagridMapper->add('type', null, array('label' => 'Activity Type', 'field_options' => array('choices' => array('joined' => 'Joined')), 'field_type' => 'choice', 'field_name' => 'type'), 'choice', array('choices' => array('joined' => 'Joined')))
in /path/to/symfony/project/src/AppBundle/SonAdminBundle/Admin/ActivityAdmin.php line 64
at AppBundle\SonAdminBundle\Admin\ActivityAdmin->configureDatagridFilters(object(DatagridMapper))
in /path/to/symfony/project/app/cache/dev/classes.php line 10609
at Sonata\AdminBundle\Admin\AbstractAdmin->buildDatagrid()
in /path/to/symfony/project/app/cache/dev/classes.php line 10910
at Sonata\AdminBundle\Admin\AbstractAdmin->getDatagrid()
in /path/to/symfony/project/vendor/sonata-project/admin-bundle/Controller/CRUDController.php line 104
at Sonata\AdminBundle\Controller\CRUDController->listAction()
in line
at call_user_func_array(array(object(CRUDController), 'listAction'), array())
in /path/to/symfony/project/app/bootstrap.php.cache line 3222
at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), '1')
in /path/to/symfony/project/app/bootstrap.php.cache line 3181
at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), '1', true)
in /path/to/symfony/project/app/bootstrap.php.cache line 3335
at Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel->handle(object(Request), '1', true)
in /path/to/symfony/project/app/bootstrap.php.cache line 2540
at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
in /path/to/symfony/project/web/app_dev.php line 15
at require('/path/to/symfony/project/web/app_dev.php')
in /path/to/symfony/project/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_dev.php line 40
Any idea how I can fix it?
Thanks,
I face the same issue. I did a work around - I use doctrine_orm_callback type with doctrine INSTANCE OF operator.
Code looks like this:
->add('userType',
'doctrine_orm_callback',
[
'callback' => function ($queryBuilder, $alias, $field, $value) {
if (!is_array($value) || !array_key_exists('value', $value) || empty($value['value'])) {
return false;
}
$queryBuilder->andWhere($alias . ' INSTANCE OF :userType');
$queryBuilder->setParameter('userType', $value['value']);
return true;
},
],
ChoiceType::class,
[
'choices' => array_flip(UserType::getChoices()),
'translation_domain' => $this->getTranslationDomain(),
]
)
And it's working. Maybe it helps you.
I think Sonata is confused b/c it expects type to appear in the fields in your Doctrine mapping. I don't think filtering by type is supporting, but I recall having some support for single inheritance, especially when creating new objects. Can't find it back though.
It is not recommanded to modify the link between an inherited entity and his superclass once it is created. If you need it, you should consider using composition instead.
That's why Doctrine does not allow to directly manage the discriminator field.
All the following text is copied from this very useful response which is precious for a global and better understanding.
It is not a good sign when the type of an instance of an object needs to change over time. I'm not talking about downcasting/upcasting here, but about the need to change the real type of an object.
First of all, let me tell you why it is a bad idea:
A subclass might define more attributes and do some additionnal work
in it's constructor. Should we run the new constructor again? What
if it overwrites some of our old object's attributes?
What if you were working on an instance of that Person in some part of your code, and then it suddenly transforms into an Employee (which might have some redefined behavior you wouldn't expect)?!
That is part of the reason why most languages will not allow you to change the real class type of an object during execution (and memory, of course, but I don't want to get into details). Some let you do that (sometimes in twisted ways, e.g. the JVM), but it's really not good practice!
More often than not, the need to do so lies in bad object-oriented design decisions.
For those reasons, Doctrine will not allow you to change the type of your entity object. Of course, you could write plain SQL (at the end of this post - but please read through!) to do the change anyway, but here's two "clean" options I would suggest:
I realize you've already said the first option wasn't an option but I spent a while writing down this post so I feel like I should make it as complete as possible for future reference.
Whenever you need to "change the type" from Person to Employee, create a new instance of the Employee and copy the data you want to copy over from the old Person object to the Employee object. Don't forget to remove the old entity and to persist the new one.
Use composition instead of inheritance (see this wiki article for details and links to other articles). EDIT: For the hell of it, here's a part of a nice conversation with Erich Gamma about "Composition over Inheritance"!
See related discussions here and here.
Now, here is the plain SQL method I was talking about earlier - I hope you won't need to use it!
Make sure your query is sanitized (as the query will be executed without any verification).
$query = "UPDATE TABLE_NAME_HERE SET discr = 'employee' WHERE id = ".$entity->getId();
$entity_manager->getConnection()->exec( $query );
Here is the documentation and code for the exec method which is in the DBAL\Connection class (for your information):
/**
* Execute an SQL statement and return the number of affected rows.
*
* #param string $statement
* #return integer The number of affected rows.
*/
public function exec($statement)
{
$this->connect();
return $this->_conn->exec($statement);
}

Embed Form and EntityType multiple

I have embed form, inside embed form there is an EntityType with multiple=> true.
When i am saving to databse i get this: Doctrine\Common\Collections\ArrayCollection#000000...
When i change EntityType to CHoiceType with multiple=> true everything seems to be correct.
/**
* Set emotions
*
* #param simple_array $emotions
*
* #return Movies
*/
public function setEmotions($emotions) {
$this->emotions = $emotions;
return $this;
}
FormType
->add('emotions', EntityType::class, array(
'class' => 'MovieBundle:Emotions',
'multiple' => true,
'choice_label' => 'emotion',
))
I have no idea where the problem is. Is it something with embed form, or i need data transformer?
Whene you set multiple to true symfony set your attribute value into an array, if you set multiple to false this attribute will be set like it is so the difference between them is :
multiple: true gonna call a setter in your class this setter should have an array argument like setEmotions(ArrayCollection $emotions), and it will be persisted one by one using a loop.
multiple : false gonna call a setter in your class with an argument of type Emotion like setEmotions(Emotion $emotion) and it will be persisted only the object that he get from getEmotions() in this case he will persist only the DoctrineArrayCollection not the objects wrapped in it and this is the reason of your exception

How to force the underlying form object to overtake the parent entity for its relation?

I have one master entity and second one.
Lets say you have a map and the map has some points with coordinates.
I want be able to add dynamicaly new records for points so I've choosen the collection type of form type.
I have also the correct form type for the second entity. Everything is OK, except that the new added points are not persisted with the master entity. How can I tell the form to overtake the parent entity and set to the appropriate setter?
$builder->add('routePoints', 'collection', ['required' => false,'label' => '','attr'=>['class'=>'route-point'],'by_reference'=> true, 'type' => new MapCoordinateAdminType(), 'allow_add' => true, 'delete_empty' => true, 'allow_delete' => true, 'translation_domain' => 'maps']);
Master Entity
/**
* #var array
* #ORM\OneToMany(targetEntity="ADN\CustomBundle\Entity\MapCoordinate", cascade={"persist","remove"}, mappedBy="map")
* #ORM\JoinColumn(onDelete="CASCADE",name="route_points",nullable=true, referencedColumnName="map")
*/
protected $routePoints;
Points Entity
/**
* #ORM\ManyToOne(inversedBy="routePoints", targetEntity="ADN\CustomBundle\Entity\CycleMap")
* #ORM\JoinColumn(name="map",referencedColumnName="id")
*/
protected $map;
Your second entity instances are not persisted because they belong to the inverse side of the bidirectional relationship. You can find out more about this on the Doctrine documentation.
In order to solve your problem, you need to update the owning side as well. To do this, a one-line change is required in your master entity:
<?php
/** Master entity */
use ADN\CustomBundle\Entity\MapCoordinate;
class CycleMap
{
// ...
public function addRoutePoint(MapCoordinate $routePoint)
{
// The magical line
$routePoint->setMap($this);
$this->routePoints[] = $routePoint;
return $this;
}
}

How do I add an unbound field to a form in Symfony which is otherwise bound to an entity?

Maybe I'm missing the obvious but how do I (or can I) add an extra "unbound" field to a Symfony form that is otherwise bound to an entity?
Let's say I have an entity with fields first_name and last_name. I do the typical thing in my form class buildForm method.
$builder
->add('first_name')
->add('last_name')
;
and this in my controller:
$editForm = $this->createForm(new MyType(), $entity);
That works nicely but I'd like to add another text box, let's call it "extra", and receive the value in the POST action. If I do $builder->add('extra')‍, it complains that
NoSuchPropertyException in PropertyAccessor.php line 479:
Neither the property "extra" nor one of the methods "getExtra()", "extra()", "isExtra()", "hasExtra()", "__get()" exist and have public access in class...
Which is correct. I just want to use it to collect some extra info from the user and do something with it other than storing it with the entity.
I know how to make a completely standalone form but not one that's "mixed".
Is this possible?
In your form add a text field with a false property_path:
$builder->add('extra', 'text', array('property_path' => false));
You can then access the data in your controller:
$extra = $form->get('extra')->getData();
UPDATE
The new way since Symfony 2.1 is to use the mapped option and set that to false.
->add('extra', null, array('mapped' => false))
Credits for the update info to Henrik Bjørnskov ( comment below )
Since Symfony 2.1, use the mapped option:
$builder->add('extra', 'text', [
'mapped' => false,
]);
According to the Documentation:
allow_extra_fields
Usually, if you submit extra fields that aren't configured in your form, you'll get a "This form should not contain extra fields." validation error.
You can silence this validation error by enabling the allow_extra_fields option on the form.
mapped
If you wish the field to be ignored when reading or writing to the object, you can set the mapped option to false.
class YourOwnFormType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
array(
'allow_extra_fields' => true
)
);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$form = $builder
->add('extra', TextType::class, array(
'label' => 'Extra field'
'mapped' => false
))
;
return $form;
}
}

Resources