Symfony form has ManyToOne field but still requires the __tostring() function - symfony

In a Symfony entity that stores the address information of a client I have made a ManyToOne connection with a entity that contains all countries. So the entity has de following link:
/**
* #var string
*
* #ORM\Column(name="country_code", type="string", length=2)
*
* #ORM\ManyToOne(targetEntity="Country")
* #ORM\JoinColumn(name="country_code", referencedColumnName="country_code")
*/
private $countryCode;
In the form generated of this entity I have defined it like this:
->add('countryCode', 'entity', array(
'class' => 'MyBundle:Country',
'choice_label' => 'name_en',
'choice_value' => 'country_code',
'data' => 'nl',
))
So it does not store the primary key but the country_code a two letter code like "nl" for the Netherlands.
Then I have to add __tostring() code to make it work, but why is that? I though the __tostring function would not be required anymore as there is already a ManyToOne connection.
public function __toString()
{
return strval($this->countryCode);
}

First in your entity you just have to write this :
/**
* #ORM\ManyToOne(targetEntity="PathTo/YourBundle/Entity/Country")
*/
private $country;
Then $country will be a reference to a Country Entity (it's ID in the DB),
and will allow you to acces ALL IT'S FIELDS.
After that in your form you should not use
->add('country', 'entity', array(....
As this syntax is deprecated and use instead :
use Symfony\Bridge\Doctrine\Form\Type\EntityType; //don't forget it on top of your form file
->add('country', EntityType::class, array(....
As you probably want to order your countries by alphabetical order you will use a query to do so and eventually your code may look like :
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;
...
->add('country', EntityType::class, array(
'class' => 'MyBundle:Country',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('c')
->orderBy('c.name_en', 'ASC');
},
'choice_label' => 'name_en',
'data' => 'nl'
))
you normally don't have to worry about choice_value, which will be the unique ID of the chosen country : when you'll acces your Client Entity, you will do :
$client->getCountry()->getCountryCode() ;
to get the country code.

I guess you are missing something about how database stores the relationships.
The first problem: you don't need this:
#ORM\Column(name="country_code", type="string", length=2)
... Because #ORM\ManyToOne already describes the schema for this property.
And here we meet the second problem: #ORM\ManyToOne creates an integer column, which contains an ID of the appropriate Country entity. Which means, when you create a form field for this property, it doesn't know which property should it render as a string representation, because your address book (or whatever you called it) entity stores only digits, not the country code.

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

Perform operation on an entity before its persistence

I am trying to do a PUT request on an entity, from an angular 5 client to API Platform.
On the angular side, I retrieve an address via Google maps. This address is a property of an Entity JourneyAddress, so I send a PUT request to API Platform with my JourneyAddress model, with an address property which is an array of google map address components (street_address, locality, etc.).
Now I need to perform some operations on this property before submitting it to Doctrine, ie I need to match the locality given by google with a Locality in our Database.
I was thinking about a listener which would listen for JourneyAddress prePersist and preUpdate events, find the locality instance I need with something like LocalityRepository::findBy(['name' => 'Paris']), update the journeyAddress instance and give it back to Doctrine to perform persist/update operations.
The problem is that API Platform checks if the type of the data submitted corresponds to what Doctrine expects. I sent API Platform an array, but Doctrine actually expects a string.
For context, the array sent could be :
src/Doctrine/EventListener/JourneyAddressListener.php:32:
object(App\Entity\JourneyAddress)[2615]
private 'id' => null
private 'title' => string 'dzfkdqsmlfjsldkflm' (length=18)
private 'search' => string 'mlsqjfkldsjfsqdjlmf' (length=19)
private 'address' =>
array (size=8)
'street_number' => string '2650' (length=4)
'route' => string 'Avenida Rivadavia' (length=17)
'sublocality_level_1' => string 'Balvanera' (length=9)
'administrative_area_level_2' => string 'Comuna 3' (length=8)
'administrative_area_level_1' => string 'Buenos Aires' (length=12)
'country' => string 'Argentine' (length=9)
'postal_code' => string 'C1034' (length=5)
'postal_code_suffix' => string 'ACS' (length=3)
private 'latitude' => float 50.6507791
private 'longitude' => float 3.0657951
private 'media' => null
private 'indication' => string 'klqsjflkmqjfkqjfksflmlqfmlks' (length=28)
I need to extract the street_address and save it as the address property of my JourneyAddress, but the Doctrine entity is :
/**
* #ORM\Column(type="string", length=255)
* #Groups("journey")
* #Assert\Type("string")
* #Assert\NotBlank(
* message = "Le champs 'Adresse du point de départ' doit être rempli",
* groups={"departureFormValidation"}
* )
*/
private $address;
Is there a way that my listener will be used before actual API Platform type-checking ? I also tried to do a custom operation but the result was the same, type-checking always comes first and prevents any further action.
I could of course replace the type of address by array and then send ['my string'], but I feed it should not be that way.
Doctrine listeners are always executed after validation. There are built in api-platform (Symfony) event listeners that are executed before validation: https://api-platform.com/docs/core/events/ PRE_VALIDATE seems like a good place for this.
However, having mixed data type (array, string) for the same field is not a good idea, consider using a separate model class for your array address or a separate unmapped field.

The method name must start with either findBy or findOneBy! error

I have a table which has relationship with Application\Sonata\MediaBundle\Entity\Media (SonataMediaBundle Entity) as 'media'
Normally I can make the form for Media like this below,
$form = $this->createFormBuilder($myMedia)
->add('name')
->add('media') // make the selectbox
->add('save', SubmitType::class, array('label' => 'Create Post'))
->getForm();
However I want to restrict to some medias from all medias, then I made this.
$form = $this->createFormBuilder($myMedia)
->add('name')
->add('media','entity',array(
'class' => "Application\Sonata\MediaBundle\Entity\Media",
'query_builder' => function(EntityRepository $er) {
return $er->createQuery('SELECT r FROM ApplicationSonataMediaBundle:Media');
}))
->add('save', SubmitType::class, array('label' => 'Create Post'))
->getForm();
However it shows the error like this.
Undefined method 'createQuery'. The method name must start with either findBy or findOneBy!
I have found some articles and understood it is related with Repository.
But I am not sure which Repository should I point. THere is no Repository class under
Sonata\MediaBundle\ either Application\Sonata\MediaBundle
namespace Application\Sonata\MediaBundle\Entity;
use Sonata\MediaBundle\Entity\BaseMedia as BaseMedia;
#ORM\Entity(repositoryClass="Where is my repository???")
class Media extends BaseMedia
{
/**
* #var int $id
*/
protected $id;
BTW, my first code shows only select box for pictures(medias)
It is not useful enough to select pictures,
Is there a more suitable way for selecting pictures?
Look at the error, the createQuery method does not exist.
If you take a look at the EntityRepository class you will see that the right method is createQueryBuilder().
If you look at the content of the method you'll see that it returns a QueryBuilder instance with already the right select from statement since you are supposed to get the right repository for your media entity from the Entity form type since you pass the class of your entity in the class option.
You have defined $er as $this->getDoctrine()->getRepository('Application\Sonata\MediaBundle\Entity:Media') which is the EntityRepository. What you need rather is the EntityManager which is $this->getDoctrine()->getManager() and then use the select statement that you have in the piece of code. Hope it helps!

Choice Multiple = true, create new Entries

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

Entity form field and validation in Symfony2?

In my form I have field of type entity. How to disable validation of that entity when the form is submitted? This entity is already persisted in database, so there is no point for validator to validate this entity from my point of view.
EDIT:
Code looks like this:
class SearchRequest
{
/**
* #ORM\ManyToOne(targetEntity="ArticlePattern")
* #ORM\JoinColumn(name="article_pattern_id", onDelete="CASCADE")
* #Assert\NotBlank
*/
private $articlePattern;
}
form field:
$builder
->add('articlePattern', 'entity', array('class' => 'LonbitItssBundle:ArticlePattern')
Validation groups won't work because what I want to accomplish is for validator to check the constraint #Assert\NotBlank on $articlePattern field, but I don't want him to check the constraints defined in class ArticlePattern. So in other words, I want prevent validator from descending inside the $articlePattern object, but I want the constraint placed on $articlePattern field itself to be validated.
Any idea how to do that?
EDIT2:
Validation groups will work. So the final solution is to add groups={"search_request"} constraint option (name of the group is arbitrary) to every field assertion in SearchRequest class, like this:
/**
* #Assert\NotBlank(groups={"search_request"})
*/
private $articlePattern;
That way, validation won't descend to associated objects (assuming they don't belong to - in this case - "search_request" group).
1) If you want to disable this field, just don't use it in your class form ! (and remove it from the template)
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('content')
->add('username')
//->add('yourEntity')
;
}
2) Or better, use validation groups. You create a validation_group which don't call your validator entity and then you use this group in your class form:
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Sybio\BuzzBundle\Entity\SearchRequest',
'csrf_protection' => true,
'csrf_field_name' => '_token',
'intention' => '865c0c0b4ab0e063e5caa3387c1a8741',
'validation_groups' => array('without_article_pattern_ckecking'),
);
}
In your Entity class:
/**
* #ORM\ManyToOne(targetEntity="ArticlePattern")
* #ORM\JoinColumn(name="article_pattern_id", onDelete="CASCADE")
* #Assert\NotBlank(
* groups={"without_article_pattern_ckecking"}
* )
*/
private $articlePattern;
Your form will only validate validators that refer to without_article_pattern_ckecking group, so it should not test validators inside your ArticlePattern entity if they don't have this validation group.
I hope this helps you !

Resources