From cf28ef9adebae2a1a9ec3ae98d89e6dcc3e81673 Mon Sep 17 00:00:00 2001 From: Teal Bauer Date: Thu, 15 Jan 2026 12:58:00 +0100 Subject: [PATCH] fix(UniqueGenerator): prevent memory exhaustion when chaining unique()->optional() When calling unique()->optional()->method(), the __call handler would serialize the returned ChanceGenerator object (which contains the entire Generator with all providers) rather than the final value. This caused exponential memory growth. Adding explicit optional() and valid() methods ensures proper generator chaining - ChanceGenerator now wraps UniqueGenerator, delegating back for actual value generation while preserving uniqueness tracking. Fixes #1027 --- src/UniqueGenerator.php | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/UniqueGenerator.php b/src/UniqueGenerator.php index fef167b6d1..ab6012b541 100644 --- a/src/UniqueGenerator.php +++ b/src/UniqueGenerator.php @@ -45,6 +45,45 @@ public function ext(string $id) return new self($this->generator->ext($id), $this->maxRetries, $this->uniques); } + /** + * Returns a ChanceGenerator that wraps this UniqueGenerator. + * + * This ensures that chaining unique()->optional() works correctly + * by having ChanceGenerator delegate back to UniqueGenerator for + * actual value generation, rather than bypassing uniqueness tracking. + * + * @param float $weight A probability between 0 and 1, 0 means that we always get the default value. + * @param mixed $default The default value to return when the random check fails. + * + * @return ChanceGenerator + */ + public function optional(float $weight = 0.5, $default = null) + { + if ($weight > 1) { + trigger_deprecation('fakerphp/faker', '1.16', 'First argument ($weight) to method "optional()" must be between 0 and 1. You passed %f, we assume you meant %f.', $weight, $weight / 100); + $weight /= 100; + } + + return new ChanceGenerator($this, $weight, $default); + } + + /** + * Returns a ValidGenerator that wraps this UniqueGenerator. + * + * This ensures that chaining unique()->valid() works correctly + * by having ValidGenerator delegate back to UniqueGenerator for + * actual value generation. + * + * @param \Closure|null $validator A function returning true for valid values + * @param int $maxRetries Maximum number of retries to find a valid value + * + * @return ValidGenerator + */ + public function valid(?\Closure $validator = null, int $maxRetries = 10000) + { + return new ValidGenerator($this, $validator, $maxRetries); + } + /** * Catch and proxy all generator calls but return only unique values *