diff --git a/src/Console/Command/GenerateCommand.php b/src/Console/Command/GenerateCommand.php index 78a16692..09ff88d2 100644 --- a/src/Console/Command/GenerateCommand.php +++ b/src/Console/Command/GenerateCommand.php @@ -18,6 +18,10 @@ use Symfony\Component\Console\Output\OutputInterface; use Rhumsaa\Uuid\Console\Exception; use Rhumsaa\Uuid\Uuid; +use Rhumsaa\Uuid\Generator\CombGenerator; +use Rhumsaa\Uuid\Codec\GuidStringCodec; +use Rhumsaa\Uuid\FeatureSet; +use Rhumsaa\Uuid\UuidFactory; /** * Provides the console command to generate UUIDs @@ -60,6 +64,18 @@ protected function configure() InputOption::VALUE_REQUIRED, 'Generate count UUIDs instead of just a single one.', 1 + ) + ->addOption( + 'comb', + null, + InputOption::VALUE_NONE, + 'For version 4 UUIDs, uses the COMB strategy to generate the random data.' + ) + ->addOption( + 'guid', + 'g', + InputOption::VALUE_NONE, + 'Returns a GUID formatted UUID.' ); } @@ -82,6 +98,21 @@ protected function execute(InputInterface $input, OutputInterface $output) ) ); + if (((bool) $input->getOption('guid')) == true) { + $features = new FeatureSet(true); + + Uuid::setFactory(new UuidFactory($features)); + } + + if (((bool) $input->getOption('comb')) === true) { + Uuid::getFactory()->setRandomGenerator( + new CombGenerator( + Uuid::getFactory()->getRandomGenerator(), + Uuid::getFactory()->getNumberConverter() + ) + ); + } + for ($i = 0; $i < $count; $i++) { $uuids[] = $this->createUuid( $input->getArgument('version'), diff --git a/src/Generator/CombGenerator.php b/src/Generator/CombGenerator.php new file mode 100644 index 00000000..24e34282 --- /dev/null +++ b/src/Generator/CombGenerator.php @@ -0,0 +1,68 @@ +converter = $numberConverter; + $this->randomGenerator = $generator; + $this->timestampBytes = 6; + } + + /** + * (non-PHPdoc) @see \Rhumsaa\Uuid\RandomGeneratorInterface::generate() + */ + public function generate($length) + { + if ($length < $this->timestampBytes || $length < 0) { + throw new \InvalidArgumentException('Length must be a positive integer.'); + } + + $hash = ''; + + if ($this->timestampBytes > 0 && $length > $this->timestampBytes) { + $hash = $this->randomGenerator->generate($length - $this->timestampBytes); + } + + $lsbTime = str_pad($this->converter->toHex($this->timestamp()), $this->timestampBytes * 2, '0', STR_PAD_LEFT); + + if ($this->timestampBytes > 0 && strlen($lsbTime) > $this->timestampBytes * 2) { + $lsbTime = substr($lsbTime, 0 - ($this->timestampBytes * 2)); + } + + return hex2bin(str_pad(bin2hex($hash), $length - $this->timestampBytes, '0')) . hex2bin($lsbTime); + } + + /** + * Returns current timestamp as integer, precise to 0.00001 seconds + * @return number + */ + private function timestamp() + { + $time = explode(' ', microtime(false)); + + return $time[1] . substr($time[0], 2, 5); + } +} \ No newline at end of file diff --git a/src/UuidFactory.php b/src/UuidFactory.php index 98857420..632cb4d5 100644 --- a/src/UuidFactory.php +++ b/src/UuidFactory.php @@ -74,6 +74,21 @@ public function getCodec() return $this->codec; } + public function getRandomGenerator() + { + return $this->randomGenerator; + } + + public function getNumberConverter() + { + return $this->numberConverter; + } + + public function getTimeConverter() + { + return $this->timeConverter; + } + public function setTimeConverter(TimeConverterInterface $converter) { $this->timeConverter = $converter; diff --git a/tests/UuidTest.php b/tests/UuidTest.php index 8d609fe1..d43f1f2c 100644 --- a/tests/UuidTest.php +++ b/tests/UuidTest.php @@ -4,6 +4,7 @@ use Rhumsaa\Uuid\Provider\Time\SystemTimeProvider; use Rhumsaa\Uuid\Provider\Time\FixedTimeProvider; +use Rhumsaa\Uuid\Generator\CombGenerator; class UuidTest extends TestCase { @@ -785,6 +786,50 @@ public function testUuid4WithoutOpensslRandomPseudoBytes() $this->assertEquals(4, $uuid->getVersion()); } + /** + * Tests that generated UUID's using COMB are sequential + * @return string + */ + public function testUuid4Comb() + { + $mock = $this->getMock('Rhumsaa\Uuid\RandomGeneratorInterface'); + $mock->expects($this->any()) + ->method('generate') + ->willReturnCallback(function ($length) + { + // Makes first fields of UUIDs equal + return str_pad('', $length, '0'); + }); + + $factory = new UuidFactory(); + $generator = new CombGenerator($mock, $factory->getNumberConverter()); + $factory->setRandomGenerator($generator); + + $previous = $factory->uuid4(); + + for ($i = 0; $i < 1000; $i ++) { + $uuid = $factory->uuid4(); + $this->assertGreaterThan($previous->toString(), $uuid->toString()); + + $previous = $uuid; + } + } + + /** + * Test that COMB UUID's have a version 4 flag + */ + public function testUuid4CombVersion() + { + $factory = new UuidFactory(); + $generator = new CombGenerator(RandomGeneratorFactory::getGenerator(), $factory->getNumberConverter()); + + $factory->setRandomGenerator($generator); + + $uuid = $factory->uuid4(); + + $this->assertEquals(4, $uuid->getVersion()); + } + /** * The "python.org" UUID is a known entity, so we're testing that this * library generates a matching UUID for the same name.