Symfony 2 set default values on collection field entities - symfony

I've these 2 forms:
Lineups form which edits the lineups field of a match entity
<?php
namespace Acme\MatchBundle\Form\Type;
use Acme\UserBundle\Entity\UserRepository;
use Acme\TeamBundle\Entity\TeamRepository;
use Acme\ApiBundle\Listener\PatchSubscriber;
use Acme\CoreBundle\Form\DataTransformer\TimestampToDateTimeTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class LineupsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('lineups', 'collection', array(
'type' => new LineupType(),
'allow_add' => true,
'allow_delete' => false
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\MatchBundle\Entity\Match',
'csrf_protection' => false
));
}
public function getName()
{
return 'match';
}
}
Lineup form which creates/edits a lineup entity
<?php
namespace Acme\MatchBundle\Form\Type;
use Acme\PlayerBundle\Entity\PlayerRepository;
use Acme\ApiBundle\Listener\PatchSubscriber;
use Acme\CoreBundle\Form\DataTransformer\TimestampToDateTimeTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class LineupType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('player', 'entity', array(
'class' => 'AcmePlayerBundle:Player',
'property' => 'id',
'query_builder' => function(PlayerRepository $er) {
$query = $er->createQueryBuilder('p');
return $query;
}
))
->add('status', 'text')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\MatchBundle\Entity\Lineup',
'csrf_protection' => false
));
}
public function getName()
{
return 'match';
}
}
Lineup entity fields
/**
* #var Match $match
*
* #ORM\ManyToOne(targetEntity="Match", inversedBy="lineups")
* #Assert\NotBlank()
*/
private $match;
/**
* #var \Acme\PlayerBundle\Entity\Player $player
*
* #ORM\ManyToOne(targetEntity="Acme\PlayerBundle\Entity\Player", inversedBy="lineups")
* #Assert\NotBlank()
*/
private $player;
/**
* #var string
*
* #ORM\Column(name="status", type="string", length=16)
* #Assert\NotBlank()
*/
private $status;
Now I've successfully got to add/remove Lineup entities to the lineups field, what I want is to set the $match field of lineup entity to the match edited with the lineups form.
Is that possible?

Found by myself that binding to the BIND form event let me do what I've needed:
$builder->addEventListener(
FormEvents::BIND,
function (FormEvent $event) {
$data = $event->getData();
$lineups = $data->getLineups();
foreach ($lineups as &$lineup) {
$lineup->setMatch($data);
}
$event->setData($data);
}
);
Which works fine ;)

Related

Symfony collection forms - Save to json_array

Hove to create collections of field and store in one db column type json_array?
My entity have column date_data which is json_array type. I want to render two fields on frontent.
First Field -> from - date type.
Second Field -> to - date type.
I use jQuery repeater lib, for render this fields as repeater field on frontend. And want to store fields data from repeater in date_data column in db like this.
[{"from": '12/31/2009' , "to": '01/16/2010' }, {"from": '02/10/2011' , "to": '02/16/2011' }]
You can create entity with json column for your data:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Test
*
* #ORM\Table(name="test")
* #ORM\Entity(repositoryClass="App\Repository\TestRepository")
*/
class Test
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", options={"unsigned":true})
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var array|null
*
* #ORM\Column(name="data", type="json", nullable=true)
*/
private $data;
public function getId(): ?int
{
return $this->id;
}
public function getData(): ?array
{
return $this->data;
}
public function setData(?array $data): self
{
$this->data = $data;
return $this;
}
}
and 2 forms: first for entity and second for data collection item:
App\Form\Test
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type as FormType;
class Test extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('data', FormType\CollectionType::class, [
'allow_add' => true,
'allow_delete' => true,
'entry_type' => 'App\\Form\\Data',
'label' => 'Data',
])
->add('save', FormType\SubmitType::class, [
'label' => 'Save',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'App\\Entity\\Test',
]);
}
}
App\Form\Data
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type as FormType;
class Data extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('from', FormType\TextType::class, [
'label' => 'from',
])
->add('to', FormType\TextType::class, [
'label' => 'to',
]);
}
public function configureOptions(OptionsResolver $resolver)
{
}
}
And in controller
$test = $this->getDoctrine()->getRepository('App:Test')->find(1);
$form = $this->createForm(\App\Form\Test::class, $test, []);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
dump($form->getData());
$this->getDoctrine()->getManager()->flush();
}

Load a service in a FormBuilderInterface class

I have a symfony service which loads a list of available languages from my database.
I have a FormBuilderInterface class where i define my form structure:
<?php
namespace UserBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
class UserProfileType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// $this->get('locations')->getCountries()
echo 'options<pre>';
print_r($options);
echo '</pre>';
$builder
->add('name')
->add('surname')
->add('birthdate')
->add('country', 'choice',
array(
'choices' => $listOfCountries, // i want this !!
'choices_as_values' => true
)
)
->add('province')
->add('city')
->add('occupation')
->add('interests')
->add('languages')
->add('aboutMe')
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'UserBundle\Entity\UserProfile'
));
}
/**
* #return string
*/
public function getName()
{
return 'userbundle_userprofile';
}
}
I tried to load my Locations service in my FormBuilderInterface with:
$this->get('locations);
But it doesn't works.
I have searching in the Internet but i haven't found anything about this.
How i can do it?
Thanks you!
you shouldn't be trying to call a service inside the formType. Inject what you need through the controller.
controller (note the 3rd param)
$form = $this->createForm( new UserProfileType(), $entity, array('locations' => $locations ) );
then in your formType class
public function buildForm(FormBuilderInterface $builder, array $options)
{
$listOfCountries = $options['locations']
// ......
}
/*
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'UserBundle\Entity\UserProfile',
'locations' => array()
));
}

Symfony - Setting validation_groups on submit skips the validation_groups callback for the type

In symfony I have a multi-stage form that embeds other forms. Some of these embedded forms have dynamic validation groups set via the validation_groups callback in the configureOptions (used to be setDefaultOptions) method for the type.
When the form is submitted via a submit that does not have its validation_groups option set, then these callbacks are run and the correct validation groups are used. But when I set the validation_groups option of a submit to a type that has this callback, then the callback is not run and the groups are not set as needed.
Are there any options that need to be set to have this working?
Controller
$form = $this->createForm(new RegistrationType());
$form->add('Submit1', 'submit', array(
'validation_groups' => array('Person'),
))
->add('Submit2', 'submit', array(
'validation_groups' => array('Contact', 'Address'),
))
;
...
Registration type
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class RegistrationType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('person', new PersonType())
->add('maritalStatus', 'entity', array(
'class' => 'AppBundle:MaritalStatus',
'choice_label' => 'status',
))
->add('spouse', new SpouseType())
->add('homeAddress', new AddressType())
->add('postalAddress', new AddressType())
->add('contact', new ContactType())
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Registration',
'cascade_validation' => true,
));
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_registration';
}
}
PersonType
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PersonType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', 'text')
->add('name', 'text')
->add('surname', 'text')
->add('fullName', 'text', array('required' => false))
->add('southAfrican', 'checkbox', array(
'required' => false,
'data' => true,
))
->add('identification', 'text')
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Person',
// Setting some groups dynamically based on input data
'validation_groups' => function (FormInterface $form) {
$data = $form->getData();
$groups = array('Person');
// Add southAfrican validation group if is a southAfrican
if ($data->isSouthAfrican() === true) {
$groups[] = 'southAfrican';
}
// Add Married validation group is this is spouse and Married is selected
if ($form->getParent()->getConfig()->getName() === 'spouse') {
if ($form->getParent()->getParent()->getData()->getMaritalStatus()->getStatus() === 'Married') {
$groups[] = 'married';
} elseif (($key = array_search('southAfrican', $groups)) !== false) {
unset($groups[$key]);
}
// If this is not spouse then this is applicant so add its validation group
} else {
$groups[] = 'applicant';
}
return $groups;
},
'cascade_validation' => true,
));
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_person';
}
}
Submit button validation groups have precedence over Type's validation groups. Form::getClickedButton returns a clicked button that is bound to a current form or its parents.
The case you described can be resolved by passing callbacks instead of plain groups to the submit buttons. E.g.:
$form->add('Submit1', 'submit', array(
'validation_groups' => function (FormInterface $form) {
if ($form->isRoot()) {
return array('Person');
} else {
return ($form->getConfig()->getOptions('validation_groups'))();
}
},
))

Type of choice field Symfony2

I try to save values of one - three checkboxes in field category in database, but i get the error :
Notice: Array to string conversion in /var/www/OnTheWay/vendor/doctrine/dbal/lib/Doctrine/DBAL/Statement.php line 120
The field:
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $category;
Get & Set:
/**
* #return mixed
*/
public function getCategory()
{
return $this->category;
}
/**
* #param $category
*/
public function setCategory($category)
{
$this->category[] = $category;
}
Profile type:
namespace Vputi\UserBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ProfileType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('fio');
$builder->add('birthDate', null, array('widget' => 'single_text'));
$builder->add('file');
$builder->add('yearOnRoad');
$builder->add('telephone');
$builder->add('contactMail');
$builder->add('role', 'choice', array('choices' => array(1 => 'За рулем') ,'expanded'=>true, 'multiple' => true,));
$builder->add('category', 'choice', array(
'choices' => array('A' => 'Категория А', 'B' => 'Категория B', 'C' => 'Категория C',),
'expanded' => true,
'multiple' => true,
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' =>'Vputi\UserBundle\Entity\Profile',
'cascade_validation' => true,
));
}
}
Here is my form type, I hope you help me, and iam ommit getName() method.
The problem is $category is defined as a string but you're using it like an array.
The solution depends on exactly what you want to accomplish. If you want it to be mapped as an array you have to do this:
/**
* #ORM\Column(type="array", nullable=true)
*/
private $category;
When using Doctrine's array type, make sure you take this into account: How to force Doctrine to update array type fields?

Symfony2 and Doctrine - 'Catchable fatal error' on flush

Names changed due to NDA.
I'm trying to come up with a survey form. Each survey question can have multiple answers/scores, so there's a natural 1:* relationship to them. That said, for the public-facing form, I need to have a 1:1 relationship between the score and the question it relates to, which is what I'm working on now. Right now, the survey is open to the public, so each completed survey is not related to a user.
The interesting parts of my current setup are as follows...
Question:
namespace Acme\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
class Question
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string question
*
* #ORM\Column(name="question", type="string", length=255)
*/
private $question;
/**
* #var ArrayCollection scores
*
* #ORM\OneToMany(targetEntity="Score", mappedBy="question")
*/
private $scores;
public function __construct()
{
$this->scores = new ArrayCollection();
}
// other getters and setters
/**
* #param $score
*/
public function setScore($score)
{
$this->scores->add($score);
}
/**
* #return mixed
*/
public function getScore()
{
if (get_class($this->scores) === 'ArrayCollection') {
return $this->scores->current();
} else {
return $this->scores;
}
}
}
Those last two are helper methods so I can add/retrieve individual scores. The type checking convolutions were due to an error I encountered here
Score:
namespace Acme\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
class Score
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var integer $question
*
* #ORM\ManyToOne(targetEntity="Question", inversedBy="scores")
* #ORM\JoinColumn(name="question_id", referencedColumnName="id")
*/
private $question;
/**
* #var float score
*
* #ORM\Column(name="score", type="float")
*/
private $score;
// getters and setters
}
Controller method:
public function takeSurveyAction(Request $request)
{
$em = $this->get('doctrine')->getManager();
$questions = $em->getRepository('Acme\MyBundle\Entity\Question')->findAll();
$viewQuestions = array();
foreach ($questions as $question) {
$viewQuestions[] = $question;
$rating = new Score();
$rating->setQuestion($question->getId());
$question->setRatings($rating);
}
$form = $this->createForm(new SurveyType(), array('questions' => $questions));
if ('POST' === $request->getMethod()) {
$form->bind($request);
if ($form->isValid()) {
foreach ($questions as $q) {
$em->persist($q);
}
$em->flush();
$em->clear();
$url = $this->get('router')->generate('_main');
$response = new RedirectResponse($url);
return $response;
}
}
return $this->render('MyBundle:Survey:take.html.twig', array('form' => $form->createView(), 'questions' => $viewQuestions));
}
My form types....
SurveyType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('questions', 'collection', array('type' => new SurveyListItemType()));
}
public function getName()
{
return 'survey';
}
}
SurveyListItemType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyListItemType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('rating', new SurveyScoreType());
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Acme\MyBundle\Entity\Question'));
}
public function getName()
{
return 'survey_list_item_type';
}
}
SurveyScoreType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyRatingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('score', 'choice', array('choices' => array(
'0' => '',
'0.5' => '',
'1' => '',
'1.5' => '',
'2' => '',
'2.5' => '',
'3' => '',
'3.5' => '',
'4' => '',
'4.5' => '',
'5' => ''
), 'expanded' => true, 'multiple' => false));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Acme\MyBundle\Entity\Score'));
}
public function getName()
{
return 'survey_score_type';
}
}
Okay, with all of that, I'm getting the following error when Doctrine's EntityManager attempts to flush() in my controller action:
Catchable Fatal Error: Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be of the type array, object given, called in /home/kevin/www/project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php on line 547 and defined in /home/kevin/www/project/vendor/doctrine/collections/lib/Doctrine/Common/Collections/ArrayCollection.php line 47
I believe it has to do with the questions' related scores, as they're supposed to be an array(collection) in Question, but they're individual instances in this case. The only problem is I'm not sure how to fix it.
I'm thinking my form setup may be too complex. All I really need to do is attach each Question.id to each related Score. I'm just not sure the best way to build the form part of it so everything is persisted properly.
I believe your error is here
$rating = new Score();
//...
$question->setRatings($rating);
Usually if you have an ArrayCollection in your Entity, then you have addChildEntity and removeChildEntity methods that add and remove elements from the ArrayCollection.
setRatings() would take an array of entities, rather than a single entity.
Assuming that you do have this method, try
$question->addRating($rating);
I think you have a mistake in your setRating method.
You have
$this->score->add($score);
It should be:
$this->scores->add($score);
I was able to solve it by simply handling the Scores. So, with that approach, I was able to remove SurveyListItemType, and make the following changes:
SurveyType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('scores', 'collection', array('type' => new SurveyRatingType()));
}
public function getName()
{
return 'survey';
}
}
Note how the collection type is now mapped to SurveyRatingType.
SurveyRatingType:
namespace Acme\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class SurveyRatingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('score', 'choice', array('choices' => array(
'0' => '',
'0.5' => '',
'1' => '',
'1.5' => '',
'2' => '',
'2.5' => '',
'3' => '',
'3.5' => '',
'4' => '',
'4.5' => '',
'5' => ''
), 'expanded' => true, 'multiple' => false));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Acme\MyBundle\Entity\Score'));
}
public function getName()
{
return 'survey_rating_type';
}
}
And my modified controller action:
public function takeSurveyAction(Request $request)
{
$em = $this->get('doctrine')->getManager();
$questions = $em->getRepository('Acme\MyBundle\Entity\Question')->findAll();
$ratings = array();
foreach ($questions as $question) {
$rating = new SurveyRating();
$rating->setQuestion($question);
$ratings[] = $rating;
}
$form = $this->createForm(new SurveyType(), array('ratings' => $ratings));
if ('POST' === $request->getMethod()) {
$form->bind($request);
if ($form->isValid()) {
foreach ($ratings as $r) {
$em->persist($r);
}
$em->flush();
$em->clear();
$url = $this->get('router')->generate('_main');
$response = new RedirectResponse($url);
return $response;
}
}
return $this->render('MyBundle:Survey:take.html.twig', array('form' => $form->createView(), 'questions' => $questions));
}
I had a feeling I was doing it wrong due to the three form types. That really jumped out as a bad code smell. Thanks to everyone for their patience and attempts at helping. :)

Resources