I have a User entity that has a collection of List Items associated with it.
Each List Item entity also references another entity, Topic. I have setup a UNIQUE constraint on the List Item table that will only allow unique combinations of the User and Topic foreign keys. No List Items with a duplicate reference to the Topic entity are allowed for each user. I am also ordering the results by "completion_week".
There are times when I will be attempting to persist a form collection and it will fail with an integrity constraint violation. For some reason Symfony seems to think updates are being made to the form and is incorrectly attempting to update collection items - but is switching the a foreign key on some of the updated entities seemingly randomly - which is causing the error because of the above mentioned constraints.
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '6-1' for key 'list_item_user_topic'
The User Entity:
<?php
/**
* #ORM\Entity(repositoryClass="App\MyBundle\Repository\UserRepository")
* #ORM\Table(name="users")
* #Gedmo\SoftDeleteable(fieldName="deleted_at", timeAware=true)
*/
class User implements UserInterface, EquatableInterface
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=30, nullable=false)
*/
private $first_name;
/**
* #ORM\Column(type="string", length=30, nullable=false)
*/
private $last_name;
/**
* #ORM\Column(type="string", unique=true, length=100, nullable=true)
*/
private $email;
/**
* #ORM\OneToMany(
* targetEntity="App\MyBundle\Entity\ListItem",
* mappedBy="user",
* orphanRemoval=true,
* fetch="EAGER",
* cascade={"all"}
* )
* #ORM\OrderBy({"completion_week"="ASC"})
*
*/
private $listItems;
...
The List Item Entity:
<?php
/**
* #ORM\Entity(repositoryClass="App\MyBundle\Repository\ListItemRepository")
* #ORM\Table(
* name="list_items",
* uniqueConstraints={#ORM\UniqueConstraint(name="list_item_user_topic", columns={"user_id","topic_id"})}
* )
*
* #Gedmo\SoftDeleteable(fieldName="deleted_at", timeAware=true)
*/
class ListItem
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\MyBundle\Entity\User", inversedBy="listItems", fetch="EAGER")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
/**
* #ORM\Column(type="integer", length=11, nullable=true)
*/
private $completion_week;
/**
* #ORM\ManyToOne(targetEntity="App\MyBundle\Entity\Topic", inversedBy="listItems", fetch="EAGER")
* #ORM\JoinColumn(name="topic_id", referencedColumnName="id")
*/
private $topic;
...
I am using Symfony2 form builder to build the form. This is working great. I have added javascript for add/remove buttons on the front end. In general - I am able to save and persist the form collection without any problems.
User Form Type:
<?php
/**
* Class UserType
* #package App\MyBundle\Form\Type
*/
class UserType extends AbstractType
{
/**
* #param OptionsResolverInterface $resolver]
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'App\MyBundle\Entity\User',
'method' => 'POST',
'cascade_validation' => true
));
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Set User data
$user = $builder->getData();
// Generate form
$builder
->add('listItems', 'collection', array(
'options' => array(
'required' => false,
'attr' => array('class'=>'col-sm-12')
),
'type' => new ListItemType(),
'label' => false,
'allow_add' => true,
'allow_delete' => true,
'delete_empty' => true,
'prototype' => true,
'by_reference' => false
))
->add('first_name')
->add('last_name')
->add('email');
}
/**
* #return string
*/
public function getName()
{
return 'user';
}
}
List Item Form Type:
<?php
/**
* Class ListItemType
* #package App\MyBundle\Form\Type
*/
class ListItemType extends AbstractType
{
/**
* #param OptionsResolverInterface $resolver]
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'App\MyBundle\Entity\ListItem',
'method' => 'POST',
));
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Generate form
$builder
->add('topic', 'entity', array(
'attr' => array('class' => 'form-control chosen-select-10'),
'class' => 'AppMyBundle:Topic',
'empty_value' => 'Choose a Topic',
'label' => false,
'property' => 'name',
'expanded' => false,
'multiple' => false
))
->add('completion_week', 'integer', array(
'attr' => array('class' => 'form-control'),
'label' => false,
));
}
/**
* #return string
*/
public function getName()
{
return 'list_item';
}
}
What I discovered is that when the form is being processed - something is happening within the handleRequest() method that is swapping out foreign key references on different list items in the collection. In some cases - without making any changes to the form collection on the front end. Like so:
Original collection of List Items for a User:
User's List Item Collection after handleRequest():
This then causes the integrity constraint violation when Doctrine attempts to write the first record because it is violating the unique constraint on the List Items table. What I do not understand is how/why the handleRequest() method would be swapping foreign keys on update.
Also - in many cases - the form will persist fine for a user. I hate to use the word "random" here but I have not been able to identify a way to duplicate the issue other than just working with the entity for a while and performing CRUD operations on it. Many times the form persists fine - other times the foreign key references get swapped and I am unable to submit the form to update the entity because of the UNIQUE constraint.
Has anyone experienced similar issues or have some insight on why this might be occurring? Is this a bug in the handleRequest() method? This will occur even if I have not made any changes to the List Item collection. As in - if I edit a user and simply submit the form without making any changes - this behavior will still occur.
Is there a better way to do this?
The solution was to add a Doctrine "IndexBy" annotation to the $listItems property on the User entity. By specifying a column here, the results returned will be indexed by its value. This must be a unique value. In this case I used the primary key.
/**
* #ORM\OneToMany(
* targetEntity="App\MyBundle\Entity\ListItem",
* mappedBy="user",
* orphanRemoval=true,
* fetch="EAGER",
* indexBy="id",
* cascade={"all"}
* )
* #ORM\OrderBy({"completion_week"="ASC"})
*
*/
private $listItems;
This then changed the way each of the collection items were indexed on the frontend.
From this:
<div class="row" data-content="user[listItems][0]">...</div>
<div class="row" data-content="user[listItems][1]">...</div>
<div class="row" data-content="user[listItems][2]">...</div>
<div class="row" data-content="user[listItems][3]">...</div>
<div class="row" data-content="user[listItems][4]">...</div>
To this:
<div class="row" data-content="user[listItems][1950]">...</div>
<div class="row" data-content="user[listItems][1951]">...</div>
<div class="row" data-content="user[listItems][1955]">...</div>
<div class="row" data-content="user[listItems][1953]">...</div>
<div class="row" data-content="user[listItems][1948]">...</div>
Now, when submitting the form, each collection item is referenced by it unique ID - ensuring that the data input on the frontend is persisted correctly after form binding.
The reason it was behaving somewhat randomly was because I was ordering the results by the "completion_week" column. There was a chance for the records to be returned in a different order where they share the same ORDER BY value. If you have three records with the same value for "completion_week" and you ORDER BY "completion_week" - it's up to MySQL to determine the order of the results.
When Symfony received the POST results - the controller had to make another call to the database to get the User entity and build the form. If the results were returned in a different order, the array keys captured from the frontend would not match up - and the unique constraint error was produced onFlush.
Another solution: add the primary key to the orderBy criteria
Related
i use Symfony3.3 and want to make a form for the admin administration. The user should be have a group and the group sould have the roles for the backend access.
The form for groups (name and roles) i finished and the form for the admins (name, passwort...) is finish too.
The admin will be find and have the group. If i load the admin it have the arraycollection with the groups.
Here my classes
admin:
class Admin extends BaseUser
{
/**
* #ORM\Id()
* #ORM\Column(name="idAdmin", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string", length=255, options={"default":NULL})
*/
protected $style;
/**
* #ORM\ManyToMany(targetEntity="AdminBundle\Entity\AdminGroup")
* #ORM\JoinTable(
* name="admin_has_group",
* joinColumns={
* #ORM\JoinColumn(name="idAdmin", referencedColumnName="idAdmin")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="idGroup", referencedColumnName="idGroup")
* }
* )
*/
protected $groups;
/**
* #return string
*/
public function getStyle()
{
return $this->style;
}
/**
* #param string $style
*/
public function setStyle($style)
{
$this->style = $style;
return $this;
}
public function setGroups($groups)
{
$this->groups = $groups;
return $this;
}
public function getGroups()
{
return $this->groups;
}
}
groups
class AdminGroup extends BaseGroup
{
/**
* #var int
* #ORM\Id
* #ORM\Column(name="idGroup", type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* Group constructor.
*
* #param string $name
* #param array $roles
*/
public function __construct($name = '', $roles = array())
{
parent::__construct($name, $roles);
}
}
form generation
$admin = $this->getDoctrine()->getRepository(Admin::class)->find(1);
$admingroupList = $this->getDoctrine()->getRepository(AdminGroup::class)->findAll();
$form = $this->createFormBuilder($admin)
->add("username", TextType::class)
->add('plainPassword', PasswordType::class, $passwordSettings)
->add(
'groups', ChoiceType::class, [
'required' => false,
'multiple' => true,
'choices' => $admingroupList,
])
->add('save', SubmitType::class, ['label' => 'Save'])->getForm()->createView();
save form
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
$em->persist($user);
$em->flush();
....
}
The first problem is that i have in the overview only display the id's in the select.
The second problem is that i submit the form (with selected groups) symfony crashed with the following message
Call to a member function contains() on array
I try to convert the grouplist to an normal array they will be crashed at the save the data
Expected argument of type "FOS\UserBundle\Model\GroupInterface", "integer" given
I dont know that i sould do to make a simple symfony form with the admin data and group selection... i dont find any example for a form with fosuserbundle...
Have someone an idea what i can to without manipulate the fosuserbundle entites or the symfonycode?
If you need more source, tell me with part :)
Editing 10.12.17
I try to convert the ChoosenArray into this format
$list = [
'user' => 0,
'admin' => 1
];
but than it will be broken at
$form->handleRequest($request);
with the error:
Expected argument of type "FOS\UserBundle\Model\GroupInterface", "integer" given
I do not think the data returned from
$admingroupList = $this->getDoctrine()->getRepository(AdminGroup::class)->findAll();
Will work as you want it too. choices wants something like this
'choices' => [
'Admin' => 'admin',
'User' => 'user'
]
Where the key of the array is the name the user sees, and the value of the array is the value used in the <option>.
You probably need to manipulate the $admingroupList array to mimic the demo array above. Or write your own query in the AdminGroup Repo to return a pre-formatted array for use with a Symfony form.
I'm trying to use a simple form definition to filter some data, so I create the form with no class attached(expecting to use getData() function) and then work with the array of parameters passed to the form, but the form comes always invalid. Result that the form is trying to validate a parameter that do not belong to the context of form.
I'm getting this validation error on the field "almacen":
This value should not be blank.
With cause:
Symfony\Component\Validator\ConstraintViolation
Object(Symfony\Component\Form\Form).data[almacen].responsable = null
I tried using cascade_validation=false but did't work.
In the controller action I declared:
public function indexAction(Request $request)
{
$informeStock = $this->createForm(new BusquedaInformeStockType());
$informeStock->handleRequest($request);
if ($informeStock->isSubmitted() && $informeStock->isValid()) {
$data = $informStock->getData();
// the action logic...
}
...
}
I have a simple form definition, with an entity form type declared and no data_class asociated to the form.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('almacen', 'entity', array(
'class' => 'BusetaBodegaBundle:Bodega',
'placeholder' => '---Seleccione---',
'required' => false,
'label' => 'Bodega',
'attr' => array(
'class' => 'form-control',
),
))
...
...
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => false,
));
}
And this is the definition of the entity Bodega:
class Bodega
{
...
/**
* #var string
*
* #ORM\Column(name="codigo", type="string", nullable=true)
* #Assert\NotBlank()
*/
private $codigo;
/**
* #var string
*
* #ORM\Column(name="nombre", type="string")
* #Assert\NotBlank()
*/
private $nombre;
/**
* #ORM\ManyToOne(targetEntity="Buseta\BodegaBundle\Entity\Tercero", inversedBy="bodega")
* #Assert\NotBlank()
*/
private $responsable;
...
}
In previous versions of the entity Bodega the parameter "responsable" was left in blank, so there is some rows in the db thas has no "responsable" asociated.
But despite that this should not be happening right? What I'm doing wrong?
You have an entity form field with validation constraints:
/**
* #ORM\ManyToOne(targetEntity="Buseta\BodegaBundle\Entity\Tercero", inversedBy="bodega")
* #Assert\NotBlank()
*/
private $responsable;
This is your problem - Assert not blank
Validates that a value is not blank, defined as not strictly false,
not equal to a blank string and also not equal to null
You have few choices, either add validation group(read this and this) or simple remove that Assert. Also its better to use #Assert\Valid for associations like that.
I am confronted to a problem in Symfony 3 that is driving me crazy and I do not find any solutions. I have two entities "Advert" and "City". An advert must have a city. Like this :
class Advert
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="City", inversedBy="adverts")
* #ORM\JoinColumn(nullable=false)
*
* #Assert\Valid()
*/
private $location;
In my database, there is 36000 cities. So, when I use an EntityType for my form (to create an advert), Symfony generates a big query with 36000 <li> and my page is slow to load.
class AdvertType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('location', EntityType::class, array(
'class' => 'AQDPlatformBundle:City',
'choice_label' => 'name',
'multiple' => false,
))
}
}
My goal is to build myself the dropdown with an AJAX request and ElasticSearch (FOSElasticaBundle). I have already my request and it works. But, how to link creating advert, input fied and a "custom Type" (which could correspond to IntegerType for an id of City) ?
I am currently creating a form that lets the user choose a certain skills from a Dropdown and a Checkbox for hobbies that lets the user check as much he/she wants.
Here is my Table for that: CurriculumVitae
/* namespace ........... */
use Doctrine\ORM\Mapping as ORM;
/**
* CurriculumVitae
*
* #ORM\Table(name="foo_cv")
* #ORM\Entity
*/
class CurriculumVitae
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var integer
* #ORM\ManyToOne(targetEntity="Foo\BarBundle\Entity\Skills")
* #ORM\JoinColumn(name="skills", referencedColumnName="id")
*/
private $skills;
/**
* #var integer
* #ORM\ManyToOne(targetEntity="Foo\BarBundle\Entity\Hobby", cascade={"persist"})
* #ORM\JoinColumn(name="hobbies", referencedColumnName="id")
*/
private $hobby;
/* Setters and Getters ....... */
}
Here are some codes for my Form Type: CurriculumVitaeType
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('skills', 'entity', array('class' =>'FooBarBundle:Skills','property' => 'skills'))
->add('hobby', 'entity', array( 'class' => 'FooBarBundle:Hobby','property' => 'hobbies', 'expanded'=>true,'multiple'=>true, 'label' => 'hobbies'))
->add('save','submit',array('label'=>'Submit'))
;
}
/* OptionsResolverInterface ..... */
/* getName() .... */
I call my form in my twig this way in: cv.twig.html
{{ form(curriculumForm) }}
And lastly in my controller: CurriculumController
$em = $this->getDoctrine()->getManager();
$cv = new CurriculumVitae();
$curriculumForm = $this->createForm(new CurriculumVitaeType(), $cv);
$curriculumForm->handleRequest($request);
if ($curriculumForm->isValid()) {
$em->persist($cv);
$em->flush();
return $this->redirect($this->generateUrl('foo_main_window'));
}
return array('curriculumForm'=> $curriculumForm->createView());
The Form Displays correctly but when I choose a skill from the dropdown and assign a certain hobby and click on submit, an error is thrown.
Found entity of type Doctrine\Common\Collections\ArrayCollection on association Foo\BarBundle\Entity\CurriculumVitae#hobby, but expecting Foo\BarBundle\Entity\Hobby
I dont know if i missed something but i think the error occurs in the process of persisting the data after the form is submitted.
That's because you have many-to-one relation, which means
Many CurriculumVitaes can have (the same) single Hobby
But on the other hand you've created in your form a field with option 'multiple'=>true, which means that you let the user to choose multiple hobbies. Therefore form returns ArrayCollection of Hobby entites instead of single instance.
That doesn't match. You need to either remove multiple option, or make many-to-many relation on $hobby property.
Symfony version : 2.8.5
Context: I have an entity Restaurant which has a OneToOne relationship with an entity Coordinates which have several relations with other entities related to Coordinates informations. I my backend I create a form related to Restaurant entity with a custom nested form related to Coordinates.
Nota : I use EasyAdminBundle to generate my backend.
Entities relations scheme :
Restaurant
1 ________ 1 `Coordinates`
* ________ 1 `CoordinatesCountry`
1 ________ 1 `CoordinatesFR`
* ________ 1 `CoordinatesFRLane`
* ________ 1 `CoordinatesFRCity`
Backend view :
At this point I try the following scenario :
I create a new Restaurant, so I fill the fields related form for the first time. I let the Coordinates nested form blank (empty fields). So after form submission, validation messages are displayed (see image below).
I edit the previous form and this time I fill the fields of the Coordinates nested form. After form submission, a new Coordinates entity is hydrated and a relationship is created between Restaurant and Coordinates.
Once again I edit the previous form and this time I clear all the fields of the Coordinates nested form. The validation is not triggered and I get the following error :
Expected argument of type "FBN\GuideBundle\Entity\CoordinatesFRCity",
"NULL" given
I precise that in CoordinatesFRType (see code below), to trigger the validations message the first time I had to use the option empty_data with a closure (like described in the official doc) to instatiate a new CoordinatesFR instance in case of empty datas (all fields blank). But here, in this article (written by the creator of the Symfony form component), it is explained (see empty_data and datta mappers paragraphs) that the empty_data is only called at object creation. So I think this the reason why my validation does not work anymore in case of edition.
Question : why the validation is not effective anymore when editing my form and clearing all embedded form ?
The code (only what is necessary) :
Restaurant entity
use Symfony\Component\Validator\Constraints as Assert;
class Restaurant
{
/**
* #ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\Coordinates", inversedBy="restaurant", cascade={"persist"})
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
* #Assert\Valid()
*/
private $coordinates;
}
Coordinates entity
use Symfony\Component\Validator\Constraints as Assert;
class Coordinates
{
/**
* #ORM\ManyToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesCountry")
* #ORM\JoinColumn(nullable=false)
*/
private $coordinatesCountry;
/**
* #ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesFR", inversedBy="coordinates", cascade={"persist"})
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
* #Assert\Valid()
*/
private $coordinatesFR;
/**
* #ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\Restaurant", mappedBy="coordinates")
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $restaurant;
}
CoordinatesFR entity
use Symfony\Component\Validator\Constraints as Assert;
class CoordinatesFR extends CoordinatesISO
{
/**
* #ORM\ManyToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesFRLane")
* #ORM\JoinColumn(nullable=true)
* #Assert\NotBlank()
*/
private $coordinatesFRLane;
/**
* #ORM\ManyToOne(targetEntity="FBN\GuideBundle\Entity\CoordinatesFRCity")
* #ORM\JoinColumn(nullable=false)
* #Assert\NotBlank()
*/
private $coordinatesFRCity;
/**
* #ORM\OneToOne(targetEntity="FBN\GuideBundle\Entity\Coordinates", mappedBy="coordinatesFR")
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $coordinates;
}
Easy Admin config (equivalent to RestaurantType)
easy_admin:
entities:
Restaurant:
class : FBN\GuideBundle\Entity\Restaurant
form:
fields:
- { property: 'coordinates', type: 'FBN\GuideBundle\Form\CoordinatesType' }
CoordinatesType
class CoordinatesType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('CoordinatesCountry', EntityType::class, array(
'class' => 'FBNGuideBundle:CoordinatesCountry',
'property' => 'country',
))
->add('coordinatesFR', CoordinatesFRType::class)
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'FBN\GuideBundle\Entity\Coordinates',
));
}
}
CoordinatesFRType
class CoordinatesFRType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('laneNum', TextType::class)
->add('coordinatesFRLane', EntityType::class, array(
'class' => 'FBNGuideBundle:CoordinatesFRLane',
'property' => 'lane',
'placeholder' => 'label.form.empty_value',
))
->add('laneName', TextType::class)
->add('miscellaneous', TextType::class)
->add('locality', TextType::class)
->add('metro', TextType::class)
->add('coordinatesFRCity', EntityType::class, array(
'class' => 'FBNGuideBundle:CoordinatesFRCity',
'property' => 'display',
'query_builder' => function (CoordinatesFRCityRepository $repo) {
return $repo->getAscendingSortedCitiesQueryBuilder();
},
'placeholder' => 'label.form.empty_value',
))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'FBN\GuideBundle\Entity\CoordinatesFR',
// Ensures that validation error messages will be correctly displayed next to each field
// of the corresponding nested form (i.e if submission and CoordinatesFR nested form with all fields empty)
'empty_data' => function (FormInterface $form) {
return new CoordFR();
},
));
}
}