Skip to content

Commit ffbc061

Browse files
committed
feature: string pattern anonymizer that fetches data from other anonymizers
1 parent 5180ade commit ffbc061

File tree

15 files changed

+700
-14
lines changed

15 files changed

+700
-14
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
# Changelog
22

3+
## Next
4+
5+
* [feature] 🌟 String pattern anonymizer, build complex strings by fetching values from other anonymizers.
6+
37
## 2.0.3
48

5-
* [FIX] Fix anonymization for MySQL version >=8.0 but <8.0.29 (#220).
9+
* [fix] Fix anonymization for MySQL version >=8.0 but <8.0.29 (#220).
610
* [doc] Fix connection string examples (#219).
711

812
## 2.0.2

docs/content/anonymization/core-anonymizers.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This page list all *Anonymizers* provided by *DbToolsBundle*.
1313
<!--@include: ./core-anonymizers/constant.md-->
1414
<!--@include: ./core-anonymizers/md5.md-->
1515
<!--@include: ./core-anonymizers/string.md-->
16+
<!--@include: ./core-anonymizers/pattern.md-->
1617
<!--@include: ./core-anonymizers/lastname.md-->
1718
<!--@include: ./core-anonymizers/firstname.md-->
1819
<!--@include: ./core-anonymizers/lorem-ipsum.md-->
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
## String pattern
2+
3+
Anonymize by building text values using other anonymizers to fill holes in text.
4+
5+
### String range
6+
7+
By adding `[MIN,MAX]` anywhere in your text, it will be replaced by a random integer
8+
in the given range. For example `"I want [7-111] apples"`.
9+
10+
You can use negative integers such as `[-10,10]` or even a fully negative integer
11+
range `[-127,-34]`.
12+
13+
### Single value from another anonymizer
14+
15+
By adding `{ANONYMIZER}` anywhere in your text, it will be replaced by a random value
16+
given by the anonymizer. For example `"My email address is {email}, what's your ?"`.
17+
18+
:::warning
19+
The target anonymizer must not be a column anonymizer.
20+
:::
21+
22+
### Column value from another anonymizer
23+
24+
By adding `{ANONYMIZER:COLUMN}` anywhere in your text, it will be replaced by a random
25+
value for the named column given by the anonymizer. For example, `"The country I live into is {address:country}."`.
26+
27+
If you use more than one column of the same anonymizer in the same text value, the same
28+
generated row from the target anonymizer will be used. For example, this text will give
29+
a consistent result: `"The country I live in {address:locality} in {address:country}."`.
30+
31+
:::warning
32+
The target anonymizer must be a column anonymizer.
33+
:::
34+
35+
### Usage
36+
37+
@@@ standalone docker
38+
39+
```yaml [YAML]
40+
# db_tools.config.yaml
41+
anonymization:
42+
default:
43+
customer:
44+
biography:
45+
anonymizer: pattern
46+
options: {value: '{firstname} {lastname} lives in {address:locality} in {address:country}, he has [2,7] cats.'}
47+
#...
48+
```
49+
50+
@@@
51+
@@@ symfony
52+
53+
::: code-group
54+
```php [Attribute]
55+
namespace App\Entity;
56+
57+
use Doctrine\ORM\Mapping as ORM;
58+
use MakinaCorpus\DbToolsBundle\Attribute\Anonymize;
59+
60+
#[ORM\Entity()]
61+
#[ORM\Table(name: 'customer')]
62+
class Customer
63+
{
64+
// ...
65+
66+
#[ORM\Column]
67+
#[Anonymize(type: 'pattern', options: ['value' => '{firstname} {lastname} lives in {address:locality} in {address:country}, he has [2,7] cats.'])] // [!code ++]
68+
private ?string $biography = null;
69+
}
70+
```
71+
72+
```yml [YAML]
73+
# config/anonymization.yaml
74+
75+
customer:
76+
biography:
77+
anonymizer: pattern
78+
options: {value: '{firstname} {lastname} lives in {address:locality} in {address:country}, he has [2,7] cats.'}
79+
#...
80+
```
81+
:::
82+
83+
@@@

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: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class AnonymizerRegistry
2727
Core\NullAnonymizer::class,
2828
Core\PasswordAnonymizer::class,
2929
Core\StringAnonymizer::class,
30+
Core\StringPatternAnonymizer::class,
3031
];
3132

3233
/** @var array<string, string> */
@@ -71,7 +72,14 @@ public function createAnonymizer(
7172
): AbstractAnonymizer {
7273
$className = $this->getAnonymizerClass($name);
7374

74-
return new $className($config->table, $config->targetName, $databaseSession, $options);
75+
$ret = new $className($config->table, $config->targetName, $databaseSession, $options);
76+
\assert($ret instanceof AbstractAnonymizer);
77+
78+
if ($ret instanceof WithAnonymizerRegistry) {
79+
$ret->setAnonymizerRegistry($this);
80+
}
81+
82+
return $ret;
7583
}
7684

7785
/**

0 commit comments

Comments
 (0)