Skip to content

Commit aea6879

Browse files
authored
Merge pull request #8 from AJTimmerman/main
Allow anonymization based on Model's factory definition
2 parents 04452b6 + 493f30a commit aea6879

File tree

3 files changed

+190
-10
lines changed

3 files changed

+190
-10
lines changed

README.md

Lines changed: 134 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ return [
3535

3636
## Usage
3737

38-
In any model that contains sensitive data use `Anonymizable` trait and implement `anonymizableAttributes` method:
38+
### Model based definitions
39+
In any model that contains sensitive data use the `Anonymizable` trait and implement the `anonymizableAttributes` method:
3940

4041
```php
4142
<?php
@@ -50,10 +51,10 @@ class User extends Authenticatable
5051
return [
5152
'email' => $this->id . '@custom.dev',
5253
'password' => 'secret',
53-
'firstname' => $faker->firstName,
54-
'surname' => $faker->lastName,
55-
'phone' => $faker->e164PhoneNumber,
56-
'position' => $faker->jobTitle,
54+
'firstname' => $faker->firstName(),
55+
'surname' => $faker->lastName(),
56+
'phone' => $faker->e164PhoneNumber(),
57+
'position' => $faker->jobTitle(),
5758
'token' => null,
5859
];
5960
}
@@ -65,6 +66,133 @@ class User extends Authenticatable
6566
}
6667
}
6768
```
69+
70+
### Factory based definitions
71+
To reduce the amount of necessary code, the anonymizable attributes may also be defined on your factories.
72+
Do note that the model still needs to implement the `Anonymizable` to work with these definitions.
73+
74+
#### Attributes based on the factory's definition
75+
It is possible to use the factory's definition to use as anonymizable values.
76+
Implement the `anonymizableAttributes` method on your factory and return an array with keys that corresponds to the definition of the factory:
77+
78+
```php
79+
<?php
80+
81+
class UserFactory extends Factory
82+
{
83+
public function definition(): array
84+
{
85+
return [
86+
'email' => $this->faker()->numberBetween(1, 100_000) . '@custom.dev',
87+
'password' => 'secret',
88+
'firstname' => $this->faker->firstName(),
89+
'surname' => $this->faker->lastName(),
90+
'phone' => $this->faker->e164PhoneNumber(),
91+
'position' => $this->faker->jobTitle(),
92+
'token' => null,
93+
]
94+
}
95+
96+
public function anonymizableAttributes(): array
97+
{
98+
return [
99+
'email',
100+
'password',
101+
'firstname',
102+
'surname',
103+
'phone',
104+
'position',
105+
'token',
106+
]
107+
}
108+
}
109+
```
110+
This will use reduce the amount of code you need to write if the data you want to anonymize is the same as your factory's definition.
111+
112+
Note that only the defined keys will be anonymized!
113+
114+
#### Attributes based on a custom definition
115+
If you prefer to still use custom values, you can still define them on the factory.
116+
Implement the `anonymizableDefinition` method on your factory, and return a keyed array:
117+
118+
```php
119+
<?php
120+
121+
class UserFactory extends Factory
122+
{
123+
public function definition(): array
124+
{
125+
return [
126+
'email' => $this->faker()->numberBetween(1, 100_000) . '@custom.dev',
127+
'password' => 'secret',
128+
'firstname' => $faker->firstName(),
129+
'surname' => $faker->lastName(),
130+
'phone' => $faker->e164PhoneNumber(),
131+
'position' => $faker->jobTitle(),
132+
'token' => null,
133+
]
134+
}
135+
136+
public function anonymizableDefinition(): array
137+
{
138+
return [
139+
'email' => $this->faker()->numberBetween(1, 100_000) . '@custom.dev',
140+
'password' => 'secret',
141+
'firstname' => $faker->firstName(),
142+
'surname' => $faker->lastName(),
143+
'phone' => $faker->e164PhoneNumber(),
144+
'position' => $faker->jobTitle(),
145+
'token' => $this->faker->md5(),
146+
]
147+
}
148+
}
149+
```
150+
151+
Defining custom values is mostly useful when used in tandem with the `anonymizableAttributes` method.
152+
This allows certain attributes from the factory to be used, while still defining custom attributes when necessary:
153+
```php
154+
<?php
155+
156+
class UserFactory extends Factory
157+
{
158+
public function definition(): array
159+
{
160+
return [
161+
'email' => $this->faker()->numberBetween(1, 100_000) . '@custom.dev',
162+
'password' => 'secret',
163+
'firstname' => $faker->firstName(),
164+
'surname' => $faker->lastName(),
165+
'phone' => $faker->e164PhoneNumber(),
166+
'position' => $faker->jobTitle(),
167+
'token' => null,
168+
]
169+
}
170+
171+
public function anonymizableAttributes(): array
172+
{
173+
return [
174+
'email',
175+
'password',
176+
'firstname',
177+
'surname',
178+
'phone',
179+
'position',
180+
]
181+
}
182+
183+
public function anonymizableDefinition(): array
184+
{
185+
return [
186+
'token' => $this->faker->md5(),
187+
]
188+
}
189+
}
190+
```
191+
192+
### Important note
193+
>The data from the `anonymizable` methods on the factory will overwrite the data defined in the `anonymizableAttributes` method on the model!
194+
195+
### Running the anonymizer
68196
Anonymization is performed using command:
69197

70198
```bash
@@ -78,4 +206,4 @@ php artisan db:anonymize --model=\\App\User --model=\\App\\Profile
78206

79207
## License
80208

81-
The MIT License (MIT). Please see [License File](LICENSE) for more information.
209+
The MIT License (MIT). Please see [License File](LICENSE) for more information.

src/Anonymizable.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<?php
22

3-
namespace Outsidaz\LaravelDataAnonymization;;
3+
namespace Outsidaz\LaravelDataAnonymization;
44

55
use Faker\Generator;
66
use Illuminate\Database\Eloquent\Builder;
7+
use Illuminate\Database\Eloquent\Model;
78
use LogicException;
89

910
trait Anonymizable
@@ -15,6 +16,21 @@ public function anonymizableCondition(): Builder
1516

1617
public function anonymizableAttributes(Generator $faker): array
1718
{
18-
throw new LogicException('Please implement the anonymizable method on your model.');
19+
$class = new (static::class)();
20+
21+
if (! is_subclass_of($class, Model::class)) {
22+
throw new LogicException('Please implement the anonymizable trait on an Eloquent Model.');
23+
}
24+
25+
// Check if the model's factory has an implementation of the anonymizable attributes that the anonymizer can use
26+
if ($class::factory()) {
27+
if (method_exists($class::factory(), 'anonymizableAttributes')) {
28+
return [];
29+
}
30+
}
31+
32+
throw new LogicException(
33+
'Please implement the anonymizableAttributes() method on your model or the model\'s factory.'
34+
);
1935
}
20-
}
36+
}

src/Anonymizer.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,46 @@ public function changeData(Model $model): bool
7272
throw new \Exception(sprintf("Environment '%s' has blocking enforced.", config('app.env')));
7373
}
7474

75+
$anonymizableAttributes = $model->anonymizableAttributes($this->faker);
76+
$anonymizableAttributesBasedOnFactoryDefinition = [];
77+
$anonymizableAttributesBasedOnCustomDefinition = [];
78+
79+
/** @var \Illuminate\Database\Eloquent\Factories\Factory $factory */
80+
$factory = $model::factory();
81+
82+
// Extract attributes, including values, from the factory's definition, if available
83+
if (method_exists($factory, 'anonymizableAttributes')) {
84+
$anonymizableAttributesKeys = $factory->anonymizableAttributes();
85+
$factoryDefinition = $factory->definition();
86+
87+
$keysToLeaveAlone = array_diff_key($factoryDefinition, array_flip($anonymizableAttributesKeys));
88+
89+
$anonymizableAttributesBasedOnFactoryDefinition = array_diff_key(
90+
$factoryDefinition,
91+
array_flip(array_keys($keysToLeaveAlone))
92+
);
93+
}
94+
95+
// Extract attributes and values from the custom definition, if available
96+
if (method_exists($factory, 'anonymizableDefinition')) {
97+
$anonymizableAttributesBasedOnCustomDefinition = $factory->anonymizableDefinition($this->faker);
98+
}
99+
100+
// Merge the list of custom definitions and the factory based definitions to use as the new anonymizable attributes
101+
if (
102+
! empty($anonymizableAttributesBasedOnFactoryDefinition)
103+
|| ! (empty($anonymizableAttributesBasedOnCustomDefinition))
104+
) {
105+
$anonymizableAttributes = array_merge(
106+
$anonymizableAttributesBasedOnFactoryDefinition,
107+
$anonymizableAttributesBasedOnCustomDefinition
108+
);
109+
}
110+
75111
return $model
76112
->setTouchedRelations([]) // disable touch owners
77113
->updateQuietly( // disable events handling
78-
$model->anonymizableAttributes($this->faker)
114+
$anonymizableAttributes
79115
);
80116
}
81117
}

0 commit comments

Comments
 (0)