Skip to content

Commit 9558654

Browse files
committed
feature: php-less anonymizers packs
1 parent 5180ade commit 9558654

28 files changed

+1357
-16
lines changed

src/Anonymization/Anonymizer/AbstractMultipleColumnAnonymizer.php

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
abstract class AbstractMultipleColumnAnonymizer extends AbstractTableAnonymizer
1616
{
1717
private ?string $sampleTableName = null;
18+
private ?string $joinAlias = null;
1819

1920
/**
2021
* Get column names.
@@ -81,16 +82,30 @@ public function initialize(): void
8182
#[\Override]
8283
public function anonymize(Update $update): void
8384
{
84-
$columns = $this->getColumnNames();
85+
$columnOptions = $this->getColumnOptions();
8586

86-
if (0 >= $this->options->count()) {
87-
throw new \InvalidArgumentException(\sprintf(
88-
"Options are empty. You should at least give one of those: %s",
89-
\implode(', ', $columns)
90-
));
87+
$expr = $update->expression();
88+
89+
$joinAlias = $this->addJoinToQuery($update);
90+
91+
foreach ($columnOptions as $column) {
92+
$update->set(
93+
$this->options->get($column),
94+
$expr->column($column, $joinAlias)
95+
);
96+
}
97+
}
98+
99+
/**
100+
* Add sample table join to query and return its alias.
101+
*/
102+
public function addJoinToQuery(Update $update): string
103+
{
104+
if ($this->joinAlias) {
105+
return $this->joinAlias;
91106
}
92107

93-
$columnOptions = \array_filter($columns, fn ($column) => $this->options->has($column));
108+
$columnOptions = $this->getColumnOptions();
94109

95110
$expr = $update->expression();
96111

@@ -130,12 +145,22 @@ public function anonymize(Update $update): void
130145
$joinAlias
131146
);
132147

133-
foreach ($columnOptions as $column) {
134-
$update->set(
135-
$this->options->get($column),
136-
$expr->column($column, $joinAlias)
137-
);
148+
return $this->joinAlias = $joinAlias;
149+
}
150+
151+
/** @return array<string> */
152+
private function getColumnOptions(): array
153+
{
154+
$columns = $this->getColumnNames();
155+
156+
if (0 >= $this->options->count()) {
157+
throw new \InvalidArgumentException(\sprintf(
158+
"Options are empty. You should at least give one of those: %s",
159+
\implode(', ', $columns)
160+
));
138161
}
162+
163+
return \array_filter($columns, fn ($column) => $this->options->has($column));
139164
}
140165

141166
#[\Override]
@@ -149,6 +174,7 @@ public function clean(): void
149174
->dropTable($this->sampleTableName)
150175
->commit()
151176
;
177+
$this->sampleTableName = $this->joinAlias = null;
152178
}
153179
}
154180
}

src/Anonymization/Anonymizer/AnonymizerRegistry.php

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Composer\InstalledVersions;
88
use MakinaCorpus\DbToolsBundle\Anonymization\Config\AnonymizerConfig;
9+
use MakinaCorpus\DbToolsBundle\Anonymization\Pack\PackRegistry;
910
use MakinaCorpus\DbToolsBundle\Attribute\AsAnonymizer;
1011
use MakinaCorpus\QueryBuilder\DatabaseSession;
1112

@@ -29,15 +30,33 @@ class AnonymizerRegistry
2930
Core\StringAnonymizer::class,
3031
];
3132

33+
private PackRegistry $packRegistry;
34+
3235
/** @var array<string, string> */
3336
private ?array $classes = null;
37+
3438
/** @var array<string, AsAnonymizer> */
3539
private ?array $metadata = null;
40+
41+
/**
42+
* Paths where to lookup for custom anonymizers.
43+
*
44+
* @var array<string>
45+
*/
3646
private array $paths = [];
3747

38-
public function __construct(?array $paths = null)
48+
/**
49+
* Pack filenames where to lookup for PHP-less packs.
50+
*
51+
* @var array<string>
52+
*/
53+
private array $packs = [];
54+
55+
public function __construct(?array $paths = null, ?array $packs = null)
3956
{
4057
$this->addPath($paths ?? []);
58+
$this->addPack($packs ?? []);
59+
$this->packRegistry = new PackRegistry();
4160
}
4261

4362
/**
@@ -48,6 +67,14 @@ public function addPath(array $paths): void
4867
$this->paths = \array_unique(\array_merge($this->paths, $paths));
4968
}
5069

70+
/**
71+
* Add PHP-less configuration file pack.
72+
*/
73+
public function addPack(array $packs): void
74+
{
75+
$this->packs = \array_unique(\array_merge($this->packs, $packs));
76+
}
77+
5178
/**
5279
* Get all registered anonymizers classe names.
5380
*
@@ -69,12 +96,23 @@ public function createAnonymizer(
6996
Options $options,
7097
DatabaseSession $databaseSession,
7198
): AbstractAnonymizer {
99+
if ($this->packRegistry->hasPack($name)) {
100+
throw new \Exception("Not implemented: @todo make factory at the right place...");
101+
}
102+
72103
$className = $this->getAnonymizerClass($name);
73104

74-
return new $className($config->table, $config->targetName, $databaseSession, $options);
105+
$ret = new $className($config->table, $config->targetName, $databaseSession, $options);
106+
\assert($ret instanceof AbstractAnonymizer);
107+
108+
if ($ret instanceof WithAnonymizerRegistry) {
109+
$ret->setAnonymizerRegistry($this);
110+
}
111+
112+
return $ret;
75113
}
76114

77-
/**
115+
/**
78116
* Get anonymizer metadata.
79117
*/
80118
public function getAnonymizerMetadata(string $name): AsAnonymizer
@@ -163,6 +201,12 @@ private function initialize(): void
163201
}
164202
}
165203
}
204+
205+
if ($this->packs) {
206+
foreach ($this->packs as $filename) {
207+
$this->packRegistry->addPack($filename);
208+
}
209+
}
166210
}
167211

168212
/**
@@ -204,8 +248,10 @@ private function locatePacks(): void
204248
$path = $directory . '/src/Anonymizer/';
205249
if (\is_dir($path)) {
206250
$this->addPath([$path]);
251+
} else if (\file_exists($path . '/db_tools.pack.yaml')) {
252+
$this->addPack([$path . '/db_tools.pack.yaml']);
207253
} else {
208-
\trigger_error(\sprintf("Anonymizers pack '%s' in '%s' as no 'src/Anonymizer/' directory and is thus not usable.", $package, $directory), \E_USER_ERROR);
254+
\trigger_error(\sprintf("Anonymizers pack '%s' in '%s' as no 'src/Anonymizer/' directory nor 'db_tools.pack.yaml' file and is thus not usable.", $package, $directory), \E_USER_ERROR);
209255
}
210256
}
211257
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\Pack;
6+
7+
use MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\AbstractAnonymizer;
8+
use MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\AbstractMultipleColumnAnonymizer;
9+
use MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\AbstractSingleColumnAnonymizer;
10+
use MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\Options;
11+
use MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\WithAnonymizerRegistry;
12+
use MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer\WithAnonymizerRegistryTrait;
13+
use MakinaCorpus\DbToolsBundle\Anonymization\Config\AnonymizerConfig;
14+
use MakinaCorpus\DbToolsBundle\Anonymization\Pack\GeneratedIntRange;
15+
use MakinaCorpus\DbToolsBundle\Anonymization\Pack\GeneratedPart;
16+
use MakinaCorpus\DbToolsBundle\Anonymization\Pack\GeneratedRaw;
17+
use MakinaCorpus\DbToolsBundle\Anonymization\Pack\GeneratedRef;
18+
use MakinaCorpus\DbToolsBundle\Anonymization\Pack\GeneratedString;
19+
use MakinaCorpus\DbToolsBundle\Error\ConfigurationException;
20+
use MakinaCorpus\QueryBuilder\Expression;
21+
use MakinaCorpus\QueryBuilder\Query\Update;
22+
23+
/**
24+
* Creates a CONCAT(...) expression which is filled with all parts
25+
* of a generated string pattern, using other anonymizers.
26+
*
27+
* Delta that you will encounter in anonymizers is an information
28+
* that allows the user to create more than one JOIN statement for
29+
* the same multiple column anonymizer. Per default, it's always 0
30+
* and all columns from a same row will be shared for a single
31+
* updated line, keeping consistency.
32+
*/
33+
class GeneratedStringAnonymizer extends AbstractSingleColumnAnonymizer implements WithAnonymizerRegistry
34+
{
35+
use WithAnonymizerRegistryTrait;
36+
37+
private GeneratedString $generated;
38+
39+
/**
40+
* Child anonymizers, populated during initialize() call in order to be
41+
* able to initialize them at the same time.
42+
*
43+
* It is then being used during clean() for cleanup, then dropped.
44+
*
45+
* @var array<string, AbstractAnonymizer>
46+
*/
47+
private $childAnonymizers = [];
48+
49+
#[\Override]
50+
protected function validateOptions(): void
51+
{
52+
}
53+
54+
#[\Override]
55+
public function initialize(): void
56+
{
57+
parent::initialize();
58+
59+
// Initialize all child anonymizers.
60+
foreach ($this->generated->parts as $part) {
61+
if ($part instanceof GeneratedRef) {
62+
$this->getMultipleAnonymizer($part->id, null, $part->delta)->initialize();
63+
}
64+
}
65+
}
66+
67+
#[\Override]
68+
public function createAnonymizeExpression(Update $update): Expression
69+
{
70+
$expr = $update->expression();
71+
72+
$expressions = [];
73+
foreach ($this->generated->parts as $part) {
74+
\assert($part instanceof GeneratedPart);
75+
76+
if ($part instanceof GeneratedIntRange) {
77+
// Integer anonymizer doesn't require any initialization so we
78+
// can safely do this, without any prior initialization. For
79+
// each range we have in the pattern, options will be different
80+
// so we need to create as many instances as we have ranges.
81+
$expr[] = $this
82+
->getSingleAnonymizer(
83+
'int',
84+
new Options([
85+
'min' => $part->start,
86+
'max' => $part->stop,
87+
],
88+
))
89+
->createAnonymizeExpression($update)
90+
;
91+
} else if ($part instanceof GeneratedRaw) {
92+
$expressions[] = $expr->value($part->string, 'text');
93+
} else if ($part instanceof GeneratedRef) {
94+
// Anonymizer was prepolulated with options during initialize()
95+
// which makes passing any options useless (the cached instance
96+
// will be retrieved, already carries options).
97+
$childAnonymizer = $this->getMultipleAnonymizer($part->id, null, $part->delta);
98+
// If the anonymizer is used more than once, addJoinToQuery()
99+
// method will not add as many join as there are tables, it
100+
// prevents duplicates.
101+
$joinAlias = $childAnonymizer->addJoinToQuery($update);
102+
$expressions[] = $expr->column($part->column, $joinAlias);
103+
}
104+
}
105+
106+
return $expr->concat(...$expressions);
107+
}
108+
109+
#[\Override]
110+
public function clean(): void
111+
{
112+
try {
113+
// Clean all child anonymizers.
114+
foreach ($this->childAnonymizers as $anonymizer) {
115+
\assert($anonymizer instanceof AbstractAnonymizer);
116+
$anonymizer->clean();
117+
}
118+
$this->childAnonymizers = [];
119+
} finally {
120+
parent::clean();
121+
}
122+
}
123+
124+
/**
125+
* Create child anonymizer and ensures it's a single column anonymizer.
126+
*/
127+
private function getSingleAnonymizer(string $anonymizer, ?Options $options = null, int $delta = 0): AbstractSingleColumnAnonymizer
128+
{
129+
$key = \sprintf("%s[%d]", $anonymizer, $delta);
130+
131+
$ret = $this->childAnonymizers[$key] ??= $this->getAnonymizer($anonymizer, $options);
132+
133+
if (!$ret instanceof AbstractSingleColumnAnonymizer) {
134+
// @todo better message
135+
throw new ConfigurationException("Not a single column anonymizer");
136+
}
137+
138+
return $ret;
139+
}
140+
141+
/**
142+
* Create child anonymizer and ensures it's a multiple column anonymizer.
143+
*/
144+
private function getMultipleAnonymizer(string $anonymizer, ?Options $options = null, int $delta = 0): AbstractMultipleColumnAnonymizer
145+
{
146+
$key = \sprintf("%s[%d]", $anonymizer, $delta);
147+
148+
$ret = $this->childAnonymizers[$key] ??= $this->getAnonymizer($anonymizer, $options);
149+
150+
if (!$ret instanceof AbstractMultipleColumnAnonymizer) {
151+
// @todo better message
152+
throw new ConfigurationException("Not a multiple column anonymizer");
153+
}
154+
155+
return $ret;
156+
}
157+
158+
/**
159+
* Create child anonymizer.
160+
*/
161+
private function getAnonymizer(string $anonymizer, ?Options $options = null): AbstractAnonymizer
162+
{
163+
$config = new AnonymizerConfig($this->tableName, $this->columnName, $anonymizer, new Options());
164+
165+
return $this
166+
->getAnonymizerRegistry()
167+
->createAnonymizer(
168+
$anonymizer,
169+
$config,
170+
$options ?? new Options(),
171+
$this->databaseSession
172+
)
173+
;
174+
}
175+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer;
6+
7+
interface WithAnonymizerRegistry
8+
{
9+
public function setAnonymizerRegistry(AnonymizerRegistry $anonymizerRegistry): void;
10+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MakinaCorpus\DbToolsBundle\Anonymization\Anonymizer;
6+
7+
trait WithAnonymizerRegistryTrait /* implements WithAnonymizerRegistry */
8+
{
9+
private AnonymizerRegistry $anonymizerRegistry;
10+
11+
#[\Override]
12+
public function setAnonymizerRegistry(AnonymizerRegistry $anonymizerRegistry): void
13+
{
14+
$this->anonymizerRegistry = $anonymizerRegistry;
15+
}
16+
17+
protected function getAnonymizerRegistry(): AnonymizerRegistry
18+
{
19+
return $this->anonymizerRegistry ?? throw new \LogicException(\sprintf("Did you forget to call %s::getAnonymizerRegistry()", static::class));
20+
}
21+
}

0 commit comments

Comments
 (0)