From 63b6ac4cb5d593dccc77e42582a314372243df45 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 8 Sep 2025 14:51:16 -0400 Subject: [PATCH 01/10] Extract anonymization logic into AnonymizesAttributes trait --- src/Anonymized.php | 98 +++++++----------------------------- src/AnonymizesAttributes.php | 73 +++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 80 deletions(-) create mode 100644 src/AnonymizesAttributes.php diff --git a/src/Anonymized.php b/src/Anonymized.php index 976e794..a5cb908 100644 --- a/src/Anonymized.php +++ b/src/Anonymized.php @@ -3,80 +3,27 @@ namespace DirectoryTree\Anonymize; use Faker\Generator; -use Illuminate\Support\Facades\App; /** * @mixin Anonymizable */ trait Anonymized { + use AnonymizesAttributes; + /** * Whether to enable anonymization for the current model instance. */ protected bool $anonymizeEnabled = true; - /** - * The anonymized attributes for the current model instance and seed. - */ - protected array $anonymizedAttributeCache; - - /** - * The seed for the cached anonymized attributes. - */ - protected string $anonymizedAttributeCacheSeed; - - /** - * Get the anonymize manager instance. - */ - protected static function getAnonymizeManager(): AnonymizeManager - { - return App::make(AnonymizeManager::class); - } - /** * Get the anonymized attributes. */ abstract public function getAnonymizedAttributes(Generator $faker): array; /** - * Get all of the current attributes on the model. + * Execute a callback without anonymization. * - * @return array - */ - public function attributesToArray(): array - { - $attributes = parent::attributesToArray(); - - if ($this->anonymizeEnabled && static::getAnonymizeManager()->isEnabled()) { - $attributes = $this->addAnonymizedAttributesToArray($attributes); - } - - return $attributes; - } - - /** - * Get a plain attribute (not a relationship). - * - * @param string $key - */ - public function getAttributeValue($key): mixed - { - if (! $this->anonymizeEnabled || ! static::getAnonymizeManager()->isEnabled()) { - return parent::getAttributeValue($key); - } - - return $this->getCachedAnonymizedAttributes()[$key] ?? parent::getAttributeValue($key); - } - - /** - * Get the seed for the anonymizable model. - */ - public function getAnonymizableSeed(): string - { - return get_class($this).':'.$this->getAttributeValue('id'); - } - - /** * @template TReturn * * @param callable($this): TReturn $callback @@ -96,41 +43,32 @@ public function withoutAnonymization(callable $callback): mixed } /** - * Make the anonymized attributes. + * Get all of the current attributes on the model. + * + * @return array */ - protected function getCachedAnonymizedAttributes(): array + public function attributesToArray(): array { - return $this->withoutAnonymization(function (): array { - $seed = $this->getAnonymizableSeed(); - - if (! isset($this->anonymizedAttributeCache) || $this->anonymizedAttributeCacheSeed !== $seed) { - $this->anonymizedAttributeCache = $this->getAnonymizedAttributes( - static::getAnonymizeManager()->faker($seed) - ); + $attributes = parent::attributesToArray(); - $this->anonymizedAttributeCacheSeed = $seed; - } + if ($this->anonymizeEnabled && static::getAnonymizeManager()->isEnabled()) { + $attributes = $this->addAnonymizedAttributesToArray($attributes); + } - return $this->anonymizedAttributeCache; - }); + return $attributes; } /** - * Add the anonymized attributes to the attribute array. + * Get a plain attribute (not a relationship). * - * @param array $attributes - * @return array + * @param string $key */ - protected function addAnonymizedAttributesToArray(array $attributes): array + public function getAttributeValue($key): mixed { - foreach ($this->getCachedAnonymizedAttributes() as $key => $value) { - if (! array_key_exists($key, $attributes)) { - continue; - } - - $attributes[$key] = $value; + if (! $this->anonymizeEnabled || ! static::getAnonymizeManager()->isEnabled()) { + return parent::getAttributeValue($key); } - return $attributes; + return $this->getCachedAnonymizedAttributes()[$key] ?? parent::getAttributeValue($key); } } diff --git a/src/AnonymizesAttributes.php b/src/AnonymizesAttributes.php new file mode 100644 index 0000000..6a033c2 --- /dev/null +++ b/src/AnonymizesAttributes.php @@ -0,0 +1,73 @@ + $attributes + * @return array + */ + protected function addAnonymizedAttributesToArray(array $attributes): array + { + foreach ($this->getCachedAnonymizedAttributes() as $key => $value) { + if (! array_key_exists($key, $attributes)) { + continue; + } + + $attributes[$key] = $value; + } + + return $attributes; + } + + /** + * Make the anonymized attributes. + */ + protected function getCachedAnonymizedAttributes(): array + { + return $this->withoutAnonymization(function (): array { + $seed = $this->getAnonymizableSeed(); + + if (! isset($this->anonymizedAttributeCache) || $this->anonymizedAttributeCacheSeed !== $seed) { + $this->anonymizedAttributeCache = $this->getAnonymizedAttributes( + static::getAnonymizeManager()->faker($seed) + ); + + $this->anonymizedAttributeCacheSeed = $seed; + } + + return $this->anonymizedAttributeCache; + }); + } + + /** + * Get the seed for the anonymizable instance. + */ + public function getAnonymizableSeed(): string + { + return get_class($this).':'.$this->getAttributeValue('id'); + } + + /** + * Get the anonymize manager instance. + */ + protected static function getAnonymizeManager(): AnonymizeManager + { + return App::make(AnonymizeManager::class); + } +} From 04cd07c9c95ff819c7fa7d1e24597169f65b7f3f Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 8 Sep 2025 14:52:17 -0400 Subject: [PATCH 02/10] Add AnonymizableResource and AnonymizedResource trait --- src/AnonymizableResource.php | 11 +++++++++++ src/AnonymizedResource.php | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/AnonymizableResource.php create mode 100644 src/AnonymizedResource.php diff --git a/src/AnonymizableResource.php b/src/AnonymizableResource.php new file mode 100644 index 0000000..1c32b07 --- /dev/null +++ b/src/AnonymizableResource.php @@ -0,0 +1,11 @@ + + */ + public function resolve($request = null): array + { + $attributes = parent::resolve($request); + + if (! static::getAnonymizeManager()->isEnabled()) { + return $attributes; + } + + return $this->addAnonymizedAttributesToArray($attributes); + } +} From c0e32e6f8746a9295c9489bf7a967175bad3b7ca Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 8 Sep 2025 15:11:44 -0400 Subject: [PATCH 03/10] Remove withoutAnonymization --- src/AnonymizableResource.php | 11 ----------- src/Anonymized.php | 30 ++---------------------------- src/AnonymizesAttributes.php | 34 ++++++++++++++++------------------ 3 files changed, 18 insertions(+), 57 deletions(-) delete mode 100644 src/AnonymizableResource.php diff --git a/src/AnonymizableResource.php b/src/AnonymizableResource.php deleted file mode 100644 index 1c32b07..0000000 --- a/src/AnonymizableResource.php +++ /dev/null @@ -1,11 +0,0 @@ -anonymizeEnabled; - - $this->anonymizeEnabled = false; - - try { - return $callback($this); - } finally { - $this->anonymizeEnabled = $previous; - } - } - /** * Get all of the current attributes on the model. * @@ -51,7 +25,7 @@ public function attributesToArray(): array { $attributes = parent::attributesToArray(); - if ($this->anonymizeEnabled && static::getAnonymizeManager()->isEnabled()) { + if (static::getAnonymizeManager()->isEnabled()) { $attributes = $this->addAnonymizedAttributesToArray($attributes); } @@ -65,7 +39,7 @@ public function attributesToArray(): array */ public function getAttributeValue($key): mixed { - if (! $this->anonymizeEnabled || ! static::getAnonymizeManager()->isEnabled()) { + if (! static::getAnonymizeManager()->isEnabled()) { return parent::getAttributeValue($key); } diff --git a/src/AnonymizesAttributes.php b/src/AnonymizesAttributes.php index 6a033c2..b33fba8 100644 --- a/src/AnonymizesAttributes.php +++ b/src/AnonymizesAttributes.php @@ -16,6 +16,14 @@ trait AnonymizesAttributes */ protected string $anonymizedAttributeCacheSeed; + /** + * Get the seed for the anonymizable instance. + */ + public function getAnonymizableSeed(): string + { + return get_class($this).':'.($this->getAttributes()[$this->getKeyName()] ?? ''); + } + /** * Add the anonymized attributes to the attribute array. * @@ -40,27 +48,17 @@ protected function addAnonymizedAttributesToArray(array $attributes): array */ protected function getCachedAnonymizedAttributes(): array { - return $this->withoutAnonymization(function (): array { - $seed = $this->getAnonymizableSeed(); - - if (! isset($this->anonymizedAttributeCache) || $this->anonymizedAttributeCacheSeed !== $seed) { - $this->anonymizedAttributeCache = $this->getAnonymizedAttributes( - static::getAnonymizeManager()->faker($seed) - ); + $seed = $this->getAnonymizableSeed(); - $this->anonymizedAttributeCacheSeed = $seed; - } + if (! isset($this->anonymizedAttributeCache) || $this->anonymizedAttributeCacheSeed !== $seed) { + $this->anonymizedAttributeCache = $this->getAnonymizedAttributes( + static::getAnonymizeManager()->faker($seed) + ); - return $this->anonymizedAttributeCache; - }); - } + $this->anonymizedAttributeCacheSeed = $seed; + } - /** - * Get the seed for the anonymizable instance. - */ - public function getAnonymizableSeed(): string - { - return get_class($this).':'.$this->getAttributeValue('id'); + return $this->anonymizedAttributeCache; } /** From 08854fd1710d54cec83ac0339b0dfe71f3f2eb3f Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 8 Sep 2025 15:13:32 -0400 Subject: [PATCH 04/10] Add AnonymizedJsonResource tests --- tests/AnonymizedJsonResource.php | 26 +++++++++++++++++ tests/Unit/AnonymizedJsonResourceTest.php | 28 +++++++++++++++++++ ...ymizedTest.php => AnonymizedModelTest.php} | 14 ---------- 3 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 tests/AnonymizedJsonResource.php create mode 100644 tests/Unit/AnonymizedJsonResourceTest.php rename tests/Unit/{AnonymizedTest.php => AnonymizedModelTest.php} (91%) diff --git a/tests/AnonymizedJsonResource.php b/tests/AnonymizedJsonResource.php new file mode 100644 index 0000000..75e53c6 --- /dev/null +++ b/tests/AnonymizedJsonResource.php @@ -0,0 +1,26 @@ + $faker->name(), + 'address' => $faker->address(), + ]; + } + + public function getAnonymizableSeed(): string + { + return 1; + } +} diff --git a/tests/Unit/AnonymizedJsonResourceTest.php b/tests/Unit/AnonymizedJsonResourceTest.php new file mode 100644 index 0000000..352b0a9 --- /dev/null +++ b/tests/Unit/AnonymizedJsonResourceTest.php @@ -0,0 +1,28 @@ + 'original-name', + 'address' => 'original-address', + ]); + + expect($resource->resolve())->not->toHaveKey('name', 'original-name') + ->and($resource->resolve())->not->toHaveKey('address', 'original-address'); +}); + +it('does not anonymize json resource when anonymization is disabled', function () { + Anonymize::disable(); + + $resource = new AnonymizedJsonResource([ + 'name' => 'Foo Bar', + 'address' => '1600 Pennsylvania Avenue', + ]); + + expect($resource->resolve())->toHaveKey('name', 'Foo Bar') + ->and($resource->resolve())->toHaveKey('address', '1600 Pennsylvania Avenue'); +}); diff --git a/tests/Unit/AnonymizedTest.php b/tests/Unit/AnonymizedModelTest.php similarity index 91% rename from tests/Unit/AnonymizedTest.php rename to tests/Unit/AnonymizedModelTest.php index 06c7c07..1597635 100644 --- a/tests/Unit/AnonymizedTest.php +++ b/tests/Unit/AnonymizedModelTest.php @@ -131,20 +131,6 @@ ->and($model->getAttributeValue('address'))->toBe('1600 Pennsylvania Avenue'); }); -it('disables anonymization within withoutAnonymization block', function () { - Anonymize::enable(); - - $original = [ - 'name' => 'Foo Bar', - 'address' => '1600 Pennsylvania Avenue', - ]; - - $attributes = (new AnonymizedModel($original)) - ->withoutAnonymization(fn ($model) => $model->attributesToArray()); - - expect($attributes)->toBe($original); -}); - it('includes id in seed by default', function () { $id = fake()->randomNumber(); From be57b83b165c8bc978233932f33c4fd4ac7d43e9 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 8 Sep 2025 15:18:06 -0400 Subject: [PATCH 05/10] Tweak test --- tests/Unit/AnonymizedJsonResourceTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Unit/AnonymizedJsonResourceTest.php b/tests/Unit/AnonymizedJsonResourceTest.php index 352b0a9..6291881 100644 --- a/tests/Unit/AnonymizedJsonResourceTest.php +++ b/tests/Unit/AnonymizedJsonResourceTest.php @@ -7,12 +7,12 @@ Anonymize::enable(); $resource = new AnonymizedJsonResource([ - 'name' => 'original-name', - 'address' => 'original-address', + 'name' => 'Foo Bar', + 'address' => '1600 Pennsylvania Avenue', ]); - expect($resource->resolve())->not->toHaveKey('name', 'original-name') - ->and($resource->resolve())->not->toHaveKey('address', 'original-address'); + expect($resource->resolve())->not->toHaveKey('name', 'Foo Bar') + ->and($resource->resolve())->not->toHaveKey('address', '1600 Pennsylvania Avenue'); }); it('does not anonymize json resource when anonymization is disabled', function () { From 7678ce5162bdd9336fdac1b18963ffef7b7ec56e Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 8 Sep 2025 15:24:25 -0400 Subject: [PATCH 06/10] Re-add withoutAnonymization --- src/Anonymized.php | 4 ++-- src/AnonymizedResource.php | 2 +- src/AnonymizesAttributes.php | 44 +++++++++++++++++++++++++++++------- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/Anonymized.php b/src/Anonymized.php index cc85953..4874e0f 100644 --- a/src/Anonymized.php +++ b/src/Anonymized.php @@ -25,7 +25,7 @@ public function attributesToArray(): array { $attributes = parent::attributesToArray(); - if (static::getAnonymizeManager()->isEnabled()) { + if ($this->anonymizeEnabled && static::getAnonymizeManager()->isEnabled()) { $attributes = $this->addAnonymizedAttributesToArray($attributes); } @@ -39,7 +39,7 @@ public function attributesToArray(): array */ public function getAttributeValue($key): mixed { - if (! static::getAnonymizeManager()->isEnabled()) { + if (! $this->anonymizeEnabled || ! static::getAnonymizeManager()->isEnabled()) { return parent::getAttributeValue($key); } diff --git a/src/AnonymizedResource.php b/src/AnonymizedResource.php index 3dc3fae..26ad2a7 100644 --- a/src/AnonymizedResource.php +++ b/src/AnonymizedResource.php @@ -15,7 +15,7 @@ public function resolve($request = null): array { $attributes = parent::resolve($request); - if (! static::getAnonymizeManager()->isEnabled()) { + if (! $this->anonymizeEnabled || ! static::getAnonymizeManager()->isEnabled()) { return $attributes; } diff --git a/src/AnonymizesAttributes.php b/src/AnonymizesAttributes.php index b33fba8..219d566 100644 --- a/src/AnonymizesAttributes.php +++ b/src/AnonymizesAttributes.php @@ -6,6 +6,11 @@ trait AnonymizesAttributes { + /** + * Whether anonymization is enabled for the current instance. + */ + protected bool $anonymizeEnabled = true; + /** * The anonymized attributes for the current instance and seed. */ @@ -16,6 +21,27 @@ trait AnonymizesAttributes */ protected string $anonymizedAttributeCacheSeed; + /** + * Execute a callback without anonymization. + * + * @template TReturn + * + * @param callable($this): TReturn $callback + * @return TReturn + */ + public function withoutAnonymization(callable $callback): mixed + { + $previous = $this->anonymizeEnabled; + + $this->anonymizeEnabled = false; + + try { + return $callback($this); + } finally { + $this->anonymizeEnabled = $previous; + } + } + /** * Get the seed for the anonymizable instance. */ @@ -48,17 +74,19 @@ protected function addAnonymizedAttributesToArray(array $attributes): array */ protected function getCachedAnonymizedAttributes(): array { - $seed = $this->getAnonymizableSeed(); + return $this->withoutAnonymization(function (): array { + $seed = $this->getAnonymizableSeed(); - if (! isset($this->anonymizedAttributeCache) || $this->anonymizedAttributeCacheSeed !== $seed) { - $this->anonymizedAttributeCache = $this->getAnonymizedAttributes( - static::getAnonymizeManager()->faker($seed) - ); + if (! isset($this->anonymizedAttributeCache) || $this->anonymizedAttributeCacheSeed !== $seed) { + $this->anonymizedAttributeCache = $this->getAnonymizedAttributes( + static::getAnonymizeManager()->faker($seed) + ); - $this->anonymizedAttributeCacheSeed = $seed; - } + $this->anonymizedAttributeCacheSeed = $seed; + } - return $this->anonymizedAttributeCache; + return $this->anonymizedAttributeCache; + }); } /** From 6b8db6766644881ce77788c6e8fe43ed1bab9e62 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 8 Sep 2025 15:28:55 -0400 Subject: [PATCH 07/10] Re-add test --- tests/Unit/AnonymizedModelTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Unit/AnonymizedModelTest.php b/tests/Unit/AnonymizedModelTest.php index 1597635..06c7c07 100644 --- a/tests/Unit/AnonymizedModelTest.php +++ b/tests/Unit/AnonymizedModelTest.php @@ -131,6 +131,20 @@ ->and($model->getAttributeValue('address'))->toBe('1600 Pennsylvania Avenue'); }); +it('disables anonymization within withoutAnonymization block', function () { + Anonymize::enable(); + + $original = [ + 'name' => 'Foo Bar', + 'address' => '1600 Pennsylvania Avenue', + ]; + + $attributes = (new AnonymizedModel($original)) + ->withoutAnonymization(fn ($model) => $model->attributesToArray()); + + expect($attributes)->toBe($original); +}); + it('includes id in seed by default', function () { $id = fake()->randomNumber(); From 09e3d3329abf2a491c6ab3a106cbdd08177ee2b5 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Mon, 8 Sep 2025 23:04:39 -0400 Subject: [PATCH 08/10] Use getKey instead of assuming ID --- src/AnonymizesAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AnonymizesAttributes.php b/src/AnonymizesAttributes.php index 219d566..a3c1d01 100644 --- a/src/AnonymizesAttributes.php +++ b/src/AnonymizesAttributes.php @@ -47,7 +47,7 @@ public function withoutAnonymization(callable $callback): mixed */ public function getAnonymizableSeed(): string { - return get_class($this).':'.($this->getAttributes()[$this->getKeyName()] ?? ''); + return get_class($this).':'.$this->getKey(); } /** From efef4e0f85bfa7ec8876c545a17760e3b59e16cb Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 9 Sep 2025 10:09:40 -0400 Subject: [PATCH 09/10] Add missing param doc --- src/AnonymizedResource.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/AnonymizedResource.php b/src/AnonymizedResource.php index 26ad2a7..a8fa7c5 100644 --- a/src/AnonymizedResource.php +++ b/src/AnonymizedResource.php @@ -9,6 +9,7 @@ trait AnonymizedResource /** * Transform the resource into an array. * + * @param \Illuminate\Http\Request|null $request * @return array */ public function resolve($request = null): array From c3f6d4822101e23c2dada70536791079b9403d5c Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 9 Sep 2025 10:14:44 -0400 Subject: [PATCH 10/10] Extract Anonymizable key retrieval into method --- src/Anonymizable.php | 5 +++++ src/AnonymizesAttributes.php | 10 +++++++++- tests/AnonymizedJsonResource.php | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Anonymizable.php b/src/Anonymizable.php index 992560b..cd48267 100644 --- a/src/Anonymizable.php +++ b/src/Anonymizable.php @@ -6,6 +6,11 @@ interface Anonymizable { + /** + * Get the key value used to ensure consistent fake data generation. + */ + public function getAnonymizableKey(): ?string; + /** * Get the seed value used to ensure consistent fake data generation. */ diff --git a/src/AnonymizesAttributes.php b/src/AnonymizesAttributes.php index a3c1d01..089c353 100644 --- a/src/AnonymizesAttributes.php +++ b/src/AnonymizesAttributes.php @@ -42,12 +42,20 @@ public function withoutAnonymization(callable $callback): mixed } } + /** + * Get the key for the anonymizable instance. + */ + public function getAnonymizableKey(): ?string + { + return $this->getKey(); + } + /** * Get the seed for the anonymizable instance. */ public function getAnonymizableSeed(): string { - return get_class($this).':'.$this->getKey(); + return get_class($this).':'.$this->getAnonymizableKey(); } /** diff --git a/tests/AnonymizedJsonResource.php b/tests/AnonymizedJsonResource.php index 75e53c6..ae679b4 100644 --- a/tests/AnonymizedJsonResource.php +++ b/tests/AnonymizedJsonResource.php @@ -19,7 +19,7 @@ public function getAnonymizedAttributes(Generator $faker): array ]; } - public function getAnonymizableSeed(): string + public function getAnonymizableKey(): string { return 1; }