1

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:

Client side 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:

Hydrated data

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.

Database data

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.

rkeet
  • 3,406
  • 2
  • 23
  • 49
  • Did you [validate your schema](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/tools.html#runtime-vs-development-mapping-validation) with doctrine? Maybe you should share your entity definitions? – Wilt Aug 17 '16 at 11:40
  • Yes, schema is validated as being OK, DB schema is also good. I'll update the question in a bit to add as many of the relations as needed. – rkeet Aug 17 '16 at 13:20
  • Sorry, a bug reared it's ugly head elsewhere, end of day for me. Update to question will be here tomorrow morning (UTC+1). I'll add entities required for logic above and maybe what's necessary from Forms/Fieldsets. Brace yourself, could be quite a few. ;) – rkeet Aug 17 '16 at 15:10
  • I've updated the question to include the entities related to the screenshots/problem. Hopefully you can help :) – rkeet Aug 18 '16 at 07:47
  • Quick update. This issue is still open, though because of a quickfix requiring the save action to be executed after every modification of a `Question` (including its `Answer`'s), its importance has dropped. Meanwhile I'm open to suggestions. The `NonUniformCollection` is also still on the list as something to remove when there's time. – rkeet Sep 12 '16 at 12:46

1 Answers1

0

I am not sure if this is causing your issue, but it seems to me that your inheritance mapping is not setup correctly:

You need to declare your discriminator column inside your entity definitions as written in the docs, they should not be set as properties, doctrine takes care of setting them inside your database:

/**
 * @InheritanceType("JOINED")
 * @DiscriminatorColumn(name="type", type="string")
 * @DiscriminatorMap({"element"="AbstractElement", "question"="Question", "text"="TextItem", "media"="MediaItem"})
 */
class AbstractElement extends AbstractEntity implements GridElementInterface
{
    //...
}

And is your abstract entity properly mapped as a @MappedSuperClass?

/**
 * @MappedSuperclass
 */
class AbstractEntity
{
    //...
}

This might be part of your solution, please come back with feedback after you updated accordingly...

Wilt
  • 41,477
  • 12
  • 152
  • 203
  • Hi Wilt, I've added more info in the question based on your answer (see Update 2). However, the problem remains. Any chance of having another look? – rkeet Aug 18 '16 at 12:25
  • @Nukeface Did you also map your `AbstactEntity` as a `@MappedSuperclass`? – Wilt Aug 18 '16 at 14:40
  • Sorry I forgot to answer that, yes it's an abstract class with MappedSuperClass mapping: ` /* * @ORM\MappedSuperclass * @ORM\HasLifecycleCallbacks */ abstract class AbstractEntity implements AbstractEntityInterface {//stuff}` – rkeet Aug 18 '16 at 14:57
  • @Nukeface Hard to say where the problem is. What hydrator are you using? Maybe you should use xdebug to pin down where it goes wrong. – Wilt Aug 18 '16 at 15:55
  • Using the Doctrine Hydrator, Doctrine Entities and Doctrine Collections. However, to correctly link the discriminator type Entities the `NonUniformCollection` (link in question) is used. About debugging, been trying to pinpoint the precise location where it decides that the last questions/answers are valid for the first sets. Have noticed that it never fills up with more than the original count. Say L1-Q1 has 2A it will use the first 2 answers of the last Question available (e.g. L5-Q10). I find it all weird, but will continue debugging. Thx for the effort though, will update when solved. – rkeet Aug 19 '16 at 06:42
  • That [`NonUniformCollection`](https://github.com/adamlundrigan/LdcZendFormCTI/blob/master/src/Form/Element/NonuniformCollection.php) you are using is not part of the official Doctrine repository. Could it be that there is some unsuspected bug in there causing your issues. – Wilt Aug 19 '16 at 07:17
  • Could be, thought that before. However, it's ingrained into much of the whole element business, so cannot easily take it out. Also, it works fine with the other element Entities. Though with `Question` + `Answers` it does not. I'm thinking because, as opposed to the other elements, `Question`'s can have a `Collection` (Doctrine, not Zend) of `Answer`s. – rkeet Aug 19 '16 at 17:57
  • Question has been a year, think I might have a solution for this issue. (The issue still exists though the project is no longer in development). Working on something else now, though with ZF2/Doctrine2. Maybe you've come across this as well in the mean time, but [you do not need declare a discriminator map for either JTI or CTI](https://stackoverflow.com/questions/45015356/zf2-doctrine-2-child-level-discriminators-with-class-table-inheritance). Might help you in future ;) – rkeet Aug 14 '17 at 09:46