1<?php
2
3namespace Faker\Provider;
4
5use Faker\Generator;
6use Faker\DefaultGenerator;
7use Faker\UniqueGenerator;
8use Faker\ValidGenerator;
9
10class Base
11{
12    /**
13     * @var \Faker\Generator
14     */
15    protected $generator;
16
17    /**
18     * @var \Faker\UniqueGenerator
19     */
20    protected $unique;
21
22    /**
23     * @param \Faker\Generator $generator
24     */
25    public function __construct(Generator $generator)
26    {
27        $this->generator = $generator;
28    }
29
30    /**
31     * Returns a random number between 0 and 9
32     *
33     * @return integer
34     */
35    public static function randomDigit()
36    {
37        return mt_rand(0, 9);
38    }
39
40    /**
41     * Returns a random number between 1 and 9
42     *
43     * @return integer
44     */
45    public static function randomDigitNotNull()
46    {
47        return mt_rand(1, 9);
48    }
49
50    /**
51     * Generates a random digit, which cannot be $except
52     *
53     * @param int $except
54     * @return int
55     */
56    public static function randomDigitNot($except)
57    {
58        $result = self::numberBetween(0, 8);
59        if ($result >= $except) {
60            $result++;
61        }
62        return $result;
63    }
64
65    /**
66     * Returns a random integer with 0 to $nbDigits digits.
67     *
68     * The maximum value returned is mt_getrandmax()
69     *
70     * @param integer $nbDigits Defaults to a random number between 1 and 9
71     * @param boolean $strict   Whether the returned number should have exactly $nbDigits
72     * @example 79907610
73     *
74     * @return integer
75     */
76    public static function randomNumber($nbDigits = null, $strict = false)
77    {
78        if (!is_bool($strict)) {
79            throw new \InvalidArgumentException('randomNumber() generates numbers of fixed width. To generate numbers between two boundaries, use numberBetween() instead.');
80        }
81        if (null === $nbDigits) {
82            $nbDigits = static::randomDigitNotNull();
83        }
84        $max = pow(10, $nbDigits) - 1;
85        if ($max > mt_getrandmax()) {
86            throw new \InvalidArgumentException('randomNumber() can only generate numbers up to mt_getrandmax()');
87        }
88        if ($strict) {
89            return mt_rand(pow(10, $nbDigits - 1), $max);
90        }
91
92        return mt_rand(0, $max);
93    }
94
95    /**
96     * Return a random float number
97     *
98     * @param int       $nbMaxDecimals
99     * @param int|float $min
100     * @param int|float $max
101     * @example 48.8932
102     *
103     * @return float
104     */
105    public static function randomFloat($nbMaxDecimals = null, $min = 0, $max = null)
106    {
107        if (null === $nbMaxDecimals) {
108            $nbMaxDecimals = static::randomDigit();
109        }
110
111        if (null === $max) {
112            $max = static::randomNumber();
113            if ($min > $max) {
114                $max = $min;
115            }
116        }
117
118        if ($min > $max) {
119            $tmp = $min;
120            $min = $max;
121            $max = $tmp;
122        }
123
124        return round($min + mt_rand() / mt_getrandmax() * ($max - $min), $nbMaxDecimals);
125    }
126
127    /**
128     * Returns a random number between $int1 and $int2 (any order)
129     *
130     * @param integer $int1 default to 0
131     * @param integer $int2 defaults to 32 bit max integer, ie 2147483647
132     * @example 79907610
133     *
134     * @return integer
135     */
136    public static function numberBetween($int1 = 0, $int2 = 2147483647)
137    {
138        $min = $int1 < $int2 ? $int1 : $int2;
139        $max = $int1 < $int2 ? $int2 : $int1;
140        return mt_rand($min, $max);
141    }
142
143    /**
144     * Returns the passed value
145     *
146     * @param mixed $value
147     *
148     * @return mixed
149     */
150    public static function passthrough($value)
151    {
152        return $value;
153    }
154
155    /**
156     * Returns a random letter from a to z
157     *
158     * @return string
159     */
160    public static function randomLetter()
161    {
162        return chr(mt_rand(97, 122));
163    }
164
165    /**
166     * Returns a random ASCII character (excluding accents and special chars)
167     */
168    public static function randomAscii()
169    {
170        return chr(mt_rand(33, 126));
171    }
172
173    /**
174     * Returns randomly ordered subsequence of $count elements from a provided array
175     *
176     * @param  array            $array           Array to take elements from. Defaults to a-c
177     * @param  integer          $count           Number of elements to take.
178     * @param  boolean          $allowDuplicates Allow elements to be picked several times. Defaults to false
179     * @throws \LengthException When requesting more elements than provided
180     *
181     * @return array New array with $count elements from $array
182     */
183    public static function randomElements($array = array('a', 'b', 'c'), $count = 1, $allowDuplicates = false)
184    {
185        $traversables = array();
186
187        if ($array instanceof \Traversable) {
188            foreach ($array as $element) {
189                $traversables[] = $element;
190            }
191        }
192
193        $arr = count($traversables) ? $traversables : $array;
194
195        $allKeys = array_keys($arr);
196        $numKeys = count($allKeys);
197
198        if (!$allowDuplicates && $numKeys < $count) {
199            throw new \LengthException(sprintf('Cannot get %d elements, only %d in array', $count, $numKeys));
200        }
201
202        $highKey = $numKeys - 1;
203        $keys = $elements = array();
204        $numElements = 0;
205
206        while ($numElements < $count) {
207            $num = mt_rand(0, $highKey);
208
209            if (!$allowDuplicates) {
210                if (isset($keys[$num])) {
211                    continue;
212                }
213                $keys[$num] = true;
214            }
215
216            $elements[] = $arr[$allKeys[$num]];
217            $numElements++;
218        }
219
220        return $elements;
221    }
222
223    /**
224     * Returns a random element from a passed array
225     *
226     * @param  array $array
227     * @return mixed
228     */
229    public static function randomElement($array = array('a', 'b', 'c'))
230    {
231        if (!$array || ($array instanceof \Traversable && !count($array))) {
232            return null;
233        }
234        $elements = static::randomElements($array, 1);
235
236        return $elements[0];
237    }
238
239    /**
240     * Returns a random key from a passed associative array
241     *
242     * @param  array $array
243     * @return int|string|null
244     */
245    public static function randomKey($array = array())
246    {
247        if (!$array) {
248            return null;
249        }
250        $keys = array_keys($array);
251        $key = $keys[mt_rand(0, count($keys) - 1)];
252
253        return $key;
254    }
255
256    /**
257     * Returns a shuffled version of the argument.
258     *
259     * This function accepts either an array, or a string.
260     *
261     * @example $faker->shuffle([1, 2, 3]); // [2, 1, 3]
262     * @example $faker->shuffle('hello, world'); // 'rlo,h eold!lw'
263     *
264     * @see shuffleArray()
265     * @see shuffleString()
266     *
267     * @param array|string $arg The set to shuffle
268     * @return array|string The shuffled set
269     */
270    public static function shuffle($arg = '')
271    {
272        if (is_array($arg)) {
273            return static::shuffleArray($arg);
274        }
275        if (is_string($arg)) {
276            return static::shuffleString($arg);
277        }
278        throw new \InvalidArgumentException('shuffle() only supports strings or arrays');
279    }
280
281    /**
282     * Returns a shuffled version of the array.
283     *
284     * This function does not mutate the original array. It uses the
285     * Fisher–Yates algorithm, which is unbiased, together with a Mersenne
286     * twister random generator. This function is therefore more random than
287     * PHP's shuffle() function, and it is seedable.
288     *
289     * @link http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
290     *
291     * @example $faker->shuffleArray([1, 2, 3]); // [2, 1, 3]
292     *
293     * @param array $array The set to shuffle
294     * @return array The shuffled set
295     */
296    public static function shuffleArray($array = array())
297    {
298        $shuffledArray = array();
299        $i = 0;
300        reset($array);
301        foreach ($array as $key => $value) {
302            if ($i == 0) {
303                $j = 0;
304            } else {
305                $j = mt_rand(0, $i);
306            }
307            if ($j == $i) {
308                $shuffledArray[]= $value;
309            } else {
310                $shuffledArray[]= $shuffledArray[$j];
311                $shuffledArray[$j] = $value;
312            }
313            $i++;
314        }
315        return $shuffledArray;
316    }
317
318    /**
319     * Returns a shuffled version of the string.
320     *
321     * This function does not mutate the original string. It uses the
322     * Fisher–Yates algorithm, which is unbiased, together with a Mersenne
323     * twister random generator. This function is therefore more random than
324     * PHP's shuffle() function, and it is seedable. Additionally, it is
325     * UTF8 safe if the mb extension is available.
326     *
327     * @link http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
328     *
329     * @example $faker->shuffleString('hello, world'); // 'rlo,h eold!lw'
330     *
331     * @param string $string The set to shuffle
332     * @param string $encoding The string encoding (defaults to UTF-8)
333     * @return string The shuffled set
334     */
335    public static function shuffleString($string = '', $encoding = 'UTF-8')
336    {
337        if (function_exists('mb_strlen')) {
338            // UTF8-safe str_split()
339            $array = array();
340            $strlen = mb_strlen($string, $encoding);
341            for ($i = 0; $i < $strlen; $i++) {
342                $array []= mb_substr($string, $i, 1, $encoding);
343            }
344        } else {
345            $array = str_split($string, 1);
346        }
347        return implode('', static::shuffleArray($array));
348    }
349
350    private static function replaceWildcard($string, $wildcard = '#', $callback = 'static::randomDigit')
351    {
352        if (($pos = strpos($string, $wildcard)) === false) {
353            return $string;
354        }
355        for ($i = $pos, $last = strrpos($string, $wildcard, $pos) + 1; $i < $last; $i++) {
356            if ($string[$i] === $wildcard) {
357                $string[$i] = call_user_func($callback);
358            }
359        }
360        return $string;
361    }
362
363    /**
364     * Replaces all hash sign ('#') occurrences with a random number
365     * Replaces all percentage sign ('%') occurrences with a not null number
366     *
367     * @param  string $string String that needs to bet parsed
368     * @return string
369     */
370    public static function numerify($string = '###')
371    {
372        // instead of using randomDigit() several times, which is slow,
373        // count the number of hashes and generate once a large number
374        $toReplace = array();
375        if (($pos = strpos($string, '#')) !== false) {
376            for ($i = $pos, $last = strrpos($string, '#', $pos) + 1; $i < $last; $i++) {
377                if ($string[$i] === '#') {
378                    $toReplace[] = $i;
379                }
380            }
381        }
382        if ($nbReplacements = count($toReplace)) {
383            $maxAtOnce = strlen((string) mt_getrandmax()) - 1;
384            $numbers = '';
385            $i = 0;
386            while ($i < $nbReplacements) {
387                $size = min($nbReplacements - $i, $maxAtOnce);
388                $numbers .= str_pad(static::randomNumber($size), $size, '0', STR_PAD_LEFT);
389                $i += $size;
390            }
391            for ($i = 0; $i < $nbReplacements; $i++) {
392                $string[$toReplace[$i]] = $numbers[$i];
393            }
394        }
395        $string = self::replaceWildcard($string, '%', 'static::randomDigitNotNull');
396
397        return $string;
398    }
399
400    /**
401     * Replaces all question mark ('?') occurrences with a random letter
402     *
403     * @param  string $string String that needs to bet parsed
404     * @return string
405     */
406    public static function lexify($string = '????')
407    {
408        return self::replaceWildcard($string, '?', 'static::randomLetter');
409    }
410
411    /**
412     * Replaces hash signs ('#') and question marks ('?') with random numbers and letters
413     * An asterisk ('*') is replaced with either a random number or a random letter
414     *
415     * @param  string $string String that needs to bet parsed
416     * @return string
417     */
418    public static function bothify($string = '## ??')
419    {
420        $string = self::replaceWildcard($string, '*', function () {
421            return mt_rand(0, 1) ? '#' : '?';
422        });
423        return static::lexify(static::numerify($string));
424    }
425
426    /**
427     * Replaces * signs with random numbers and letters and special characters
428     *
429     * @example $faker->asciify(''********'); // "s5'G!uC3"
430     *
431     * @param  string $string String that needs to bet parsed
432     * @return string
433     */
434    public static function asciify($string = '****')
435    {
436        return preg_replace_callback('/\*/u', 'static::randomAscii', $string);
437    }
438
439    /**
440     * Transforms a basic regular expression into a random string satisfying the expression.
441     *
442     * @example $faker->regexify('[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}'); // sm0@y8k96a.ej
443     *
444     * Regex delimiters '/.../' and begin/end markers '^...$' are ignored.
445     *
446     * Only supports a small subset of the regex syntax. For instance,
447     * unicode, negated classes, unbounded ranges, subpatterns, back references,
448     * assertions, recursive patterns, and comments are not supported. Escaping
449     * support is extremely fragile.
450     *
451     * This method is also VERY slow. Use it only when no other formatter
452     * can generate the fake data you want. For instance, prefer calling
453     * `$faker->email` rather than `regexify` with the previous regular
454     * expression.
455     *
456     * Also note than `bothify` can probably do most of what this method does,
457     * but much faster. For instance, for a dummy email generation, try
458     * `$faker->bothify('?????????@???.???')`.
459     *
460     * @see https://github.com/icomefromthenet/ReverseRegex for a more robust implementation
461     *
462     * @param string $regex A regular expression (delimiters are optional)
463     * @return string
464     */
465    public static function regexify($regex = '')
466    {
467        // ditch the anchors
468        $regex = preg_replace('/^\/?\^?/', '', $regex);
469        $regex = preg_replace('/\$?\/?$/', '', $regex);
470        // All {2} become {2,2}
471        $regex = preg_replace('/\{(\d+)\}/', '{\1,\1}', $regex);
472        // Single-letter quantifiers (?, *, +) become bracket quantifiers ({0,1}, {0,rand}, {1, rand})
473        $regex = preg_replace('/(?<!\\\)\?/', '{0,1}', $regex);
474        $regex = preg_replace('/(?<!\\\)\*/', '{0,' . static::randomDigitNotNull() . '}', $regex);
475        $regex = preg_replace('/(?<!\\\)\+/', '{1,' . static::randomDigitNotNull() . '}', $regex);
476        // [12]{1,2} becomes [12] or [12][12]
477        $regex = preg_replace_callback('/(\[[^\]]+\])\{(\d+),(\d+)\}/', function ($matches) {
478            return str_repeat($matches[1], Base::randomElement(range($matches[2], $matches[3])));
479        }, $regex);
480        // (12|34){1,2} becomes (12|34) or (12|34)(12|34)
481        $regex = preg_replace_callback('/(\([^\)]+\))\{(\d+),(\d+)\}/', function ($matches) {
482            return str_repeat($matches[1], Base::randomElement(range($matches[2], $matches[3])));
483        }, $regex);
484        // A{1,2} becomes A or AA or \d{3} becomes \d\d\d
485        $regex = preg_replace_callback('/(\\\?.)\{(\d+),(\d+)\}/', function ($matches) {
486            return str_repeat($matches[1], Base::randomElement(range($matches[2], $matches[3])));
487        }, $regex);
488        // (this|that) becomes 'this' or 'that'
489        $regex = preg_replace_callback('/\((.*?)\)/', function ($matches) {
490            return Base::randomElement(explode('|', str_replace(array('(', ')'), '', $matches[1])));
491        }, $regex);
492        // All A-F inside of [] become ABCDEF
493        $regex = preg_replace_callback('/\[([^\]]+)\]/', function ($matches) {
494            return '[' . preg_replace_callback('/(\w|\d)\-(\w|\d)/', function ($range) {
495                return implode('', range($range[1], $range[2]));
496            }, $matches[1]) . ']';
497        }, $regex);
498        // All [ABC] become B (or A or C)
499        $regex = preg_replace_callback('/\[([^\]]+)\]/', function ($matches) {
500            return Base::randomElement(str_split($matches[1]));
501        }, $regex);
502        // replace \d with number and \w with letter and . with ascii
503        $regex = preg_replace_callback('/\\\w/', 'static::randomLetter', $regex);
504        $regex = preg_replace_callback('/\\\d/', 'static::randomDigit', $regex);
505        $regex = preg_replace_callback('/(?<!\\\)\./', 'static::randomAscii', $regex);
506        // remove remaining backslashes
507        $regex = str_replace('\\', '', $regex);
508        // phew
509        return $regex;
510    }
511
512    /**
513     * Converts string to lowercase.
514     * Uses mb_string extension if available.
515     *
516     * @param  string $string String that should be converted to lowercase
517     * @return string
518     */
519    public static function toLower($string = '')
520    {
521        return extension_loaded('mbstring') ? mb_strtolower($string, 'UTF-8') : strtolower($string);
522    }
523
524    /**
525     * Converts string to uppercase.
526     * Uses mb_string extension if available.
527     *
528     * @param  string $string String that should be converted to uppercase
529     * @return string
530     */
531    public static function toUpper($string = '')
532    {
533        return extension_loaded('mbstring') ? mb_strtoupper($string, 'UTF-8') : strtoupper($string);
534    }
535
536    /**
537     * Chainable method for making any formatter optional.
538     *
539     * @param float|integer $weight Set the probability of receiving a null value.
540     *                              "0" will always return null, "1" will always return the generator.
541     *                              If $weight is an integer value, then the same system works
542     *                              between 0 (always get false) and 100 (always get true).
543     * @return mixed|null
544     */
545    public function optional($weight = 0.5, $default = null)
546    {
547        // old system based on 0.1 <= $weight <= 0.9
548        // TODO: remove in v2
549        if ($weight > 0 && $weight < 1 && mt_rand() / mt_getrandmax() <= $weight) {
550            return $this->generator;
551        }
552
553        // new system with percentage
554        if (is_int($weight) && mt_rand(1, 100) <= $weight) {
555            return $this->generator;
556        }
557
558        return new DefaultGenerator($default);
559    }
560
561    /**
562     * Chainable method for making any formatter unique.
563     *
564     * <code>
565     * // will never return twice the same value
566     * $faker->unique()->randomElement(array(1, 2, 3));
567     * </code>
568     *
569     * @param boolean $reset      If set to true, resets the list of existing values
570     * @param integer $maxRetries Maximum number of retries to find a unique value,
571     *                                       After which an OverflowException is thrown.
572     * @throws \OverflowException When no unique value can be found by iterating $maxRetries times
573     *
574     * @return UniqueGenerator A proxy class returning only non-existing values
575     */
576    public function unique($reset = false, $maxRetries = 10000)
577    {
578        if ($reset || !$this->unique) {
579            $this->unique = new UniqueGenerator($this->generator, $maxRetries);
580        }
581
582        return $this->unique;
583    }
584
585    /**
586     * Chainable method for forcing any formatter to return only valid values.
587     *
588     * The value validity is determined by a function passed as first argument.
589     *
590     * <code>
591     * $values = array();
592     * $evenValidator = function ($digit) {
593     *   return $digit % 2 === 0;
594     * };
595     * for ($i=0; $i < 10; $i++) {
596     *   $values []= $faker->valid($evenValidator)->randomDigit;
597     * }
598     * print_r($values); // [0, 4, 8, 4, 2, 6, 0, 8, 8, 6]
599     * </code>
600     *
601     * @param Closure $validator  A function returning true for valid values
602     * @param integer $maxRetries Maximum number of retries to find a unique value,
603     *                            After which an OverflowException is thrown.
604     * @throws \OverflowException When no valid value can be found by iterating $maxRetries times
605     *
606     * @return ValidGenerator A proxy class returning only valid values
607     */
608    public function valid($validator = null, $maxRetries = 10000)
609    {
610        return new ValidGenerator($this->generator, $validator, $maxRetries);
611    }
612}
613