In a REST API that I'm developing a Company has a parent attribute which is also of the Company Class.
That way I can create a three of Companies. A Company has one parent Company (Company Class) and can have multiple children Companies (Collection)
/**
* #ORM\ManyToOne(targetEntity="Company", inversedBy="child")
* #Expose()
*/
protected $parent;
/**
* #ORM\OneToMany(targetEntity="Company", mappedBy="parent")
*/
protected $child;
public function __construct()
{
...
$this->child = new \Doctrine\Common\Collections\ArrayCollection();
}
How would I go about and make/remove that relationship of parents and children companies?
I've read about the LINK verb but I'm afraid it's not supported by all webservers.
Should I set the relationship with PUT?
How would I then remove the relationship to the parent (set it to NULL).
My CompanyController looks like this:
/**
* Edit Action
*
* #Rest\View()
*/
public function editAction(Company $company)
{
return $this->processForm($company);
}
/**
* Remove Action
*
* #Rest\View(statusCode=204)
*/
public function removeAction(Company $company)
{
$em = $this->getDoctrine()->getManager();
$em->remove($company);
$em->flush();
}
/**
* ProcessForm Action
*/
private function processForm(Company $company)
{
$statusCode = $this->getRequest()->getMethod() == 'POST' ? Codes::HTTP_CREATED : Codes::HTTP_SEE_OTHER;
$form = $this->createForm(new CompanyType(), $company);
$form->bind($this->getRequest());
if($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$em->persist($company);
$em->flush();
return $this->redirectView(
$this->generateUrl(
'company_get',
array('id' => $company->getId())
),
$statusCode
);
}
return View::create($form, 400);
}
Any suggestions?
Related
I've created CollectionType form, which can add unlimited amount of TicketTime entities into one Ticket entity. It is connected via database by a OneToMany relation.
What I am trying to do is to add one Ticket and multiple TicketTimes to the database at one time. For example, one Ticket and 3 TicketTimes.
I would like to know how to make this method, so I can take one Ticket & multiple TicketTimes and flush them to the database.
My Controller action:
public function createTicket(Request $request)
{
if ($this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
if ($this->getUser()->getrole() == 3) {
$em = $this->getDoctrine()->getManager();
$ticket = new Ticket();
$timeArray = new ArrayCollection();
$form = $this->createForm(TicketType::class, $ticket);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
foreach ($timeArray as $time) {
$time->setTicket($ticket);
$em->persist($time);
}
$ticket->setTaken(0);
$em->persist($ticket);
$em->flush();
return $this->redirectToRoute('main');
}
return $this->render(
'create_ticket.html.twig',
array('form' => $form->createView())
);
}
}
return $this->redirectToRoute('main');
}
Ticket entity:
/**
* #Assert\NotBlank()
* #ORM\OneToMany(targetEntity="TicketTime", mappedBy="ticket", fetch="EXTRA_LAZY", orphanRemoval=true, cascade={"persist"})
*/
private $ticketTimes;
public function setTicketTimes($time): void
{
$this->ticketTimes = $time;
}
/**
* #return ArrayCollection|TicketTime[]
*/
public function getTicketTimes()
{
return $this->ticketTimes;
}
TicketTime entity:
/**
* #ORM\ManyToOne(targetEntity="Ticket", inversedBy="time")
* #ORM\JoinColumn(nullable=false)
*/
private $ticket;
public function getTicket()
{
return $this->ticket;
}
public function setTicket($ticket): void
{
$this->ticket = $ticket;
}
I managed to fix it by myself :)
What was wrong:
I needed to create add method in Ticket entity, so I will be able to add as many TicketTime's as I want, here's the code which I added into Ticket entity:
public function addTicketTime(TicketTime $time)
{
$this->ticketTimes[] = $time;
$time->setTicket($this);
return $this;
}
I've also added some lines into Controller
........
if ($form->isSubmitted() && $form->isValid()) {
$ticketTimes = $form->get('ticketTimes')->getData();
foreach ($ticketTimes as $time) {
$ticket->addTicketTime($time);
}
.......
i have two entities Survey.php and Choice.php I create a form to add new survey and multi choices, I used a many-to-one relation between the two entities, the problem is when I submit for a new survey, the foreign key of choice entity return null
here's my code
Survey.PHP
/**
* Survey
*
* #ORM\Table(name="survey")
* #ORM\Entity(repositoryClass="AppBundle\Repository\SurveyRepository")
*/
class Survey
{
/....
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Choice", mappedBy="survey_id",cascade="persist")
* #ORM\JoinColumn(nullable=false, referencedColumnName="id")
*/
private $choice;
public function __construct()
{
$this->choice = new ArrayCollection();
}
/**
* Add choice
*
* #param \AppBundle\Entity\Choice $choice
*
* #return Survey
*/
public function addChoice(\AppBundle\Entity\Choice $choice)
{
$this->choice[] = $choice;
$choice->setSurvey($this);
return $this;
}
/**
* Remove choice
*
* #param \AppBundle\Entity\Choice $choice
*/
public function removeChoice(\AppBundle\Entity\Choice $choice)
{
$this->choice->removeElement($choice);
}
/**
* Get choice
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getChoice()
{
return $this->choice;
}
}
Choice.php
/**
* Choice
* #ORM\Table(name="choice")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ChoiceRepository")
*/
class Choice
{
/**
* #var int
* #ORM\ManyToOne(targetEntity="Survey",inversedBy="choice")
*/
private $survey;
/**
* Set survey
*
* #param \AppBundle\Entity\Survey $survey
*
* #return Choice
*/
public function setSurveyId(\AppBundle\Entity\Survey $survey)
{
$this->survey = $survey;
return $this;
}
/**
* Get surveyId
*
* #return \AppBundle\Entity\Survey
*/
public function getSurveyId()
{
return $this->survey_id;
}
}
SurveyController.php
<?php
namespace AppBundle\Controller;
use AppBundle\Entity\Survey;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
/**
* Survey controller.
*
*/
class SurveyController extends Controller
{
/**
* Creates a new survey entity.
* #param Request $request
* #return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function newAction(Request $request)
{
$survey = new Survey();
//
$form = $this->createForm('AppBundle\Form\SurveyType', $survey);
$form->handleRequest($request);
$survey->setCreateDate(new \DateTime('NOW'));
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($survey);
$em->flush();
return $this->redirectToRoute('survey_show', ['id' => $survey->getId()]);
}
return $this->render('survey/new.html.twig', [
'survey' => $survey,
'form' => $form->createView(),
]);
}
any suggestion, btw I think the problem is in the getters and setters )
Link the choice to the survey:
// Survey
public function addChoice(\AppBundle\Entity\Choice $choice)
{
$this->choice[] = $choice;
$choice->setSurvey($this);
return $this;
}
And change the survey_id stuff to survey. Dealing with objects not ids. And of course Survey::choice should be Survey::choices. The name changes might seem minor but will make your easier to maintain.
I fixed the problem by adding a for each loop inside the SurveyController.php
and it works just fine
SurveyController.php
if ($form->isSubmitted() && $form->isValid())
{
foreach ($survey->getChoices() as $choice){
$choice->setSurvey($survey);
}
$em = $this->getDoctrine()->getManager();
$em->persist($survey);
$em->flush();
not "THE best solution" but it gets the job done
It worked with me, but I had to remove the explicit foreign key mapping with the "inversedBy" setting from the class definition. I use a composite foreign key (using two columns), which maybe makes things harder though...
I'm trying to insert into my Reviews table, within my controller I have:
public function indexAction(Request $request, $id)
{
if($id != null)
{
// Create a new Review entity
$review = new Review();
$form = $this->createForm(ReviewType::class, $review,[
'action' => $request->getUri()
]);
$form->handleRequest($request);
if($form->isValid()) {
$manager = $this->getDoctrine()->getManager();
$review->setPosted(new \DateTime());
$review->setBookID($id);
$review->setUserID($this->getUser());
$manager->persist($review);
$manager->flush();
}
return $this->render('ReviewBookBundle:Book:index.html.twig',
['form' => $form->createView());
}
}
However on the line $review->setBookID($id); i get this error:
Expected value of type "Review\BookBundle\Entity\Book" for association field "Review\ReviewsBundle\Entity\Review#$bookID", got "string" instead.
How do i overcome this issues? Since I've tried creating a Book entity and setting the bookID and then passing the Book entity into the $review-setBookID, like so:
$review->setBookID($book);
but that still doesn't work?
Can you try this:
Add this in your Review Entity:
/**
* #var Book
* #ORM\ManyToOne(targetEntity="YourBundle\Entity\Book", inversedBy="review", fetch="LAZY")
* #ORM\JoinColumn(name="book_id", referencedColumnName="id")
*/
protected $book;
/**
* #return Book
*/
public function getBook()
{
return $this->book;
}
/**
* #param $book
*/
public function setBook($book)
{
$this->book = $book;
}
And use:
$review->setBook($book);
$book must be instance of Book Entity
Edit
Book Entity:
public function __construct() {
$this->reviews = new ArrayCollection();
}
/**
* #var Review
* #ORM\OneToMany(targetEntity="YourBundle\Entity\Review", fetch="LAZY")
* #ORM\JoinColumn(name="review_id", mappedBy="book", referencedColumnName="id")
*/
protected $reviews;
/**
* #return Review
*/
public function getReviews()
{
return $this->reviews;
}
/**
* #param Review $review
*/
public function addReview(Review $review)
{
$this->reviews->add($review);
}
/**
* #param Review $review
*/
public function removeReview(Review $review)
{
$this->reviews->removeElement($review);
}
You should use $review->addBook() and pass book object not ID.
A simple problem that has many answers on SO... Yet none of them work on my project... So I get this error:
ContextErrorException: Catchable Fatal Error: Argument 1 passed to Doctrine\Common\Collections\ArrayCollection::__construct() must be of the type array, object given, called in C:\wamp\www\Dig\front\vendor\doctrine\orm\lib\Doctrine\ORM\UnitOfWork.php on line 528 and defined in C:\wamp\www\Digidis\front\vendor\doctrine\collections\lib\Doctrine\Common\Collections\ArrayCollection.php line 48
This happens everytime I create a new Email and try to save it in the database. The email is in a relationship with skin..
This is how I try to save it:
/**
* #Route("/{skin_id}/new", name="cms_email_new")
* #Method({"GET"})
* #Template()
*/
public function newAction($skin_id) {
$skin = $this->getRepository('ProjectSkinBundle:Skin')->find($skin_id);
$item = new Email();
$form = $this->createForm(new EmailType($this->container->getParameter("langs")), $item);
return array('form' => $form->createView(), 'item' => $item, 'skin' => $skin_id);
}
/**
* #Route("/{skin_id}/save", name="cms_email_save")
* #Template("ProjectUserBundle:EmailAdmin:new.html.twig")
* #Method({"POST"})
*/
public function saveAction(Request $request, $skin_id) {
$skin = $this->getRepository('ProjectSkinBundle:Skin')->find($skin_id);
$item = new Email();
$type = new EmailType($this->container->getParameter("langs"));
$form = $this->createForm($type, $item);
$form->handleRequest($request);
$em = $this->getEntityManager();
if ($form->isValid()) {
$this->upload($form, $item);
$skin->setEmailId($item);
$item->setSkin($skin); /// the error is here
$em->persist($skin);
$em->persist($item);
$em->flush();
return $this->redirect($this->generateUrl('cms_skin_email_edit', array('skin_id' => $skin_id)));
}
return array('form' => $form->createView(), 'item' => $item);
}
So by doing some testing I found out that this line is causing the problem:
$item->setSkin($skin);
Without this line everything works like a charm. However I need this line to work.
So this is the Entity with the setSkin method:
/**
*
* #ORM\OneToMany(targetEntity="Project\SkinBundle\Entity\Skin", mappedBy="email_id")
* #ORM\JoinColumn(name="skin", referencedColumnName="id")
*/
protected $skin;
/**
* Set skin
*
* #param \Project\SkinBundle\Entity\Skin $skin
* #return Email
*/
public function setSkin(\Project\SkinBundle\Entity\Skin $skin = null)
{
$this->skin = $skin;
return $this;
}
/**
* Get skin
*
* #return \Project\SkinBundle\Entity\Skin
*/
public function getSkin()
{
return $this->skin;
}
So what can I do to make his object become an array?
I have this little line but id doesnt help me :
public function __construct()
{
$this->skin = new ArrayCollection();
}
The form for creating a new email is this:
public function buildForm(FormBuilderInterface $builder, array $option) {
$builder->add('title', 'text', array('label' => 'cms.Title'));
}
public function getDefaultOptions(array $options) {
return array(
'data_class' => 'Project\UserBundle\Entity\Email',
);
}
public function getName()
{
return 'my_email';
}
}
The $skin property is a One to Many relationship in your doctrine mapping. Doctrine is expecting an ArrayCollection object or array.
This is causing your exception:
/**
*
* #ORM\OneToMany(targetEntity="Project\SkinBundle\Entity\Skin", mappedBy="email_id")
* #ORM\JoinColumn(name="skin", referencedColumnName="id")
*/
protected $skin;
If you need a one to many relationship you should pass an array instead of a single object because you can have multiple skins. If you want a one to one relationship (a single skin per entity) you should change you doctrine mapping.
Possible solution 1:
public function __construct()
{
$this->skin = new ArrayCollection();
}
/**
* Set skin
*
* #param \Project\SkinBundle\Entity\Skin $skin
* #return Email
*/
public function setSkin(array $skin)
{
$this->skin = $skin;
return $this;
}
/**
* Get skin
*
* #return \Project\SkinBundle\Entity\Skin[]|ArrayCollection
*/
public function getSkin()
{
return $this->skin;
}
Possible solution 2 (OneToOne, but this could be a ManyToOne, that's up to you):
/**
*
* #ORM\OneToOne(targetEntity="Project\SkinBundle\Entity\Skin", mappedBy="email_id")
* #ORM\JoinColumn(name="skin", referencedColumnName="id")
*/
protected $skin;
You could prevent the error by simply wrapping the object (which you should confirm is an "Email" object) in an array:
$item->setSkin(array($skin));
However something else is going wrong here and the error is coming from when Doctrine compiles a unit-of-work to save to the database.
The skin relationship declartion of the Email entity is incorrect. The Join column declaration should be on the manyToOne side, so Email should be:
Email entity:
/*
* #ORM\OneToMany(targetEntity="Project\SkinBundle\Entity\Skin", mappedBy="email")
*/
protected $skins;
Skin entity:
/*
* #ORM\ManyToOne(targetEntity="Project\SkinBundle\Entity\Email", inversedBy="emails")
* #ORM\JoinColumn(name="email_id", referencedColumnName="id")
*/
protected $email
Running app/console doctrine:generate:entities SkinBundle:Email (or however the entity is referenced) will then generate a methods like addSkin(Skin $skin) which are used to add objects to the relationship.
More info can be found on Doctrine associations.
For a one to many relationship you should have and be using methods addSkin() and removeSkin() in place of setSkin(). Also, as a convention I recommend pluralising collection properties i.e. $skin -> $skins. It makes the code clearer and errors in declaring and using entities become more obvious.
So for your entity that has many $skins I would recommend:
/**
* #var \Doctrine\Common\Collections\Collection
*/
private $skins;
/**
* Constructor
*/
public function __construct()
{
$this->skins = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add skin
*
* #param Skin $skin
* #return Email
*/
public function addSkin(Skin $skin)
{
$this->skins[] = $skin;
return $this;
}
/**
* Remove skin
*
* #param Skin $skin
*/
public function removeSkin(Skin $skin)
{
$this->skins->removeElement($skin);
}
/**
* Get skins
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getSkins()
{
return $this->skins;
}
Then where you have:
$item->setSkin($skin);
You should instead use:
$item->addSkin($skin);
I have this entity:
<?php
namespace Comakai\MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM,
Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
*/
class Stuff {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\Column(type="text")
* #Assert\NotBlank()
*/
private $content;
/**
* #ORM\ManyToMany(targetEntity="Apple", cascade={"persist"})
*/
private $apples;
/**
* #ORM\ManyToMany(targetEntity="Pig")
*/
private $pigs;
public function __construct() {
$this->apples = new \Doctrine\Common\Collections\ArrayCollection();
$this->pigs = new \Doctrine\Common\Collections\ArrayCollection();
}
public function setApples($apples) {
$this->getApples()->clear();
foreach ($apples as $apple) {
$this->addApple($apple);
}
}
public function setPigs($pigs) {
$this->getPigs()->clear();
foreach ($pigs as $pig) {
$this->addPig($pig);
}
}
/**
* Get id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Set content
*
* #param text $content
*/
public function setContent($content) {
$this->content = $content;
}
/**
* Get content
*
* #return text
*/
public function getContent() {
return $this->content;
}
/**
* Add apples
*
* #param Comakai\MyBundle\Entity\Apple $apples
*/
public function addApple(\Comakai\MyBundle\Entity\Apple $apples) {
$this->apples[] = $apples;
}
/**
* Get apples
*
* #return Doctrine\Common\Collections\Collection
*/
public function getApples() {
return $this->apples;
}
/**
* Add pigs
*
* #param Comakai\MyBundle\Entity\Pig $pigs
*/
public function addPig(\Comakai\MyBundle\Entity\Pig $pigs) {
$this->pigs[] = $pigs;
}
/**
* Get pigs
*
* #return Doctrine\Common\Collections\Collection
*/
public function getPigs() {
return $this->pigs;
}
}
and this listener:
<?php
namespace Comakai\MyBundle\Listener;
use Comakai\MyBundle\Util\SluggerParser
Doctrine\ORM\Event\OnFlushEventArgs,
Comakai\MyBundle\Entity\Stuff,
Comakai\MyBundle\Entity\Apple,
Comakai\MyBundle\Entity\Pig;
class Listener {
/**
* #param \Doctrine\ORM\Event\OnFlushEventArgs $ea
*/
public function onFlush(OnFlushEventArgs $ea) {
$em = $ea->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() AS $entity) {
$this->save($entity, $em, $uow);
}
foreach ($uow->getScheduledEntityUpdates() AS $entity) {
$this->save($entity, $em, $uow);
}
}
public function save($entity, $em, $uow) {
if ($entity instanceof Stuff) {
$pigRepository = $em->getRepository('Comakai\MyBundle\Entity\Pig');
$content = $entity->getContent();
preg_match_all('/## pig:(\d+) ##/i', $content, $matches);
$entity->getPigs()->clear();
foreach($matches[1] as $pigID) {
$pig = $pigRepository->find($pigID);
if(!empty($pig)) {
$entity->addPig($pig);
}
}
$entity->setContent($content);
$meta = $em->getClassMetadata(get_class($entity));
$uow->recomputeSingleEntityChangeSet($meta, $entity);
$uow->computeChangeSet($meta, $entity);
}
}
}
And it works fine if apple's collection is empty, but if it has some item I get a duplication error.
How can I tell to the UnitOfWork that I only want to recalculate the pig's collection?
UPDATE
There is a new preFlush event (https://github.com/doctrine/doctrine2/pull/169) and I think this kind of things can be done there. That PR is not in the branch I'm using but let's try it!
When updating an entity during a listener's onFlush event, all you need to call is computeChangeSet():
// make changes to entity
$entity->field = 'value';
// or assign an existing entity to an assocation
$entity->user = $myExistingUserEntity;
$entity->tags->add($myExistingTagEntity);
$meta = $em->getClassMetadata(get_class($entity));
$uow->computeChangeSet($meta, $entity);
If you're creating other entities too, you need to persist them and compute their changes first!
$myNewUserEntity = new Entity\User;
$myNewTagEntity = new Entity\Tag;
$entity->user = $myNewUserEntity;
// make sure you call add() on the owning side for *ToMany associations
$entity->tags->add($myNewTagEntity);
$em->persist($myNewUserEntity);
$em->persist($myNewTagEntity);
$metaUser = $em->getClassMetadata(get_class($myNewUserEntity));
$uow->computeChangeSet($metaUser, $myNewUserEntity);
$metaTag = $em->getClassMetadata(get_class($myNewTagEntity));
$uow->computeChangeSet($metaTag, $myNewTagEntity);
$meta = $em->getClassMetadata(get_class($entity));
$uow->computeChangeSet($meta, $entity);
This can be done with the new preFlush event (Symfony 2.1).
Add a listener to the event (is a bad practice to inject the whole service container but sometimes is the way to go):
services:
mybundle.updater.listener:
class: Foo\MyBundle\Listener\UpdaterListener
arguments: ["#service_container"]
tags:
- { name: doctrine.event_listener, event: preFlush }
And the listener should be something like:
<?php
namespace Foo\MyBundle\Listener;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Foo\MyBundle\SomeInterface;
class UpdaterListener
{
/**
* #param \Doctrine\ORM\Event\PreFlushEventArgs $ea
*/
public function preFlush(PreFlushEventArgs $ea)
{
/* #var $em \Doctrine\ORM\EntityManager */
$em = $ea->getEntityManager();
/* #var $uow \Doctrine\ORM\UnitOfWork */
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if($entity instanceof SomeInterface) {
/*
* do your stuff here and don't worry because
* it'll execute before the flush
*/
}
}
}
}
When wanting to update the current entity you are sending to onFlush and also creating an association to that entity
(for this example I will use Parent object and child object)
Let's say when I change the parent object property 'stressed' to 1 I also want to associate a brand new child object to the parent object in my onflush method, it will look something like this:
public function onFlush(onFlushEventArgs $args)
{
....
$child = $this->createChild($em, $entity); // return the new object. just the object.
$uow->persist($child);
$childMeta = $em->getMetadataFactory()->getMetadataFor('AcmeFamilyTreeBundle:Child');
$uow->computeChangeSet($childMeta, $child)
$parent->setStressed(1);
$parentMeta = $em->getMetadataFactory()->getMetadataFor('AcmeFamilyTreeBundle:Parent');
$uow->recomputeSingleEntityChangeSet($parentMeta, $parent)
}
So there you see:
you need to persist your child object using $uow->persist() not $em->persist()
computeChangeSet on the child object.
recomputeSingleEntityChangeSet on the parent object
For help with creating the onFlush method, please see the documentation