PHP GuidCreator with Fallbacks

      No Comments on PHP GuidCreator with Fallbacks

I’m revisiting PHP after a long time away.  My time in the Microsoft .NET world has gotten me accustomed to the built-in type of Guid and the complementary storage in SQL Server of a “uniqueidentifier”.  Even with PHP 7, there is no data type representing “universally unique identifier”.  Searching “php guid” will take you to the documentation for the com_create_guid function.  The discussion following outlines that that function isn’t always defined and that there are a few alternative methods to generate an equivalently structured value while also assuring randomness.

These are all fine and good but we don’t want to have to cut and paste these solutions into every project where we need a GUID.  Nor do we want to worry about what method is or isn’t supported in the case our code is distributed across different systems.  Let’s first distill what it means to create a GUID into an interface called IGuidCreator.

interface IGuidCreator {
	function isSupported();
    function createGuid();
}

We have two methods: one to declare if this particular method is supported on the current system and one to actually generate the GUID. In PHP 7.x, we could declare these functions as returning bool and string, respectively. The most basic implementation of this interface would be a wrapper around (or an adapter) the com_create_guid function, as outlined in the documentation.

class ComGuidCreator implements IGuidCreator {
    function isSupported() {
        return function_exists('com_create_guid');
    }

    function createGuid()
    {
        return trim(com_create_guid(), '{}');
    }
}

That same documentation outlines a method implemented using the mt_rand function. Let’s make another adapter that implements the same interface.

class RandGuidCreator implements IGuidCreator {
    function isSupported() {
        return function_exists('mt_rand');
    }

    function createGuid()
    {
        return strtolower(sprintf('%04X%04X-%04X-%04X-%04X-%04X%04X%04X', $this->rand(), $this->rand(), $this->rand(), mt_rand(16384, 20479), mt_rand(32768, 49151), $this->rand(), $this->rand(), $this->rand()));
    }

    private function rand() {
        return mt_rand(0, 65535);
    }
}

A proposed method that is more a “cryptographically strong algorithm” leverages the openssl_random_pseudo_btyes method.

class OpenSslGuidCreator implements IGuidCreator {
    function isSupported() {
        return function_exists('openssl_random_pseudo_bytes');
    }

    function createGuid()
    {
        $data = openssl_random_pseudo_bytes(16);
        $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
        $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10

        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
    }
}

So now we have 3 different solutions that we can use interchangeably because they all implement the same interface. We might even call these different “strategies“. But that also means we’d have to make a conscience decision each time we want to make a GUID as to which we will use. We could use some complex system of dependency injection but that seems like overkill in this case. What if we just built a implementation that pulled them all together and picked the best strategy that was supported on the current system.

class GuidCreator implements IGuidCreator {
    private $creator;

    function __construct() {
		$potentialCreators = [
			new ComGuidCreator(),
			new OpenSslGuidCreator(),
			new RandGuidCreator()
		];

		foreach ($potentialCreators as $creator) {
			if ($this->creator == null && $creator->isSupported()) {
				$this->creator = $creator;
			}
		}

		if ($this->creator == null) {
			throw new \Exception("No supported GUID creator classes");
		}
    }

	function isSupported() {
		return true;
	}

    function createGuid() {
        return $this->creator->createGuid();
    }
}

We create a list of potential implementations in the order of best to worst. Since com_create_guid is the most commonly recommended, we’ll put that first. There’s is some evidence that openssl_random_pseudo_bytes performs better than mt_rand, so we’ll put it next. In our constructor, we iterate through the list and if none of the strategies are supported (unlikely), we throw an exception. That means we can go ahead and return true all the time for isSupported since we must have passed that step already.

Now the only way to know for sure that all these implementations are equivalent is to run them through some unit tests. I’m using a combination of PHPUnit and Laravel in my application.

class GuidCreatorTest extends TestCase {
    function testComGuidCreator() {
		$this->doTest(new ComGuidCreator());
    }

    function testOpenSslGuidCreator() {
        $this->doTest(new OpenSslGuidCreator());
    }

    function testRandGuidCreator() {
        $this->doTest(new RandGuidCreator());
    }

    function testGuidCreator() {
        $this->doTest(new GuidCreator());
    }

    private function doTest(IGuidCreator $creator) {
		if ($creator->isSupported()) {
			$guid = $creator->createGuid();
			echo("$guid\n");

			$r = '[0-9a-f]';
			$this->assertRegExp("/^$r{8}-$r{4}-$r{4}-$r{4}-$r{12}$/", $guid);

			$guid2 = $creator->createGuid();
			echo("$guid2\n");
			$this->assertNotEquals($guid, $guid2);
		}
		else {
			$this->markTestIncomplete(get_class($creator) . ' is not supported.');
		}
    }
}

For every implementation, I check that it is supported. If not, I skip the test. If it is, I check that it matches my regex for being structurally accurate. I also generate a second GUID immediately after to hopefully check that two identical GUIDs aren’t created in succession.

guidcreator-phpunit-results

Conclusion

We’ve taken three separate solutions to a common problem and standardized them around a common interface.  While that seems like an extra step, it opens up much more flexibility down the road.  It’s good practice and keeps our mindset into breaking down our problems in understanding the inputs and outputs.

Leave a Reply