Search code examples
postgresqldoctrine-ormdoctrinelaravel-query-builder

how to handle morph relation with doctrine


i have an Entity that contains morph relation to represent relation with different Entities. for example

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class BinderContent
{
    /**
     * @Id
     * @GeneratedValue
     * @Column
     * @OA\Property()
     */
    public ?int $id;

    /**
     * @Column(name="content_id", type="integer")
     * @OA\Property()
     */
    public ?int $contentId;
    
    /**
     * @Column(name="content_type", type="string")
     * @OA\Property()
     */
    public ?string $contentType;

    /**
     * @ORM\ManyToOne(targetEntity="Document")
     * @ORM\JoinColumn(name="content_id", referencedColumnName="id")
     */
    private $document;

    /**
     * @ORM\ManyToOne(targetEntity="Tab")
     * @ORM\JoinColumn(name="content_id", referencedColumnName="id")
     */
    private $tab;

    public function setContent($content)
    {
        if ($content instanceof Document) {
            $this->document = $content;
        } elseif ($content instanceof Tab) {
            $this->tab = $content;
        }
    }

}

And I'm trying to create new BinderContent and save it

public function attachContentToBinder($binderContentArray)
    {
        $binderContent = new BinderContent();
        $binderContent->setContent($binderContentArray['content']);;
        $binderContent->setContentType($binderContentArray['content_type']);
        $this->binderContentRepository->save($binderContent);

        return $binderContent->getId();
    }

but Doctorine display this error

nSQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "content_id" violates not-null constraint\nDETAIL

as far as i understand from the error the content_id passed as null although I'm printing the initialized object before calling save() method and it contains the right object (Document or Tab) and can't figure out what is the issue


Solution

  • The error occurs because the $tab property of the BinderContent entity is not set. As you 're only setting the $document property the $tab property remains unitialized and Doctrine will interpret that as null. As the $tab property is noted as last property it will override the $document property, because both have the same column name as stated in the JoinColumn doc comment.

    At all this kind of morphing will not work with doctrine because of the above stated reasons. A better approach will be adding two different columns for tab and document entites. These are definitely two different entites.

    Another approach could be inheritance mapping of doctrine. Establish a parent class which holds the properties, that have tab and document in common and then just extend this parent class with tab and document entities. In the parent class state out a discriminator map.

    I personally would choose the first approach since it follows the separation of concerns principle. Just use two different join colums for the different related entities.