Symfony2 entity relationships not working as expected - symfony

This is either a huge bug in Symfony2 or I'm just not getting it. I've spent literally days trying to understand what is going on here. I have two entities:
Event
Date
I want a relationship where there are many dates to one event. Sounds simple enough, so in my Event entity I have:
/**
* #ORM\OneToMany(targetEntity="Date", mappedBy="event")
*/
protected $dates;
And in my Date entity I have:
/**
* #ORM\ManyToOne(targetEntity="Event", inversedBy="dates")
*/
private $event;
I have also generated a CRUD (doctrine:generate:crud) on the Event entity so that I may add events to the database. In the form builder in my EventType I have added:
->add('date', new DateType())
This is so that I may include the date field in the form, as per the Symfony documentation.
Now comes my problem.
Whenever I run doctrine:generate:entities my entities are created on the Event and Date entity, but they seem to be the wrong way around. On my Event entity I get:
/**
* Constructor
*/
public function __construct()
{
$this->dates = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add dates
*
* #param \Raygun\EventBundle\Entity\Date $dates
* #return Event
*/
public function addDate(\Raygun\EventBundle\Entity\Date $dates)
{
$this->dates[] = $dates;
return $this;
}
/**
* Remove dates
*
* #param \Raygun\EventBundle\Entity\Date $dates
*/
public function removeDate(\Raygun\EventBundle\Entity\Date $dates)
{
$this->dates->removeElement($dates);
}
/**
* Get dates
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getDates()
{
return $this->dates;
}
and on my Date entity I get:
/**
* Set event
*
* #param \Raygun\EventBundle\Entity\Event $event
* #return Date
*/
public function setEvent(\Raygun\EventBundle\Entity\Event $event = null)
{
$this->event = $event;
return $this;
}
/**
* Get event
*
* #return \Raygun\EventBundle\Entity\Event
*/
public function getEvent()
{
return $this->event;
}
Now when I try to load the form so I can add the event/date to the database I get
Neither the property "date" nor one of the methods "getDate()", "date()", "isDate()", "hasDate()", "__get()" exist and have public access in class "Raygun\EventBundle\Entity\Event".
It's like it should be adding getters and setters to the Event entity, NOT the Date entity. I'm really tearing my hair out with this and am thinking of ditching Symfony entirely, as it seems to go completely against logic.

if you want Form component automatically mappes the fields you should change date field to dates:
->add('dates', 'collection', [
'type' => new DateType()
])
or you can add mapped => false option to your date field to map it manually.

Your Event form type should contain a collection type for the protected $dates field, so this line is incorrect:
->add('date', new DateType())
It should be:
->add('dates', 'collection', array('type' => new DateType()))
Please have a look at this Symfony cookbook entry on how to work with form collections:
http://symfony.com/doc/current/cookbook/form/form_collections.html

Related

Looping through entities and updating them causes error on flush

I am new to symfony and doctrine. And I am compeleting a code that someone else has started. I mainly have a form for which I wrote a validation function in my controller. In this form a BusReservation object along with its BusReservationDetails are created and saved to the db. so at the end of the form validation function, after the entities are saved in DB, I call a BusReservation Manager method which is transformBusReservationDetailIntoBusTicket which aim is to take each BusReservationDetail in the BusReservation oject and create a a new entity BusTicket based on it.
so I created this loop (please let me know if there is something wrong in my code so that i can write in a good syntax). I tried to put the 3 persist that you see at the end of the code but I got : Notice: Undefined index: 0000000..
I tried to merge (the last 3 lines in code ) I got the following :
A new entity was found through the relationship 'MyBundle\Entity\CustomInfo#busTicket' that was not configured to cascade persist operations for entity: . 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"}).
I got this same error when i commented all theh 6 lines of merge and flush.
PS: I am not expecting the flush to fully work. There are some properties that are nullable=false so I assume that I must set them as well so that the entities can be saved to DB. But the error i got is by far different than this.
PS : I noticed that there is a onFlush where the customInfo is updated and persisted again and other things happen, but i am trying to debug step by step. I tried to detach this event but still got the same errors. so I want to fix my code and make sure that the code part that i wrote in the manager is correct and if that's the case then I can move to debugging the event Listener. so please I would like to know if the following code is correct and why the flush is not working.
/**
* #param $idBusReservation
* #return bool
* #throws \Doctrine\ORM\NonUniqueResultException
*/
public function transformBusReservationIntoBusTicket($idBusReservation): bool
{ $result = "into the function";
/** #var BusReservation $busReservation */
$busReservation = $this->em->getRepository('MyBundle:BusReservation')->find($idBusReservation);
if ($busReservation !== null) {
/** #var BusReservationDetail $busReservationDetail */
foreach ($busReservation->getBusReservationDetails() as $busReservationDetail) {
$busTicket = new BusTicket($busReservationDetail->getBusModel(), $busReservation->getPassenger());
$busReservationDetail->setBusTicket($busTicket);
$busTicket->setBusReservationDetail($busReservationDetail);
$busTicket->setOwner($busreservation->getPassenger()->getName());
if ($busReservationDetail->getBusModel()->getCode() === 'VIPbus') {
// perform some logic .. later on
} else {
$customInfo = new CustomInfo();
$customInfo->setNumber(1551998);
// $customInfo->setCurrentMode(
// $this->em->getRepository('MyBundle:Mode')
// ->find(['code' => 'Working'])
// );
$customInfo->setBusTicket($busTicket);
// Bus ticket :
$busTicket->addCustomInfo($customInfo);
$busTicket->setComment($busReservation->getComment());
}
/** #var Mode $currentMode */
$currentMode = $this->em->getRepository('MyBundle:Mode')
->findOneBy(['code' => 'Working']);
$busTicket->setCurrentMode($currentMode);
// $this->em->merge($customInfo);
// $this->em->merge($busReservationDetail);
// $this->em->merge($busTicket);
// $this->em->persist($customInfo);
// $this->em->persist($busReservationDetail);
// $this->em->persist($busTicket);
}
$this->em->flush();
// $this->em->clear();
}
return $result;
}
// *************** In BusReservation.php ********************
/**
* #ORM\OneToMany(targetEntity="MyBundle\Entity\BusReservationDetail", mappedBy="busReservation")
*/
private $busReservationDetails;
/**
* Get busReservationDetails
*
*#return Collection
*/
public function getBusReservationDetails()
{
return $this->busReservationDetails;
}
// ---------------------------------------------------------------------
// *************** In BusReservationDetail.php ********************
/**
* #ORM\ManyToOne(targetEntity="MyBundle\Entity\BusReservation", inversedBy="busReservationDetails")
* #ORM\JoinColumn(name="id_bus_reservation", referencedColumnName="id_bus_reservation", nullable=false)
*/
private $busReservation;
/**
* #ORM\ManyToOne(targetEntity="MyBundle\Entity\BusModel")
* #ORM\JoinColumn(name="bus_model_code", referencedColumnName="bus_model_code", nullable=false)
*/
private $busModel;
/**
* #ORM\OneToOne(targetEntity="MyBundle\Entity\BusTicket", inversedBy="busReservationDetail", cascade={"merge","remove","persist"})
* #ORM\JoinColumn(name="id_bus_ticket", referencedColumnName="id_bus_ticket")
*/
private $busTicket;
/**
* #return BusModel
*/
public function getBusModel()
{
return $this->busModel;
}
//-------------------------------------------------------------------------
// ************ IN BusTicket.php *****************************
/**
* #ORM\OneToMany(targetEntity="MyBundle\Entity\CustomInfo", mappedBy="busTicket")
*/
private $customInfos;
/**
*
* #param CustomInfo $customInfo
*
* #return BusTicket
*/
public function addCustomInfot(CustomInfo $customInfo)
{
if (!$this->customInfos->contains($customInfo)) {
$this->customInfos[] = $customInfo;
}
return $this;
}
/**
* #ORM\OneToOne(targetEntity="MyBundle\Entity\busReservationDetail", mappedBy="busTicket")
*/
private $busReservationDetail;
// --------------------------------------------------------------------
// CUSTOMINFO ENTITY
/**
* #ORM\ManyToOne(targetEntity="MyBundle\Entity\BusTicket", inversedBy="customInfos")
* #ORM\JoinColumn(name="id_bus_ticket", referencedColumnName="id_bus_ticket", nullable=false)
*/
private $busTicket;
The answer is in your error message. You either have to add cascade={"persist"} to your entity annotation, or explicitly call persist. I don't believe you need em->merge() in this situation as you're never taking the entities out of context.
Where you have all your persist lines commented out, just try putting this in
$this->em->persist($busTicket);
$this->em->persist($busReservationDetail);
$this->em->persist($customInfo);
and if you're looping through a ton of entities, you could try adding the flush inside the loop at the end instead of a huge flush at the end.

The identifier generation strategy for this entity requires the ID field to be populated before EntityManager#persist() is called

I'm tying to create one to many relations
A have class
class Interview {
/**
* #OneToMany(targetEntity="Question", mappedBy="question")
*/
private $questions;
public function __construct() {
$this->questions = new ArrayCollection();
}
public function __toString() {
return $this->id;
}
/**
* #return Collection|Question[]
*/
public function getQuestions() {
return $this->questions;
}
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
......
}
another
class Question {
/**
* #ManyToOne(targetEntity="Interview", inversedBy="interview")
* #JoinColumn(name="interview_id", referencedColumnName="id")
*/
private $interview;
public function getInterview() {
return $this->interview;
}
public function setInterview(Interview $interview) {
$this->interview = $interview;
return $this;
}
/**
* #ORM\Column(type="integer")
* #ORM\Id
*/
private $interview_id;
......
}
and Controller for all this
if ($form->isSubmitted() && $form->isValid()) {
$interview = new Interview();
$question = new Question();
$em->persist($interview);
$question->setInterview($interview);
$question->setTitle($request->get('title'));
$em->persist($question);
$em->flush();
return $this->redirectToRoute('homepage');
}
i'm receiving an error:
Entity of type AppBundle\Entity\Question is missing an assigned ID for
field 'interview_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.
Don't understand what the problem and how to fix it.
To enforce loading objects from the database again instead of serving them from the identity map. You can call $em->clear(); after you did $em->persist($interview);, i.e.
$interview = new Interview();
$em->persist($interview);
$em->clear();
It seems like your project config have an error in doctrine mapped part.
If you want automatically generated identifiers instead you need to
adjust the metadata mapping accordingly.
Try to see full doctrine config and do some manipulation with
auto_mapping: false
to true as example or something else...
Also go this , maybe it will be useful.
I am sure, its too late to answer but maybe someone else will get this error :-D
You get this error when your linked entity (here, the Interview entity) is null.
Of course, you have already instantiate a new instance of Interview.But, as this entity contains only one field (id), before this entity is persited, its id is equal to NULL. As there is no other field, so doctrine think that this entity is NULL. You can solve it by calling flush() before linking this entity to another entity

Symfony Form ManyToOne OneToMany

I have three entities, Block, BlockPlacement, BlockPosition:
class BlockEntity
{
private $bid;
/**
* #ORM\OneToMany(
* targetEntity="BlockPlacementEntity",
* mappedBy="block",
* cascade={"remove"})
*/
private $placements;
}
class BlockPlacementEntity
{
/**
* The id of the block postion
*
* #ORM\Id
* #ORM\ManyToOne(targetEntity="BlockPositionEntity", inversedBy="placements")
* #ORM\JoinColumn(name="pid", referencedColumnName="pid", nullable=false)
*/
private $position;
/**
* The id of the block
*
* #var BlockEntity
* #ORM\Id
* #ORM\ManyToOne(targetEntity="BlockEntity", inversedBy="placements")
* #ORM\JoinColumn(name="bid", referencedColumnName="bid", nullable=false)
*/
private $block;
private $sortorder;
}
class BlockPositionEntity
{
private $pid;
/**
* #ORM\OneToMany(
* targetEntity="BlockPlacementEntity",
* mappedBy="position",
* cascade={"remove"})
* #ORM\OrderBy({"sortorder" = "ASC"})
*/
private $placements;
}
So, you can see the relationship: Block < OneToMany > Placement < ManyToOne > Position.
Now I am trying to construct a form to create/edit a block:
$builder
->add($builder->create('placements', 'entity', [
'class' => 'Zikula\BlocksModule\Entity\BlockPositionEntity',
'choice_label' => 'name',
'multiple' => true,
'required' => false
]))
;
This gives me a good select box with multiple selections possible with a proper list of positions to choose from. But it does not show previous selections for placement (I am using existing data) e.g. marking positions as 'selected'. I have not tried creating a new Block yet, only editing existing data.
I suspect I will need to be using addModelTransformer() or addViewTransformer() but have tried some of this an cannot get it to work.
I've looked at the collection form type and I don't like that solution because it isn't a multi-select box. It requires JS and isn't as intuitive as a simple select element.
This seems like such a common issue for people. I've searched and found no common answer and nothing that helps.
Update: please look at this example repo
Update 2: i've updated the repo.
I did it with form event listeners and unmapped choice field.
Take a closer look at BlockType form type
Feel free to ask any questions about it.
OK - so in the end, I found a different way. #Stepan Yudin's answer worked, but is complicated (listeners, etc) and not quite like I was hoping.
So, I have the same three entities. BlockPlacement and BlockPosition remain the same (and so aren't reposted, see above) but I have made some changes to the BlockEntity:
class BlockEntity
{
private $bid;
/**
* #ORM\OneToMany(
* targetEntity="BlockPlacementEntity",
* mappedBy="block",
* cascade={"remove", "persist"},
* orphanRemoval=true)
*/
private $placements;
/**
* Get an ArrayCollection of BlockPositionEntity that are assigned to this Block
* #return ArrayCollection
*/
public function getPositions()
{
$positions = new ArrayCollection();
foreach($this->getPlacements() as $placement) {
$positions->add($placement->getPosition());
}
return $positions;
}
/**
* Set BlockPlacementsEntity from provided ArrayCollection of positionEntity
* requires
* cascade={"remove, "persist"}
* orphanRemoval=true
* on the association of $this->placements
* #param ArrayCollection $positions
*/
public function setPositions(ArrayCollection $positions)
{
// remove placements and skip existing placements.
foreach ($this->placements as $placement) {
if (!$positions->contains($placement->getPosition())) {
$this->placements->removeElement($placement);
} else {
$positions->removeElement($placement->getPosition()); // remove from positions to add.
}
}
// add new placements
foreach ($positions as $position) {
$placement = new BlockPlacementEntity();
$placement->setPosition($position);
// sortorder is irrelevant at this stage.
$placement->setBlock($this); // auto-adds placement
}
}
}
So you can see that the BlockEntity is now handling a positions parameter which doesn't exist in the entity at all. Here is the relevant form component:
$builder
->add('positions', 'Symfony\Bridge\Doctrine\Form\Type\EntityType', [
'class' => 'Zikula\BlocksModule\Entity\BlockPositionEntity',
'choice_label' => 'name',
'multiple' => true,
])
note that I have changed to Symfony 2.8 form style since my first post
This renders a multiple select element on the page which accepts any number of positions and converts them to an ArrayCollection on submit. This is then handled directly by the form's get/set position methods and these convert to/from the placement property. The cascade and orphanRemoval are important because they take care to 'clean up' the leftover entities.
because it is references above here is the BlockPlacement setBlock($block) method:
public function setBlock(BlockEntity $block = null)
{
if ($this->block !== null) {
$this->block->removePlacement($this);
}
if ($block !== null) {
$block->addPlacement($this);
}
$this->block = $block;
return $this;
}

Found the public method "add", but did not find a public "remove" in symfony2 entity

I get this exeption when I submit my form:
Found the public method "addRemote", but did not find a public "removeRemote" on class App\CoreBundle\Entity\Scene
The weired think is that the remove method exist ...
But i wrote it myself (When I did php app/console doctrine:generate:entities) doctrine didn't generated it. Did I make something wrong ?
/**
* #var array $remote
*
* #ORM\Column(name="remote", type="array", nullable=true)
*/
private $remote;
/**
* Set remote
*
* #param array $remote
* #return Scene
*/
public function addRemote($value, $key=null) {
if($key!=null){
$this->remote[$key] = $value;
}else{
$this->remote[] = $value;
}
return $this;
}
/**
* Remove remote
*/
public function removeRemote(){
unset($this->remote);
}
I allso tried:
/**
* Remove remote
*/
public function removeRemote($key=null){
if($key!=null && array_key_exists($key, $this->remote)){
unset($this->remote[$key]);
}
unset($this->remote);
return $this;
}
You have bigger problem than this; you are abusing your forms :)
Add.. and Remove... methods should be used for relations, not columns as per your code. Also, both add and remove methods must accept parameter that will be either added or removed.
If you still need an array, than getRemotes() method should return key=>value array. Adder and remover will later get that key, based on what user have picked in choice form type.

Get entities from a unidirectional many to many relation with Doctrine2 and Symfony2

I'm currently working on a language assessment project which enables you to take an exam in the language you want and evaluate your level. I use Symfony2 framework and work with Doctrine2 as well. My issue is the following one:
I have two entities Exam and Question linked by a Many-To-Many relation (Exam being the owner). Each exam can be related to several questions, and each question can be related to several exams.
Here is my code:
Exam entity
/**
* Exam
*
* #ORM\Table(name="cids_exam")
* #ORM\Entity(repositoryClass="LA\AdminBundle\Entity\ExamRepository")
*/
class Exam
{
...
/**
* #ORM\ManyToMany(targetEntity="LA\AdminBundle\Entity\Question", cascade={"persist"})
* #ORM\JoinTable(name="cids_exam_question")
*/
private $questions;
...
/**
* Constructor
*/
public function __construct()
{
$this->questions = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add questions
*
* #param \LA\AdminBundle\Entity\Question $questions
* #return Exam
*/
public function addQuestion(\LA\AdminBundle\Entity\Question $questions)
{
$this->questions[] = $questions;
return $this;
}
/**
* Remove questions
*
* #param \LA\AdminBundle\Entity\Question $questions
*/
public function removeQuestion(\LA\AdminBundle\Entity\Question $questions)
{
$this->questions->removeElement($questions);
}
/**
* Get questions
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getQuestions()
{
return $this->questions;
}
}
As long as it is a unidirectional relation, there is no 'exams' attribute in my Question class.
Now, what I want to do is getting all the questions related to a specific exam, calling the getQuestions() method, like this:
$questions = $exam->getQuestions();
But this method returns an empty array, even if I have data in my database. If I var_dump the $exam variable, I can see the questions array is empty:
object(LA\AdminBundle\Entity\Exam)[47]
private 'id' => int 5
...
private 'questions' =>
object(Doctrine\ORM\PersistentCollection)[248]
private 'snapshot' =>
array (size=0)
empty
private 'owner' => null
private 'association' => null
private 'em' => null
private 'backRefFieldName' => null
private 'typeClass' => null
private 'isDirty' => boolean false
private 'initialized' => boolean false
private 'coll' =>
object(Doctrine\Common\Collections\ArrayCollection)[249]
private '_elements' =>
array (size=0)
...
I think I could maybe write a findByExam() function in my QuestionRepository, but I don't really know how to implement the joins in this case.
Any help would be great!
To have a findByExam() method in your QuestionRepository do the following:
public function findByExam($exam)
{
$q = $this->createQueryBuilder('q')
->where('q.exam = :exam')
->setParameter('exam', $exam)
->getQuery();
return $q->getResult();
}
You could also create a bi-directional relationship not uni-directional !
Each exam can be related to several questions, and each question can
be related to several exams.
Create a bi-directional relationship by adding this to your Question entity:
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Vendor\YourExamBundle\Entity\ExamInterface;
class Question
{
protected $exams;
public function __construct()
{
$this->exams = new ArrayCollection();
}
public function getExams()
{
return $this->exams;
}
public function addExam(ExamInterface $exam)
{
if !($this->exams->contains($exam)) {
$this->exams->add($exam);
}
return $this;
}
public function setExams(Collection $exams)
{
$this->exams = $exams;
return $this;
}
// ...
Afterwards you can use...
$question->getExams()
... in your controller.
To automatically join your related entities doctrine's fetch option can be used with:
LAZY ( loads the relations when accessed )
EAGER ( auto-joins the relations )
EXTRA_LAZY ( manual fetching )
example:
/**
* #ManyToMany(targetEntity="Question",inversedBy="exams", cascade={"all"}, fetch="EAGER")
*/
Though eager loading has a downside in terms of performance it might be an option for you.
Doctrine Fetch with EAGER
Whenever you query for an entity that has persistent associations and
these associations are mapped as EAGER, they will automatically be
loaded together with the entity being queried and is thus immediately
available to your application.
Read more about it in the Doctrine Documentation.
Another option you should check when working with relations is the cascade option.
See the Doctrine - Working with Associations chapter of the documentation.
Tip:
You should create interfaces for exams and questions and use them instead of the original entity in your set and add methods to allow easier extending.
Bi-Directional Relations using Doctrine2 ORM with association table exam_questions
exam_id question_id
<?php
class Exams
....OTHER PROPERTIES...
/**
* Owning Side
*
* #ManyToMany(targetEntity="Questions", inversedBy="exams")
* #JoinTable(name="exam_questions",
* joinColumns={#JoinColumn(name="exam_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="question_id", referencedColumnName="id")}
* )
*/
private $questions;
..OTHER CODES..
}
class Questions{
..OTHER CODES..
/**
* Inverse Side
*
* #ManyToMany(targetEntity="Exams", mappedBy="questions")
*/
private $exams;
..OTHER CODES..
}
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html#annref-manytomany

Resources