Search code examples
phpdoctrine-ormddd-repositories

Doctrine Mapping 2 Objects with 1 table


Given i have 2 classes, User and UserId which look like this:

<?php

class UserId {
    /** @var int */
    private $value;

    public function __construct($value) {
        $this->value = (int) $value;
    }
}

class User {
    /** @var UserId */
    private $id;
    private $firstName;
    private $lastName;

    public function __construct(UserId $id, $firstName, $lastName) {
        $this->id = $id;
        $this->firstName = $firstName;
        $this->lastName = $lastName;
    }
}

Database:

User: 

id : INT PK
firstName: VARCHAR
lastName: VARCHAR

Is it possible to tell doctrine to generate me a User with a UserId when calling "find()"?


Solution

  • Yes, this is possible, I'll explain further on.

    The simple way

    First I have to say that for this simple use-case, the answer provided by Andresch Serj should suffice.

    Set the UserId object using the constructor, get the UserId object by lazily instantiating it if it isn't present yet:

    class User
    {
        /**
         * @var UserId
         */
        private $userId;
    
        /**
         * @var int
         */
        private $id;
    
        /**
         * @param UserId $userId
         * ...
         */
        public function __construct(UserId $userId /* ... */)
        {
            $this->userId = $userId;
            $this->id     = $userId->getId();
            // ...
        }
    
        /**
         * @return UserId
         */
        public function getUserId()
        {
            if (!$this->userId) {
                $this->userId = new UserId($this->id);
            }
    
            return $this->userId;
        }
    
        // ...
    }
    

    The real answer

    In order to manipulate an entity when it's loaded (fetched from the database) you could use Doctrine's PostLoad event.

    First create a listener that will create a new UserId object and set it on the User entity:

    use Doctrine\ORM\Event\LifecycleEventArgs;
    
    class UserListener
    {
        /**
         * @param  User               $user
         * @param  LifecycleEventArgs $event
         * @return void
         */
        public function postLoad(User $user, LifecycleEventArgs $event)
        {
            $userId = new UserId($user->getId());
            $user->setUserId($userId);
        }
    }
    

    Then refactor the getUserId() method of User to simple return the UserId object. Add a setUserId() method (it's wise to make sure an already present UserId object can't be swapped out). Also note the @EntityListeners annotation, which tells Doctrine there's a listener in place for this entity:

    /**
     * @EntityListeners({"UserListener"})
     */
    class User
    {
        /**
         * @return UserId
         */
        public function getUserId()
        {
            return $this->userId;
        }
    
        /**
         * @param  UserId $userId
         * @return void
         * @throws RuntimeException
         */
        public function setUserId(UserId $userId)
        {
            if ($this->userId) {
                throw new RuntimeException("UsedId already present, you can't swap it!");
            }
    
            $this->userId = $userId;
        }
    
        // ...
    }
    

    Finally, in the configuration/bootstrap phase of your application you should register the listener:

    $listener = new UserListener();
    $em->getConfiguration()->getEntityListenerResolver()->register($listener);
    

    Now, whenever a User entity is loaded by Doctrine, a UserId object will be inserted into it. This setup is very useful for more advanced use-cases than the one you described.

    You can read more about this here and here.