Symfony 2 - ManyToOne Bidirectional relationship behaviour - symfony

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

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.

symfony2 twig template load checkboxes from many to many entity relationship

New Symfony2 User here. I have 2 entities that are related, one to many that is unidirectional. I'm doing it as ManyToMany as the doctrine documentation suggests, Article(one) and Tags(many). I'd like to have checkboxes show up that show the tag names on the article.new page and the article.edit page. On form submission the id of the tag entity is stored in the article_tags side table that the entity generator created for me.
Posting only relevant code.
Tag Entity AppBundle/Entity/Tag.php
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=20)
*/
public $name;
Article Entity AppBundle/Entity/Article.php
/**
* #ORM\ManyToMany(targetEntity="Tag")
* #ORM\JoinTable(
* name="article_tags",
* joinColumns={#ORM\JoinColumn(name="article_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="tag_id", referencedColumnName="id", unique=true)}
* )
*/
protected $tags;
/**
* Add tag
*
* #param \AppBundle\Entity\Tag $tag
*
* #return Article
*/
public function addTag(\AppBundle\Entity\Tag $tag)
{
$this->tags[] = $tag;
return $this;
}
/**
* Remove tag
*
* #param \AppBundle\Entity\Tag $tag
*/
public function removeTag(\AppBundle\Entity\Tag $tag)
{
$this->tags->removeElement($tag);
}
/**
* Get tags
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getTags()
{
return $this->tags;
}
Article Form Type AppBundle/Form/ArticleType
$builder->add('title')
->add('body')
->add('author')
->add('tags', 'entity', array(
'class' => 'AppBundle\Entity\Tag',
'property' => 'name',
'expanded' => 'true', ));
ArticleController AppBundle/Controller/ArticleController.php
* #Template()
*/
public function newAction()
{
$entity = new Article();
$tags = new Tag();
$entity->addTag($tags);
$form = $this->createCreateForm($entity);
return array('entity' => $entity,'form' => $form->createView(), );
}
As of now the error I receive is...
Entities passed to the choice field must be managed. Maybe persist
them in the entity manager?
I'm not entirely sure I'm on the right track. I just want to attach tags to articles!
Thanks
In the controller, you create a blank Tag and add it to the new Article before creating the form. That doesn't make sense to me, and I suspect that's where the error is coming from.
If there are any tags in the database, Symfony will automatically get them and display them with a checkbox in the form. If the user checks a checkbox, this Tag will be added to the Article.
Just delete these two lines and you should be fine:
$tags = new Tag();
$entity->addTag($tags);

Update two entities sharing the primary key with a Symfony2 form

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

How to use the registration procedure of FOSUserBundle from a third controller

I have to persist an entity (let's call it Entity for simplicity) in the database that has to be referenced to a User handled with FOSUserBundle. To make this reference I have a column entity_table.userId.
When the new Entity is created, I have to:
Create the User through the registration procedure of FosUserBundle;
Get the ID of the new created User: [meta code] $userId = $get->newCreatedUserId();
Set this id in Entity: $entity->setUserId($userId);
Persist the Entity to the database.
How can I integrate the registration procedure of FosUserBundle into the controller that persists my Entity?
MORE DETAILS
In the first time I tried to simply copy the code from the method registerAction() of the RegistrationController of FOSUserBundle: a quick and dirty approach that, anyway didn't work as i get an error as the User class i passed was wrong (I passed my custom User entity I use to overwrite the bundle).
This kind of approach has also other drawbacks:
I cannot control the registration procedure (send or decide to not send confirmation e-mails, for example);
I cannot use the builtin checks on passed data;
I cannot be sure that on FOSUserBundles updates my custom method continue to work
Others I cannot imagine at the moment...
So, I'd like to create the user in the cleanest way possible: how can i do this? Which should be a good approach?
A controller forwarding?
Anyway, an "hardcoded" custom method that emulates the registerAction() method?
A custom registration form?
I have read a lot of discussions here at StackOverflow and on Internet, I read the documentation of FOSUserBundle and of Symfony too, but I cannot decide for the good approach, also because I'm not sure I have understood all the pros and cons of each method.
If someone can put me on the right way... :)
SOMETHING MORE ABOUT MY REGISTRATION FLOW
I have a getStarted procedure handled by the controller GetStarteController.
In it I have two methods:
indexAction(), that displays a registration form with only the field "email";
endAction(), that receive the form and creates a Company using the passed e-mail (it gets the domain part only of the email).
HERE IS A WORKING MESSY CODE (inside it for Companies and Stores are called some methods that exists in the source code but are not in the posted classes below, as setBrand() or setUrl(), for example).
// AppBundle/Controller/getStartedController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use MyVendor\UserBundle\Entity\User;
use AppBundle\Entity\Companies;
use AppBundle\Entity\Stores;
class GetStartedController extends Controller
{
/**
* #Route("getstarted")
* #Template()
*/
public function indexAction()
{
$data = array();
$form = $this->createFormBuilder($data, array(
'action' => $this->generateUrl('getStartedEnd'),
))
->add('email', 'email')
->add('submit', 'submit')
->getForm();
return array(
'form' => $form->createView(),
);
}
/**
* #Route("getstarted/end", name="getStartedEnd")
* #Template()
*/
public function endAction(Request $request)
{
$form = $this->createFormBuilder()
->add('email', 'email')
->add('submit', 'submit')
->getForm();
$form->handleRequest($request);
if ($form->isValid()) {
$data = $form->getData();
} else {
/** #todo here we have to raise some sort of exception or error */
echo 'no data submitted (See the todo in the code)';exit;
}
// Pass the email to the template
$return['email'] = $data['email'];
// Get the domain part of the email and pass it to the template
$domain = explode('#', $data['email']);
$return['domain'] = $domain[1];
// 1) Create the new user
$user = new User();
// Get the token generator
$tokenGenerator = $this->container->get('fos_user.util.token_generator');
$user->setEmail($return['email']);
$userRandomUsername = substr($tokenGenerator->generateToken(), 0, 12);
$user->setUsername('random-' . $userRandomUsername);
$plainPassword = substr($tokenGenerator->generateToken(), 0, 12);
$encoder = $this->container->get('security.password_encoder');
$encoded = $encoder->encodePassword($user, $plainPassword);
// Set the password for the user
$user->setPassword($encoded);
/** #var $userManager \FOS\UserBundle\Model\UserManagerInterface */
$userManager = $this->get('fos_user.user_manager');
// Perstist the user in the database
$userManager->updateUser($user);
$userId = $user->getId();
// 2) Create the Company object
$company = new Companies();
$company->setBrand($return['domain'])
->setAdded(new \DateTime())
->setOwnerId($userId);
// 3) Create the Store object
$store = new Stores();
$store->setEmail($return['email'])
->setUrl($return['domain'])
->setAdded(new \DateTime());
// Get the Entity Manager
$em = $this->getDoctrine()->getManager();
// Persist Company and get its ID
$em->persist($company);
$em->flush();
$return['companyId'] = $company->getId();
// Set the property branchOf of the Store object
$store->setBranchOf($return['companyId']);
// Persist the Store object
$em->persist($store);
$em->flush();
$return['storeId'] = $store->getId();
return $return;
}
}
Here the User Entity that ovewrites the one provided by FOSUserBundle
// MyVendor/UserBundle/Entity/User.php
namespace MyVendor\UserBundle\Entity;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="prefix_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function __construct()
{
parent::__construct();
// your own logic
}
}
Some essential code of Companies.php
// AppBundle/Entity/Companies.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Companies
*
* #ORM\Table(name="companies")
* #ORM\Entity
*/
class Companies
{
/**
* #var integer
*
* #ORM\Column(name="ownerId", type="integer", nullable=false)
*/
private $ownerid;
/**
* Set ownerid
*
* #param integer $ownerid
* #return Companies
*/
public function setOwnerid($ownerid)
{
$this->ownerid = $ownerid;
return $this;
}
/**
* Get ownerid
*
* #return integer
*/
public function getOwnerid()
{
return $this->ownerid;
}
}
Some essential code of Stores.php
// AppBundle/Entity/Stores.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Stores
*
* #ORM\Table(name="stores", uniqueConstraints={#ORM\UniqueConstraint(name="branchOf", columns={"branchOf"})})
* #ORM\Entity
*/
class Stores
{
/**
* #var integer
*
* #ORM\Column(name="branchOf", type="integer", nullable=false)
*/
private $branchof;
/**
* Set branchof
*
* #param integer $branchof
* #return Stores
*/
public function setBranchof($branchof)
{
$this->branchof = $branchof;
return $this;
}
/**
* Get branchof
*
* #return integer
*/
public function getBranchof()
{
return $this->branchof;
}
}
You can use a custom registration form but the best way is clearly to listen to registration event dispatched by FOSUser.
Here is an example :
class RegistrationListener implements EventSubscriberInterface
{
/**
* L'entity manager
*
* #var EntityManager
*/
private $em;
/**
* Constructeur de l'EventListener
*
* #param \Doctrine\ORM\EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager)
{
$this->em = $entityManager;
}
/**
* {#inheritDoc}
*/
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_INITIALIZE => 'onRegistrationInit',
);
}
/**
* Triggered when FOSUserEvents::REGISTRATION_INITIALIZE is caught.
*
* #param \FOS\UserBundle\Event\UserEvent $userEvent
*/
public function onRegistrationInit(UserEvent $userEvent)
{
$user = $userEvent->getUser();
// Define your own logic there
}
}
Don't forget to make this listener a service:
#services.yml
services:
oe_user.registration:
class: OrienteExpress\UserBundle\EventListener\RegistrationListener
# arguments are optional but you still can need them
# so I let the EM as example which is an often used parameter
arguments:
entityManager: "#doctrine.orm.entity_manager"
tags:
- { name: kernel.event_subscriber }
You'll find the complete list of event dispatched by FOSUser here
Moreover, Symfony entities are a model of objects. That said, you need to understand that you don't work with ids within your model, but object.
You should not use thing such as $var->setUserId() within entites. Doctrine is there to manage your relations, so be carefull about this. You might face unexpected problem by not using Symfony & Doctrine the way it has been designed for.
EDIT:
In your company entity, your relation is beetween a Company and a User objects. That means you dont need a User id in your company but just a instance of User.
I think you might go back to the basics before wanting to do advanced stuff.
Your relation beetween the user and the company should not be designed by an integer attribute but a real doctrine relation.
Ex:
class Company {
/**
* #ORM\ManyToOne(targetEntity="Path\To\User")
* #ORM\JoinColumn(nullable=false)
*/
private $owner;
/**
* #param $user User
*/
public function setUser(User $user)
{
$this->user = $user;
}
}
Then when you'll create a new company. You won't need to know the User's id or even insert it to make the link between them. But if you are not aware yet of this, once again, I think you should go back to the basics of Symfony since this is one of the most (maybe the most) important feature to master.

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