Ransford Okpoti's Blog

May 26, 2011

How To Add A Custom Id Generation Strategy To Doctrine 2.1

Filed under: PHP — Tags: — ranskills @ 12:50 pm

Object-Relational Mapping (ORM) has caught up with PHP. In case you have been living under a rock, here’s the list of the popular ORM solutions in PHP:

With the advent of Doctrine 2.0.0 (which at the time of writing this article is at Beta 2), which requires a minimum of PHP version 5.3, our domain objects (models) are now free from subclassing a mandatory superclass as imposed by its predecessors (previous versions before 2.0.0).
Below is a code snippet of a User model as would be implemented in earlier versions of Doctrine before version 2.0.0.

class User extends BaseUser
{
}

The 2.0 version is totally different in that it now resembles the popular ORM solution in Java, Hibernate.
Now your entity classes can be a plain old PHP object (POPO) with annotations providing persistence information on how to update the persistent store with the field values of an instance of the entity class. There is also the option of using YAML, XML, or plain PHP to provide persistence information to the persistence manager.

I will just stick to the topic of this post by not delving into the nitty-gritties of how to setup and use Doctrine because it can boast of one of the best documentations, and tutorials on how to get it up and running.

In this artcle, we will be looking at how to use a UUID or GUID, for the primary keys of our entities instead of the traditional auto incremented unsigned numeric values, to demonstrate how easy it is to add a custom id generation strategy to your ever popular Doctrine.

If you ever decide to use a Universally Unique Identifier (UUID) or Globally Unique Identifier (GUID) as the primary keys for tables in your database, for various reasons such as having an application used by various clients in various geographical locations in a disconnected environment where each client has its own copy of the database with the likelihood of the various databases been merged into a centralized database server in the near future, then this would be particularly helpful.

1. In order to stick to the adopted convention, id generation classes reside in the Doctrine\ORM\Id namespace or package or directory (or which ever jargon you prefer to use), we let our UUIDGenerator extend the AbstractIdGenerator abstract class.

Below is the AbstractIdGenerator abstract class provided by Doctrine to be extended by all id generation strategies.

namespace Doctrine\ORM\Id;

use Doctrine\ORM\EntityManager;

abstract class AbstractIdGenerator
{
    abstract public function generate(EntityManager $em, $entity);

    public function isPostInsertGenerator()
    {
        return false;
    }
}

Now, we have to provide an implementation for the generate abstract method, and let the isPostInsertGeneration return false since the id will be generated and assigned to the entity/model before insertion into the related table. Various implementations of the UUID algorithm can be found on the net, so if you decide to implement your own version or find a more reliable and appropriate one just feel free to plug it in. By the way, I’ve even forgot the source of the UUID implementation am using in this article, so please forgive me if i haven’t credited the author(s) of the code.

Create UUIDGenerator.php in Doctrine\ORM\Id with the code below.

/**
 * @author Ransford Okpoti
 */
namespace Doctrine\ORM\Id;

use Doctrine\ORM\EntityManager;

class UUIDGenerator extends AbstractIdGenerator {
    /**
     * Generates an ID for the given entity.
     *
     * @param object $entity
     * @return string
     * @override
     */
    public function generate(EntityManager $em, $entity) {
        return self::v4();
    }

    /**
     * @return boolean
     * @override
     */
    public function isPostInsertGenerator() {
        return false;
    }
    

    public static function v3($namespace, $name) {
        if(!self::is_valid($namespace)) return false;

        // Get hexadecimal components of namespace
        $nhex = str_replace(array('-','{','}'), '', $namespace);

        // Binary Value
        $nstr = '';

        // Convert Namespace UUID to bits
        for($i = 0; $i < strlen($nhex); $i+=2) {
            $nstr .= chr(hexdec($nhex[$i].$nhex[$i+1]));
        }

        // Calculate hash value
        $hash = md5($nstr . $name);

        return sprintf('%08s-%04s-%04x-%04x-%12s',

                // 32 bits for "time_low"
                substr($hash, 0, 8),

                // 16 bits for "time_mid"
                substr($hash, 8, 4),

                // 16 bits for "time_hi_and_version",
                // four most significant bits holds version number 3
                (hexdec(substr($hash, 12, 4)) & 0x0fff) | 0x3000,

                // 16 bits, 8 bits for "clk_seq_hi_res",
                // 8 bits for "clk_seq_low",
                // two most significant bits holds zero and one for variant DCE1.1
                (hexdec(substr($hash, 16, 4)) & 0x3fff) | 0x8000,

                // 48 bits for "node"
                substr($hash, 20, 12)
        );
    }

    public static function v4() {
        return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',

                // 32 bits for "time_low"
                mt_rand(0, 0xffff), mt_rand(0, 0xffff),

                // 16 bits for "time_mid"
                mt_rand(0, 0xffff),

                // 16 bits for "time_hi_and_version",
                // four most significant bits holds version number 4
                mt_rand(0, 0x0fff) | 0x4000,

                // 16 bits, 8 bits for "clk_seq_hi_res",
                // 8 bits for "clk_seq_low",
                // two most significant bits holds zero and one for variant DCE1.1
                mt_rand(0, 0x3fff) | 0x8000,

                // 48 bits for "node"
                mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }

    public static function v5($namespace, $name) {
        if(!self::is_valid($namespace)) return false;

        // Get hexadecimal components of namespace
        $nhex = str_replace(array('-','{','}'), '', $namespace);

        // Binary Value
        $nstr = '';

        // Convert Namespace UUID to bits
        for($i = 0;
        $i < strlen($nhex);
        $i+=2) {
            $nstr .= chr(hexdec($nhex[$i].$nhex[$i+1]));
        }

        // Calculate hash value
        $hash = sha1($nstr . $name);

        return sprintf('%08s-%04s-%04x-%04x-%12s',

                // 32 bits for "time_low"
                substr($hash, 0, 8),

                // 16 bits for "time_mid"
                substr($hash, 8, 4),

                // 16 bits for "time_hi_and_version",
                // four most significant bits holds version number 5
                (hexdec(substr($hash, 12, 4)) & 0x0fff) | 0x5000,

                // 16 bits, 8 bits for "clk_seq_hi_res",
                // 8 bits for "clk_seq_low",
                // two most significant bits holds zero and one for variant DCE1.1
                (hexdec(substr($hash, 16, 4)) & 0x3fff) | 0x8000,

                // 48 bits for "node"
                substr($hash, 20, 12)
        );
    }

    public static function is_valid($uuid) {
        return preg_match('/^\{?[0-9a-f]{8}\-?[0-9a-f]{4}\-?[0-9a-f]{4}\-?'.
                '[0-9a-f]{4}\-?[0-9a-f]{12}\}?$/i', $uuid) === 1;
    }

}

2. Next, add GENERATOR_TYPE_UUID = 6, just ensure that the assigned number is unique within the other GENERATOR_TYPE constants, to Doctrine\ORM\Mapping\ClassMetadataInfo.php

const GENERATOR_TYPE_UUID = 6;

3. Finally, we need to add our UUID generation strategy to the list of provided generation strategies. Go to the completeIdGeneratorMapping function in Doctrine\ORM\Mapping\ClassMetadataFactory.php, and add the code below to the switch condition just before the default keyword.

            case ClassMetadata::GENERATOR_TYPE_UUID:
                $class->setIdGenerator(new \Doctrine\ORM\Id\UUIDGenerator());
                break;

I guess you are expecting a step 4? But am sorry to disappoint you, that is all it takes to accomplish our task. We can now make use of UUIDs as demonstrated through the use of annotations below:

/**
 * @Entity
 */
class User{
    /**
     *
     * @Id
     * @Column(type="string", length=36)
     * @GeneratedValue(strategy="UUID")
     */
    private $id;
}

Need I remind you that the downside of this approach is that you’ll have to repeat this process each time you decide to upgrade to a higher version, since Doctrine does not implicitly have UUIDs as one of its id generation strategies and there is no convenient way to programmatically register an id generation strategy at runtime. In my honest opinion, this 2 minutes step is well worth the effort.

About these ads

4 Comments »

  1. Thanks for this. I needed to create a custom Doctrine2 Generator, and it was good to see that someone had already succeeded, and that my approach was valid. I also like the idea of a guid generator that does not make a database query in order to get the guid, as has been implemented by the Doctrine team. Your approach makes more sense to me, from a scalability point of view – let the nodes compute the guid.

    Comment by g2-6d59065471eac77b8e525327fd24d440 — April 4, 2012 @ 1:51 am

  2. Thank you very much for the tutorial, it works very well!!!! but i have a question:

    I wrote my custom id generator that is an extension of the doctrine identity generator but how can i cause db change when
    force db schema update??

    PS: scuse for my english…

    Comment by Emilio (@bistrulli) — April 4, 2012 @ 7:18 pm

    • I am glad it was helpful to you. Can you elaborate on the usage scenario so I get a clearer understanding of what you want to accomplish?
      Thanks.

      Comment by ranskills — April 7, 2012 @ 10:54 am

  3. I know this is rather late to the party but Doctrine ORM does now have a UUID generator built in – I am using version 2.4 and you can use exactly your resulting code (@GeneratedValue(strategy=”UUID”)) straight out of the box. Thought I’d mention it as this appears quite high on the search terms in google :)

    Comment by Matt — October 8, 2013 @ 12:20 pm


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

The WordPress Classic Theme Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: