├── src ├── Events │ ├── AnonymizationDisabled.php │ └── AnonymizationEnabled.php ├── Anonymizable.php ├── AnonymizedResource.php ├── Facades │ └── Anonymize.php ├── AnonymizeServiceProvider.php ├── Anonymized.php ├── AnonymizeManager.php └── AnonymizesAttributes.php ├── LICENSE ├── LICENSE.md ├── composer.json └── README.md /src/Events/AnonymizationDisabled.php: -------------------------------------------------------------------------------- 1 | $attributes 13 | * @return array 14 | */ 15 | public function toAnonymized(array $attributes): array 16 | { 17 | if (! $this->anonymizeEnabled || ! static::getAnonymizeManager()->isEnabled()) { 18 | return $attributes; 19 | } 20 | 21 | return $this->addAnonymizedAttributesToArray($attributes); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Facades/Anonymize.php: -------------------------------------------------------------------------------- 1 | app->singleton(AnonymizeManager::class, static fn (Application $app): AnonymizeManager => ( 19 | new AnonymizeManager($app->bound(Generator::class) ? $app->make(Generator::class) : Factory::create()) 20 | )); 21 | } 22 | 23 | /** 24 | * Get the services provided by the provider. 25 | */ 26 | public function provides(): array 27 | { 28 | return [AnonymizeManager::class]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 DirectoryTree 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Steve Bauman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Anonymized.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function attributesToArray(): array 25 | { 26 | $attributes = parent::attributesToArray(); 27 | 28 | if ($this->anonymizeEnabled && static::getAnonymizeManager()->isEnabled()) { 29 | $attributes = $this->addAnonymizedAttributesToArray($attributes); 30 | } 31 | 32 | return $attributes; 33 | } 34 | 35 | /** 36 | * Get a plain attribute (not a relationship). 37 | * 38 | * @param string $key 39 | */ 40 | public function getAttributeValue($key): mixed 41 | { 42 | if (! $this->anonymizeEnabled || ! static::getAnonymizeManager()->isEnabled()) { 43 | return parent::getAttributeValue($key); 44 | } 45 | 46 | return $this->getCachedAnonymizedAttributes()[$key] ?? parent::getAttributeValue($key); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/AnonymizeManager.php: -------------------------------------------------------------------------------- 1 | faker->seed(is_string($seed) ? crc32($seed) : $seed); 30 | 31 | return $this->faker; 32 | } 33 | 34 | /** 35 | * Enable anonymization. 36 | */ 37 | public function enable(): void 38 | { 39 | $this->enabled = true; 40 | 41 | Event::dispatch(new AnonymizationEnabled); 42 | } 43 | 44 | /** 45 | * Disable anonymization. 46 | */ 47 | public function disable(): void 48 | { 49 | $this->enabled = false; 50 | 51 | Event::dispatch(new AnonymizationDisabled); 52 | } 53 | 54 | /** 55 | * Determine if anonymization is enabled. 56 | */ 57 | public function isEnabled(): bool 58 | { 59 | return $this->enabled; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "directorytree/anonymize", 3 | "description": "Anonymize sensitive model data with realistic fake data using Faker", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Steve Bauman", 9 | "email": "steven_bauman@outlook.com" 10 | }, 11 | { 12 | "name": "Josh Parillon", 13 | "email": "jojo.parillon@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.2", 18 | "fakerphp/faker": "^1.0", 19 | "illuminate/support": "^11.0|^12.0" 20 | }, 21 | "require-dev": { 22 | "pestphp/pest": "^3.8", 23 | "laravel/pint": "^1.24", 24 | "phpbench/phpbench": "^1.4", 25 | "orchestra/testbench": "^9.0|^10.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "DirectoryTree\\Anonymize\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "DirectoryTree\\Anonymize\\Tests\\": "tests/", 35 | "DirectoryTree\\Anonymize\\Benchmarks\\": "benchmarks/" 36 | } 37 | }, 38 | "extra": { 39 | "laravel": { 40 | "providers": [ 41 | "DirectoryTree\\Anonymize\\AnonymizeServiceProvider" 42 | ] 43 | } 44 | }, 45 | "minimum-stability": "dev", 46 | "prefer-stable": true, 47 | "config": { 48 | "allow-plugins": { 49 | "pestphp/pest-plugin": true 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/AnonymizesAttributes.php: -------------------------------------------------------------------------------- 1 | anonymizeEnabled; 35 | 36 | $this->anonymizeEnabled = false; 37 | 38 | try { 39 | return $callback($this); 40 | } finally { 41 | $this->anonymizeEnabled = $previous; 42 | } 43 | } 44 | 45 | /** 46 | * Get the key for the anonymizable instance. 47 | */ 48 | public function getAnonymizableKey(): ?string 49 | { 50 | return $this->getKey(); 51 | } 52 | 53 | /** 54 | * Get the seed for the anonymizable instance. 55 | */ 56 | public function getAnonymizableSeed(): string 57 | { 58 | return get_class($this).':'.$this->getAnonymizableKey(); 59 | } 60 | 61 | /** 62 | * Add the anonymized attributes to the attribute array. 63 | * 64 | * @param array $attributes 65 | * @return array 66 | */ 67 | protected function addAnonymizedAttributesToArray(array $attributes): array 68 | { 69 | foreach ($this->getCachedAnonymizedAttributes() as $key => $value) { 70 | if (! array_key_exists($key, $attributes)) { 71 | continue; 72 | } 73 | 74 | if (! is_array($attributes[$key])) { 75 | $attributes[$key] = $value; 76 | 77 | continue; 78 | } 79 | 80 | $attributes[$key] = $this->addAnonymizedAttributesToArray($value) + $attributes[$key]; 81 | } 82 | 83 | return $attributes; 84 | } 85 | 86 | /** 87 | * Make the anonymized attributes. 88 | */ 89 | protected function getCachedAnonymizedAttributes(): array 90 | { 91 | return $this->withoutAnonymization(function (): array { 92 | $seed = $this->getAnonymizableSeed(); 93 | 94 | if (! isset($this->anonymizedAttributeCache) || $this->anonymizedAttributeCacheSeed !== $seed) { 95 | $this->anonymizedAttributeCache = $this->getAnonymizedAttributes( 96 | static::getAnonymizeManager()->faker($seed) 97 | ); 98 | 99 | $this->anonymizedAttributeCacheSeed = $seed; 100 | } 101 | 102 | return $this->anonymizedAttributeCache; 103 | }); 104 | } 105 | 106 | /** 107 | * Get the anonymize manager instance. 108 | */ 109 | protected static function getAnonymizeManager(): AnonymizeManager 110 | { 111 | return App::make(AnonymizeManager::class); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anonymize 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/directorytree/anonymize.svg?style=flat-square)](https://packagist.org/packages/directorytree/anonymize) 4 | [![Tests](https://img.shields.io/github/actions/workflow/status/DirectoryTree/Anonymize/run-tests.yml?branch=master&label=tests&style=flat-square)](https://github.com/DirectoryTree/Anonymize/actions/workflows/tests.yml) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/directorytree/anonymize.svg?style=flat-square)](https://packagist.org/packages/directorytree/anonymize) 6 | 7 | Anonymize replaces sensitive model data with realistic fake data using Faker. Perfect for development environments, demos, and data sharing scenarios where you need to protect user privacy while maintaining data structure and relationships. 8 | 9 | ## Features 10 | 11 | - **Privacy-First**: Automatically anonymize sensitive model attributes 12 | - **Consistent Data**: Same model ID always generates the same fake data 13 | - **Seamless Integration**: Works transparently with existing Eloquent models 14 | - **Granular Control**: Enable/disable anonymization globally or per-model instance 15 | - **Performance Optimized**: Intelligent caching prevents redundant fake data generation 16 | 17 | ## Requirements 18 | 19 | - PHP >= 8.2 20 | - Laravel >= 11 21 | 22 | ## Installation 23 | 24 | You can install the Anonymize using Composer: 25 | 26 | ```bash 27 | composer require directorytree/anonymize 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Set Up Your Model 33 | 34 | Implement the `Anonymizable` interface and use the `Anonymized` trait on your Eloquent model. 35 | 36 | Then, define the attributes you want to anonymize in the `getAnonymizedAttributes()` method: 37 | 38 | ```php 39 | $faker->name(), 56 | 'email' => $faker->safeEmail(), 57 | 'phone' => $faker->phoneNumber(), 58 | 'address' => $faker->address(), 59 | ]; 60 | } 61 | } 62 | ``` 63 | 64 | The attributes returned by `getAnonymizedAttributes()` will replace its original attributes when anonymization is enabled. 65 | 66 | Attributes that are not defined in the `getAnonymizedAttributes()` will not be anonymized, and will be returned as-is. 67 | 68 | ### Enable Anonymization 69 | 70 | Somewhere within your application, enable anonymization: 71 | 72 | > [!note] 73 | > This is typically done within a service provider or middleware, and controlled with a session variable. 74 | 75 | ```php 76 | use DirectoryTree\Anonymize\Facades\Anonymize; 77 | 78 | class AppServiceProvider extends ServiceProvider 79 | { 80 | public function boot(): void 81 | { 82 | if (session('anonymize')) { 83 | Anonymize::enable(); 84 | } 85 | } 86 | } 87 | ``` 88 | 89 | ### Controlling Anonymization 90 | 91 | Control anonymization across your application using the `Anonymize` facade: 92 | 93 | ```php 94 | use DirectoryTree\Anonymize\Facades\Anonymize; 95 | 96 | // Enable anonymization 97 | Anonymize::enable(); 98 | 99 | // Disable anonymization 100 | Anonymize::disable(); 101 | 102 | // Check if anonymization is enabled 103 | if (Anonymize::isEnabled()) { 104 | // Anonymization is active 105 | } 106 | ``` 107 | 108 | ### Consistent Fake Data 109 | 110 | Anonymize ensures that the same model always generates the same fake data. 111 | 112 | This makes browsing your application consistent and predictable: 113 | 114 | ```php 115 | Anonymize::enable(); 116 | 117 | $user1 = User::find(1); 118 | $user2 = User::find(1); 119 | 120 | // Both instances will have identical fake data 121 | $user1->name === $user2->name; // true 122 | $user1->email === $user2->email; // true 123 | ``` 124 | 125 | Different models generate different fake data: 126 | 127 | ```php 128 | $user1 = User::find(1); 129 | $user2 = User::find(2); 130 | 131 | // Different users get different fake data 132 | $user1->name !== $user2->name; // true 133 | $user1->email !== $user2->email; // true 134 | ``` 135 | 136 | ### Custom Seed Generation 137 | 138 | Override the seed generation logic for more control. 139 | 140 | The seed is used to ensure consistent fake data generation: 141 | 142 | ```php 143 | class User extends Model implements Anonymizable 144 | { 145 | use Anonymized; 146 | 147 | public function getAnonymizableSeed(): string 148 | { 149 | return "my-custom-seed:{$this->id}"; 150 | } 151 | 152 | public function getAnonymizedAttributes(Generator $faker): array 153 | { 154 | return [ 155 | 'name' => $faker->name(), 156 | 'email' => $faker->safeEmail(), 157 | ]; 158 | } 159 | } 160 | ``` 161 | 162 | ### Conditional Anonymization 163 | 164 | Only anonymize specific attributes based on conditions: 165 | 166 | ```php 167 | public function getAnonymizedAttributes(Generator $faker): array 168 | { 169 | $attributes = []; 170 | 171 | // Always anonymize email 172 | $attributes['email'] = $faker->safeEmail(); 173 | 174 | // Only anonymize name for non-admin users 175 | if (! $this->is_admin) { 176 | $attributes['name'] = $faker->name(); 177 | } 178 | 179 | return $attributes; 180 | } 181 | ``` 182 | 183 | ### Anonymizing JSON Resources 184 | 185 | You can also anonymize Laravel JSON resources by implementing the `Anonymizable` interface and using the `AnonymizedResource` trait: 186 | 187 | ```php 188 | toAnonymized([ 205 | 'id' => $this->id, 206 | 'name' => $this->name, 207 | 'email' => $this->email, 208 | 'phone' => $this->phone, 209 | ]); 210 | } 211 | 212 | public function getAnonymizedAttributes(Generator $faker): array 213 | { 214 | return [ 215 | 'name' => $faker->name(), 216 | 'email' => $faker->safeEmail(), 217 | 'phone' => $faker->phoneNumber(), 218 | ]; 219 | } 220 | } 221 | ``` 222 | 223 | When anonymization is enabled, the resource will automatically replace sensitive data in the JSON response: 224 | 225 | ```php 226 | Anonymize::enable(); 227 | 228 | $user = User::find(1); 229 | 230 | $resource = new UserResource($user); 231 | 232 | // Returns anonymized data instead of original values. 233 | $response = $resource->resolve(); 234 | ``` 235 | 236 | ## Testing 237 | 238 | ```bash 239 | ./vendor/bin/pest 240 | ``` 241 | 242 | ## Benchmarking 243 | 244 | ```bash 245 | ./vendor/bin/phpbench run 246 | ``` 247 | 248 | ## License 249 | 250 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 251 | --------------------------------------------------------------------------------