Skip to content

Commit a72984f

Browse files
authored
Merge pull request #2 from recranet/feature/custom-fields
Add extra fields and indices to audit tables, based on configuration
2 parents 393972a + 4a7c1d7 commit a72984f

File tree

9 files changed

+193
-31
lines changed

9 files changed

+193
-31
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"symfony/cache": "^5.4|^6.0|^7.0",
1818
"symfony/event-dispatcher": "^5.4|^6.0|^7.0",
1919
"symfony/lock": "^5.4|^6.0|^7.0",
20-
"symfony/options-resolver": "^5.4|^6.0|^7.0"
20+
"symfony/options-resolver": "^5.4|^6.0|^7.0",
21+
"symfony/property-access": "^7.2"
2122
},
2223
"suggest": {
2324
"damienharper/auditor-bundle": "Integrate auditor library in your Symfony projects."

src/Model/Entry.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ final class Entry
2323

2424
private string $diffs = '{}';
2525

26+
private array $extra_fields = [];
27+
2628
private null|int|string $blame_id = null;
2729

2830
private ?string $blame_user = null;
@@ -43,6 +45,29 @@ public function getId(): ?int
4345
return $this->id;
4446
}
4547

48+
49+
public function setExtraField($key, $value = null): void
50+
{
51+
if ($key === (array)$key) {
52+
$this->extra_fields = $key;
53+
} else {
54+
$this->extra_fields[$key] = $value;
55+
}
56+
}
57+
58+
public function getExtraField($key = ''): mixed
59+
{
60+
if ('' === $key) {
61+
return $this->extra_fields;
62+
}
63+
64+
if (isset($this->extra_fields[$key])) {
65+
return $this->extra_fields[$key];
66+
}
67+
68+
return null;
69+
}
70+
4671
/**
4772
* Get the value of type.
4873
*/
@@ -137,6 +162,8 @@ public static function fromArray(array $row): self
137162
foreach ($row as $key => $value) {
138163
if (property_exists($entry, $key)) {
139164
$entry->{$key} = 'id' === $key ? (int) $value : $value;
165+
} else {
166+
$entry->extra_fields[$key] = $value;
140167
}
141168
}
142169

src/Provider/Doctrine/Auditing/Transaction/AuditTrait.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Doctrine\ORM\Mapping\ClassMetadata;
1616
use Doctrine\ORM\Mapping\FieldMapping;
1717
use Doctrine\ORM\Mapping\MappingException as ORMMappingException;
18+
use Symfony\Component\PropertyAccess\PropertyAccess;
1819

1920
trait AuditTrait
2021
{
@@ -132,7 +133,29 @@ private function value(EntityManagerInterface $entityManager, Type $type, mixed
132133
}
133134

134135
/**
135-
* Computes a usable diff formatted as follow:
136+
* Returns the extra fields if set.
137+
*/
138+
private function extraFields(object $entity): array
139+
{
140+
$configuration = $this->provider->getConfiguration();
141+
$extraFieldProperties = array_keys($configuration->getExtraFields());
142+
$propertyAccessor = PropertyAccess::createPropertyAccessor();
143+
144+
$extraFields = [];
145+
146+
foreach ($extraFieldProperties as $extraField) {
147+
if (!$propertyAccessor->isReadable($entity, $extraField)) {
148+
continue;
149+
}
150+
151+
$extraFields[$extraField] = $propertyAccessor->getValue($entity, $extraField);
152+
}
153+
154+
return $extraFields;
155+
}
156+
157+
/**
158+
* Computes a usable diff formatted as follows:
136159
* [
137160
* // field1 value has changed
138161
* 'field1' => [

src/Provider/Doctrine/Auditing/Transaction/TransactionProcessor.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ private function insert(EntityManagerInterface $entityManager, object $entity, a
5858
'action' => 'insert',
5959
'blame' => $this->blame(),
6060
'diff' => $this->diff($entityManager, $entity, $ch),
61+
'extra_fields' => $this->extraFields($entity),
6162
'table' => $meta->getTableName(),
6263
'schema' => $meta->getSchemaName(),
6364
'id' => $this->id($entityManager, $entity),
@@ -84,6 +85,7 @@ private function update(EntityManagerInterface $entityManager, object $entity, a
8485
'action' => 'update',
8586
'blame' => $this->blame(),
8687
'diff' => $diff,
88+
'extra_fields' => $this->extraFields($entity),
8789
'table' => $meta->getTableName(),
8890
'schema' => $meta->getSchemaName(),
8991
'id' => $this->id($entityManager, $entity),
@@ -103,6 +105,7 @@ private function remove(EntityManagerInterface $entityManager, object $entity, m
103105
'action' => 'remove',
104106
'blame' => $this->blame(),
105107
'diff' => $this->summarize($entityManager, $entity, ['id' => $id]),
108+
'extra_fields' => $this->extraFields($entity),
106109
'table' => $meta->getTableName(),
107110
'schema' => $meta->getSchemaName(),
108111
'id' => $id,
@@ -183,6 +186,7 @@ private function associateOrDissociate(string $type, EntityManagerInterface $ent
183186
'target' => $this->summarize($entityManager, $target, ['field' => $mapping['isOwningSide'] ? $mapping['inversedBy'] : $mapping['mappedBy']]),
184187
'is_owning_side' => $mapping['isOwningSide'],
185188
],
189+
'extra_fields' => $this->extraFields($source),
186190
'table' => $meta->getTableName(),
187191
'schema' => $meta->getSchemaName(),
188192
'id' => $this->id($entityManager, $source),
@@ -220,6 +224,7 @@ private function audit(array $data): void
220224
'discriminator' => $data['discriminator'],
221225
'transaction_hash' => (string) $data['transaction_hash'],
222226
'diffs' => json_encode($diff, JSON_THROW_ON_ERROR),
227+
...$data['extra_fields'] ?? [],
223228
'blame_id' => $data['blame']['user_id'],
224229
'blame_user' => $data['blame']['username'],
225230
'blame_user_fqdn' => $data['blame']['user_fqdn'],

src/Provider/Doctrine/Configuration.php

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

77
use DH\Auditor\Provider\ConfigurationInterface;
88
use DH\Auditor\Provider\Doctrine\Persistence\Helper\DoctrineHelper;
9+
use DH\Auditor\Provider\Doctrine\Persistence\Helper\SchemaHelper;
910
use DH\Auditor\Provider\Doctrine\Persistence\Reader\Reader;
1011
use DH\Auditor\Provider\Doctrine\Persistence\Schema\SchemaManager;
1112
use DH\Auditor\Provider\Doctrine\Service\AuditingService;
@@ -30,6 +31,10 @@ final class Configuration implements ConfigurationInterface
3031

3132
private ?array $entities = null;
3233

34+
private array $extraFields = [];
35+
36+
private array $extraIndices = [];
37+
3338
private bool $isViewerEnabled;
3439

3540
private int $viewerPageSize;
@@ -60,6 +65,20 @@ public function __construct(array $options)
6065
}
6166
}
6267

68+
if (isset($config['extra_fields']) && !empty($config['extra_fields'])) {
69+
// use field names as array keys for easier lookup
70+
foreach ($config['extra_fields'] as $fieldName => $fieldOptions) {
71+
$this->extraFields[$fieldName] = $fieldOptions;
72+
}
73+
}
74+
75+
if (isset($config['extra_indices']) && !empty($config['extra_indices'])) {
76+
// use index names as array keys for easier lookup
77+
foreach ($config['extra_indices'] as $indexName => $indexOptions) {
78+
$this->extraIndices[$indexName] = $indexOptions;
79+
}
80+
}
81+
6382
$this->isViewerEnabled = self::isViewerEnabledInConfig($config['viewer']);
6483
$this->viewerPageSize = self::getViewerPageSizeFromConfig($config['viewer']);
6584
$this->storageMapper = $config['storage_mapper'];
@@ -229,6 +248,65 @@ public function getEntities(): array
229248
return $this->entities ?? [];
230249
}
231250

251+
public function getExtraFields(): array
252+
{
253+
return $this->extraFields;
254+
}
255+
256+
public function getAllFields(): array
257+
{
258+
return array_merge(
259+
SchemaHelper::getAuditTableColumns(),
260+
$this->extraFields
261+
);
262+
}
263+
264+
/**
265+
* @param array<string, mixed> $extraFields
266+
*/
267+
public function setExtraFields(array $extraFields): self
268+
{
269+
$this->extraFields = $extraFields;
270+
271+
return $this;
272+
}
273+
274+
public function getExtraIndices(): array
275+
{
276+
return $this->extraIndices;
277+
}
278+
279+
public function prepareExtraIndices(string $tableName): array
280+
{
281+
$indices = [];
282+
foreach ($this->extraIndices as $extraIndexField => $extraIndexOptions) {
283+
$indices[$extraIndexField] = [
284+
'type' => $extraIndexOptions['type'] ?? 'index',
285+
'name' => sprintf('%s_%s_idx', $extraIndexOptions['name_prefix'] ?? $extraIndexField, md5($tableName)),
286+
];
287+
}
288+
289+
return $indices;
290+
}
291+
292+
public function getAllIndices(string $tableName): array
293+
{
294+
return array_merge(
295+
SchemaHelper::getAuditTableIndices($tableName),
296+
$this->prepareExtraIndices($tableName)
297+
);
298+
}
299+
300+
/**
301+
* @param array<string, mixed> $extraIndices
302+
*/
303+
public function setExtraIndices(array $extraIndices): self
304+
{
305+
$this->extraIndices = $extraIndices;
306+
307+
return $this;
308+
}
309+
232310
/**
233311
* Enables auditing for a specific entity.
234312
*
@@ -296,6 +374,8 @@ private function configureOptions(OptionsResolver $resolver): void
296374
'table_suffix' => '_audit',
297375
'ignored_columns' => [],
298376
'entities' => [],
377+
'extra_fields' => [],
378+
'extra_indices' => [],
299379
'storage_services' => [],
300380
'auditing_services' => [],
301381
'viewer' => true,
@@ -305,6 +385,8 @@ private function configureOptions(OptionsResolver $resolver): void
305385
->setAllowedTypes('table_suffix', 'string')
306386
->setAllowedTypes('ignored_columns', 'array')
307387
->setAllowedTypes('entities', 'array')
388+
->setAllowedTypes('extra_fields', 'array')
389+
->setAllowedTypes('extra_indices', 'array')
308390
->setAllowedTypes('storage_services', 'array')
309391
->setAllowedTypes('auditing_services', 'array')
310392
->setAllowedTypes('viewer', ['bool', 'array'])

src/Provider/Doctrine/DoctrineProvider.php

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,6 @@
3131
*/
3232
final class DoctrineProvider extends AbstractProvider
3333
{
34-
/**
35-
* @var array<string, string>
36-
*/
37-
private const FIELDS = [
38-
'type' => '?',
39-
'object_id' => '?',
40-
'discriminator' => '?',
41-
'transaction_hash' => '?',
42-
'diffs' => '?',
43-
'blame_id' => '?',
44-
'blame_user' => '?',
45-
'blame_user_fqdn' => '?',
46-
'blame_user_firewall' => '?',
47-
'ip' => '?',
48-
'created_at' => '?',
49-
];
50-
5134
private readonly TransactionManager $transactionManager;
5235

5336
public function __construct(ConfigurationInterface $configuration)
@@ -133,20 +116,22 @@ public function persist(LifecycleEvent $event): void
133116
$entity = $payload['entity'];
134117
unset($payload['table'], $payload['entity']);
135118

136-
$keys = array_keys(self::FIELDS);
137-
$query = \sprintf(
119+
$fields = array_combine(array_keys($payload), array_map(function ($x) {return ":{$x}"; }, array_keys($payload)));
120+
\assert(\is_array($fields)); // helps PHPStan
121+
122+
$query = sprintf(
138123
'INSERT INTO %s (%s) VALUES (%s)',
139124
$auditTable,
140-
implode(', ', $keys),
141-
implode(', ', array_values(self::FIELDS))
125+
implode(', ', array_keys($fields)),
126+
implode(', ', array_values($fields))
142127
);
143128

144129
/** @var StorageService $storageService */
145130
$storageService = $this->getStorageServiceForEntity($entity);
146131
$statement = $storageService->getEntityManager()->getConnection()->prepare($query);
147132

148133
foreach ($payload as $key => $value) {
149-
$statement->bindValue(array_search($key, $keys, true) + 1, $value);
134+
$statement->bindValue($key, $value);
150135
}
151136

152137
$statement->executeStatement();

src/Provider/Doctrine/Persistence/Reader/Query.php

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

77
use DH\Auditor\Exception\InvalidArgumentException;
88
use DH\Auditor\Model\Entry;
9+
use DH\Auditor\Provider\ConfigurationInterface;
910
use DH\Auditor\Provider\Doctrine\Persistence\Helper\SchemaHelper;
1011
use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\DateRangeFilter;
1112
use DH\Auditor\Provider\Doctrine\Persistence\Reader\Filter\FilterInterface;
@@ -67,7 +68,7 @@ final class Query implements QueryInterface
6768

6869
private readonly \DateTimeZone $timezone;
6970

70-
public function __construct(private readonly string $table, private readonly Connection $connection, string $timezone)
71+
public function __construct(private readonly string $table, private readonly Connection $connection, private readonly ConfigurationInterface $configuration, string $timezone)
7172
{
7273
$this->timezone = new \DateTimeZone($timezone);
7374

@@ -162,8 +163,7 @@ public function limit(int $limit, int $offset = 0): self
162163

163164
public function getSupportedFilters(): array
164165
{
165-
return array_keys(SchemaHelper::getAuditTableIndices('fake'));
166-
}
166+
return array_keys($this->configuration->getAllIndices('fake')); }
167167

168168
public function getFilters(): array
169169
{

src/Provider/Doctrine/Persistence/Reader/Reader.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,13 @@ public function createQuery(string $entity, array $options = []): Query
4848
$connection = $this->provider->getStorageServiceForEntity($entity)->getEntityManager()->getConnection();
4949
$timezone = $this->provider->getAuditor()->getConfiguration()->getTimezone();
5050

51-
$query = new Query($this->getEntityAuditTableName($entity), $connection, $timezone);
51+
$query = new Query(
52+
$this->getEntityAuditTableName($entity),
53+
$connection,
54+
$this->provider->getConfiguration(),
55+
$timezone,
56+
);
57+
5258
$query
5359
->addOrderBy(Query::CREATED_AT, 'DESC')
5460
->addOrderBy(Query::ID, 'DESC')
@@ -88,6 +94,12 @@ public function createQuery(string $entity, array $options = []): Query
8894
$query->addFilter(new SimpleFilter(Query::DISCRIMINATOR, $entity));
8995
}
9096

97+
foreach ($this->provider->getConfiguration()->getExtraIndices() as $indexedField => $extraIndexConfig) {
98+
if (null !== $config[$indexedField]) {
99+
$query->addFilter(new SimpleFilter($indexedField, $config[$indexedField]));
100+
}
101+
}
102+
91103
return $query;
92104
}
93105

@@ -204,6 +216,11 @@ private function configureOptions(OptionsResolver $resolver): void
204216
->setAllowedValues('page', static fn (?int $value): bool => null === $value || $value >= 1)
205217
->setAllowedValues('page_size', static fn (?int $value): bool => null === $value || $value >= 1)
206218
;
219+
220+
foreach ($this->provider->getConfiguration()->getExtraIndices() as $indexedField => $extraIndexConfig) {
221+
$resolver->setDefault($indexedField, null);
222+
$resolver->setAllowedTypes($indexedField, ['null', 'int', 'string', 'array']);
223+
}
207224
}
208225

209226
/**

0 commit comments

Comments
 (0)