EDIT 2018-05-22: no answer fully fixed issue - can no longer replicate issue as no longer have access. Not removing based on this meta discussion
Please do not spend time/effort creating an answer
Discussion in @Wilt's answer led me to what I know about using discriminators now, it might help future questioners. In my case it helped, but did not provide an answer.
I've got a bit of complex problem where the hydration of data received from client-side gets hydrated incorrectly. I've been trying to fix the problem for close to a week now, so I thought to ask you guys.
We've got this application that allows for the creation of assignments for students. Assignments may contain Questions, Text items, Media items, and more. The problem is with the Questions and associated Answers.
Scenario
Assignment
- QuestionSheet 1 (L1)
- Question 1 (L1 - V1)
- Answer A (L1 - V1 - A1)
- Answer B (L1 - V1 - A2)
- Question 2 (L1 - V2)
- Answer A (L1 - V2 - A1) (just 1 answer)
- QuestionSheet 2 (L2)
- Question 1 (... and so on)
- Answer A
- Answer B
- Question 2
- Answer A
- Answer B
- Question 3
- Answer A
- Answer B
The above gets send properly from the client-side. Screenshot of snipped of that data:
Important to note is that, as you can see above, a Question
is in fact a GridElements
entity. It might've also been Text
or Image
, this is based on the property type = question
which is a Discriminator
.
After hydrating the data we get the following Entity structure:
As you can see, the data is no longer correct after hydration. This is done during the $form->isValid()
. QuestionSheet 1
contains the first Question
for QuestionSheet 2
and that Question
has the first Answer
from the third Question
of the second QuestionSheet
.
When reading through the full hydrated dataset, I see that the Answers
created for the first QuestionSheet
have been dropped. The Answers
from the second QuestionSheet
have been duplicated and have overwritten the Answers in the first QuestionSheet. In essence, what you see in the picture above.
Worse still
The below is all the data that is saved to the database after the above, with the mentioned scenario of 2 lists, 5 questions and 9 answers.
So of the first Question, no Q&A left. The second QuestionSheet's Questions have been used to overwrite them. Also , only the last 2 Answers are there, filling the space of what should've been 9!.
Btw, the query returning this is completely LEFT JOIN
so as to show all empty data as well, this is all there's left.
It seems that it grabs the last set of whatever child entities there are to fill up previous entities, or something. I've gotten lost.
How is this possible?
As I mentioned, I've been at it a good long while, but cannot find a solution. Hope you guys can help.
If you need any info on code I'll do my best to either show it here or explain it as best as possible in case of some proprietary code.
Update - Entities
Assignment entity
/**
* @ORM\Entity(repositoryClass="Wms\Admin\Assignment\Repository\AssignmentRepository" )
* @ORM\HasLifecycleCallbacks
* @ORM\Table(name="ass_assignment")
* @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
*/
class Assignment extends SeoUrl
{
//Traits and properties
/**
* @ORM\OneToMany(targetEntity="Wms\Admin\Assignment\Entity\QuestionSheet", mappedBy="assignment", cascade={"persist", "remove"}, orphanRemoval=true)
**/
protected $questionSheets;
public function __construct()
{
$this->abstractEntity_entityCategories = new ArrayCollection();
$this->questionSheets = new ArrayCollection();
$this->documents = new ArrayCollection();
}
public function __toString()
{
return (string)$this->id;
}
//More getters/setters
}
QuestionSheet entity
/**
* @ORM\Entity(repositoryClass="Wms\Admin\Assignment\Repository\QuestionSheetRepository")
* @ORM\Table(name="ass_questionsheet")
**/
class QuestionSheet extends AbstractEntity
{
/**
* @ORM\ManyToOne(targetEntity="Wms\Admin\Assignment\Entity\Assignment", inversedBy="questionSheets")
* @ORM\JoinColumn(name="assignment_id", referencedColumnName="id", onDelete="CASCADE")
**/
protected $assignment;
/**
* @ORM\OneToOne(targetEntity="Wms\Admin\LayoutGrid\Entity\Grid", cascade={"persist", "remove"})
* @ORM\JoinColumn(name="grid_id", referencedColumnName="id")
**/
protected $grid;
public function __construct()
{
$this->gridElements = new ArrayCollection();
}
//More getters/setters
}
Grid entity
/**
* @ORM\Entity
* @ORM\Table(name="lg_grid")
**/
class Grid extends AbstractEntity
{
/**
* @ORM\OneToMany(targetEntity="Wms\Admin\LayoutGrid\Entity\Element\AbstractElement", mappedBy="grid", cascade={"persist", "remove"}, orphanRemoval=true)
* @ORM\OrderBy({"y" = "ASC", "x" = "ASC"})
*/
protected $gridElements;
public function __construct()
{
$this->gridElements = new ArrayCollection();
}
}
Grid Element entity
/**
* @ORM\Table(name="lg_grid_element")
* @ORM\Entity
* @ORM\InheritanceType("JOINED")
* @ORM\HasLifecycleCallbacks
**/
class AbstractElement extends AbstractEntity implements GridElementInterface
{
/**
* @ORM\ManyToOne(targetEntity="Wms\Admin\LayoutGrid\Entity\Grid", inversedBy="gridElements")
* @ORM\JoinColumn(name="grid_id", referencedColumnName="id", onDelete="CASCADE")
**/
protected $grid;
public $type = ''; //This is a discriminator
}
Question entity
/**
* @ORM\Entity
* @ORM\Table(name="lg_grid_question")
**/
class Question extends AbstractElement
{
/**
* @ORM\OneToMany(targetEntity="Wms\Admin\LayoutGrid\Entity\Element\Answer", mappedBy="question", cascade={"persist"})
*/
protected $answers;
public $type = 'question'; //Inherited property, now filled in with discriminator value
public function __construct()
{
$this->answers = new ArrayCollection();
}
}
Answer entity
/**
* @ORM\Entity
* @ORM\Table(name="lg_grid_answer")
**/
class Answer extends AbstractEntity
{
/**
* @ORM\ManyToOne(targetEntity="Wms\Admin\LayoutGrid\Entity\Element\Question", inversedBy="answers", cascade={"persist"})
* @ORM\JoinColumn(name="question_id", referencedColumnName="id", onDelete="CASCADE")
**/
protected $question;
public function __toSting() {
return (string) $this->getId();
}
}
Update 2
Updated AbstractElement
entity based on @Wilt's answer.
/**
* @ORM\Table(name="lg_grid_element")
* @ORM\Entity
* @ORM\InheritanceType("JOINED")
* @ORM\HasLifecycleCallbacks
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({
* "abstractElement"="AbstractElement",
* "question"="Question",
* //Others
* })
**/
class AbstractElement extends AbstractEntity implements GridElementInterface
{
//Same as above
}
This update created some problems with the NonUniformCollection
which handles getting the correct Entity. This used to be based on the $type
property.
However, having a $type
property in an Entity which has * @ORM\DiscriminatorColumn(name="type", type="string")
as a notation, is not allowed. Therefore all the classes making use of a discriminator have also been updated with the following.
const ELEMENT_TYPE = 'question'; //Overwritten from AbstractElement
/**
* @return string
*/
public function getType() //Overwritten from AbstractElement
{
return self::ELEMENT_TYPE;
}
Alas, the original problem remains.