Skip to content

Commit 847ddb3

Browse files
committed
feature: fakerphp/faker integration
1 parent 782cc63 commit 847ddb3

File tree

12 files changed

+450
-14
lines changed

12 files changed

+450
-14
lines changed

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"require-dev": {
2727
"doctrine/doctrine-bundle": "^2.10.0",
2828
"doctrine/orm": "^2.15|^3.0",
29+
"fakerphp/faker": "^1.24",
2930
"friendsofphp/php-cs-fixer": "^3.34",
3031
"phpstan/phpstan": "^1.10",
3132
"phpunit/phpunit": "^10.4",
@@ -35,6 +36,7 @@
3536
"symfony/validator": "^6.3|^7.0"
3637
},
3738
"suggest": {
39+
"fakerphp/faker": "In order to use faker backed anonymizers",
3840
"symfony/password-hasher": "In order to use the password hash anonymizer"
3941
},
4042
"conflict": {

src/Anonymization/Anonymizer/AbstractAnonymizer.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,41 @@ protected function getSalt(): string
110110
*
111111
* @throws \Exception if any option is invalid.
112112
*/
113-
protected function validateOptions(): void {}
113+
protected function validateOptions(): void
114+
{
115+
if ($this->hasSampleSizeOption()) {
116+
if ($this->options->has('sample_size')) {
117+
$value = $this->options->getInt('sample_size');
118+
if ($value <= 0) {
119+
throw new \InvalidArgumentException("'sample_size' option must be a positive integer.");
120+
}
121+
}
122+
}
123+
}
124+
125+
/**
126+
* Does this anonymizer has a "sample size" option.
127+
*/
128+
protected function hasSampleSizeOption(): bool
129+
{
130+
return false;
131+
}
132+
133+
/**
134+
* Default sample size, goes along the "sample size" option set to true.
135+
*/
136+
protected function getDefaultSampleSize(): int
137+
{
138+
return 500;
139+
}
140+
141+
/**
142+
* Default sample size, goes along the "sample size" option set to true.
143+
*/
144+
protected function getSampleSize(): int
145+
{
146+
return $this->options->get('sample_size', $this->getDefaultSampleSize());
147+
}
114148

115149
/**
116150
* Initialize your anonymizer.

src/Anonymization/Anonymizer/AbstractMultipleColumnAnonymizer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ protected function getColumnTypes(): array
4949
#[\Override]
5050
protected function validateOptions(): void
5151
{
52+
parent::validateOptions();
53+
5254
if (0 === \count($this->options->all())) {
5355
throw new \InvalidArgumentException("You must provide at least one option.");
5456
}

src/Anonymization/Anonymizer/AnonymizerRegistry.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class AnonymizerRegistry
2424
Core\NullAnonymizer::class,
2525
Core\PasswordAnonymizer::class,
2626
Core\StringAnonymizer::class,
27+
Faker\FakerAddressAnonymizer::class,
28+
Faker\FakerMethodAnonymizer::class,
2729
];
2830

2931
private ?array $anonymizers = null;

src/Anonymization/Anonymizer/Core/IbanBicAnonymizer.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,6 @@ protected function validateOptions(): void
2929
{
3030
parent::validateOptions();
3131

32-
if ($this->options->has('sample_size')) {
33-
$value = $this->options->getInt('sample_size');
34-
if ($value <= 0) {
35-
throw new \InvalidArgumentException("'sample_size' option must be a positive integer.");
36-
}
37-
}
3832
if ($this->options->has('country')) {
3933
$value = $this->options->getString('country');
4034
if (!\ctype_alpha($value) || 2 !== \strlen($value)) {
@@ -52,10 +46,16 @@ protected function getColumnNames(): array
5246
];
5347
}
5448

49+
#[\Override]
50+
protected function hasSampleSizeOption(): bool
51+
{
52+
return true;
53+
}
54+
5555
#[\Override]
5656
protected function getSamples(): array
5757
{
58-
$sampleSize = $this->options->getInt('sample_size', 500);
58+
$sampleSize = $this->getSampleSize();
5959
$countryCode = $this->options->getString('country', 'FR');
6060

6161
// @todo, pas d'options, pas de count ni de country, désolé.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\Faker;
6+
7+
use MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\AbstractMultipleColumnAnonymizer;
8+
use MakinaCorpus\DbToolsBundle\Attribute\AsAnonymizer;
9+
use Faker;
10+
11+
#[AsAnonymizer(
12+
name: 'address',
13+
pack: 'faker',
14+
description: <<<TXT
15+
Anonymize a mutlicolumn postal address.
16+
Map columns for each part with options ('country', 'locality', 'region', 'postal_code', 'street_address', 'secondary_address').
17+
You must set the 'locale' parameter (ex. 'fr_FR', 'en_US', 'de_AT', ...).
18+
You can also specify the sample table size using the 'sample_size' option
19+
(default is 500). The more samples you have, the less duplicates you will
20+
end up with.
21+
TXT,
22+
requires: [Faker\Factory::class],
23+
dependencies: ['fakerphp/faker'],
24+
)]
25+
class FakerAddressAnonymizer extends AbstractMultipleColumnAnonymizer
26+
{
27+
#[\Override]
28+
protected function getColumnNames(): array
29+
{
30+
return [
31+
'street_address',
32+
'secondary_address',
33+
'postal_code',
34+
'locality',
35+
'region',
36+
'country',
37+
];
38+
}
39+
40+
#[\Override]
41+
protected function validateOptions(): void
42+
{
43+
parent::validateOptions();
44+
45+
if (!$this->options->has('locale')) {
46+
throw new \InvalidArgumentException("'locale' must be set (ex. 'fr_FR', 'en_US', 'de_AT', ...");
47+
}
48+
}
49+
50+
#[\Override]
51+
protected function getSamples(): array
52+
{
53+
$sampleSize = $this->getSampleSize();
54+
$faker = Faker\Factory::create($this->options->get('locale', null, true));
55+
56+
$supportsSecondaryAddress = $supportsRegion = true;
57+
58+
try {
59+
$faker->secondaryAddress();
60+
} catch (\InvalidArgumentException) {
61+
$supportsSecondaryAddress = false;
62+
}
63+
64+
try {
65+
$faker->region();
66+
} catch (\InvalidArgumentException) {
67+
$supportsRegion = false;
68+
}
69+
70+
// Importante notice: when faker does not support some methods, depending
71+
// upon the locale, we simply set an empty string, because sample table
72+
// columns are not nullable.
73+
// @todo Should this constraint be kept?
74+
$ret = [];
75+
for ($i = 0; $i < $sampleSize; ++$i) {
76+
$ret[] = [
77+
$faker->streetAddress(),
78+
$supportsSecondaryAddress ? $faker->secondaryAddress() : '',
79+
$faker->postcode(),
80+
$faker->city(),
81+
$supportsRegion ? $faker->region() : '',
82+
$faker->country(),
83+
];
84+
}
85+
return $ret;
86+
}
87+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\Faker;
6+
7+
use MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\AbstractEnumAnonymizer;
8+
use MakinaCorpus\DbToolsBundle\Attribute\AsAnonymizer;
9+
use Faker;
10+
11+
#[AsAnonymizer(
12+
name: 'method',
13+
pack: 'faker',
14+
description: <<<TXT
15+
Anonymize using any faker method, method being provided as the 'method' option.
16+
You can set the 'locale' parameter (ex. 'fr_FR', 'en_US', 'de_AT', ...).
17+
You can also specify the sample table size using the 'sample_size' option
18+
(default is 500). The more samples you have, the less duplicates you will
19+
end up with.
20+
TXT,
21+
requires: [Faker\Factory::class],
22+
dependencies: ['fakerphp/faker'],
23+
)]
24+
class FakerMethodAnonymizer extends AbstractEnumAnonymizer
25+
{
26+
private ?Faker\Generator $generator;
27+
28+
protected function getFakerGenerator(): Faker\Generator
29+
{
30+
return $this->generator ??= Faker\Factory::create($this->options->get('locale', Faker\Factory::DEFAULT_LOCALE));
31+
}
32+
33+
#[\Override]
34+
protected function validateOptions(): void
35+
{
36+
parent::validateOptions();
37+
38+
$method = $this->options->get('method', null, true);
39+
try {
40+
$this->getFakerGenerator()->{$method}();
41+
} catch (\InvalidArgumentException $e) {
42+
throw new \InvalidArgumentException(\sprintf("'%s' option is not a valid faker method.", $method), 0, $e);
43+
}
44+
}
45+
46+
#[\Override]
47+
protected function getSample(): array
48+
{
49+
$sampleSize = $this->getSampleSize();
50+
$faker = $this->getFakerGenerator()->unique(true);
51+
$method = $this->options->get('method', null, true);
52+
53+
$ret = [];
54+
for ($i = 0; $i < $sampleSize; ++$i) {
55+
$ret[] = $faker->{$method}();
56+
}
57+
return $ret;
58+
}
59+
}

src/Attribute/AsAnonymizer.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ public function __construct(
1414
public string $name,
1515
public string $pack,
1616
public ?string $description = null,
17+
/**
18+
* @var array<string>
19+
* Array of class or interface names required for this anonymizer to work.
20+
* Anonymizers whose requirements are not met will not be usable and raise
21+
* an error in listing.
22+
*/
23+
public array $requires = [],
24+
/**
25+
* @var array<string>
26+
* List of composer dependencies for filling the requirements.
27+
*/
28+
public ?array $dependencies = [],
1729
) {}
1830

1931
public function id(): string
@@ -22,4 +34,14 @@ public function id(): string
2234
// which don't have prefix.
2335
return ('core' !== $this->pack ? $this->pack . '.' : '') . $this->name;
2436
}
37+
38+
public function missingRequirements(): bool
39+
{
40+
foreach ($this->requires as $classOrInterfaceName) {
41+
if (!\class_exists($classOrInterfaceName) && !\interface_exists($classOrInterfaceName)) {
42+
return true;
43+
}
44+
}
45+
return false;
46+
}
2547
}

src/Command/Anonymization/AnonymizerListCommand.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
4343
$list[$metadata->pack] = [];
4444
}
4545

46-
$list[$metadata->pack]['<info>' . $metadata->id() . '</info>'] = $metadata->description;
46+
$description = $metadata->description;
47+
if ($metadata->missingRequirements()) {
48+
$description .= "\n" . \sprintf('<error>Dependencies are missing: "%s"</error>', \implode('", "', $metadata->dependencies));
49+
}
50+
51+
$list[$metadata->pack]['<info>' . $metadata->id() . '</info>'] = $description;
4752
}
4853

4954
\array_walk($list, fn (array &$anonymizers) => \ksort($anonymizers, SORT_STRING));

tests/Functional/Anonymizer/Core/AddressAnonymizerTest.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@
22

33
declare(strict_types=1);
44

5-
namespace DbToolsBundle\PackFrFR\Tests\Functional\Anonymizer;
5+
namespace DbToolsBundle\PackFrFR\Tests\Functional\Anonymizer\Core;
66

7-
use MakinaCorpus\DbToolsBundle\Anonymization\Config\AnonymizationConfig;
8-
use MakinaCorpus\DbToolsBundle\Anonymization\Anonymizator;
9-
use MakinaCorpus\DbToolsBundle\Anonymization\Config\AnonymizerConfig;
10-
use MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\AnonymizerRegistry;
117
use MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\Options;
8+
use MakinaCorpus\DbToolsBundle\Anonymization\Config\AnonymizerConfig;
129
use MakinaCorpus\DbToolsBundle\Test\FunctionalTestCase;
1310

1411
class AddressAnonymizerTest extends FunctionalTestCase

0 commit comments

Comments
 (0)