diff --git a/readme.md b/readme.md index 8fe4bfda..40471a9c 100644 --- a/readme.md +++ b/readme.md @@ -195,15 +195,46 @@ Each of the generator properties (like `name`, `address`, and `lorem`) are calle safeColorName // 'fuchsia' colorName // 'Gainsbor' -## Optional data +## Unique and Optional modifiers -All formatters can be made optional by chaining `optional`. When optional, the formatter will randomly return `NULL`, which can be useful for seeding non-required fields. For example: +Faker provides two special providers, `unique()` and `optional()`, to be called before any provider. `optional()` can be useful for seeding non-required fields, like a mobile telephone number ; `unique()` is required to populate fields that cannot accept twice the same value, like primary identifiers. - $faker->optional->country - -You can skew the randomization towards more nulls or less by passing an argument to `optional()`. At 0, *only* `NULL` is returned. At 1, it is never returned. +```php +// unique() forces providers to return unique values +$values = array(); +for ($i=0; $i < 10; $i++) { + // get a random digit, but always a new one, to avoid duplicates + $values []= $faker->unique()->randomDigit; +} +print_r($values); // [4, 1, 8, 5, 0, 2, 6, 9, 7, 3] - $faker->optional(.75)->country +// providers with a limited range will throw an exception when no new unique value can be generated +$values = array(); +try { + for ($i=0; $i < 10; $i++) { + $values []= $faker->unique()->randomDigitNotNull; + } +} catch (\OverflowException $e) { + echo "There are only 9 unique digits not null, Faker can't generate 10 of them!"; +} + +// you can reset the unique modifier for all providers by passing true as first argument +$faker->unique($reset = true)->randomDigitNotNull; // will not throw OverflowException since unique() was reset +// tip: unique() keeps one array of values per provider + +// optional() sometimes bypasses the provider to return null instead +$values = array(); +for ($i=0; $i < 10; $i++) { + // get a random digit, but also null sometimes + $values []= $faker->optional()->randomDigit; +} +print_r($values); // [1, 4, null, 9, 5, null, null, 4, 6, null] + +// optional takes a weight argument to make the null occurrence impossible (value 0) or systematic (value 1) +$faker->optional($weight = 0.1)->randomDigit; // 10% chance to get null +$faker->optional($weight = 0.9)->randomDigit; // 90% chance to get null +// the default $weight value is 0.5 +``` ## Localization diff --git a/src/Faker/Provider/Base.php b/src/Faker/Provider/Base.php index 62a08f80..654e9a12 100644 --- a/src/Faker/Provider/Base.php +++ b/src/Faker/Provider/Base.php @@ -3,6 +3,7 @@ namespace Faker\Provider; use Faker\Generator; +use Faker\UniqueGenerator; class Base { @@ -11,6 +12,11 @@ class Base */ protected $generator; + /** + * @var \Faker\UniqueGenerator + */ + protected $unique; + /** * @param \Faker\Generator $generator */ @@ -203,4 +209,28 @@ class Base return new \Faker\NullGenerator(); } + + /** + * Chainable method for making any formatter unique. + * + * + * // will never return twice the same value + * $faker->unique()->randomElement(array(1, 2, 3)); + * + * + * @param boolean $reset If set to true, resets the list of existing values + * @param integer $maxRetries Maximum number of retries to find a unique value, + * After which an OverflowExcption is thrown. + * @throws OverflowException When no unique value can be found by iterating $maxRetries times + * + * @return UniqueGenerator A proxy class returning only existing values + */ + public function unique($reset = false, $maxRetries = 10000) + { + if ($reset || !$this->unique) { + $this->unique = new UniqueGenerator($this->generator, $maxRetries); + } + + return $this->unique; + } } diff --git a/src/Faker/UniqueGenerator.php b/src/Faker/UniqueGenerator.php new file mode 100644 index 00000000..63250ad0 --- /dev/null +++ b/src/Faker/UniqueGenerator.php @@ -0,0 +1,49 @@ +unique() + */ +class UniqueGenerator +{ + protected $generator; + protected $maxRetries; + protected $uniques = array(); + + public function __construct(Generator $generator, $maxRetries) + { + $this->generator = $generator; + $this->maxRetries = $maxRetries; + } + + /** + * Catch and proxy all generator calls but return only unique values + */ + public function __get($attribute) + { + return $this->__call($attribute, array()); + } + + /** + * Catch and proxy all generator calls with arguments but return only unique values + */ + public function __call($name, $arguments) + { + if (!isset($this->uniques[$name])) { + $this->uniques[$name] = array(); + } + $i = 0; + do { + $res = call_user_func_array(array($this->generator, $name), $arguments); + $i++; + if ($i > $this->maxRetries) { + throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a unique value', $this->maxRetries)); + } + } while (in_array($res, $this->uniques[$name])); + $this->uniques[$name][]= $res; + + return $res; + } +} diff --git a/test/Faker/Provider/BaseTest.php b/test/Faker/Provider/BaseTest.php index 5371ed06..64967b69 100644 --- a/test/Faker/Provider/BaseTest.php +++ b/test/Faker/Provider/BaseTest.php @@ -131,17 +131,102 @@ class BaseTest extends \PHPUnit_Framework_TestCase $this->assertRegExp('/foo[a-z]Ba\dr/', BaseProvider::bothify('foo?Ba#r')); } - public function testOptionalChainingOfProperty() + public function testOptionalReturnsProviderValueWhenCalledWithWeight1() { - $faker = \Faker\Factory::create(); - $this->assertNotNull($faker->optional(1)->randomNumber); - $this->assertNull($faker->optional(0)->randomNumber); + $faker = new \Faker\Generator(); + $faker->addProvider(new \Faker\Provider\Base($faker)); + $this->assertNotNull($faker->optional(1)->randomDigit); } - public function testOptionalChainingOfMethod() + public function testOptionalReturnsNullWhenCalledWithWeight0() { - $faker = \Faker\Factory::create(); - $this->assertNotNull($faker->optional(1)->randomNumber(4)); - $this->assertNull($faker->optional(0)->randomNumber(4)); + $faker = new \Faker\Generator(); + $faker->addProvider(new \Faker\Provider\Base($faker)); + $this->assertNull($faker->optional(0)->randomDigit); + } + + public function testOptionalAllowsChainingPropertyAccess() + { + $faker = new \Faker\Generator(); + $faker->addProvider(new \Faker\Provider\Base($faker)); + $faker->addProvider(new \ArrayObject(array(1))); // hack because method_exists forbids stubs + $this->assertEquals(1, $faker->optional(1)->count); + $this->assertNull($faker->optional(0)->count); + } + + public function testOptionalAllowsChainingMethodCall() + { + $faker = new \Faker\Generator(); + $faker->addProvider(new \Faker\Provider\Base($faker)); + $faker->addProvider(new \ArrayObject(array(1))); // hack because method_exists forbids stubs + $this->assertEquals(1, $faker->optional(1)->count()); + $this->assertNull($faker->optional(0)->count()); + } + + public function testOptionalAllowsChainingProviderCallRandomlyReturnNull() + { + $faker = new \Faker\Generator(); + $faker->addProvider(new \Faker\Provider\Base($faker)); + $values = array(); + for ($i=0; $i < 10; $i++) { + $values[]= $faker->optional()->randomDigit; + } + $this->assertContains(null, $values); + } + + public function testUniqueAllowsChainingPropertyAccess() + { + $faker = new \Faker\Generator(); + $faker->addProvider(new \Faker\Provider\Base($faker)); + $faker->addProvider(new \ArrayObject(array(1))); // hack because method_exists forbids stubs + $this->assertEquals(1, $faker->unique()->count); + } + + public function testUniqueAllowsChainingMethodCall() + { + $faker = new \Faker\Generator(); + $faker->addProvider(new \Faker\Provider\Base($faker)); + $faker->addProvider(new \ArrayObject(array(1))); // hack because method_exists forbids stubs + $this->assertEquals(1, $faker->unique()->count()); + } + + public function testUniqueReturnsOnlyUniqueValues() + { + $faker = new \Faker\Generator(); + $faker->addProvider(new \Faker\Provider\Base($faker)); + $values = array(); + for ($i=0; $i < 10; $i++) { + $values[]= $faker->unique()->randomDigit; + } + sort($values); + $this->assertEquals(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), $values); + } + + /** + * @expectedException OverflowException + */ + public function testUniqueThrowsExceptionWhenNoUniqueValueCanBeGenerated() + { + $faker = new \Faker\Generator(); + $faker->addProvider(new \Faker\Provider\Base($faker)); + for ($i=0; $i < 11; $i++) { + $faker->unique()->randomDigit; + } + } + + public function testUniqueCanResetUniquesWhenPassedTrueAsArgument() + { + $faker = new \Faker\Generator(); + $faker->addProvider(new \Faker\Provider\Base($faker)); + $values = array(); + for ($i=0; $i < 10; $i++) { + $values[]= $faker->unique()->randomDigit; + } + $values[]= $faker->unique(true)->randomDigit; + for ($i=0; $i < 9; $i++) { + $values[]= $faker->unique()->randomDigit; + } + sort($values); + $this->assertEquals(array(0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9), $values); } }