├── .gitignore ├── .travis.yml ├── tests ├── bootstrap.php └── Dflydev │ └── Tests │ └── IdentityGenerator │ ├── Generator │ ├── RandomStringGeneratorTest.php │ ├── Base32CrockfordGeneratorTest.php │ └── ArbitraryBaseGeneratorTest.php │ ├── Exception │ ├── MobsUnsupportedExceptionTest.php │ └── DataStoreExceptionTest.php │ └── IdentityGeneratorTest.php ├── src └── Dflydev │ └── IdentityGenerator │ ├── Exception │ ├── Exception.php │ ├── NonUniqueIdentityException.php │ ├── DataStoreException.php │ ├── MobsUnsupportedException.php │ └── GenerateException.php │ ├── Generator │ ├── GeneratorInterface.php │ ├── RandomNumberGenerator.php │ ├── AbstractSeededGenerator.php │ ├── Base32CrockfordGenerator.php │ ├── ArbitraryBaseGenerator.php │ └── RandomStringGenerator.php │ ├── DataStore │ └── DataStoreInterface.php │ └── IdentityGenerator.php ├── phpunit.xml.dist ├── LICENSE ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3.3 5 | - 5.3 6 | - 5.4 7 | 8 | before_script: 9 | - wget -nc http://getcomposer.org/composer.phar 10 | - php composer.phar install --dev 11 | 12 | script: phpunit --coverage-text --verbose 13 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('Dflydev\\Tests\\IdentityGenerator', 'tests'); 14 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/Dflydev/Tests/IdentityGenerator 6 | 7 | 8 | 9 | 10 | 11 | ./src/Dflydev/IdentityGenerator/ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/Generator/GeneratorInterface.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | interface GeneratorInterface 20 | { 21 | /** 22 | * Generate a string suitable to represent an identity 23 | * 24 | * @return string 25 | */ 26 | public function generateIdentity(); 27 | } 28 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/DataStore/DataStoreInterface.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | interface DataStoreInterface 20 | { 21 | /** 22 | * Store an identity 23 | * 24 | * @param string $identity Identity 25 | * @param string|null $mob Group identifier 26 | */ 27 | public function storeIdentity($identity, $mob = null); 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Dragonfly Development Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dflydev/identity-generator", 3 | "type": "library", 4 | "description": "Provides a standard interface for generating unique identifiers.", 5 | "homepage": "https://github.com/dflydev/dflydev-identity-generator", 6 | "keywords": ["identity", "generator", "generation", "id"], 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Dragonfly Development Inc.", 11 | "email": "info@dflydev.com", 12 | "homepage": "http://dflydev.com" 13 | }, 14 | { 15 | "name": "Beau Simensen", 16 | "email": "beau@dflydev.com", 17 | "homepage": "http://beausimensen.com" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=5.3.2" 22 | }, 23 | "require-dev": { 24 | "dflydev/base32-crockford": "1.*" 25 | }, 26 | "suggest": { 27 | "dflydev/base32-crockford": "Required for Base32 Crockford based Generators" 28 | }, 29 | "autoload": { 30 | "psr-0": { 31 | "Dflydev\\IdentityGenerator": "src" 32 | } 33 | }, 34 | "extra": { 35 | "branch-alias": { 36 | "dev-master": "1.0-dev" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/Generator/RandomNumberGenerator.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class RandomNumberGenerator implements GeneratorInterface 20 | { 21 | protected $minValue; 22 | protected $maxValue; 23 | 24 | /** 25 | * Constructor 26 | * 27 | * @param int $minValue Minimum value 28 | * @param int $maxValue Maximum value (defaults to mt_getrandmax()) 29 | */ 30 | public function __construct($minValue = 0, $maxValue = null) 31 | { 32 | $this->minValue = $minValue; 33 | $this->maxValue = null === $maxValue ? mt_getrandmax() : $maxValue; 34 | } 35 | 36 | /** 37 | * {@inheritdocs} 38 | */ 39 | public function generateIdentity() 40 | { 41 | return mt_rand($this->minValue, $this->maxValue); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/Generator/AbstractSeededGenerator.php: -------------------------------------------------------------------------------- 1 | seedGenerator = $seedGenerator; 22 | 23 | return $this; 24 | } 25 | 26 | protected function generateSeed() 27 | { 28 | if (null === $this->seedGenerator) { 29 | $this->seedGenerator = new RandomNumberGenerator; 30 | } 31 | 32 | return $this->lastSeedValue = $this->seedGenerator->generateIdentity(); 33 | } 34 | 35 | /** 36 | * Last seed value created by seed generator 37 | * 38 | * There are few reasons (outside of testing) that might require this 39 | * value to be exposed. Probably best to forget that it exists. 40 | * 41 | * @return int|null 42 | */ 43 | public function getLastSeedValue() 44 | { 45 | return $this->lastSeedValue; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Dflydev/Tests/IdentityGenerator/Generator/RandomStringGeneratorTest.php: -------------------------------------------------------------------------------- 1 | generateIdentity(); 24 | 25 | $this->assertEquals(10, strlen($identity)); 26 | } 27 | } 28 | 29 | public function testGeneratorWithAllowedCharacters() 30 | { 31 | $generator = new RandomStringGenerator(10, 'a'); 32 | 33 | $identity = $generator->generateIdentity(); 34 | 35 | $this->assertEquals(10, strlen($identity)); 36 | $this->assertEquals('aaaaaaaaaa', $identity); 37 | 38 | $generator = new RandomStringGenerator(10, 'abc'); 39 | 40 | $identity = $generator->generateIdentity(); 41 | 42 | $this->assertEquals(10, strlen($identity)); 43 | $this->assertEquals(0, preg_match('/[^abc]/', $identity)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/Exception/NonUniqueIdentityException.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class NonUniqueIdentityException extends Exception 21 | { 22 | protected $identity; 23 | protected $mob; 24 | 25 | /** 26 | * Constructor 27 | * 28 | * @param string $identity Identifier 29 | * @param string $mob Group identifier 30 | */ 31 | public function __construct($identity, $mob = null) 32 | { 33 | $mobString = $mob ? ' (with mob '.$mob.')' : ''; 34 | parent::__construct("Could not store generated identity as it is not unique: ".$identity.$mobString); 35 | $this->identity = $identity; 36 | $this->mob = $mob; 37 | } 38 | 39 | /** 40 | * Identity 41 | * 42 | * @return string 43 | */ 44 | public function getIdentity() 45 | { 46 | return $this->identity; 47 | } 48 | 49 | /** 50 | * Mob 51 | * 52 | * @return string 53 | */ 54 | public function getMob() 55 | { 56 | return $this->mob; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/Generator/Base32CrockfordGenerator.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class Base32CrockfordGenerator extends AbstractSeededGenerator 22 | { 23 | protected $withChecksum = false; 24 | 25 | /** 26 | * Constructor 27 | * 28 | * @param GeneratorInterface|null $seedGenerator 29 | */ 30 | public function __construct(GeneratorInterface $seedGenerator = null) 31 | { 32 | $this->setSeedGenerator($seedGenerator); 33 | } 34 | 35 | /** 36 | * {@inheritdocs} 37 | */ 38 | public function generateIdentity() 39 | { 40 | return $this->withChecksum 41 | ? Crockford::encodeWithChecksum($this->generateSeed()) 42 | : Crockford::encode($this->generateSeed()) 43 | ; 44 | } 45 | 46 | /** 47 | * Generate strings with checksum? 48 | * 49 | * @param bool $withChecksum 50 | * 51 | * @return Base32CrockfordGenerator 52 | */ 53 | public function setWithChecksum($withChecksum) 54 | { 55 | $this->withChecksum = $withChecksum; 56 | 57 | return $this; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Dflydev/Tests/IdentityGenerator/Exception/MobsUnsupportedExceptionTest.php: -------------------------------------------------------------------------------- 1 | fail('Expected exception was not thrown'); 23 | } catch (MobsUnsupportedException $e) { 24 | $this->assertEquals('A', $e->getIdentity()); 25 | $this->assertEquals('B', $e->getMob()); 26 | $this->assertEquals('Mobs are unsupported under current configuration. A (with mob B)', $e->getMessage()); 27 | } 28 | } 29 | 30 | public function testThrowMessage() 31 | { 32 | try { 33 | throw new MobsUnsupportedException('A', 'B', 'Some Reason Here.'); 34 | $this->fail('Expected exception was not thrown'); 35 | } catch (MobsUnsupportedException $e) { 36 | $this->assertEquals('A', $e->getIdentity()); 37 | $this->assertEquals('B', $e->getMob()); 38 | $this->assertEquals('Mobs are unsupported under current configuration. Some Reason Here. A (with mob B)', $e->getMessage()); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/Exception/DataStoreException.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class DataStoreException extends Exception 22 | { 23 | protected $identity; 24 | protected $mob; 25 | 26 | /** 27 | * Constructor 28 | * 29 | * @param \Exception $previous Previous exception 30 | * @param string $identity Identifier 31 | * @param string $mob Group identifier 32 | */ 33 | public function __construct(\Exception $previous, $identity, $mob = null) 34 | { 35 | $mobString = $mob ? ' (with mob '.$mob.')' : ''; 36 | parent::__construct("Could not store generated identity for an unexpected reason: ".$identity.$mobString, 0, $previous); 37 | $this->identity = $identity; 38 | $this->mob = $mob; 39 | } 40 | 41 | /** 42 | * Identity 43 | * 44 | * @return string 45 | */ 46 | public function getIdentity() 47 | { 48 | return $this->identity; 49 | } 50 | 51 | /** 52 | * Mob 53 | * 54 | * @return string 55 | */ 56 | public function getMob() 57 | { 58 | return $this->mob; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/Exception/MobsUnsupportedException.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class MobsUnsupportedException extends Exception 21 | { 22 | protected $identity; 23 | protected $mob; 24 | 25 | /** 26 | * Constructor 27 | * 28 | * @param string $identity Identifier 29 | * @param string $mob Group identifier 30 | * @param string $message Message 31 | */ 32 | public function __construct($identity, $mob, $message = null) 33 | { 34 | $mobString = $mob ? ' (with mob '.$mob.')' : ''; 35 | $messageString = $message ? ' ' . $message : ''; 36 | parent::__construct('Mobs are unsupported under current configuration.'.$messageString.' '.$identity.$mobString); 37 | $this->identity = $identity; 38 | $this->mob = $mob; 39 | } 40 | 41 | /** 42 | * Identity 43 | * 44 | * @return string 45 | */ 46 | public function getIdentity() 47 | { 48 | return $this->identity; 49 | } 50 | 51 | /** 52 | * Mob 53 | * 54 | * @return string 55 | */ 56 | public function getMob() 57 | { 58 | return $this->mob; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Dflydev/Tests/IdentityGenerator/Exception/DataStoreExceptionTest.php: -------------------------------------------------------------------------------- 1 | fail('Expected exception was not thrown'); 24 | } catch (DataStoreException $e) { 25 | $this->assertEquals('A', $e->getIdentity()); 26 | $this->assertEquals('B', $e->getMob()); 27 | $this->assertEquals('Could not store generated identity for an unexpected reason: A (with mob B)', $e->getMessage()); 28 | $this->assertEquals('Previous Exception', $e->getPrevious()->getMessage()); 29 | } 30 | } 31 | 32 | public function testThrowWithoutMob() 33 | { 34 | $previous = new \Exception('Previous Exception'); 35 | try { 36 | throw new DataStoreException($previous, 'A'); 37 | $this->fail('Expected exception was not thrown'); 38 | } catch (DataStoreException $e) { 39 | $this->assertEquals('A', $e->getIdentity()); 40 | $this->assertNull($e->getMob()); 41 | $this->assertEquals('Could not store generated identity for an unexpected reason: A', $e->getMessage()); 42 | $this->assertEquals('Previous Exception', $e->getPrevious()->getMessage()); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/Exception/GenerateException.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class GenerateException extends Exception 22 | { 23 | protected $reasons = array(); 24 | 25 | /** 26 | * Constructor 27 | * 28 | * @param string $identity Identifier 29 | * @param string $mob Group identifier 30 | * @param array $reasons Array of reasons (exceptions) that the exception was thrown. 31 | * @param \Exception $previous Previous exception 32 | */ 33 | public function __construct($identity = null, $mob = null, array $reasons = null, \Exception $previous = null) 34 | { 35 | $identityString = $identity ? ': '.$identity : ''; 36 | $mobString = $mob ? ' (with mob '.$mob.')' : ''; 37 | parent::__construct("Could not generate and store a new identity".$identityString.$mobString, 0, $previous); 38 | $this->identity = $identity; 39 | $this->mob = $mob; 40 | $this->reasons = $reasons; 41 | } 42 | 43 | /** 44 | * Identifier 45 | * 46 | * @return string 47 | */ 48 | public function getIdentity() 49 | { 50 | return $this->identity; 51 | } 52 | 53 | /** 54 | * Group identifier 55 | * 56 | * @return string 57 | */ 58 | public function getMob() 59 | { 60 | return $this->mob; 61 | } 62 | 63 | /** 64 | * Array of reasons (exceptions) that the exception was thrown. 65 | * 66 | * @return array 67 | */ 68 | public function getReasons() 69 | { 70 | return $this->reasons; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/Generator/ArbitraryBaseGenerator.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class ArbitraryBaseGenerator extends AbstractSeededGenerator 20 | { 21 | public static $BASE32_CROCKFORD = array( 22 | '0', '1', '2', '3', '4', 23 | '5', '6', '7', '8', '9', 24 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 25 | 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 26 | 'T', 'V', 'W', 'X', 'Y', 'Z', 27 | ); 28 | 29 | /** 30 | * Encode number using only values from symbols 31 | * 32 | * @param array $symbols Symbols to choose from 33 | * @param int $number Number to encode 34 | * 35 | * @return string 36 | * @throws \RuntimeException 37 | */ 38 | public static function encode($symbols, $number) 39 | { 40 | if (!is_numeric($number)) { 41 | throw new \RuntimeException("Specified number '{$number}' is not numeric"); 42 | } 43 | 44 | if (!$number) { 45 | return $symbols[0]; 46 | } 47 | 48 | $base = count($symbols); 49 | 50 | $response = array(); 51 | while ($number) { 52 | $remainder = $number % $base; 53 | $number = (int) ($number/$base); 54 | $response[] = $symbols[$remainder]; 55 | } 56 | 57 | return implode('', array_reverse($response)); 58 | 59 | } 60 | 61 | /** 62 | * Constructor 63 | * 64 | * @param GeneratorInterface|null $seedGenerator Underlying seed generator 65 | */ 66 | public function __construct(GeneratorInterface $seedGenerator = null) 67 | { 68 | $this->setSeedGenerator($seedGenerator); 69 | $this->allowedChars = static::$BASE32_CROCKFORD; 70 | } 71 | 72 | /** 73 | * {@inheritdocs} 74 | */ 75 | public function generateIdentity() 76 | { 77 | return static::encode($this->allowedChars, $this->generateSeed()); 78 | } 79 | 80 | /** 81 | * Set allowed chars 82 | * 83 | * @param array $allowedChars 84 | * 85 | * @return ArbitraryBaseGenerator 86 | */ 87 | public function setAllowedChars(array $allowedChars) 88 | { 89 | $this->allowedChars = $allowedChars; 90 | 91 | return $this; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Identity Generator 2 | ================== 3 | 4 | Provide a standard interface for generating unique identifiers. 5 | 6 | The purpose of this library is to solve the problem of generating 7 | unique values that are "guaranteed" to be unique and are suitable 8 | to be used as an identity for objects. 9 | 10 | 11 | Why? 12 | ---- 13 | 14 | While it is generally not a problem for data stores that implement 15 | sequential or auto increment ID fields natively to provide a 16 | guaranteed unique identity, there are use cases where it makes 17 | sense to randomize identity such that it is not able to be guessed 18 | as it follows no predictable pattern. 19 | 20 | As such, most of the `GeneratorInterface` implementations provided 21 | by this library will be somewhat random in nature. This does not 22 | preclude the use of sequential `GeneratorInterface` implementations. 23 | 24 | 25 | Mobs 26 | ---- 27 | 28 | Mobs are used to group identities. If a mob is specified, the 29 | value requested to be stored by the data store need only be 30 | unique to that mob. This allows a single data store to potentially 31 | store and manage unique identities across several namespaces. 32 | 33 | The name 'group' can be ambiguous (and is a reserved word for potential 34 | data stores) so 'mob' was chosen based on the definition: 35 | 36 | > any group or collection of persons or things. 37 | 38 | 39 | 40 | The "Uniqueness Guarantee" 41 | -------------------------- 42 | 43 | The uniqueness guarantee is accomplished by attempting to add 44 | a generated value to a data store. The data store needs to be 45 | capable of knowing whether or not the value passed in is unique. 46 | This guarantee of uniqueness is only as strong as the given 47 | data store's ability to effectively determine uniqueness of a 48 | given value. 49 | 50 | A data store should throw `NonUniqueIdentityException` in the 51 | case that a given value is not unique. The requested identity 52 | and mob values are available via this exception. 53 | 54 | 55 | What About Collisions? 56 | ---------------------- 57 | 58 | The `IdentityGenerator` will make `maxRetries` attempts to 59 | store generated values into the data store. This should allow 60 | for handling a finite number of collisions gracefully. 61 | 62 | Should `maxRetries` be exhausted, `GenerateException` is thrown. 63 | It will contain a list of `NonUniqueIdentityException` exceptions 64 | equal to the number of `maxRetries`. 65 | 66 | 67 | 68 | Requirements 69 | ------------ 70 | 71 | * PHP 5.3+ 72 | 73 | 74 | License 75 | ------- 76 | 77 | MIT, see LICENSE. 78 | 79 | 80 | Community 81 | --------- 82 | 83 | If you have questions or want to help out, join us in the 84 | [#dflydev](irc://irc.freenode.net/#dflydev) channel on irc.freenode.net. -------------------------------------------------------------------------------- /tests/Dflydev/Tests/IdentityGenerator/Generator/Base32CrockfordGeneratorTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('The Base32 Crockford library is not available'); 24 | } 25 | } 26 | 27 | public function testGenerator() 28 | { 29 | for ($i = 0; $i < 1000; $i++) { 30 | $generator = new Base32CrockfordGenerator(); 31 | 32 | $identity = $generator->generateIdentity(); 33 | 34 | $this->assertEquals(Crockford::normalize($identity), $identity); 35 | $this->assertEquals(Crockford::decode($identity), $generator->getLastSeedValue()); 36 | } 37 | } 38 | 39 | public function testGeneratorWithChecksum() 40 | { 41 | for ($i = 0; $i < 1000; $i++) { 42 | $generator = new Base32CrockfordGenerator(); 43 | $generator->setWithChecksum(true); 44 | 45 | $identity = $generator->generateIdentity(); 46 | 47 | $this->assertEquals(Crockford::normalize($identity), $identity); 48 | $this->assertEquals(Crockford::decodeWithChecksum($identity), $generator->getLastSeedValue()); 49 | } 50 | } 51 | 52 | /** 53 | * @dataProvider provideKnownValues 54 | */ 55 | public function testKnownValues($decodedValue, $encodedValue, $encodedValueWithChecksum) 56 | { 57 | $randomNumberGenerator = new RandomNumberGenerator($decodedValue, $decodedValue); 58 | $generator = new Base32CrockfordGenerator($randomNumberGenerator); 59 | 60 | $identity = $generator->generateIdentity(); 61 | 62 | $this->assertEquals($decodedValue, $generator->getLastSeedValue()); 63 | $this->assertEquals($encodedValue, $identity); 64 | 65 | $generator->setWithChecksum(true); 66 | 67 | $identity = $generator->generateIdentity(); 68 | 69 | $this->assertEquals($decodedValue, $generator->getLastSeedValue()); 70 | $this->assertEquals($encodedValueWithChecksum, $identity); 71 | } 72 | 73 | public function provideKnownValues() 74 | { 75 | return array( 76 | array(0, '0', '00', false), 77 | array(1, '1', '11', false), 78 | array(2, '2', '22', false), 79 | array(194, '62', '629', false), 80 | array(456789, 'DY2N', 'DY2NR', false), 81 | array(398373, 'C515', 'C515Z', false), 82 | array(519571, 'FVCK', 'FVCKH', false), 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/IdentityGenerator.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class IdentityGenerator 25 | { 26 | protected $dataStore; 27 | protected $generator; 28 | protected $mob; 29 | protected $maxRetries = 0; 30 | 31 | /** 32 | * Constructor 33 | * 34 | * @param DataStoreInterface $dataStore 35 | * @param GeneratorInterface $generator 36 | */ 37 | public function __construct(DataStoreInterface $dataStore, GeneratorInterface $generator) 38 | { 39 | $this->dataStore = $dataStore; 40 | $this->generator = $generator; 41 | } 42 | 43 | /** 44 | * Generate an identity string 45 | * 46 | * @param string|null $suggestion 47 | * 48 | * @return string 49 | * 50 | * @throws \Dflydev\IdentityGenerator\Exception\Exception 51 | */ 52 | public function generate($suggestion = null) 53 | { 54 | if (null !== $suggestion) { 55 | $this->dataStore->storeIdentity($suggestion, $this->mob); 56 | 57 | return $suggestion; 58 | } 59 | 60 | $exceptions = array(); 61 | for ($i = 0; $i <= $this->maxRetries; $i++) { 62 | $generatedIdentity = null; 63 | try { 64 | $generatedIdentity = $this->generator->generateIdentity(); 65 | $this->dataStore->storeIdentity($generatedIdentity, $this->mob); 66 | 67 | return $generatedIdentity; 68 | } catch (NonUniqueIdentityException $e) { 69 | // We expect non unique identity exceptions so this is fine. 70 | // Collect them and move on and try again if we still have 71 | // some tries left. 72 | $exceptions[] = $e; 73 | } catch (\Exception $e) { 74 | // All other exceptions are unexpected. 75 | throw new GenerateException($generatedIdentity, $this->mob, $exceptions, $e); 76 | } 77 | } 78 | 79 | throw new GenerateException(null, $this->mob, $exceptions); 80 | } 81 | 82 | /** 83 | * Set mob 84 | * 85 | * @param string|null $mob 86 | * 87 | * @return IdentityGenerator 88 | */ 89 | public function setMob($mob = null) 90 | { 91 | $this->mob = $mob; 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * Set max retries 98 | * 99 | * @param int $maxRetries 100 | * 101 | * @return IdentityGenerator 102 | */ 103 | public function setMaxRetries($maxRetries) 104 | { 105 | $this->maxRetries = $maxRetries; 106 | 107 | return $this; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Dflydev/IdentityGenerator/Generator/RandomStringGenerator.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class RandomStringGenerator implements GeneratorInterface 20 | { 21 | /** 22 | * Letters and numbers 23 | * 24 | * @var array 25 | */ 26 | public static $lettersAndNumbers = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 27 | 28 | /** 29 | * Base32 Crockford allowable characters 30 | * 31 | * @var array 32 | */ 33 | public static $base32Crockford = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; 34 | 35 | /** 36 | * Length of generated string 37 | * 38 | * @var int 39 | */ 40 | protected $length; 41 | 42 | /** 43 | * Allowed characters 44 | * 45 | * @var string 46 | */ 47 | protected $allowedCharacters; 48 | 49 | /** 50 | * Constructor 51 | * 52 | * @param int $length Length of generated string 53 | * @param string $allowedCharacters Allowed characters in generated string 54 | */ 55 | public function __construct($length, $allowedCharacters = null) 56 | { 57 | $this->length = $length; 58 | $this->allowedCharacters = null !== $allowedCharacters 59 | ? $allowedCharacters 60 | : static::$lettersAndNumbers; 61 | } 62 | 63 | /** 64 | * Set the allowed characters 65 | * 66 | * @param array $allowedCharacters Allowed characters 67 | * 68 | * @return RandomStringGenerator 69 | */ 70 | public function setAllowedCharacters(array $allowedCharacters = null) 71 | { 72 | $this->allowedCharacters = $allowedCharacters; 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function generateIdentity() 81 | { 82 | $out = ''; 83 | $allowedMax = strlen($this->allowedCharacters) - 1; 84 | 85 | if ($allowedMax < 0) { 86 | throw new \LogicException("No allowed characters were specified, cannot generate an identity"); 87 | } 88 | 89 | while ( strlen($out) < $this->length ) { 90 | $out .= $this->allowedCharacters[rand(0, $allowedMax)]; 91 | } 92 | 93 | return $out; 94 | } 95 | 96 | /** 97 | * Create a letters and numbers (mixed case) Random String Generator 98 | * 99 | * @param int $length Length of generated string 100 | * 101 | * @return RandomStringGenerator 102 | */ 103 | public static function createLettersAndNumbers($length) 104 | { 105 | return new static($length, static::$lettersAndNumbers); 106 | } 107 | 108 | /** 109 | * Create a Base32 Crockford Random String Generator 110 | * 111 | * @param int $length Length of generated string 112 | * 113 | * @return RandomStringGenerator 114 | */ 115 | public static function createBase32Crockford($length) 116 | { 117 | return new static($length, static::$base32Crockford); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/Dflydev/Tests/IdentityGenerator/Generator/ArbitraryBaseGeneratorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($encodedValue, ArbitraryBaseGenerator::encode(ArbitraryBaseGenerator::$BASE32_CROCKFORD, $decodedValue)); 25 | } 26 | 27 | /** 28 | * @dataProvider provideKnownBase32CrockfordValues 29 | */ 30 | public function testKnownBase32CrockfordValues($decodedValue, $encodedValue) 31 | { 32 | $randomNumberGenerator = new RandomNumberGenerator($decodedValue, $decodedValue); 33 | $generator = new ArbitraryBaseGenerator($randomNumberGenerator); 34 | 35 | $identity = $generator->generateIdentity(); 36 | 37 | $this->assertEquals($decodedValue, $generator->getLastSeedValue()); 38 | $this->assertEquals($encodedValue, $identity); 39 | } 40 | 41 | /** 42 | * @dataProvider provideKnownValues 43 | */ 44 | public function testEncode($allowedChars, $decodedValue, $encodedValue) 45 | { 46 | $this->assertEquals($encodedValue, ArbitraryBaseGenerator::encode($allowedChars, $decodedValue)); 47 | } 48 | 49 | /** 50 | * @dataProvider provideKnownValues 51 | */ 52 | public function testKnownValues($allowedChars, $decodedValue, $encodedValue) 53 | { 54 | $randomNumberGenerator = new RandomNumberGenerator($decodedValue, $decodedValue); 55 | $generator = new ArbitraryBaseGenerator($randomNumberGenerator); 56 | 57 | $generator->setAllowedChars($allowedChars); 58 | 59 | $identity = $generator->generateIdentity(); 60 | 61 | $this->assertEquals($decodedValue, $generator->getLastSeedValue()); 62 | $this->assertEquals($encodedValue, $identity); 63 | } 64 | 65 | public function testEncodeException() 66 | { 67 | try { 68 | ArbitraryBaseGenerator::encode(ArbitraryBaseGenerator::$BASE32_CROCKFORD, 'hello'); 69 | } catch (\RuntimeException $e) { 70 | $this->assertEquals('Specified number \'hello\' is not numeric', $e->getMessage()); 71 | } 72 | } 73 | 74 | public function provideKnownBase32CrockfordValues() 75 | { 76 | return array( 77 | array(0, '0', false), 78 | array(1, '1', false), 79 | array(2, '2', false), 80 | array(194, '62', false), 81 | array(456789, 'DY2N', false), 82 | array(398373, 'C515', false), 83 | array(519571, 'FVCK', false), 84 | ); 85 | } 86 | 87 | public function provideKnownValues() 88 | { 89 | return array( 90 | array(array('a', 'b', 'c'), 0, 'a'), 91 | array(array('a', 'b', 'c'), 1, 'b'), 92 | array(array('a', 'b', 'c'), 2, 'c'), 93 | array(array('a', 'b', 'c'), 3, 'ba'), 94 | array(array('a', 'b', 'c'), 4, 'bb'), 95 | array(array('a', 'b', 'c'), 5, 'bc'), 96 | array(array('a', 'b', 'c'), 6, 'ca'), 97 | array(array('a', 'b', 'c'), 7, 'cb'), 98 | array(array('a', 'b', 'c'), 8, 'cc'), 99 | array(array('a', 'b', 'c'), 9, 'baa'), 100 | array(array('a', 'b', 'c'), 10, 'bab'), 101 | array(array('a', 'b', 'c'), 11, 'bac'), 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Dflydev/Tests/IdentityGenerator/IdentityGeneratorTest.php: -------------------------------------------------------------------------------- 1 | getMock('Dflydev\IdentityGenerator\DataStore\DataStoreInterface'); 26 | $generator = $this->getMock('Dflydev\IdentityGenerator\Generator\GeneratorInterface'); 27 | 28 | $dataStore 29 | ->expects($this->once()) 30 | ->method('storeIdentity') 31 | ->with($this->equalTo($suggestion)) 32 | ->will($this->returnValue($suggestion . ' // results from data store')); 33 | 34 | $identityGenerator = new IdentityGenerator($dataStore, $generator); 35 | if (null !== $mob) { 36 | $identityGenerator->setMob($mob); 37 | } 38 | 39 | $this->assertEquals($suggestion, $identityGenerator->generate($suggestion)); 40 | } 41 | 42 | /** 43 | * @dataProvider provideGenerateWithSuggestion 44 | */ 45 | public function testGenerateWithSuggestionException($suggestion, $mob = null) 46 | { 47 | $dataStore = $this->getMock('Dflydev\IdentityGenerator\DataStore\DataStoreInterface'); 48 | $generator = $this->getMock('Dflydev\IdentityGenerator\Generator\GeneratorInterface'); 49 | 50 | $dataStore 51 | ->expects($this->any()) 52 | ->method('storeIdentity') 53 | ->with($this->equalTo($suggestion)) 54 | ->will($this->throwException(new GenerateException($suggestion, $mob))); 55 | 56 | $identityGenerator = new IdentityGenerator($dataStore, $generator); 57 | if (null !== $mob) { 58 | $identityGenerator->setMob($mob); 59 | } 60 | 61 | try { 62 | $identityGenerator->generate($suggestion); 63 | 64 | $this->fail("Should throw an exception"); 65 | } catch (GenerateException $e) { 66 | $this->assertContains('new identity: '.$suggestion, $e->getMessage()); 67 | $this->assertEquals($suggestion, $e->getIdentity()); 68 | $this->assertEquals($mob, $e->getMob()); 69 | } 70 | } 71 | 72 | public function testGenerateNoSuggestion() 73 | { 74 | foreach (array(null, 'MOB000') as $mob) { 75 | $dataStore = $this->getMock('Dflydev\IdentityGenerator\DataStore\DataStoreInterface'); 76 | $generator = $this->getMock('Dflydev\IdentityGenerator\Generator\GeneratorInterface'); 77 | 78 | $generator 79 | ->expects($this->any()) 80 | ->method('generateIdentity') 81 | ->will($this->returnValue('AAA')); 82 | $dataStore 83 | ->expects($this->any()) 84 | ->method('storeIdentity'); 85 | 86 | $identityGenerator = new IdentityGenerator($dataStore, $generator); 87 | if (null !== $mob) { 88 | $identityGenerator->setMob($mob); 89 | } 90 | 91 | $this->assertEquals('AAA', $identityGenerator->generate()); 92 | } 93 | } 94 | 95 | public function testGenerateMaxRetriesExhausted() 96 | { 97 | foreach (array(0, 5, 25) as $maxRetries) { 98 | foreach (array(null, 'MOB000') as $mob) { 99 | $dataStore = $this->getMock('Dflydev\IdentityGenerator\DataStore\DataStoreInterface'); 100 | $generator = $this->getMock('Dflydev\IdentityGenerator\Generator\GeneratorInterface'); 101 | 102 | $generator 103 | ->expects($this->any()) 104 | ->method('generateIdentity') 105 | ->will($this->returnValue('AAA')); 106 | $dataStore 107 | ->expects($this->any()) 108 | ->method('storeIdentity') 109 | ->will($this->throwException(new NonUniqueIdentityException('AAA', $mob))); 110 | 111 | $identityGenerator = new IdentityGenerator($dataStore, $generator); 112 | $identityGenerator->setMaxRetries($maxRetries); 113 | if (null !== $mob) { 114 | $identityGenerator->setMob($mob); 115 | } 116 | 117 | try { 118 | $identityGenerator->generate(); 119 | } catch (GenerateException $e) { 120 | $this->assertEquals($maxRetries + 1, count($e->getReasons())); 121 | foreach ($e->getReasons() as $reason) { 122 | $this->assertEquals('AAA', $reason->getIdentity()); 123 | $this->assertEquals($mob, $reason->getMob()); 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | public function testGenerateMaxRetriesException() 131 | { 132 | foreach (array(null, 'MOB000') as $mob) { 133 | $dataStore = $this->getMock('Dflydev\IdentityGenerator\DataStore\DataStoreInterface'); 134 | $generator = $this->getMock('Dflydev\IdentityGenerator\Generator\GeneratorInterface'); 135 | 136 | $generator 137 | ->expects($this->any()) 138 | ->method('generateIdentity') 139 | ->will($this->returnValue('AAA')); 140 | $dataStore 141 | ->expects($this->any()) 142 | ->method('storeIdentity') 143 | ->will($this->throwException(new \RuntimeException("Who Knows Why"))); 144 | 145 | $identityGenerator = new IdentityGenerator($dataStore, $generator); 146 | if (null !== $mob) { 147 | $identityGenerator->setMob($mob); 148 | } 149 | 150 | try { 151 | $identityGenerator->generate(); 152 | } catch (GenerateException $e) { 153 | $this->assertEquals('Who Knows Why', $e->getPrevious()->getMessage()); 154 | } 155 | } 156 | } 157 | 158 | public function provideGenerateWithSuggestion() 159 | { 160 | return array( 161 | array('foo'), 162 | array('foo', 'bar'), 163 | array('hello'), 164 | array('hello', 'world'), 165 | ); 166 | } 167 | } 168 | --------------------------------------------------------------------------------