Doctrine 2.5, DDD, Entities and Identities

If you’re developing applications in a Domain-Driven Design style and using Doctrine 2.5, you may be struggling with implementing your entities identities, I mean, UserId, ProductId, and so on. Embeddables are cool, but limited. Surrogates are not necessary anymore. Here are some short tips and examples about how to proceed.

tl;dr

If using Doctrine 2.5, embeddables are not an option yet for implementing your identities. The best option is using Custom Types.

What’s new in Doctrine 2.5

An interesting new feature about Doctrine 2.5, is that now, it’s possible to use Objects as identifiers for Entities as long as they implement the magic method __toString(). So we can add __toString to our Identities Value Objects and use them in our mappings. Let’s see.

Introduction

Consider the context of Last Wishes. To sum up, users that have wishes. Users are identified by a UserId, a value object that holds a UUID. Wishes are identified by a WishId and have a reference to its owner, a UserId.

Our Identity VO

First of all, let’s see our current identity objects. Remember, we need to add the __toString() method. First, UserId.

namespace Lw\Domain\Model\User;

use Rhumsaa\Uuid\Uuid;

/**
 * Class UserId.
 */
class UserId
{
    /**
     * @var string
     */
    private $id;

    /**
     * @param string $id
     */
    public function __construct($id = null)
    {
        $this->id = null === $id ? Uuid::uuid4()->toString() : $id;
    }

    /**
     * @return string
     */
    public function id()
    {
        return $this->id;
    }

    /**
     * @param UserId $userId
     *
     * @return bool
     */
    public function equals(UserId $userId)
    {
        return $this->id() === $userId->id();
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->id();
    }
}

Now, WishId.

namespace Lw\Domain\Model\Wish;

use Rhumsaa\Uuid\Uuid;

/**
 * Class WishId.
 */
class WishId
{
    /**
     * @var string
     */
    private $id;

    /**
     * @param string $id
     */
    public function __construct($id = null)
    {
        $this->id = $id ?: Uuid::uuid4()->toString();
    }

    /**
     * @return string
     */
    public function id()
    {
        return $this->id;
    }

    /**
     * @param WishId $wishId
     *
     * @return bool
     */
    public function equals(WishId $wishId)
    {
        return $this->id() === $wishId->id();
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->id();
    }
}

Custom Types

Check the implementation of the Doctrine Custom Types. They inherit from GuidType, so their internal representation will be an UUID. We need to specify what’s the database back and forward conversion. Last, we need to register our custom types before use them.

First, a base type for the UserId and WishId custom types.

namespace Lw\Infrastructure\Domain\Model;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\GuidType;

class DoctrineEntityId extends GuidType
{
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        return $value->id();
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        $className = $this->getNamespace().'\\'.$this->getName();

        return new $className($value);
    }
}

Now, the UserId custom type.

namespace Lw\Infrastructure\Domain\Model\User;

use Lw\Infrastructure\Domain\Model\DoctrineEntityId;

class DoctrineUserId extends DoctrineEntityId
{
    public function getName()
    {
        return 'UserId';
    }

    protected function getNamespace()
    {
        return 'Lw\Domain\Model\User';
    }
}

Time for the WishId custom type.

namespace Lw\Infrastructure\Domain\Model\Wish;

use Lw\Infrastructure\Domain\Model\DoctrineEntityId;

class DoctrineWishId extends DoctrineEntityId
{
    public function getName()
    {
        return 'WishId';
    }

    protected function getNamespace()
    {
        return 'Lw\Domain\Model\Wish';
    }
}

Last, custom types registration.

namespace Lw\Infrastructure\Persistence\Doctrine;

use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Setup;

class EntityManagerFactory
{
    /**
     * @return EntityManager
     */
    public function build()
    {
        \Doctrine\DBAL\Types\Type::addType('UserId', 'Lw\Infrastructure\Domain\Model\User\DoctrineUserId');
        \Doctrine\DBAL\Types\Type::addType('WishId', 'Lw\Infrastructure\Domain\Model\Wish\DoctrineWishId');

        return EntityManager::create(
            array(
                'driver' => 'pdo_sqlite',
                'path' => __DIR__.'/../../../../../db.sqlite',
            ),
            Setup::createYAMLMetadataConfiguration([__DIR__.'/config'], true)
        );
    }
}

Mappings

Check now Doctrine mappings. See how we’re using our custom types in the “type” field.

Lw\Domain\Model\User\User:
  type: entity
  id:
    userId:
      column: id
      type: UserId
  table: lw_user
  repositoryClass: Lw\Infrastructure\Domain\Model\User\DoctrineUserRepository
  fields:
    email:
      type: string
    password:
      type: string

Lw\Domain\Model\Wish\Wish:
  type: entity
  table: lw_wish
  repositoryClass: Lw\Infrastructure\Domain\Model\Wish\DoctrineWishRepository
  id:
    wishId:
      column: id
      type: WishId
  fields:
    address:
      type: string
    content:
      type: text
    userId:
      type: UserId
      column: user_id

Hope it helps to all the DDD lovers!

  • Javier Seixas

    Interesting post. However, could you explain what are the benefits of using a Value Object as id, instead of just an string?

  • ЌẪřĽőŠ

    One question.. why embedables is not an option? And, why using two ids type? UserId, and WishId, its not better to use one uuid? and put in both? that’s possible?? thanks, by he way the book of ddd rocks!

    • cbuenosvinos

      The problem comes when using embeddables as primary key. You can do it (defining that is an id), but when the same embeddable is used as non pk, it fails. Thanks for the book coment ;)

      • ЌẪřĽőŠ

        Ok, so its possible to generate a class called DoctrineId for example? and use it for all the entities? I mean, repeting the same code for each entity can be a problem no? Thanks!

        • cbuenosvinos

          It’s possible, but not an DDD approach. Each entity has its identity (a UserId is not a ProductId). Even more, using DoctrineId is infrastructure-related, not domain.

          • ЌẪřĽőŠ

            I’m a bit lost, not an expert. In my domain the entities has an id, but whenever i persist, then at this very moment are assigned to the custom type, it only lives on the persistence map… so i still dont see the need of having different types per entit, in our domain. Also the name DoctrineId is the example you used (DoctrineEntityId) , can you help me to understand why this can be a bad idea? or not ddd? Thanks!

  • ЌẪřĽőŠ

    Hello again, i tried with symfony 2.8 using doctrine 2.4, and there are things that doesnt seem to be working.. on the basetype for the custom type in the convertToDatabaseValue : return $value->id(); but $value is an scalar, not an object. Thanks!

    • cbuenosvinos

      The post is about “Doctrine 2.5”, in 2.4, you cannot use an object as identifier. Hope it helps!

  • Piotr

    Hi Carlos,
    Great article. Could you tell more why “Embeddables are cool, but limited” and how custom types are better?

    • cbuenosvinos

      When dealing with IDs, if you want an embeddable (ProductId, for example) to be ID (marked as primary in mapping) of an entity (Product), you cannot use the same embeddable (ProductId) from another entity (User) as a reference ID (because it’s marked as primary and in the User entity the ID is another one.

      Apart of that, at the moment of writing, nesting embeddables was impossible. Now, it looks like is fixed, but I should check that.

  • meetmatt

    DoctrineEntityId should be defined abstract and have a declaration of getNamespace:
    public abstract function getNamespace();

    • meetmatt

      Also why don’t you just instantiate custom identity type by using static keyword?

      public function convertToPHPValue($value, AbstractPlatform $platform)
      {
      return new static($value);
      }

  • Gbenga Sodunke

    Hello Carlos,
    This article has really worked for me. However, how can i define the mapping of an identity custom type which is a foreign key in a model?