AuditTable::dropAuditTriggers()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 8
c 0
b 0
f 0
nc 3
nop 3
dl 0
loc 13
ccs 5
cts 5
cp 1
crap 3
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace SetBased\Audit;
5
6
use SetBased\Audit\Metadata\TableColumnsMetadata;
7
use SetBased\Audit\MySql\AuditDataLayer;
8
use SetBased\Audit\MySql\Metadata\AlterColumnMetadata;
9
use SetBased\Audit\Style\AuditStyle;
10
11
/**
12
 * Class for creating audit tables and triggers.
13
 */
14
class AuditTable
15
{
16
  //--------------------------------------------------------------------------------------------------------------------
17
  /**
18
   * The metadata of the additional audit columns.
19
   *
20
   * @var TableColumnsMetadata
21
   */
22
  private TableColumnsMetadata $additionalAuditColumns;
23
24
  /**
25
   * The unique alias for this data table.
26
   *
27
   * @var string|null
28
   */
29
  private ?string $alias;
30
31
  /**
32
   * The name of the schema with the audit tables.
33
   *
34
   * @var string
35
   */
36
  private string $auditSchemaName;
37
38
  /**
39
   * The name of the schema with the data tables.
40
   *
41
   * @var string
42
   */
43
  private string $dataSchemaName;
44
45
  /**
46
   * The metadata of the columns of the data table retrieved from information_schema.
47
   *
48
   * @var TableColumnsMetadata
49
   */
50
  private TableColumnsMetadata $dataTableColumnsDatabase;
51
52
  /**
53
   * The output decorator
54
   *
55
   * @var AuditStyle
56
   */
57
  private AuditStyle $io;
58
59
  /**
60
   * The name of the MySQL user defined variable for skipping triggers. When the value of this variable is not null the
61
   * audit trigger will (effectively) be sipped.
62
   *
63
   * @var string|null
64
   */
65
  private ?string $skipVariable;
66
67
  /**
68
   * The name of the data and audit table.
69
   *
70
   * @var string
71
   */
72
  private string $tableName;
73
74
  //--------------------------------------------------------------------------------------------------------------------
75
  /**
76
   * Object constructor.
77
   *
78
   * @param AuditStyle           $io                     The output for log messages.
79
   * @param string               $dataSchemaName         The name of the schema with data tables.
80
   * @param string               $auditSchemaName        The name of the schema with audit tables.
81
   * @param string               $tableName              The name of the data and audit table.
82
   * @param TableColumnsMetadata $additionalAuditColumns The metadata of the additional audit columns.
83
   * @param string|null          $alias                  A unique alias for this table.
84
   * @param string|null          $skipVariable           The name of the MySQL user defined variable for skipping
85 30
   *                                                     triggers.
86
   */
87
  public function __construct(AuditStyle           $io,
88
                              string               $dataSchemaName,
89
                              string               $auditSchemaName,
90
                              string               $tableName,
91
                              TableColumnsMetadata $additionalAuditColumns,
92
                              ?string              $alias,
93 30
                              ?string              $skipVariable)
94 30
  {
95 30
    $this->io                       = $io;
96 30
    $this->dataSchemaName           = $dataSchemaName;
97 30
    $this->auditSchemaName          = $auditSchemaName;
98 30
    $this->tableName                = $tableName;
99 30
    $this->dataTableColumnsDatabase = new TableColumnsMetadata($this->getColumnsFromInformationSchema());
100 30
    $this->additionalAuditColumns   = $additionalAuditColumns;
101 30
    $this->alias                    = $alias;
102
    $this->skipVariable             = $skipVariable;
103
104
    $this->dataTableColumnsDatabase->makeNullable();
105
  }
106
107
  //--------------------------------------------------------------------------------------------------------------------
108
  /**
109
   * Drops all audit triggers from a table.
110
   *
111 30
   * @param AuditStyle $io         The output decorator.
112
   * @param string     $schemaName The name of the table schema.
113 30
   * @param string     $tableName  The name of the table.
114 30
   */
115
  public static function dropAuditTriggers(AuditStyle $io, string $schemaName, string $tableName): void
116 11
  {
117
    $triggers = AuditDataLayer::$dl->getTableTriggers($schemaName, $tableName);
118 11
    foreach ($triggers as $trigger)
119 11
    {
120
      if (preg_match('/^trg_audit_.*_(insert|update|delete)$/', $trigger['trigger_name']))
121
      {
122
        $io->logVerbose('Dropping trigger <dbo>%s</dbo> on <dbo>%s.%s</dbo>',
123 11
                        $trigger['trigger_name'],
124
                        $schemaName,
125
                        $tableName);
126 30
127
        AuditDataLayer::$dl->dropTrigger($schemaName, $trigger['trigger_name']);
128
      }
129
    }
130
  }
131
132
  //--------------------------------------------------------------------------------------------------------------------
133
  /**
134
   * Returns a random alias for this table.
135
   *
136
   * @return string
137
   */
138
  public static function getRandomAlias(): string
139
  {
140
    return uniqid();
141
  }
142
143 27
  //--------------------------------------------------------------------------------------------------------------------
144
  /**
145 27
   * Creates an audit table for this table.
146
   */
147
  public function createAuditTable(): void
148 27
  {
149 27
    $this->io->logInfo('Creating audit table <dbo>%s.%s<dbo>', $this->auditSchemaName, $this->tableName);
150
151 27
    // In the audit table all columns from the data table must be nullable.
152 27
    $dataTableColumnsDatabase = clone($this->dataTableColumnsDatabase);
153 27
    $dataTableColumnsDatabase->makeNullable();
154
155
    $columns = TableColumnsMetadata::combine($this->additionalAuditColumns, $dataTableColumnsDatabase);
156
    AuditDataLayer::$dl->createAuditTable($this->dataSchemaName, $this->auditSchemaName, $this->tableName, $columns);
157
  }
158
159
  //--------------------------------------------------------------------------------------------------------------------
160
  /**
161 30
   * Creates audit triggers on this table.
162
   *
163
   * @param string[] $additionalSql Additional SQL statements to be included in triggers.
164 30
   */
165
  public function createTriggers(array $additionalSql): void
166
  {
167 30
    // Lock the table to prevent insert, updates, or deletes between dropping and creating triggers.
168
    $this->lockTable();
169
170 30
    // Drop all triggers, if any.
171 30
    static::dropAuditTriggers($this->io, $this->dataSchemaName, $this->tableName);
172 30
173
    // Create or recreate the audit triggers.
174
    $this->createTableTrigger('INSERT', $this->skipVariable, $additionalSql);
175 30
    $this->createTableTrigger('UPDATE', $this->skipVariable, $additionalSql);
176 30
    $this->createTableTrigger('DELETE', $this->skipVariable, $additionalSql);
177
178
    // Insert, updates, and deletes are no audited again. So, release lock on the table.
179
    $this->unlockTable();
180
  }
181
182
  //--------------------------------------------------------------------------------------------------------------------
183
  /**
184
   * Returns the table name.
185
   *
186
   * @return string
187
   */
188
  public function getTableName(): string
189
  {
190
    return $this->tableName;
191
  }
192
193
  //--------------------------------------------------------------------------------------------------------------------
194
  /**
195 30
   * Main function for work with table.
196
   *
197 30
   * @param string[] $additionalSql Additional SQL statements to be included in triggers.
198 30
   */
199
  public function main(array $additionalSql): void
200 30
  {
201 30
    $comparedColumns = $this->getTableColumnInfo();
202 30
    $newColumns      = $comparedColumns['new_columns'];
203
204
    $this->addNewColumns($newColumns);
205
    $this->createTriggers($additionalSql);
206
  }
207
208
  //--------------------------------------------------------------------------------------------------------------------
209
  /**
210 30
   * Adds new columns to audit table.
211
   *
212
   * @param TableColumnsMetadata $columns TableColumnsMetadata array
213 30
   */
214
  private function addNewColumns(TableColumnsMetadata $columns): void
215 3
  {
216
    // Return immediately if there are no columns to add.
217 3
    if ($columns->getNumberOfColumns()===0)
218 3
    {
219
      return;
220
    }
221
222
    $alterColumns = $this->alterNewColumns($columns);
223
224
    AuditDataLayer::$dl->addNewColumns($this->auditSchemaName, $this->tableName, $alterColumns);
225
  }
226
227
  //--------------------------------------------------------------------------------------------------------------------
228 3
  /**
229
   * Returns metadata of new table columns that can be used in a 'alter table ... add column' statement.
230 3
   *
231 3
   * @param TableColumnsMetadata $newColumns The metadata new table columns.
232
   *
233 3
   * @return TableColumnsMetadata
234 3
   */
235
  private function alterNewColumns(TableColumnsMetadata $newColumns): TableColumnsMetadata
236
  {
237 3
    $alterNewColumns = new TableColumnsMetadata();
238
    foreach ($newColumns->getColumns() as $newColumn)
239
    {
240
      $properties = $newColumn->getProperties();
241
      $alterNewColumns->appendTableColumn(new AlterColumnMetadata($properties));
242
    }
243
244
    return $alterNewColumns;
245
  }
246
247
  //--------------------------------------------------------------------------------------------------------------------
248 30
  /**
249
   * Creates a triggers for this table.
250 30
   *
251
   * @param string      $action       The trigger action (INSERT, DELETE, or UPDATE).
252 30
   * @param string|null $skipVariable The name of the MySQL user defined variable for skipping triggers.
253 30
   * @param string[]    $additionSql  The additional SQL statements to be included in triggers.
254
   */
255 30
  private function createTableTrigger(string $action, ?string $skipVariable, array $additionSql): void
256 30
  {
257
    $triggerName = $this->getTriggerName($action);
258 30
259 30
    $this->io->logVerbose('Creating trigger <dbo>%s.%s</dbo> on table <dbo>%s.%s</dbo>',
260 30
                          $this->dataSchemaName,
261
                          $triggerName,
262
                          $this->dataSchemaName,
263 30
                          $this->tableName);
264 30
265
    AuditDataLayer::$dl->createAuditTrigger($this->dataSchemaName,
266
                                            $this->auditSchemaName,
267 30
                                            $this->tableName,
268
                                            $triggerName,
269
                                            $action,
270
                                            $this->additionalAuditColumns,
271
                                            $this->dataTableColumnsDatabase,
272
                                            $skipVariable,
273
                                            $additionSql);
274
  }
275 30
276
  //--------------------------------------------------------------------------------------------------------------------
277 30
  /**
278
   * Selects and returns the metadata of the columns of this table from information_schema.
279
   *
280
   * @return array[]
281
   */
282
  private function getColumnsFromInformationSchema(): array
283
  {
284
    return AuditDataLayer::$dl->getTableColumns($this->dataSchemaName, $this->tableName);
285
  }
286 30
287
  //--------------------------------------------------------------------------------------------------------------------
288 30
  /**
289 30
   * Compare columns from table in data_schema with columns in config file.
290 30
   *
291
   * @return array<string,TableColumnsMetadata>
292 30
   */
293 30
  private function getTableColumnInfo(): array
294 30
  {
295
    $actual = new TableColumnsMetadata(AuditDataLayer::$dl->getTableColumns($this->auditSchemaName, $this->tableName));
296 30
    $target = TableColumnsMetadata::combine($this->additionalAuditColumns, $this->dataTableColumnsDatabase);
297
    $target->enhanceAfter();
298 30
299 30
    $new      = TableColumnsMetadata::notInOtherSet($target, $actual);
300 30
    $obsolete = TableColumnsMetadata::notInOtherSet($actual, $target);
301
    $altered  = TableColumnsMetadata::differentColumnTypes($actual, $target, ['is_nullable']);
302
303
    $this->logColumnInfo($new, $obsolete, $altered);
304
305
    return ['new_columns'      => $new,
306
            'obsolete_columns' => $obsolete,
307
            'altered_columns'  => $altered];
308
  }
309
310
  //--------------------------------------------------------------------------------------------------------------------
311 30
  /**
312
   * Returns the trigger name for a trigger action.
313 30
   *
314
   * @param string $action Trigger on action (Insert, Update, Delete).
315
   *
316
   * @return string
317
   */
318
  private function getTriggerName(string $action): string
319
  {
320 30
    return strtolower(sprintf('trg_audit_%s_%s', $this->alias, $action));
321
  }
322 30
323 30
  //--------------------------------------------------------------------------------------------------------------------
324
  /**
325
   * Lock the data table to prevent insert, updates, or deletes between dropping and creating triggers.
326
   */
327
  private function lockTable(): void
328
  {
329
    AuditDataLayer::$dl->lockTable($this->dataSchemaName, $this->tableName);
330
  }
331
332
  //--------------------------------------------------------------------------------------------------------------------
333 30
  /**
334
   * Logs info about new, obsolete, and altered columns.
335
   *
336
   * @param TableColumnsMetadata $newColumns      The metadata of the new columns.
337 30
   * @param TableColumnsMetadata $obsoleteColumns The metadata of the obsolete columns.
338
   * @param TableColumnsMetadata $alteredColumns  The metadata of the altered columns.
339 3
   */
340 3
  private function logColumnInfo(TableColumnsMetadata $newColumns,
341 3
                                 TableColumnsMetadata $obsoleteColumns,
342
                                 TableColumnsMetadata $alteredColumns): void
343
  {
344 30
    foreach ($newColumns->getColumns() as $column)
345
    {
346 2
      $this->io->logInfo('New column <dbo>%s.%s</dbo>',
347 2
                         $this->tableName,
348 2
                         $column->getName());
349
    }
350
351 30
    foreach ($obsoleteColumns->getColumns() as $column)
352
    {
353
      $this->io->logInfo('Obsolete column <dbo>%s.%s</dbo>',
354
                         $this->tableName,
355
                         $column->getName());
356
    }
357
358 30
    foreach ($alteredColumns->getColumns() as $column)
359
    {
360
      $this->io->logInfo('Type of <dbo>%s.%s</dbo> has been altered to <dbo>%s</dbo>',
361
                         $this->tableName,
362
                         $column->getName(),
363
                         $column->getProperty('column_type'));
364 30
    }
365
  }
366 30
367 30
  //--------------------------------------------------------------------------------------------------------------------
368
  /**
369
   * Releases the table lock.
370
   */
371
  private function unlockTable(): void
372
  {
373
    AuditDataLayer::$dl->unlockTables();
374
  }
375
376
  //--------------------------------------------------------------------------------------------------------------------
377
}
378
379
//----------------------------------------------------------------------------------------------------------------------
380