Update two entities sharing the primary key with a Symfony2 form - symfony

I have two entities: User and CompanyInfo.
The relationship between both is oneToOne, the User can have zero or one CompanyInfo, and one CompanyInfo belongs to one User.
Therefore I setup them to have the same primary key (User's id):
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToOne(targetEntity="CompanyInfo", mappedBy="user", cascade={"persist"})
* #var CompanyInfo
*/
protected $companyInfo;
...
}
class CompanyInfo
{
/**
* #ORM\OneToOne(targetEntity="User", inversedBy="companyInfo", cascade={"persist"})
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* #ORM\Id
* #var User
*/
protected $user;
....
}
I'm having an issue trying to expose the them at the same time so that they can be updated by submitting only one form:
In the UserFormType I have the following line:
$builder->add('companyInfo', new CompanyInfoFormType(), ['required' => false, 'by_reference' => false])
The CompanyInfoFormType has the following:
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => '....\Entity\CompanyInfo',
'intention' => 'registration'
));
}
It all works fine, the form is rendered with both the user and the company info fields. When creating a new user & companyInfo it works but only because I did the following in the onSuccess of the UserFormHandler (Basically persist the User in first place, looks a bit hackie but couldn't find a nicer way):
if ($user->getCompanyInfo() instanceof CompanyInfo) {
$companyInfo = $user->getCompanyInfo()->setUser($user);
$user->setCompanyInfo(null);
$this->entityManager->beginTransaction();
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->entityManager->persist($companyInfo);
$this->entityManager->flush();
$this->entityManager->commit();
$this->entityManager->refresh($user);
}
Now, the issue is when I'm trying to update a user that already has a companyInfo. For some weird reason, doctrine is thinking that the CompanyInfo entity doesn't exist and it's trying to do an INSERT rather than an UPDATE. It's like if the entity CompanyInfo it's not managed by Doctrine and therefore when doing the cascade persist, tries to create a new one.

I finally managed to find a solution for this, it's not ideal though, but it works. Basically, the process method of my UpdateFormHandler looks like follows:
/**
* #param UserInterface $user
* #return bool
*/
public function process(UserInterface $user)
{
$this->form->setData($user);
$method = $this->request->getMethod();
if (in_array($method, ['PUT', 'PATCH'])) {
$this->form->submit($this->request, 'PATCH' !== $method);
if ($this->form->isValid()) {
/** #var User $user */
$this->saveCompanyInfo($user);
$this->onSuccess($user);
$this->userManager->reloadUser($user);
return true;
}
}
return false;
}
/**
* Create link companyInfo -> User when persisting CompanyInfo for first time
*
* #param User $user
*/
protected function saveCompanyInfo(User $user)
{
if ($user->getCompanyInfo() instanceof CompanyInfo && !$user->getCompanyInfo()->getUser()) {
$user->getCompanyInfo()->setUser($user);
} elseif ($user->getCompanyInfo() instanceof CompanyInfo) {
/**
* If companyInfo exists and has been modified, doctrine things that it's a new entity
* detached from the manager, and will try to insert a new row on the company_info table (which crashes, as the PK (user_id) already exists).
* By calling ->merge, we obtain an attached/managed instance,
* so that it will be properly cascade persisted when the user is saved.
*/
/** #var CompanyInfo $companyInfo */
$companyInfo = $this->getEntityManager()->merge($user->getCompanyInfo());
$user->setCompanyInfo($companyInfo);
}
}
I'm sure (I hope) there are better ways, but I'm looking forward to hear them, as this is the only thing that worked for me.
I hope it helps.
Regards,
Javier

Related

Doctrine concurrency with many-to-many and new entities

I have a Symfony controller action which adds a specified user to the list of followed users (the list is a ManyToMany). The code is quite simple:
public function followUserAction(Request $request, User $followee, $follow)
{
/** #var User */
$user = $this->getUser();
// same user
if ($user === $followee) {
return;
}
$em = $this->getDoctrine()->getManager();
$follow = boolval($follow);
if ($follow && !$user->getFollowedUsers()->contains($followee)) {
$user->addFollowedUser($followee);
} elseif (!$follow && $user->getFollowedUsers()->contains($followee)) {
$user->removeFollowedUser($followee);
}
$em->flush();
}
This is the mapping:
/**
* #var ArrayCollection<int|string, User>
*
* #ORM\ManyToMany(targetEntity="User", fetch="EXTRA_LAZY", inversedBy="followingUsers")
* #ORM\JoinTable(
* joinColumns={#ORM\JoinColumn(name="following_user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="followed_user_id", referencedColumnName="id")}
* )
*/
private $followedUsers;
/**
* #var ArrayCollection<int|string, User>
*
* #ORM\ManyToMany(targetEntity="User", fetch="EXTRA_LAZY", mappedBy="followedUsers")
*/
private $followingUsers;
The problem is that when a user clicks many times I am receiving a Duplicate entry. I followed the Doctrine's article about concurrency:
https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/transactions-and-concurrency.html
But unfortunately it doesn't cover the following case:
Check if record exists (SELECT)
If it doesn't exist => insert it
If it already exists => do nothing
If two users make the first SELECT at the same, they will both try to insert the new record.
Any way to solve it?
You can use UniqueConstraintViolationException as proposed at this answer.
Also I think it's good to block UI or throttle user requests at front.

Doctrine: Get auto increment ID before persist/flush

I'm having trouble in Doctrine-Fixtures. I'd like to add a user and a email in another entity, but in relation to the user. So here is my process:
// Create user
$user1 = new User();
// Create user email and add the foreign key to the user
$user1Mail = new UserEmail();
$user1Mail->setEmail('test#example.com');
$user1Mail->setUser($user1);
// Add attributes
$user1->setEmail($user1Mail);
// ...
$manager->persist($user1Mail);
$manager->persist($user1);
$manager->flush();
I add the user of the email in $user1Mail->setUser($user1); before the persist, but the problem is, the user has no primary key --> the ID (auto increment). So to create the relation between the email and the user, the user needs to have a primary key to refer to.
I know the solution to create a unique token before and set this to the ID of the user, but I think this is a uncomfortable way because I need to check if the user ID is already in use.
Is there a good way to handle this?
// EDIT:
Here is the necessary entity relation:
User:
class User implements UserInterface, \Serializable
{
// ...
/**
* #var Application\CoreBundle\Entity\UserEmail
*
* #ORM\OneToOne(
* targetEntity="UserEmail",
* cascade={"persist"}
* )
* #ORM\JoinColumn(
* name="primaryEmail",
* referencedColumnName="id",
* nullable=false,
* onDelete="restrict"
* )
*/
private $email;
// ...
}
UserEmail:
class UserEmail
{
// ...
/**
* #var Application\CoreBundle\Entity\User
* #ORM\ManyToOne(
* targetEntity="User",
* cascade={"persist", "remove"}
* )
* #ORM\JoinColumn(
* name="userID",
* referencedColumnName="id",
* nullable=false
* )
*/
private $user;
// ...
}
As you can see, if you add a user you have to add a UserEmail also. But the UserEmail requires that the userID is already set, but it is only set if you persist the user into the db. How can I realize a fix for it?
I find it strange to see that your User has a OneToOne association towards UserEmail, and UserEmail has a ManyToOne association towards User, and those are 2 separate associations.
I think you'd rather have a single bidirectional OneToMany association:
class User implements UserInterface, \Serializable
{
// ...
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="UserEmail", mappedBy="user", cascade={"persist", "remove"})
*/
private $emails;
public function __construct()
{
$this->emails = new ArrayCollection();
}
/**
* #param UserEmail $email
*/
public function addEmail(UserEmail $email)
{
$this->emails->add($email);
$email->setUser($this);
}
/**
* #param UserEmail $email
*/
public function removeEmail(UserEmail $email)
{
$this->emails->removeElement($email);
$email->setUser(null);
}
/**
* #return UserEmail[]
*/
public function getEmails()
{
return $this->emails->toArray();
}
// ...
}
class UserEmail
{
// ...
/**
* #var User
*
* #ORM\ManyToOne(targetEntity="User", inversedBy="emails")
* #ORM\JoinColumn(name="userID", referencedColumnName="id", nullable=FALSE)
*/
private $user;
/**
* #param User $user
*/
public setUser(User $user = null)
{
$this->user = $user;
}
/**
* #return User[]
*/
public function getUser()
{
return $this->user;
}
// ...
}
I've put a cascade on User::$emails, so any changes to User get cascaded towards UserEmail. This will make managing them easier.
Using this would look something like this:
$email = new UserEmail();
$user = new User();
$user->addEmail($email);
$em->persist($user);
$em->flush();
About foreign keys
Doctrine will manage the foreign keys of your entities for you. You don't need to manually set them on your entities when using associations.
Primary email
Personally I would add a property to UserEmail to mark it as primary. You'll need a bit more logic in the entities, but managing them will become effortless.
Here's the additional code you need:
class User
{
// ...
/**
* #param UserEmail $email
*/
public function addEmail(UserEmail $email)
{
$this->emails->add($email);
$email->setUser($this);
$this->safeguardPrimaryEmail();
}
/**
* #param UserEmail $email
*/
public function removeEmail(UserEmail $email)
{
$this->emails->removeElement($email);
$email->setUser(null);
$this->safeguardPrimaryEmail();
}
/**
* #param UserEmail $email
*/
public function setPrimaryEmail(UserEmail $newPrimaryEmail)
{
if (!$this->emails->contains($newPrimaryEmail)) {
throw new \InvalidArgumentException('Unknown email given');
}
foreach ($this->emails as $email) {
if ($email === $newPrimaryEmail) {
$email->setPrimary(true);
} else {
$email->setPrimary(false);
}
}
}
/**
* #return UserEmail|null
*/
public function getPrimaryEmail()
{
foreach ($this->emails as $email) {
if ($email->isPrimary()) {
return $email;
}
}
return null;
}
/**
* Make sure there's 1 and only 1 primary email (if there are any emails)
*/
private function safeguardPrimaryEmail()
{
$primaryFound = false;
foreach ($this->emails as $email) {
if ($email->isPrimary()) {
if ($primaryFound) {
// make sure there's no more than 1 primary email
$email->setPrimary(false);
} else {
$primaryFound = true;
}
}
}
if (!$primaryFound and !$this->emails->empty()) {
// make sure there's at least 1 primary email
$this->emails->first()->setPrimary(true);
}
}
// ...
}
class UserEmail
{
// ...
/**
* #var boolean
*
* #ORM\Column(type="boolean")
*/
private $isPrimary = false;
/**
* #internal Use
* #param bool $isPrimary
*/
public function setPrimary($isPrimary)
{
$this->isPrimary = (bool)$isPrimary;
}
/**
* #return bool
*/
public function isPrimary()
{
return $this->isPrimary;
}
// ...
}
You'll probably notice safeguardPrimaryEmail(). This will make sure the primary-mark will remain consistent when adding/removing emails.
Using this is very simple:
An email that's created is not primary by default.
If it's the first email added to a user, it will automatically become primary.
Additionally added emails will remain not primary.
When the primary email is removed, the first remaining email will become primary.
You can manually set another primary email by calling User::setPrimaryEmail().
There are many variations to this concept possible, so just view this as an example and refine it to your needs.
It's because Doctrine will generate the entity id when it's inserted into the database.
You can do it with an extra flush():
$user1 = new User();
$manager->persist($user1);
$manager->flush();
// Create user email and add the foreign key to the user
$user1Mail = new UserEmail();
$user1Mail->setEmail('test#example.com');
$user1Mail->setUser($user1);
// Add attributes
$user1->setEmail($user1Mail);
// ...
$manager->persist($user1Mail);
$manager->persist($user1);
$manager->flush();
Or you can set the email mapping in your User class to cascade persist.
It means that if a new not persisted object is added to that object, then the new object will be saved as well.
I don't know the exact structure of the entites, but it would look like
/**
* #ORM\OneToOne(targetEntity="user", cascade={"persist"})
* #ORM\JoinColumn(name="user_email_id", referencedColumnName="id")
*/
private $userEmail
So if you set a new e-mail to the user it will be auto-persisted if you persist the User entity.
I would prefer the second method if it works. I hope it will help.
Doctrine reference: Transitive persistence / Cascade Operations

Collection Prototype and Doctrine Persistance ManyToOne Relation

Context : I am building my little TodoList bundle (which is a good exercice to go deep progressively with Symfony2), the difficulty comes with recursivity : each Task can has children and parent, so I used Gedmo Tree.
I have a collection of tasks each having a sub collection of children, children collection has prototype enabled so I can display a new sub task form when clicking "add sub task".
I wanted the default name of the subtask to be "New Sub Task" instead of "New Task" set in Task constructor, so I figured out how to pass a custom instance for the prototype and took some care for preventing infinite loop.
So I am almost done and my new task is added with the name I set when saving...
Problem : I am not able to persist the parent task to the new sub task, the new task persist the name well, but not the parentId, I probably forgot somewhere with Doctrine, here is some relevant parts :
// Entity Task
/**
* #Gedmo\Tree(type="nested")
* #ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
* #ORM\HasLifecycleCallbacks
* #ORM\Table(name="task")
*/
class Task {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #Gedmo\Timestampable(on="create")
* #ORM\Column(type="datetime")
*/
protected $created;
/**
* #ORM\Column(type="string", length=255)
* #Assert\NotBlank(message="Name must be not empty")
*/
protected $name = 'New Task';
//....
/**
* #Gedmo\TreeLeft
* #ORM\Column(name="lft", type="integer")
*/
private $lft;
/**
* #Gedmo\TreeLevel
* #ORM\Column(name="lvl", type="integer")
*/
private $lvl;
/**
* #Gedmo\TreeRight
* #ORM\Column(name="rgt", type="integer")
*/
private $rgt;
/**
* #Gedmo\TreeRoot
* #ORM\Column(name="root", type="integer", nullable=true)
*/
private $root;
/**
* #Gedmo\TreeParent
* #ORM\ManyToOne(targetEntity="Task", inversedBy="children")
* #ORM\JoinColumn(name="parentId", referencedColumnName="id", onDelete="SET NULL")
*/
protected $parent = null;//
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $parentId = null;
/**
* #Assert\Valid()
* #ORM\OneToMany(targetEntity="Task", mappedBy="parent", cascade={"persist", "remove"})
* #ORM\OrderBy({"status" = "ASC", "created" = "DESC"})
*/
private $children;
public function __construct(){
$this->children = new ArrayCollection();
}
/**
* Set parentId
*
* #param integer $parentId
* #return Task
*/
public function setParentId($parentId){
$this->parentId = $parentId;
return $this;
}
/**
* Get parentId
*
* #return integer
*/
public function getParentId(){
return $this->parentId;
}
/**
* Set parent
*
* #param \Dmidz\TodoBundle\Entity\Task $parent
* #return Task
*/
public function setParent(\Dmidz\TodoBundle\Entity\Task $parent = null){
$this->parent = $parent;
return $this;
}
/**
* Get parent
*
* #return \Dmidz\TodoBundle\Entity\Task
*/
public function getParent(){
return $this->parent;
}
/**
* Add children
*
* #param \Dmidz\TodoBundle\Entity\Task $child
* #return Task
*/
public function addChild(\Dmidz\TodoBundle\Entity\Task $child){
$this->children[] = $child;
return $this;
}
/**
* Remove child
*
* #param \Dmidz\TodoBundle\Entity\Task $child
*/
public function removeChild(\Dmidz\TodoBundle\Entity\Task $child){
$this->children->removeElement($child);
}
}
// TaskType
class TaskType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options){
$builder
->add('name', null, ['label' => false])
->add('notes', null, ['label' => 'Notes'])
->add('status', 'hidden')
->add('parentId', 'hidden')
;
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($builder){
$record = $event->getData();
$form = $event->getForm();
if(!$record || $record->getId() === null){// if prototype
$form->add('minutesEstimated', null, ['label' => 'Durée', 'attr'=>['title'=>'Durée estimée en minutes']]);
}elseif($record && ($children = $record->getChildren())) {
// this is where I am able to customize the prototype default values
$protoTask = new Task();
$protoTask->setName('New Sub Task');
// here I am loosely trying to set the parentId I want
// so the prototype form input has the right value
// BUT it goes aways when INSERT in mysql, the value is NULL
$protoTask->setParentId($record->getId());
$form->add('sub', 'collection', [// warn don't name the field 'children' or it will conflict
'property_path' => 'children',
'type' => new TaskType(),
'allow_add' => true,
'by_reference' => false,
// this option comes from a form type extension
// allowing customizing prototype default values
// extension code : https://gist.github.com/jumika/e2f0a5b3d4faf277307a
'prototype_data' => $protoTask
]);
}
});
}
public function setDefaultOptions(OptionsResolverInterface $resolver){
$resolver->setDefaults([
'data_class' => 'Dmidz\TodoBundle\Entity\Task',
'label' => false,
]);
}
public function getParent(){ return 'form';}
}
// my controller
/**
* #Route("/")
* #Template("DmidzTodoBundle:Task:index.html.twig")
*/
public function indexAction(Request $request){
$this->request = $request;
$repo = $this->doctrine->getRepository('DmidzTodoBundle:Task');
$em = $this->doctrine->getManager();
//__ list of root tasks (parent null)
$query = $repo->createQueryBuilder('p')
->select(['p','FIELD(p.status, :progress, :wait, :done) AS HIDDEN field'])
->addOrderBy('field','ASC')
->addOrderBy('p.id','DESC')
->andWhere('p.parent IS NULL')
->setParameters([
'progress' => Task::STATUS_PROGRESS,
'wait' => Task::STATUS_WAIT,
'done' => Task::STATUS_DONE
])
->setMaxResults(20)
->getQuery();
$tasks = $query->getResult();
//__ form building : collection of tasks
$formList = $this->formFactory->createNamed('list_task', 'form', [
'records' => $tasks
])
->add('records', 'collection', [
'type'=>new TaskType(),
'label'=>false,
'required'=>false,
'by_reference' => false,
])
;
//__ form submission
if ($request->isMethod('POST')) {
$formList->handleRequest($request);
if($formList->isValid()){
// persist tasks
// I thought persisting root tasks will persist their children relation
foreach($tasks as $task){
$em->persist($task);
}
$em->flush();
return new RedirectResponse($this->router->generate('dmidz_todo_task_index'));
}
}
return [
'formList' => $formList->createView(),
];
}
As mentionned in the comments in TaskType, the form prototype of the new sub task has the right value for parentId which is posted, BUT the value is gone and NULL on INSERT in db (looking at the doctrine log).
So do you think it is the right way of doing, and then what thing I forgot for persisting correctly the parent task of the new sub task ?
On your child setting you should set the parent when adding, like so..
/**
* Add children
*
* #param \Dmidz\TodoBundle\Entity\Task $children
* #return Task
*/
public function addChild(\Dmidz\TodoBundle\Entity\Task $children){
$this->children->add($children);
$children->setParent($this);
return $this;
}
/**
* Remove children
*
* #param \Dmidz\TodoBundle\Entity\Task $children
*/
public function removeChild(\Dmidz\TodoBundle\Entity\Task $children){
$this->children->removeElement($children);
$children->setParent(null);
}
When your prototype adds and deletes a row it calls addChild and removeChild but it doesn't call the setParent in the associated child.
This way any child that is added or removed/deleted get automatically set in the process.
Also you could change the $children to $child as it makes grammatical sense and it's really bugging me because I am a child(ren).
It seems weird to me that you try using the parentId field as a simple column, whereas it is a relation column. Theoretically, you should not:
$task->getParentId(); //fetching a DB column's value
but instead:
$task->getParent()->getId(); //walking through relations to find an object's attribute
However, if you really need this feature to avoid loading the full parent object and just get its ID, your setParentId method should be transparent (although, as mentionned, I'm not sure using the same DB field is valid):
public function setParent(Task $t = null) {
$this->parent = $t;
$this->parentId = null === $t ? null : $t->getId();
return $this;
}
Back to your issue: in the TaskType class, you should call:
$protoTask->setParent($record);
instead of:
$protoTask->setParentId($record->getId());
The reason:
you tell Doctrine parentId is a relation field (in the $parent attribute declaration), therefore Doctrine expects an object of the proper type
you also tell Doctrine to map this relation field directly to an attribute (the $parentId attribute declaration), I'm neither convinced this is valid, nor convinced this is good practice, but I guess you did some research before going for this structure
you set $parentId, but $parent has not been set (i.e. null), so Doctrine must erase the $parentId value with the $parent value: your code is proof that Doctrine handles attributes first, then computes relations ;)
Keep in mind Doctrine is an Object Relational Mapper, not a simple query helper: mapper is what it does (mapping persistence layer with your code), relational is how it does it (one-to-many and the like), object is what it does it on (therefore not directly using IDs).
Hope this helps!

Symfony 2 - ManyToOne Bidirectional relationship behaviour

I had a big time trying to figure out how to setup a ManyToOne -> OneToMany relationship with Doctrine 2 and it still not working...
Here is the application behaviour:
A site has Pages
A User can write Comment on a Page
Here are my Entities (simplified):
Comment Entity:
**
* #ORM\Entity
* #ORM\Table(name="comment")
*/
class Comment {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* Many Comments have One User
*
* #ORM\ManyToOne(targetEntity="\Acme\UserBundle\Entity\User", inversedBy="comments")
*/
protected $user;
/**
* Many Comments have One Page
*
* #ORM\ManyToOne(targetEntity="\Acme\PageBundle\Entity\Page", inversedBy="comments")
*/
protected $page;
...
/**
* Set user
*
* #param \Acme\UserBundle\Entity\User $user
* #return Comment
*/
public function setUser(\Acme\UserBundle\Entity\User $user)
{
$this->user = $user;
return $this;
}
/**
* Set page
*
* #param \Acme\PageBundle\Entity\Page $page
* #return Comment
*/
public function setPage(\Acme\PageBundle\Entity\Page $page)
{
$this->page = $page;
return $this;
}
User Entity:
/**
* #ORM\Entity
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* The User create the Comment so he's supposed to be the owner of this relationship
* However, Doctrine doc says: "The many side of OneToMany/ManyToOne bidirectional relationships must be the owning
* side", so Comment is the owner
*
* One User can write Many Comments
*
* #ORM\OneToMany(targetEntity="Acme\CommentBundle\Entity\Comment", mappedBy="user")
*/
protected $comments;
...
/**
* Get Comments
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getComments() {
return $this->comments ?: $this->comments = new ArrayCollection();
}
Page Entity:
/**
* #ORM\Entity
* #ORM\Table(name="page")
*/
class Page
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* One Page can have Many Comments
* Owner is Comment
*
* #ORM\OneToMany(targetEntity="\Acme\CommentBundle\Entity\Comment", mappedBy="page")
*/
protected $comments;
...
/**
* #return \Doctrine\Common\Collections\Collection
*/
public function getComments(){
return $this->comments ?: $this->comments = new ArrayCollection();
}
I want a bidirectional relationship to be able to get the collection of Comments from the Page or from the User (using getComments()).
My problem is that when I try to save a new Comment, I get an error saying that doctrine is not able to create a Page entity. I guess this is happening because it's not finding the Page (but it should) so it's trying to create a new Page entity to later link it to the Comment entity that I'm trying to create.
Here is the method from my controller to create a Comment:
public function createAction()
{
$user = $this->getUser();
$page = $this->getPage();
$comment = new EntityComment();
$form = $this->createForm(new CommentType(), $comment);
if ($this->getRequest()->getMethod() === 'POST') {
$form->bind($this->getRequest());
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$comment->setPage($page);
$comment->setUser($user);
$em->persist($comment);
$em->flush();
return $this->redirect($this->generateUrl('acme_comment_listing'));
}
}
return $this->render('AcmeCommentBundle:Default:create.html.twig', array(
'form' => $form->createView()
));
}
I don't understand why this is happening. I've checked my Page object in this controller (returned by $this->getPage() - which return the object stored in session) and it's a valid Page entity that exists (I've checked in the DB too).
I don't know what to do now and I can't find anyone having the same problem :(
This is the exact error message I have:
A new entity was found through the relationship
'Acme\CommentBundle\Entity\Comment#page' that was not configured to
cascade persist operations for entity:
Acme\PageBundle\Entity\Page#000000005d8a1f2000000000753399d4. To solve
this issue: Either explicitly call EntityManager#persist() on this
unknown entity or configure cascade persist this association in the
mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot
find out which entity causes the problem implement
'Acme\PageBundle\Entity\Page#__toString()' to get a clue.
But I don't want to add cascade={"persist"} because I don't want to create the page on cascade, but just link the existing one.
UPDATE1:
If I fetch the page before to set it, it's working. But I still don't know why I should.
public function createAction()
{
$user = $this->getUser();
$page = $this->getPage();
// Fetch the page from the repository
$page = $this->getDoctrine()->getRepository('AcmePageBundle:page')->findOneBy(array(
'id' => $page->getId()
));
$comment = new EntityComment();
// Set the relation ManyToOne
$comment->setPage($page);
$comment->setUser($user);
$form = $this->createForm(new CommentType(), $comment);
if ($this->getRequest()->getMethod() === 'POST') {
$form->bind($this->getRequest());
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($comment);
$em->flush();
return $this->redirect($this->generateUrl('acme_comment_listing'));
}
}
return $this->render('AcmeCommentBundle:Default:create.html.twig', array(
'form' => $form->createView()
));
}
UPDATE2:
I've ended up storing the page_id in the session (instead of the full object) which I think is a better idea considering the fact that I won't have a use session to store but just the id. I'm also expecting Doctrine to cache the query when retrieving the Page Entity.
But can someone explain why I could not use the Page entity from the session? This is how I was setting the session:
$pages = $site->getPages(); // return doctrine collection
if (!$pages->isEmpty()) {
// Set the first page of the collection in session
$session = $request->getSession();
$session->set('page', $pages->first());
}
Actually, your Page object is not known by the entity manager, the object come from the session. (The correct term is "detached" from the entity manager.)
That's why it tries to create a new one.
When you get an object from different source, you have to use merge function. (from the session, from an unserialize function, etc...)
Instead of
// Fetch the page from the repository
$page = $this->getDoctrine()->getRepository('AcmePageBundle:page')->findOneBy(array(
'id' => $page->getId()
));
You can simply use :
$page = $em->merge($page);
It will help you if you want to work with object in your session.
More information on merging entities here

Symfony2/Doctrine2.1: Persisting OneToOne relations with simple derived identity

I am trying to persist an user entity with a profile entity from a single form submit. Following the instructions at the Doctrine2 documentation and after adding additional attributes this seemed to be sufficient to achieve the goal.
Entities
Setting up the entites in accordance is pretty straight forward and resulted in this (I left out the generated getter/setter):
// ...
/**
* #ORM\Entity
*/
class User
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(type="string", length=64)
*/
private $data;
/**
* #ORM\OneToOne(targetEntity="Profile", mappedBy="user", cascade={"persist", "remove"})
*/
private $Profile;
// ...
}
// ...
/**
* #ORM\Entity
*/
class Profile
{
/**
* #ORM\Id
* #ORM\OneToOne(targetEntity="User")
*/
private $user;
/**
* #ORM\Column(type="string", length=64)
*/
private $data;
// ...
}
Forms
Now modifiying the forms is not too difficult as well:
// ...
class ProfileType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('data')
;
}
public function getName()
{
return 'profile';
}
public function getDefaultOptions(array $options)
{
return array('data_class' => 'Acme\TestBundle\Entity\Profile');
}
}
// ...
class TestUserType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('data')
->add('Profile', new ProfileType())
;
}
public function getName()
{
return 'user';
}
}
Controller
class UserController extends Controller
{
// ...
public function newAction()
{
$entity = new User();
$form = $this->createForm(new UserType(), $entity);
return array(
'entity' => $entity,
'form' => $form->createView()
);
}
public function createAction()
{
$entity = new User();
$request = $this->getRequest();
$form = $this->createForm(new UserType(), $entity);
$form->bindRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getEntityManager();
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('user_show',
array('id' => $entity->getId())));
}
return array(
'entity' => $entity,
'form' => $form->createView()
);
}
// ...
}
But now comes the part where testing takes place. I start to create a new user-object, the embedded form shows up as expected, but hitting submit returns this:
Exception
Entity of type Acme\TestBundle\Entity\Profile is missing an
assigned ID. The identifier generation strategy for this entity
requires the ID field to be populated before EntityManager#persist()
is called. If you want automatically generated identifiers instead
you need to adjust the metadata mapping accordingly.
A possible solution I am already aware of is to add an additional column for a stand-alone primary key on the Profile entity.
However I wonder if there is a way to keep the mapping roughly the same but deal with persisting the embedded form instead?
After debating for quite a while with a couple of people via IRC I modified the mapping and came up with this:
Entities
// ...
/**
* #ORM\Entity
*/
class User
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=64)
*/
private $data;
/**
* #ORM\OneToOne(targetEntity="Profile", cascade={"persist", "remove"})
*/
private $Profile;
// ...
}
// ...
/**
* #ORM\Entity
*/
class Profile
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=64)
*/
private $data;
// ...
}
So what does this change? First of all I removed the mappedBy and inversedBy options for the relation. In addition the OneToOne annotation on the Profile-entity was not needed.
The relation between User and Profile can be bi-directional however a uni-directional relation with User being the owning side is sufficient to have control over the data. Due to the cascade option you can be sure there are no left-over Profiles without Users and Users can maintain a Profile but do not have to.
If you want to use a bi-directional relation I recommand taking a look at Github: Doctrine2 - Tests - DDC117 and especially pay attention to Article and ArticleDetails' OneToOne relation. However you need to be aware that saving this bi-directional relation is a bit more tricky as can be seen from the test file (link provided in comment): you need to persist the Article first and setup the constructor in ArticleDetails::__construct accordingly to cover the bi-directional nature of the relationship.
The problem from what I can see is that you're only creating / saving a User object.
As the User / Profile is a One to One relation (with User being the owning side) would it be safe to assume that a User will always have a Profile relation, and so could be initialised in the Users construction
class User
{
public function __construct()
{
$this->profile = new Profile();
}
}
After all you've set User up to cascade persistence of the related Profile object. This will then have your entity manager create both Entities and establish the relation.

Resources