├── bin
└── audit
├── src
├── MySql
│ ├── Metadata
│ │ ├── TableMetadata.php
│ │ ├── AlterColumnMetadata.php
│ │ ├── AuditColumnMetadata.php
│ │ └── ColumnMetadata.php
│ ├── AlterTableCodeStore.php
│ ├── Sql
│ │ ├── AlterAuditTableAddColumns.php
│ │ ├── CreateAuditTable.php
│ │ └── CreateAuditTrigger.php
│ └── AuditDataLayer.php
├── Application
│ └── AuditApplication.php
├── Command
│ ├── DiffCommand.php
│ ├── AuditCommand.php
│ ├── DropTriggersCommand.php
│ ├── AlterAuditTableCommand.php
│ └── BaseCommand.php
├── Style
│ └── AuditStyle.php
├── Metadata
│ ├── TableMetadata.php
│ ├── ColumnMetadata.php
│ └── TableColumnsMetadata.php
├── Audit
│ ├── Audit.php
│ ├── AlterAuditTable.php
│ └── Diff.php
├── DiffTable.php
└── AuditTable.php
├── LICENSE
├── composer.json
└── CODE_OF_CONDUCT.md
/bin/audit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | registerErrorHandler();
26 |
27 | $application = new AuditApplication();
28 | $application->run();
29 |
--------------------------------------------------------------------------------
/src/MySql/Metadata/TableMetadata.php:
--------------------------------------------------------------------------------
1 | =8.3",
12 | "ext-json": "*",
13 | "setbased/error-handler": "^1.4.0",
14 | "setbased/exception": "^2.6.0",
15 | "setbased/helper-code-store-mysql": "^2.3.0",
16 | "setbased/php-stratum-middle": "^5.14.0",
17 | "setbased/php-stratum-mysql": "^7.1.1",
18 | "setbased/typed-config": "^2.1.0",
19 | "symfony/console": "^6.0|^7.0"
20 | },
21 | "require-dev": {
22 | "ext-pcntl": "*",
23 | "ext-posix": "*",
24 | "phing/phing": "^3.0.1",
25 | "phpunit/phpunit": "^11.5.22",
26 | "setbased/phing-extensions": "^3.3.0"
27 | },
28 | "autoload": {
29 | "psr-4": {
30 | "SetBased\\Audit\\": "src/"
31 | }
32 | },
33 | "autoload-dev": {
34 | "psr-4": {
35 | "SetBased\\Audit\\Test\\": "test/"
36 | }
37 | },
38 | "bin": [
39 | "bin/audit"
40 | ],
41 | "config": {
42 | "bin-dir": "bin/",
43 | "sort-packages": true,
44 | "allow-plugins": {
45 | "phing/phing-composer-configurator": true
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Application/AuditApplication.php:
--------------------------------------------------------------------------------
1 | setName('diff')
25 | ->setDescription('Compares data tables and audit tables')
26 | ->addArgument('config file', InputArgument::REQUIRED, 'The audit configuration file')
27 | ->addOption('full', 'f', InputOption::VALUE_NONE, 'Show all columns');
28 | }
29 |
30 | //--------------------------------------------------------------------------------------------------------------------
31 | /**
32 | * @inheritdoc
33 | */
34 | protected function execute(InputInterface $input, OutputInterface $output): int
35 | {
36 | $this->io = new AuditStyle($input, $output);
37 |
38 | $this->configFileName = $input->getArgument('config file');
39 | $this->readConfigFile();
40 |
41 | $this->connect();
42 |
43 | $diff = new Diff($this->config, $this->io, $input, $output);
44 | $diff->main();
45 |
46 | return 0;
47 | }
48 |
49 | //--------------------------------------------------------------------------------------------------------------------
50 | }
51 |
52 | //----------------------------------------------------------------------------------------------------------------------
53 |
--------------------------------------------------------------------------------
/src/Command/AuditCommand.php:
--------------------------------------------------------------------------------
1 | setName('audit')
25 | ->setDescription('Maintains audit tables and audit triggers')
26 | ->setHelp("Maintains audit tables and audit triggers:\n".
27 | "- creates new audit tables\n".
28 | "- adds new columns to exiting audit tables\n".
29 | "- creates new and recreates existing audit triggers\n")
30 | ->addArgument('config file', InputArgument::REQUIRED, 'The audit configuration file');
31 | }
32 |
33 | //--------------------------------------------------------------------------------------------------------------------
34 | /**
35 | * @inheritdoc
36 | */
37 | protected function execute(InputInterface $input, OutputInterface $output): int
38 | {
39 | $this->io = new AuditStyle($input, $output);
40 |
41 | $this->configFileName = $input->getArgument('config file');
42 | $this->readConfigFile();
43 |
44 | $this->connect();
45 |
46 | $audit = new Audit($this->config, $this->io);
47 | $audit->main();
48 |
49 | AuditDataLayer::$dl->disconnect();
50 |
51 | $this->rewriteConfig();
52 |
53 | return 0;
54 | }
55 |
56 | //--------------------------------------------------------------------------------------------------------------------
57 | }
58 |
59 | //----------------------------------------------------------------------------------------------------------------------
60 |
--------------------------------------------------------------------------------
/src/Command/DropTriggersCommand.php:
--------------------------------------------------------------------------------
1 | setName('drop-triggers')
24 | ->setDescription('Drops all triggers')
25 | ->setHelp('Drops all triggers (including triggers not created by audit) from all tables (including tables '.
26 | 'excluded for auditing) in the data schema.')
27 | ->addArgument('config file', InputArgument::REQUIRED, 'The audit configuration file');
28 | }
29 |
30 | //--------------------------------------------------------------------------------------------------------------------
31 | /**
32 | * @inheritdoc
33 | */
34 | protected function execute(InputInterface $input, OutputInterface $output): int
35 | {
36 | $this->io = new AuditStyle($input, $output);
37 |
38 | $this->configFileName = $input->getArgument('config file');
39 | $this->readConfigFile();
40 |
41 | $this->connect();
42 |
43 | $this->dropTriggers();
44 |
45 | AuditDataLayer::$dl->disconnect();
46 |
47 | $this->rewriteConfig();
48 |
49 | return 0;
50 | }
51 |
52 | //--------------------------------------------------------------------------------------------------------------------
53 | /**
54 | * Drops all triggers.
55 | */
56 | private function dropTriggers(): void
57 | {
58 | $dataSchema = $this->config->getManString('database.data_schema');
59 | $triggers = AuditDataLayer::$dl->getTriggers($dataSchema);
60 | foreach ($triggers as $trigger)
61 | {
62 | $this->io->logInfo('Dropping trigger %s from table %s',
63 | $trigger['trigger_name'],
64 | $trigger['table_name']);
65 |
66 | AuditDataLayer::$dl->dropTrigger($dataSchema, $trigger['trigger_name']);
67 | }
68 | }
69 |
70 | //--------------------------------------------------------------------------------------------------------------------
71 | }
72 |
73 | //----------------------------------------------------------------------------------------------------------------------
74 |
--------------------------------------------------------------------------------
/src/Command/AlterAuditTableCommand.php:
--------------------------------------------------------------------------------
1 | setName('alter-audit-table')
24 | ->setDescription('Creates alter SQL statements for audit tables')
25 | ->addArgument('config file', InputArgument::REQUIRED, 'The audit configuration file')
26 | ->addArgument('sql file', InputArgument::OPTIONAL, 'The destination file for the SQL statements');
27 |
28 | $this->setHelp(<<io = new AuditStyle($input, $output);
53 |
54 | $sqlFilename = $input->getArgument('sql file');
55 |
56 | $this->configFileName = $input->getArgument('config file');
57 | $this->readConfigFile();
58 |
59 | $this->connect();
60 |
61 | $alter = new AlterAuditTable($this->config);
62 | $sql = $alter->main();
63 |
64 | if ($sqlFilename!==null)
65 | {
66 | $this->writeTwoPhases($sqlFilename, $sql);
67 | }
68 | else
69 | {
70 | $this->io->write($sql);
71 | }
72 |
73 | return 0;
74 | }
75 |
76 | //--------------------------------------------------------------------------------------------------------------------
77 | }
78 |
79 | //----------------------------------------------------------------------------------------------------------------------
80 |
--------------------------------------------------------------------------------
/src/MySql/Sql/AlterAuditTableAddColumns.php:
--------------------------------------------------------------------------------
1 | auditSchemaName = $auditSchemaName;
48 | $this->tableName = $tableName;
49 | $this->columns = $columns;
50 | }
51 |
52 | //--------------------------------------------------------------------------------------------------------------------
53 | /**
54 | * Returns a SQL statement for adding new columns to the audit table.
55 | *
56 | * @return string
57 | */
58 | public function buildStatement(): string
59 | {
60 | $code = new MySqlCompoundSyntaxCodeStore();
61 |
62 | $code->append(sprintf('alter table `%s`.`%s`', $this->auditSchemaName, $this->tableName));
63 | foreach ($this->columns->getColumns() as $column)
64 | {
65 | $code->append(sprintf(' add `%s` %s', $column->getName(), $column->getColumnAuditDefinition()), false);
66 | $after = $column->getProperty('after');
67 | if (isset($after))
68 | {
69 | $code->appendToLastLine(sprintf(' after `%s`', $after));
70 | }
71 | else
72 | {
73 | $code->appendToLastLine(' first');
74 | }
75 | $columns = $this->columns->getColumns();
76 | if (end($columns)!==$column)
77 | {
78 | $code->appendToLastLine(',');
79 | }
80 | }
81 |
82 | return $code->getCode();
83 | }
84 |
85 | //--------------------------------------------------------------------------------------------------------------------
86 | }
87 |
88 | //----------------------------------------------------------------------------------------------------------------------
89 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at info@setbased.nl. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/src/Style/AuditStyle.php:
--------------------------------------------------------------------------------
1 | getFormatter()
27 | ->setStyle('note', $style);
28 |
29 | // Create style for database objects.
30 | $style = new OutputFormatterStyle('green', null, ['bold']);
31 | $output->getFormatter()
32 | ->setStyle('dbo', $style);
33 |
34 | // Create style for file and directory names.
35 | $style = new OutputFormatterStyle(null, null, ['bold']);
36 | $output->getFormatter()
37 | ->setStyle('fso', $style);
38 |
39 | // Create style for SQL statements.
40 | $style = new OutputFormatterStyle('magenta', null, ['bold']);
41 | $output->getFormatter()
42 | ->setStyle('sql', $style);
43 | }
44 |
45 | //--------------------------------------------------------------------------------------------------------------------
46 | /**
47 | * Logs a message if verbosity is OutputInterface::VERBOSITY_NORMAL or higher.
48 | *
49 | * This method takes arguments like sprintf.
50 | */
51 | public function logInfo(): void
52 | {
53 | if ($this->getVerbosity()>=OutputInterface::VERBOSITY_NORMAL)
54 | {
55 | $args = func_get_args();
56 | $format = array_shift($args);
57 |
58 | $this->writeln(vsprintf(''.$format.'', $args));
59 | }
60 | }
61 |
62 | //--------------------------------------------------------------------------------------------------------------------
63 | /**
64 | * Logs a message if verbosity is OutputInterface::VERBOSITY_VERBOSE or higher.
65 | *
66 | * This method takes arguments like sprintf.
67 | */
68 | public function logVerbose(): void
69 | {
70 | if ($this->getVerbosity()>=OutputInterface::VERBOSITY_VERBOSE)
71 | {
72 | $args = func_get_args();
73 | $format = array_shift($args);
74 |
75 | $this->writeln(vsprintf(''.$format.'', $args));
76 | }
77 | }
78 |
79 | //--------------------------------------------------------------------------------------------------------------------
80 | /**
81 | * Logs a message if verbosity is OutputInterface::VERBOSITY_VERY_VERBOSE or higher.
82 | *
83 | * This method takes arguments like sprintf.
84 | */
85 | public function logVeryVerbose(): void
86 | {
87 | if ($this->getVerbosity()>=OutputInterface::VERBOSITY_VERY_VERBOSE)
88 | {
89 | $args = func_get_args();
90 | $format = array_shift($args);
91 |
92 | $this->writeln(vsprintf(''.$format.'', $args));
93 | }
94 | }
95 |
96 | //--------------------------------------------------------------------------------------------------------------------
97 | }
98 |
99 | //----------------------------------------------------------------------------------------------------------------------
100 |
--------------------------------------------------------------------------------
/src/MySql/Metadata/ColumnMetadata.php:
--------------------------------------------------------------------------------
1 | getProperty('column_type')!==null)
34 | {
35 | $parts[] = $this->getProperty('column_type');
36 | }
37 |
38 | if ($this->getProperty('character_set_name')!==null)
39 | {
40 | $parts[] = 'character set '.$this->getProperty('character_set_name');
41 | }
42 |
43 | if ($this->getProperty('collation_name')!==null)
44 | {
45 | $parts[] = 'collate '.$this->getProperty('collation_name');
46 | }
47 |
48 | $parts[] = ($this->getProperty('is_nullable')==='YES') ? 'null' : 'not null';
49 |
50 | if ($this->getProperty('column_default')!==null && $this->getProperty('column_default')!=='NULL')
51 | {
52 | $parts[] = 'default '.$this->getProperty('column_default');
53 | }
54 | elseif ($this->getProperty('column_type')==='timestamp' && $this->getProperty('is_nullable')==='YES')
55 | {
56 | // Prevent automatic updates of timestamp columns.
57 | $parts[] = 'default null';
58 | }
59 |
60 | return implode(' ', $parts);
61 | }
62 |
63 | //--------------------------------------------------------------------------------------------------------------------
64 | /**
65 | * @inheritdoc
66 | */
67 | public function getColumnDefinition(): string
68 | {
69 | $parts = [];
70 |
71 | if ($this->getProperty('column_type')!==null)
72 | {
73 | $parts[] = $this->getProperty('column_type');
74 | }
75 |
76 | if ($this->getProperty('character_set_name')!==null)
77 | {
78 | $parts[] = 'character set '.$this->getProperty('character_set_name');
79 | }
80 |
81 | if ($this->getProperty('collation_name')!==null)
82 | {
83 | $parts[] = 'collate '.$this->getProperty('collation_name');
84 | }
85 |
86 | $parts[] = ($this->getProperty('is_nullable')==='YES') ? 'null' : 'not null';
87 |
88 | if ($this->getProperty('column_default')!==null && $this->getProperty('column_default')!=='NULL')
89 | {
90 | $parts[] = 'default '.$this->getProperty('column_default');
91 | }
92 |
93 | return implode(' ', $parts);
94 | }
95 |
96 | //--------------------------------------------------------------------------------------------------------------------
97 | /**
98 | * @inheritdoc
99 | */
100 | public function getTypeInfo1(): string
101 | {
102 | if ($this->getProperty('is_nullable')==='YES')
103 | {
104 | return $this->getProperty('column_type');
105 | }
106 |
107 | return $this->getProperty('column_type').' not null';
108 | }
109 |
110 | //--------------------------------------------------------------------------------------------------------------------
111 | /**
112 | * @inheritdoc
113 | */
114 | public function getTypeInfo2(): ?string
115 | {
116 | if ($this->getProperty('collation_name')!==null)
117 | {
118 | return sprintf('[%s] [%s]', $this->getProperty('character_set_name'), $this->getProperty('collation_name'));
119 | }
120 |
121 | return null;
122 | }
123 |
124 | //--------------------------------------------------------------------------------------------------------------------
125 | }
126 |
127 | //----------------------------------------------------------------------------------------------------------------------
128 |
--------------------------------------------------------------------------------
/src/MySql/Sql/CreateAuditTable.php:
--------------------------------------------------------------------------------
1 | dataSchemaName = $dataSchemaName;
60 | $this->auditSchemaName = $auditSchemaName;
61 | $this->tableName = $tableName;
62 | $this->columns = $columns;
63 | }
64 |
65 | //--------------------------------------------------------------------------------------------------------------------
66 | /**
67 | * Returns a SQL statement for creating the audit table.
68 | *
69 | * @return string
70 | */
71 | public function buildStatement(): string
72 | {
73 | $code = new MySqlCompoundSyntaxCodeStore();
74 |
75 | $code->append(sprintf('create table `%s`.`%s`', $this->auditSchemaName, $this->tableName));
76 |
77 | // Create SQL for columns.
78 | $code->append('(');
79 | $code->append($this->getColumnDefinitions());
80 |
81 | // Create SQL for table options.
82 | $tableOptions = AuditDataLayer::$dl->getTableOptions($this->dataSchemaName, $this->tableName);
83 | $code->append(sprintf(') engine=%s character set=%s collate=%s',
84 | $tableOptions['engine'],
85 | $tableOptions['character_set_name'],
86 | $tableOptions['table_collation']));
87 |
88 | return $code->getCode();
89 | }
90 |
91 | //--------------------------------------------------------------------------------------------------------------------
92 | /**
93 | * Returns an array with SQL code for column definitions.
94 | *
95 | * @return string[]
96 | */
97 | private function getColumnDefinitions(): array
98 | {
99 | $lines = [];
100 |
101 | $columns = $this->columns->getColumns();
102 | $maxLength = $this->columns->getLongestColumnNameLength();
103 | foreach ($columns as $column)
104 | {
105 | $name = $column->getName();
106 | $filler = str_repeat(' ', $maxLength - mb_strlen($name) + 1);
107 |
108 | $line = sprintf(' `%s`%s%s', $name, $filler, $column->getColumnAuditDefinition());
109 |
110 | if (end($columns)!==$column)
111 | {
112 | $line .= ',';
113 | }
114 |
115 | $lines[] = $line;
116 | }
117 |
118 | return $lines;
119 | }
120 |
121 | //--------------------------------------------------------------------------------------------------------------------
122 | }
123 |
124 | //----------------------------------------------------------------------------------------------------------------------
125 |
--------------------------------------------------------------------------------
/src/Metadata/TableMetadata.php:
--------------------------------------------------------------------------------
1 | properties[$field] = $properties[$field];
47 | }
48 | }
49 |
50 | $this->columns = $columns;
51 | }
52 |
53 | //--------------------------------------------------------------------------------------------------------------------
54 | /**
55 | * Compares two metadata of two tables. Returns an array with the names of the different properties.
56 | *
57 | * @param TableMetadata $table1 The metadata of the first table.
58 | * @param TableMetadata $table2 The metadata of the second table.
59 | *
60 | * @return string[]
61 | */
62 | public static function compareOptions(TableMetadata $table1, TableMetadata $table2): array
63 | {
64 | $diff = [];
65 |
66 | foreach (static::$fields as $field)
67 | {
68 | if (!in_array($field, ['table_schema', 'table_name']))
69 | {
70 | if ($table1->getProperty($field)!==$table2->getProperty($field))
71 | {
72 | $diff[] = $field;
73 | }
74 | }
75 | }
76 |
77 | return $diff;
78 | }
79 |
80 | //--------------------------------------------------------------------------------------------------------------------
81 | /**
82 | * Returns table columns.
83 | *
84 | * @return TableColumnsMetadata
85 | */
86 | public function getColumns(): TableColumnsMetadata
87 | {
88 | return $this->columns;
89 | }
90 |
91 | //--------------------------------------------------------------------------------------------------------------------
92 | /**
93 | * Returns the options of this table.
94 | *
95 | * @return array[]
96 | */
97 | public function getOptions(): array
98 | {
99 | $ret = $this->properties;
100 |
101 | unset($ret['table_name']);
102 | unset($ret['table_schema']);
103 |
104 | return $ret;
105 | }
106 |
107 | //--------------------------------------------------------------------------------------------------------------------
108 | /**
109 | * Returns a property of this table.
110 | *
111 | * @param string $name The name of the property.
112 | *
113 | * @return string|null
114 | */
115 | public function getProperty(string $name): ?string
116 | {
117 | if (isset($this->properties[$name]))
118 | {
119 | return $this->properties[$name];
120 | }
121 |
122 | return null;
123 | }
124 |
125 | //--------------------------------------------------------------------------------------------------------------------
126 | /**
127 | * Returns the name of schema.
128 | *
129 | * @return string
130 | */
131 | public function getSchemaName(): string
132 | {
133 | return $this->properties['table_schema'];
134 | }
135 |
136 | //--------------------------------------------------------------------------------------------------------------------
137 | /**
138 | * Returns the name of this table.
139 | *
140 | * @return string
141 | */
142 | public function getTableName(): string
143 | {
144 | return $this->properties['table_name'];
145 | }
146 |
147 | //--------------------------------------------------------------------------------------------------------------------
148 | }
149 |
150 | //----------------------------------------------------------------------------------------------------------------------
151 |
--------------------------------------------------------------------------------
/src/Metadata/ColumnMetadata.php:
--------------------------------------------------------------------------------
1 | properties[$field] = $properties[$field];
43 | }
44 | }
45 | }
46 |
47 | //--------------------------------------------------------------------------------------------------------------------
48 | /**
49 | * Compares the metadata of two columns.
50 | *
51 | * @param ColumnMetadata $column1 The metadata of the first column.
52 | * @param ColumnMetadata $column2 The metadata of the second column.
53 | * @param string[] $ignore The properties to be ignored.
54 | *
55 | * @return bool True if the columns are equal, false otherwise.
56 | */
57 | public static function compare(ColumnMetadata $column1, ColumnMetadata $column2, array $ignore = []): bool
58 | {
59 | $equal = true;
60 |
61 | foreach (static::$fields as $field)
62 | {
63 | if (!in_array($field, $ignore))
64 | {
65 | if ($column1->getProperty($field)!==$column2->getProperty($field))
66 | {
67 | $equal = false;
68 | }
69 | }
70 | }
71 |
72 | return $equal;
73 | }
74 |
75 | //--------------------------------------------------------------------------------------------------------------------
76 | /**
77 | * Returns a SQL snippet with the column definition (without column name) of this column to be used in audit tables.
78 | *
79 | * @return string
80 | */
81 | abstract public function getColumnAuditDefinition(): string;
82 |
83 | //--------------------------------------------------------------------------------------------------------------------
84 | /**
85 | * Returns a SQL snippet with the column definition (without column name) of this column.
86 | *
87 | * @return string
88 | */
89 | abstract public function getColumnDefinition(): string;
90 |
91 | //--------------------------------------------------------------------------------------------------------------------
92 | /**
93 | * Returns the name of this column.
94 | *
95 | * @return string
96 | */
97 | public function getName(): string
98 | {
99 | return $this->properties['column_name'];
100 | }
101 |
102 | //--------------------------------------------------------------------------------------------------------------------
103 | /**
104 | * Returns the properties of this table column as an array.
105 | *
106 | * @return array
107 | */
108 | public function getProperties(): array
109 | {
110 | return $this->properties;
111 | }
112 |
113 | //--------------------------------------------------------------------------------------------------------------------
114 | /**
115 | * Returns a property of this table column.
116 | *
117 | * @param string $name The name of the property.
118 | *
119 | * @return string|null
120 | */
121 | public function getProperty(string $name): ?string
122 | {
123 | return $this->properties[$name] ?? null;
124 | }
125 |
126 | //--------------------------------------------------------------------------------------------------------------------
127 | /**
128 | * Returns column type info
129 | *
130 | * @return string
131 | */
132 | abstract public function getTypeInfo1(): string;
133 |
134 | //--------------------------------------------------------------------------------------------------------------------
135 | /**
136 | * Returns additional column type info
137 | *
138 | * @return string|null
139 | */
140 | abstract public function getTypeInfo2(): ?string;
141 |
142 | //--------------------------------------------------------------------------------------------------------------------
143 | /**
144 | * Make this column nullable.
145 | */
146 | public function makeNullable(): void
147 | {
148 | $this->properties['is_nullable'] = 'YES';
149 | }
150 |
151 | //--------------------------------------------------------------------------------------------------------------------
152 | /**
153 | * Sets property 'after'.
154 | *
155 | * @param string|null $after
156 | */
157 | public function setAfter(?string $after): void
158 | {
159 | $this->properties['after'] = $after;
160 | }
161 |
162 | //--------------------------------------------------------------------------------------------------------------------
163 | /**
164 | * Removes the default value.
165 | */
166 | public function unsetDefault(): void
167 | {
168 | if (isset($this->properties['column_default']))
169 | {
170 | $this->properties['column_default'] = 'NULL';
171 | }
172 | }
173 |
174 | //--------------------------------------------------------------------------------------------------------------------
175 | }
176 |
177 | //----------------------------------------------------------------------------------------------------------------------
178 |
--------------------------------------------------------------------------------
/src/Command/BaseCommand.php:
--------------------------------------------------------------------------------
1 | config = new TypedConfig(new Config($this->configFileName));
62 | $config = $this->config->getConfig();
63 |
64 | foreach (self::$sections as $key)
65 | {
66 | if (!isset($config[$key]))
67 | {
68 | $config[$key] = [];
69 | }
70 | }
71 |
72 | $credentials = $this->config->getOptString('database.credentials');
73 | if ($credentials!==null)
74 | {
75 | $tmp = new TypedConfig(new Config(dirname($this->configFileName).'/'.$credentials));
76 | foreach ($tmp->getManArray('database') as $key => $value)
77 | {
78 | $config->set('database.'.$key, $value);
79 | }
80 | }
81 | }
82 |
83 | //--------------------------------------------------------------------------------------------------------------------
84 | /**
85 | * Use for testing only.
86 | *
87 | * @param bool $rewriteConfigFile If true, the config file must be rewritten. Otherwise, the config must not be
88 | * rewritten.
89 | */
90 | public function setRewriteConfigFile(bool $rewriteConfigFile): void
91 | {
92 | $this->rewriteConfigFile = $rewriteConfigFile;
93 | }
94 |
95 | //--------------------------------------------------------------------------------------------------------------------
96 | /**
97 | * Connects to a MySQL instance.
98 | */
99 | protected function connect(): void
100 | {
101 | $connector = new MySqlDefaultConnector($this->config->getManString('database.host'),
102 | $this->config->getManString('database.user'),
103 | $this->config->getManString('database.password'),
104 | $this->config->getManString('database.data_schema'),
105 | $this->config->getManInt('database.port', 3306));
106 | $dl = new AuditDataLayer($connector, $this->io);
107 | $dl->connect();
108 | }
109 |
110 | //--------------------------------------------------------------------------------------------------------------------
111 | /**
112 | * Rewrites the config file with updated data.
113 | */
114 | protected function rewriteConfig(): void
115 | {
116 | // Return immediately when the config file must not be rewritten.
117 | if (!$this->rewriteConfigFile)
118 | {
119 | return;
120 | }
121 |
122 | $tables = $this->config->getManArray('tables');
123 | ksort($tables);
124 |
125 | $config = new Config($this->configFileName);
126 | $config['tables'] = $tables;
127 |
128 | $data = [];
129 | foreach (self::$sections as $key)
130 | {
131 | if (!empty($config->get($key)))
132 | {
133 | $data[$key] = $config->get($key);
134 | }
135 | }
136 |
137 | $this->writeTwoPhases($this->configFileName, json_encode($data, JSON_PRETTY_PRINT));
138 | }
139 |
140 | //--------------------------------------------------------------------------------------------------------------------
141 | /**
142 | * Writes a file in two phase to the filesystem.
143 | *
144 | * First write the data to a temporary file (in the same directory) and than renames the temporary file. If the file
145 | * already exists and its content is equal to the data that must be written no action is taken. This has the
146 | * following advantages:
147 | * * In case of some write error (e.g. disk full) the original file is kept in tact and no file with partially data
148 | * is written.
149 | * * Renaming a file is atomic. So, running processes will never read a partially written data.
150 | *
151 | * @param string $filename The name of the file were the data must be stored.
152 | * @param string $data The data that must be written.
153 | */
154 | protected function writeTwoPhases(string $filename, string $data): void
155 | {
156 | $write_flag = true;
157 | if (file_exists($filename))
158 | {
159 | $old_data = file_get_contents($filename);
160 | if ($data===$old_data)
161 | {
162 | $write_flag = false;
163 | }
164 | }
165 |
166 | if ($write_flag)
167 | {
168 | $tmp_filename = $filename.'.tmp';
169 | file_put_contents($tmp_filename, $data);
170 | rename($tmp_filename, $filename);
171 |
172 | $this->io->text(sprintf('Wrote %s', OutputFormatter::escape($filename)));
173 | }
174 | else
175 | {
176 | $this->io->text(sprintf('File %s is up to date', OutputFormatter::escape($filename)));
177 | }
178 | }
179 |
180 | //--------------------------------------------------------------------------------------------------------------------
181 | }
182 |
183 | //----------------------------------------------------------------------------------------------------------------------
184 |
--------------------------------------------------------------------------------
/src/Audit/Audit.php:
--------------------------------------------------------------------------------
1 | config = $config;
64 | $this->io = $io;
65 |
66 | $this->additionalAuditColumns =
67 | AuditDataLayer::$dl->resolveCanonicalAdditionalAuditColumns($this->config->getManString('database.audit_schema'),
68 | $this->config->getManArray('audit_columns'));
69 | }
70 |
71 | //--------------------------------------------------------------------------------------------------------------------
72 | /**
73 | * Getting list of all tables from information_schema of database from config file.
74 | */
75 | public function listOfTables(): void
76 | {
77 | $this->dataSchemaTables = AuditDataLayer::$dl->getTablesNames($this->config->getManString('database.data_schema'));
78 | $this->auditSchemaTables = AuditDataLayer::$dl->getTablesNames($this->config->getManString('database.audit_schema'));
79 | }
80 |
81 | //--------------------------------------------------------------------------------------------------------------------
82 | /**
83 | * The main method: executes the auditing actions for tables.
84 | */
85 | public function main(): void
86 | {
87 | $this->listOfTables();
88 | $this->unknownTables();
89 | $this->obsoleteTables();
90 | $this->knownTables();
91 | }
92 |
93 | //--------------------------------------------------------------------------------------------------------------------
94 | /**
95 | * Removes tables listed in the config file that are no longer in the data schema from the config file.
96 | */
97 | public function obsoleteTables(): void
98 | {
99 | foreach ($this->config->getManArray('tables') as $tableName => $dummy)
100 | {
101 | if (RowSetHelper::searchInRowSet($this->dataSchemaTables, 'table_name', $tableName)===null)
102 | {
103 | $this->io->writeln(sprintf('Removing obsolete table %s from config file', $tableName));
104 |
105 | // Unset table (work a round bug in \Noodlehaus\Config::remove()).
106 | $config = $this->config->getConfig();
107 | $tables = $config['tables'];
108 | unset($tables[$tableName]);
109 | $config->set('tables', $tables);
110 | }
111 | }
112 | }
113 |
114 | //--------------------------------------------------------------------------------------------------------------------
115 | /**
116 | * Compares the tables listed in the config file and the tables found in the data schema.
117 | */
118 | public function unknownTables(): void
119 | {
120 | foreach ($this->dataSchemaTables as $table)
121 | {
122 | if ($this->config->getOptArray('tables.'.$table['table_name'])!==null)
123 | {
124 | if ($this->config->getOptBool('tables.'.$table['table_name'].'.audit')===null)
125 | {
126 | $this->io->writeln(sprintf('Audit not set for table %s', $table['table_name']));
127 | }
128 | else
129 | {
130 | if ($this->config->getManBool('tables.'.$table['table_name'].'.audit'))
131 | {
132 | if ($this->config->getOptString('tables.'.$table['table_name'].'.alias')===null)
133 | {
134 | $this->config->getConfig()
135 | ->set('tables.'.$table['table_name'].'.alias', AuditTable::getRandomAlias());
136 | }
137 | }
138 | }
139 | }
140 | else
141 | {
142 | $this->io->writeln(sprintf('Found new table %s', $table['table_name']));
143 | $config = $this->config->getConfig();
144 | $config->set('tables.'.$table['table_name'], ['audit' => null, 'alias' => null, 'skip' => null]);
145 | }
146 | }
147 | }
148 |
149 | //--------------------------------------------------------------------------------------------------------------------
150 | /**
151 | * Processed known tables.
152 | */
153 | private function knownTables(): void
154 | {
155 | foreach ($this->dataSchemaTables as $table)
156 | {
157 | $audit = $this->config->getOptBool('tables.'.$table['table_name'].'.audit');
158 | if ($audit===true)
159 | {
160 | $currentTable = new AuditTable($this->io,
161 | $this->config->getManString('database.data_schema'),
162 | $this->config->getManString('database.audit_schema'),
163 | $table['table_name'],
164 | $this->additionalAuditColumns,
165 | $this->config->getOptString('tables.'.$table['table_name'].'.alias'),
166 | $this->config->getOptString('tables.'.$table['table_name'].'.skip'));
167 |
168 | // Ensure the audit table exists.
169 | if (RowSetHelper::searchInRowSet($this->auditSchemaTables, 'table_name', $table['table_name'])===null)
170 | {
171 | $currentTable->createAuditTable();
172 | }
173 |
174 | // Drop and create audit triggers and add new columns to the audit table.
175 | $currentTable->main($this->config->getManArray('additional_sql'));
176 | }
177 | elseif ($audit===false)
178 | {
179 | AuditTable::dropAuditTriggers($this->io,
180 | $this->config->getManString('database.data_schema'),
181 | $table['table_name']);
182 | }
183 | else /* $audit===null */
184 | {
185 | $this->io->logVerbose('Ignoring table %s', $table['table_name']);
186 | }
187 | }
188 | }
189 |
190 | //--------------------------------------------------------------------------------------------------------------------
191 | }
192 |
193 | //----------------------------------------------------------------------------------------------------------------------
194 |
--------------------------------------------------------------------------------
/src/Audit/AlterAuditTable.php:
--------------------------------------------------------------------------------
1 | config = $config;
49 | $this->codeStore = new AlterTableCodeStore();
50 |
51 | $this->additionalAuditColumns =
52 | AuditDataLayer::$dl->resolveCanonicalAdditionalAuditColumns($this->config->getManString('database.audit_schema'),
53 | $this->config->getManArray('audit_columns'));
54 | }
55 |
56 | //--------------------------------------------------------------------------------------------------------------------
57 | /**
58 | * The main method: executes the creates alter table statement actions for tables.
59 | */
60 | public function main(): string
61 | {
62 | $tables = $this->getTableList();
63 | foreach ($tables as $table)
64 | {
65 | $this->compareTable($table);
66 | }
67 |
68 | return $this->codeStore->getCode();
69 | }
70 |
71 | //--------------------------------------------------------------------------------------------------------------------
72 | /**
73 | * Compares a table in the data schema and its counterpart in the audit schema.
74 | *
75 | * @param string $tableName The name of the table.
76 | */
77 | private function compareTable(string $tableName): void
78 | {
79 | $dataTable = $this->getTableMetadata($this->config->getManString('database.data_schema'), $tableName);
80 | $auditTable = $this->getTableMetadata($this->config->getManString('database.audit_schema'), $tableName);
81 |
82 | // In the audit schema columns corresponding with the columns from the data table are always nullable.
83 | $dataTable->getColumns()
84 | ->makeNullable();
85 | $dataTable->getColumns()
86 | ->prependTableColumns($this->additionalAuditColumns);
87 |
88 | $this->compareTableOptions($dataTable, $auditTable);
89 | $this->compareTableColumns($dataTable, $auditTable);
90 | }
91 |
92 | //--------------------------------------------------------------------------------------------------------------------
93 | /**
94 | * Compares the columns of the data and audit tables and generates the appropriate alter table statement.
95 | *
96 | * @param TableMetadata $dataTable The metadata of the data table.
97 | * @param TableMetadata $auditTable The metadata of the audit table.
98 | */
99 | private function compareTableColumns(TableMetadata $dataTable, TableMetadata $auditTable): void
100 | {
101 | $diff = TableColumnsMetadata::differentColumnTypes($auditTable->getColumns(), $dataTable->getColumns());
102 |
103 | if (!empty($diff->getColumns()))
104 | {
105 | $maxLength = $diff->getLongestColumnNameLength();
106 |
107 | $this->codeStore->append(sprintf('alter table `%s`.`%s`',
108 | $this->config->getManString('database.audit_schema'),
109 | $auditTable->getTableName()));
110 |
111 | $first = true;
112 | foreach ($diff->getColumns() as $column)
113 | {
114 | $name = $column->getName();
115 | $filler = str_repeat(' ', $maxLength - mb_strlen($name) + 1);
116 |
117 | if (!$first)
118 | {
119 | $this->codeStore->appendToLastLine(',');
120 | }
121 |
122 | $this->codeStore->append(sprintf('change column `%s`%s`%s`%s%s',
123 | $name,
124 | $filler,
125 | $name,
126 | $filler,
127 | $column->getColumnAuditDefinition()));
128 |
129 | $first = false;
130 | }
131 |
132 | $this->codeStore->append(';');
133 | $this->codeStore->append('');
134 | }
135 | }
136 |
137 | //--------------------------------------------------------------------------------------------------------------------
138 | /**
139 | * Compares the table options of the data and audit tables and generates the appropriate alter table statement.
140 | *
141 | * @param TableMetadata $dataTable The metadata of the data table.
142 | * @param TableMetadata $auditTable The metadata of the audit table.
143 | */
144 | private function compareTableOptions(TableMetadata $dataTable, TableMetadata $auditTable): void
145 | {
146 | $options = TableMetadata::compareOptions($dataTable, $auditTable);
147 |
148 | if (!empty($options))
149 | {
150 | $parts = [];
151 | foreach ($options as $option)
152 | {
153 | switch ($option)
154 | {
155 | case 'engine':
156 | $parts[] = 'engine '.$dataTable->getProperty('engine');
157 | break;
158 |
159 | case 'character_set_name':
160 | $parts[] = 'default character set '.$dataTable->getProperty('character_set_name');
161 | break;
162 |
163 | case 'table_collation':
164 | $parts[] = 'default collate '.$dataTable->getProperty('table_collation');
165 | break;
166 |
167 | default:
168 | throw new FallenException('option', $option);
169 | }
170 | }
171 |
172 | $this->codeStore->append(sprintf('alter table `%s`.`%s` %s;',
173 | $this->config->getManString('database.audit_schema'),
174 | $auditTable->getTableName(),
175 | implode(' ', $parts)));
176 | $this->codeStore->append('');
177 | }
178 | }
179 |
180 | //--------------------------------------------------------------------------------------------------------------------
181 | /**
182 | * Returns the names of the tables that must be compared.
183 | *
184 | * @return string[]
185 | */
186 | private function getTableList(): array
187 | {
188 | $tables1 = [];
189 | foreach ($this->config->getManArray('tables') as $tableName => $config)
190 | {
191 | if ($config['audit'])
192 | {
193 | $tables1[] = $tableName;
194 | }
195 | }
196 |
197 | $tables = AuditDataLayer::$dl->getTablesNames($this->config->getManString('database.data_schema'));
198 | $tables2 = [];
199 | foreach ($tables as $table)
200 | {
201 | $tables2[] = $table['table_name'];
202 | }
203 |
204 | $tables = AuditDataLayer::$dl->getTablesNames($this->config->getManString('database.audit_schema'));
205 | $tables3 = [];
206 | foreach ($tables as $table)
207 | {
208 | $tables3[] = $table['table_name'];
209 | }
210 |
211 | return array_intersect($tables1, $tables2, $tables3);
212 | }
213 |
214 | //--------------------------------------------------------------------------------------------------------------------
215 | /**
216 | * Returns the metadata of a table.
217 | *
218 | * @param string $schemaName The name of the schema of the table.
219 | * @param string $tableName The name of the table.
220 | *
221 | * @return TableMetadata
222 | */
223 | private function getTableMetadata(string $schemaName, string $tableName): TableMetadata
224 | {
225 | $table = AuditDataLayer::$dl->getTableOptions($schemaName, $tableName);
226 | $columns = AuditDataLayer::$dl->getTableColumns($schemaName, $tableName);
227 |
228 | return new TableMetadata($table, new TableColumnsMetadata($columns));
229 | }
230 |
231 | //--------------------------------------------------------------------------------------------------------------------
232 | }
233 |
234 | //----------------------------------------------------------------------------------------------------------------------
235 |
--------------------------------------------------------------------------------
/src/Audit/Diff.php:
--------------------------------------------------------------------------------
1 | io = $io;
69 | $this->config = $config;
70 | $this->input = $input;
71 | $this->output = $output;
72 |
73 | $this->additionalAuditColumns =
74 | AuditDataLayer::$dl->resolveCanonicalAdditionalAuditColumns($this->config->getManString('database.audit_schema'),
75 | $this->config->getManArray('audit_columns'));
76 | }
77 |
78 | //--------------------------------------------------------------------------------------------------------------------
79 | /**
80 | * The main method: executes the auditing actions for tables.
81 | */
82 | public function main(): void
83 | {
84 | // Style for column names with miss matched column types.
85 | $style = new OutputFormatterStyle(null, 'red');
86 | $this->output->getFormatter()
87 | ->setStyle('mm_column', $style);
88 |
89 | // Style for column types of columns with miss matched column types.
90 | $style = new OutputFormatterStyle('yellow');
91 | $this->output->getFormatter()
92 | ->setStyle('mm_type', $style);
93 |
94 | // Style for obsolete tables.
95 | $style = new OutputFormatterStyle('yellow');
96 | $this->output->getFormatter()
97 | ->setStyle('obsolete_table', $style);
98 |
99 | // Style for missing tables.
100 | $style = new OutputFormatterStyle('red');
101 | $this->output->getFormatter()
102 | ->setStyle('miss_table', $style);
103 |
104 | $lists = $this->getTableLists();
105 |
106 | $this->currentAuditTables($lists['current']);
107 | $this->missingAuditTables($lists['missing']);
108 | $this->obsoleteAuditTables($lists['obsolete']);
109 | }
110 |
111 | //--------------------------------------------------------------------------------------------------------------------
112 | /**
113 | * Prints the difference between a data and its related audit table.
114 | *
115 | * @param string $tableName The table name.
116 | */
117 | private function currentAuditTable(string $tableName): void
118 | {
119 | $columns = AuditDataLayer::$dl->getTableColumns($this->config->getManString('database.data_schema'),
120 | $tableName);
121 | $dataTableColumns = new TableColumnsMetadata($columns);
122 | $columns = AuditDataLayer::$dl->getTableColumns($this->config->getManString('database.audit_schema'),
123 | $tableName);
124 | $auditTableColumns = new TableColumnsMetadata($columns, 'AuditColumnMetadata');
125 |
126 | // In the audit table columns coming from the data table are always nullable.
127 | $dataTableColumns->makeNullable();
128 | $dataTableColumns->unsetDefaults();
129 | $dataTableColumns = TableColumnsMetadata::combine($this->additionalAuditColumns, $dataTableColumns);
130 |
131 | // In the audit table columns coming from the data table don't have defaults.
132 | foreach ($auditTableColumns->getColumns() as $column)
133 | {
134 | if (!in_array($column->getName(), $this->additionalAuditColumns->getColumnNames()))
135 | {
136 | $column->unsetDefault();
137 | }
138 | }
139 |
140 | $dataTableOptions = AuditDataLayer::$dl->getTableOptions($this->config->getManString('database.data_schema'),
141 | $tableName);
142 | $auditTableOptions = AuditDataLayer::$dl->getTableOptions($this->config->getManString('database.audit_schema'),
143 | $tableName);
144 |
145 | $dataTable = new TableMetadata($dataTableOptions, $dataTableColumns);
146 | $auditTable = new TableMetadata($auditTableOptions, $auditTableColumns);
147 |
148 | $helper = new DiffTable($dataTable, $auditTable);
149 | $helper->print($this->io, $this->input->getOption('full'));
150 | }
151 |
152 | //--------------------------------------------------------------------------------------------------------------------
153 | /**
154 | * Prints the difference between data and audit tables.
155 | *
156 | * @param string[] $tableNames The names of the current tables.
157 | */
158 | private function currentAuditTables(array $tableNames): void
159 | {
160 | foreach ($tableNames as $tableName)
161 | {
162 | $this->currentAuditTable($tableName);
163 | }
164 | }
165 |
166 | //--------------------------------------------------------------------------------------------------------------------
167 | /**
168 | * Returns the names of the tables that must be compared.
169 | *
170 | * @return array[]
171 | */
172 | private function getTableLists(): array
173 | {
174 | $tables1 = [];
175 | foreach ($this->config->getManArray('tables') as $tableName => $config)
176 | {
177 | if ($config['audit'])
178 | {
179 | $tables1[] = $tableName;
180 | }
181 | }
182 |
183 | $tables = AuditDataLayer::$dl->getTablesNames($this->config->getManString('database.data_schema'));
184 | $tables2 = [];
185 | foreach ($tables as $table)
186 | {
187 | $tables2[] = $table['table_name'];
188 | }
189 |
190 | $tables = AuditDataLayer::$dl->getTablesNames($this->config->getManString('database.audit_schema'));
191 | $tables3 = [];
192 | foreach ($tables as $table)
193 | {
194 | $tables3[] = $table['table_name'];
195 | }
196 |
197 | return ['current' => array_intersect($tables1, $tables2, $tables3),
198 | 'obsolete' => array_diff($tables3, $tables1),
199 | 'missing' => array_diff($tables1, $tables3)];
200 | }
201 |
202 | //--------------------------------------------------------------------------------------------------------------------
203 | /**
204 | * Prints the missing audit tables.
205 | *
206 | * @param string[] $tableNames The names of the obsolete tables.
207 | */
208 | private function missingAuditTables(array $tableNames): void
209 | {
210 | if (empty($tableNames))
211 | {
212 | return;
213 | }
214 |
215 | $this->io->title('Missing Audit Tables');
216 | $this->io->listing($tableNames);
217 | }
218 |
219 | //--------------------------------------------------------------------------------------------------------------------
220 | /**
221 | * Prints the obsolete audit tables.
222 | *
223 | * @param string[] $tableNames The names of the obsolete tables.
224 | */
225 | private function obsoleteAuditTables(array $tableNames): void
226 | {
227 | if (empty($tableNames) || !$this->input->getOption('full'))
228 | {
229 | return;
230 | }
231 |
232 | $this->io->title('Obsolete Audit Tables');
233 | $this->io->listing($tableNames);
234 | }
235 |
236 | //--------------------------------------------------------------------------------------------------------------------
237 | }
238 |
239 | //----------------------------------------------------------------------------------------------------------------------
240 |
--------------------------------------------------------------------------------
/src/MySql/Sql/CreateAuditTrigger.php:
--------------------------------------------------------------------------------
1 | dataSchemaName = $dataSchemaName;
115 | $this->auditSchemaName = $auditSchemaName;
116 | $this->tableName = $tableName;
117 | $this->triggerName = $triggerName;
118 | $this->triggerAction = $triggerAction;
119 | $this->skipVariable = $skipVariable;
120 | $this->additionalAuditColumns = $additionalAuditColumns;
121 | $this->tableColumns = $tableColumns;
122 | $this->additionalSql = $additionalSql;
123 | }
124 |
125 | //--------------------------------------------------------------------------------------------------------------------
126 | /**
127 | * Returns the SQL code for creating an audit trigger.
128 | *
129 | * @return string
130 | *
131 | * @throws FallenException
132 | */
133 | public function buildStatement(): string
134 | {
135 | $this->code = new MySqlCompoundSyntaxCodeStore();
136 |
137 | $rowState = [];
138 | switch ($this->triggerAction)
139 | {
140 | case 'INSERT':
141 | $rowState[] = 'NEW';
142 | break;
143 |
144 | case 'DELETE':
145 | $rowState[] = 'OLD';
146 | break;
147 |
148 | case 'UPDATE':
149 | $rowState[] = 'OLD';
150 | $rowState[] = 'NEW';
151 | break;
152 |
153 | default:
154 | throw new FallenException('action', $this->triggerAction);
155 | }
156 |
157 | $this->code->append(sprintf('create trigger `%s`.`%s`', $this->dataSchemaName, $this->triggerName));
158 | $this->code->append(sprintf('after %s on `%s`.`%s`',
159 | strtolower($this->triggerAction),
160 | $this->dataSchemaName,
161 | $this->tableName));
162 | $this->code->append('for each row');
163 | $this->code->append('begin');
164 |
165 | if ($this->skipVariable!==null)
166 | {
167 | $this->code->append(sprintf('if (%s is null) then', $this->skipVariable));
168 | }
169 |
170 | $this->code->append($this->additionalSql);
171 |
172 | $this->createInsertStatement($rowState[0]);
173 | if (count($rowState)===2)
174 | {
175 | $this->createInsertStatement($rowState[1]);
176 | }
177 |
178 | if ($this->skipVariable!==null)
179 | {
180 | $this->code->append('end if;');
181 | }
182 | $this->code->append('end');
183 |
184 | return $this->code->getCode();
185 | }
186 |
187 | //--------------------------------------------------------------------------------------------------------------------
188 | /**
189 | * Adds an insert SQL statement to SQL code for a trigger.
190 | *
191 | * @param string $rowState The row state (i.e. OLD or NEW).
192 | */
193 | private function createInsertStatement(string $rowState): void
194 | {
195 | $this->createInsertStatementInto();
196 | $this->createInsertStatementValues($rowState);
197 | }
198 |
199 | //--------------------------------------------------------------------------------------------------------------------
200 | /**
201 | * Adds the "insert into" part of an insert SQL statement to SQL code for a trigger.
202 | */
203 | private function createInsertStatementInto(): void
204 | {
205 | $columnNames = '';
206 |
207 | // First the audit columns.
208 | foreach ($this->additionalAuditColumns->getColumns() as $column)
209 | {
210 | if ($columnNames!=='')
211 | {
212 | $columnNames .= ',';
213 | }
214 | $columnNames .= sprintf('`%s`', $column->getName());
215 | }
216 |
217 | // Second the audit columns.
218 | foreach ($this->tableColumns->getColumns() as $column)
219 | {
220 | if ($columnNames!=='')
221 | {
222 | $columnNames .= ',';
223 | }
224 | $columnNames .= sprintf('`%s`', $column->getName());
225 | }
226 |
227 | $this->code->append(sprintf('insert into `%s`.`%s`(%s)', $this->auditSchemaName, $this->tableName, $columnNames));
228 | }
229 |
230 | //--------------------------------------------------------------------------------------------------------------------
231 | /**
232 | * Adds the "values" part of an insert SQL statement to SQL code for a trigger.
233 | *
234 | * @param string $rowState The row state (i.e. OLD or NEW).
235 | */
236 | private function createInsertStatementValues(string $rowState): void
237 | {
238 | $values = '';
239 |
240 | // First the values for the audit columns.
241 | foreach ($this->additionalAuditColumns->getColumns() as $column)
242 | {
243 | $column = $column->getProperties();
244 | if ($values!=='')
245 | {
246 | $values .= ',';
247 | }
248 |
249 | switch (true)
250 | {
251 | case (isset($column['value_type'])):
252 | switch ($column['value_type'])
253 | {
254 | case 'ACTION':
255 | $values .= AuditDataLayer::$dl->quoteString($this->triggerAction);
256 | break;
257 |
258 | case 'STATE':
259 | $values .= AuditDataLayer::$dl->quoteString($rowState);
260 | break;
261 |
262 | default:
263 | throw new FallenException('value_type', ($column['value_type']));
264 | }
265 | break;
266 |
267 | case (isset($column['expression'])):
268 | $values .= $column['expression'];
269 | break;
270 |
271 | default:
272 | throw new RuntimeException('None of value_type and expression are set.');
273 | }
274 | }
275 |
276 | // Second the values for the audit columns.
277 | foreach ($this->tableColumns->getColumns() as $column)
278 | {
279 | if ($values!=='')
280 | {
281 | $values .= ',';
282 | }
283 | $values .= sprintf('%s.`%s`', $rowState, $column->getName());
284 | }
285 |
286 | $this->code->append(sprintf('values(%s);', $values));
287 | }
288 |
289 | //--------------------------------------------------------------------------------------------------------------------
290 | }
291 |
292 | //----------------------------------------------------------------------------------------------------------------------
293 |
--------------------------------------------------------------------------------
/src/Metadata/TableColumnsMetadata.php:
--------------------------------------------------------------------------------
1 | columns[$column['column_name']] = static::columnFactory($type, $column);
36 | }
37 | }
38 |
39 | //--------------------------------------------------------------------------------------------------------------------
40 | /**
41 | * Combines the metadata of two lists of table columns.
42 | *
43 | * @param TableColumnsMetadata $columns1 The first metadata of a list of table columns.
44 | * @param TableColumnsMetadata $columns2 The second metadata of a list of table columns.
45 | *
46 | * @return TableColumnsMetadata
47 | */
48 | public static function combine(TableColumnsMetadata $columns1, TableColumnsMetadata $columns2): TableColumnsMetadata
49 | {
50 | $columns = new TableColumnsMetadata();
51 |
52 | $columns->appendTableColumns($columns1);
53 | $columns->appendTableColumns($columns2);
54 |
55 | return $columns;
56 | }
57 |
58 | //--------------------------------------------------------------------------------------------------------------------
59 | /**
60 | * Compares two lists of table columns and returns a list of table columns that are in both lists but have different
61 | * metadata.
62 | *
63 | * @param TableColumnsMetadata $oldColumns The old metadata of the table columns.
64 | * @param TableColumnsMetadata $newColumns The new metadata of the table columns.
65 | * @param string[] $ignore The properties to be ignored.
66 | *
67 | * @return TableColumnsMetadata
68 | */
69 | public static function differentColumnTypes(TableColumnsMetadata $oldColumns,
70 | TableColumnsMetadata $newColumns,
71 | array $ignore = []): TableColumnsMetadata
72 | {
73 | $diff = new TableColumnsMetadata();
74 | foreach ($oldColumns->columns as $columnName => $oldColumn)
75 | {
76 | if (isset($newColumns->columns[$columnName]))
77 | {
78 | if (!ColumnMetadata::compare($oldColumn, $newColumns->columns[$columnName], $ignore))
79 | {
80 | $diff->appendTableColumn($newColumns->columns[$columnName]);
81 | }
82 | }
83 | }
84 |
85 | return $diff;
86 | }
87 |
88 | //--------------------------------------------------------------------------------------------------------------------
89 | /**
90 | * Compares two lists of table columns and returns a list of table columns that are in the first list of table columns
91 | * but not in the second list of table columns.
92 | *
93 | * @param TableColumnsMetadata $columns1 The first list of table columns.
94 | * @param TableColumnsMetadata $columns2 The second list of table columns.
95 | *
96 | * @return TableColumnsMetadata
97 | */
98 | public static function notInOtherSet(TableColumnsMetadata $columns1,
99 | TableColumnsMetadata $columns2): TableColumnsMetadata
100 | {
101 | $diff = new TableColumnsMetadata();
102 | foreach ($columns1->columns as $columnName => $column1)
103 | {
104 | if (!isset($columns2->columns[$columnName]))
105 | {
106 | $diff->appendTableColumn($column1);
107 | }
108 | }
109 |
110 | return $diff;
111 | }
112 |
113 | //--------------------------------------------------------------------------------------------------------------------
114 | /**
115 | * A factory for table column metadata.
116 | *
117 | * @param string $type The type of the metadata.
118 | * @param array $column The metadata of the column
119 | */
120 | private static function columnFactory(string $type, array $column): ColumnMetadata
121 | {
122 | switch ($type)
123 | {
124 | case 'ColumnMetadata':
125 | return new ColumnMetadata($column);
126 |
127 | case 'AlterColumnMetadata':
128 | return new AlterColumnMetadata($column);
129 |
130 | case 'AuditColumnMetadata':
131 | return new AuditColumnMetadata($column);
132 |
133 | default:
134 | throw new FallenException('type', $type);
135 | }
136 | }
137 |
138 | //--------------------------------------------------------------------------------------------------------------------
139 | /**
140 | * Appends a table column to this list of table columns.
141 | *
142 | * @param ColumnMetadata $column The metadata of the table column.
143 | */
144 | public function appendTableColumn(ColumnMetadata $column): void
145 | {
146 | $this->columns[$column->getName()] = $column;
147 | }
148 |
149 | //--------------------------------------------------------------------------------------------------------------------
150 | /**
151 | * Appends table columns to this list of table columns.
152 | *
153 | * @param TableColumnsMetadata $columns The metadata of the table columns.
154 | */
155 | public function appendTableColumns(TableColumnsMetadata $columns): void
156 | {
157 | foreach ($columns->columns as $column)
158 | {
159 | $this->appendTableColumn($column);
160 | }
161 | }
162 |
163 | //--------------------------------------------------------------------------------------------------------------------
164 | /**
165 | * Enhances all columns with field 'after'.
166 | */
167 | public function enhanceAfter(): void
168 | {
169 | $previous = null;
170 | foreach ($this->columns as $column)
171 | {
172 | $column->setAfter($previous);
173 | $previous = $column->getName();
174 | }
175 | }
176 |
177 | //--------------------------------------------------------------------------------------------------------------------
178 | /**
179 | * Returns a column given the column name.
180 | *
181 | * @param string $columnName The name of the column.
182 | *
183 | * @return ColumnMetadata
184 | */
185 | public function getColumn(string $columnName): ColumnMetadata
186 | {
187 | return $this->columns[$columnName];
188 | }
189 |
190 | //--------------------------------------------------------------------------------------------------------------------
191 | /**
192 | * Returns the columns names.
193 | *
194 | * @return string[]
195 | */
196 | public function getColumnNames(): array
197 | {
198 | return array_keys($this->columns);
199 | }
200 |
201 | //--------------------------------------------------------------------------------------------------------------------
202 | /**
203 | * Returns the underlying array with metadata of this list of table columns.
204 | *
205 | * @return ColumnMetadata[]
206 | */
207 | public function getColumns(): array
208 | {
209 | return $this->columns;
210 | }
211 |
212 | //--------------------------------------------------------------------------------------------------------------------
213 | /**
214 | * Returns the length of the longest column name.
215 | *
216 | * @return int
217 | */
218 | public function getLongestColumnNameLength(): int
219 | {
220 | $max = 0;
221 | foreach ($this->columns as $column)
222 | {
223 | $max = max($max, mb_strlen($column->getName()));
224 | }
225 |
226 | return $max;
227 | }
228 |
229 | //--------------------------------------------------------------------------------------------------------------------
230 | /**
231 | * Returns the number of columns.
232 | *
233 | * @return int
234 | */
235 | public function getNumberOfColumns(): int
236 | {
237 | return count($this->columns);
238 | }
239 |
240 | //--------------------------------------------------------------------------------------------------------------------
241 | /**
242 | * Makes all columns nullable.
243 | */
244 | public function makeNullable(): void
245 | {
246 | foreach ($this->columns as $column)
247 | {
248 | $column->makeNullable();
249 | }
250 | }
251 |
252 | //--------------------------------------------------------------------------------------------------------------------
253 | /**
254 | * Prepends table columns to this list of table columns.
255 | *
256 | * @param TableColumnsMetadata $columns The metadata of the table columns.
257 | */
258 | public function prependTableColumns(TableColumnsMetadata $columns): void
259 | {
260 | $this->columns = array_merge($columns->columns, $this->columns);
261 | }
262 |
263 | //--------------------------------------------------------------------------------------------------------------------
264 | /**
265 | * Removes a table column.
266 | *
267 | * @param string $columnName The table column name.
268 | */
269 | public function removeColumn(string $columnName): void
270 | {
271 | unset($this->columns[$columnName]);
272 | }
273 |
274 | //--------------------------------------------------------------------------------------------------------------------
275 | /**
276 | * Removes the default values from all columns.
277 | */
278 | public function unsetDefaults(): void
279 | {
280 | foreach ($this->columns as $column)
281 | {
282 | $column->unsetDefault();
283 | }
284 | }
285 |
286 | //--------------------------------------------------------------------------------------------------------------------
287 | }
288 |
289 | //----------------------------------------------------------------------------------------------------------------------
290 |
--------------------------------------------------------------------------------
/src/DiffTable.php:
--------------------------------------------------------------------------------
1 | dataTable = $dataTable;
49 | $this->auditTable = $auditTable;
50 | }
51 |
52 | //--------------------------------------------------------------------------------------------------------------------
53 | /**
54 | * @param AuditStyle $io The IO object.
55 | * @param bool $full If false and only if only differences are shown.
56 | */
57 | public function print(AuditStyle $io, bool $full): void
58 | {
59 | $this->rowsEnhanceWithTableColumns();
60 | $this->rowsEnhanceWithTableOptions();
61 | $this->rowsEnhanceWithDiffIndicator();
62 | $this->rowsEnhanceWithColumnTypeInfo();
63 | $this->rowsEnhanceWithFormatting();
64 |
65 | if ($full || $this->hasDifferences())
66 | {
67 | $io->writeln(''.$this->dataTable->getTableName().'');
68 |
69 | $table = new Table($io);
70 | $table->setHeaders(['column', 'audit table', 'config / data table'])
71 | ->setRows($this->getRows($full));
72 | $table->render();
73 |
74 | $io->writeln('');
75 | }
76 | }
77 |
78 | //--------------------------------------------------------------------------------------------------------------------
79 | /**
80 | * Returns the rows suitable for Symfony's table.
81 | *
82 | * @param bool $full If false and only if only differences are shown.
83 | *
84 | * @return array
85 | */
86 | private function getRows(bool $full): array
87 | {
88 | $ret = [];
89 | $options = false;
90 | foreach ($this->rows as $row)
91 | {
92 | // Add separator between columns and options.
93 | if ($options===false && $row['type']==='option')
94 | {
95 | if (!empty($ret))
96 | {
97 | $ret[] = new TableSeparator();
98 | }
99 | $options = true;
100 | }
101 |
102 | if ($full || $row['diff'])
103 | {
104 | $ret[] = [$row['name'], $row['audit1'], $row['data1']];
105 |
106 | if ($row['rowspan']===2)
107 | {
108 | $ret[] = ['', $row['audit2'], $row['data2']];
109 | }
110 | }
111 | }
112 |
113 | return $ret;
114 | }
115 |
116 | //--------------------------------------------------------------------------------------------------------------------
117 | /**
118 | * Returns true if and only if the audit and data tables have differences.
119 | *
120 | * @return bool
121 | */
122 | private function hasDifferences(): bool
123 | {
124 | foreach ($this->rows as $row)
125 | {
126 | if ($row['diff'])
127 | {
128 | return true;
129 | }
130 | }
131 |
132 | return false;
133 | }
134 |
135 | //--------------------------------------------------------------------------------------------------------------------
136 | /**
137 | * Enhances rows with column type info.
138 | */
139 | private function rowsEnhanceWithColumnTypeInfo(): void
140 | {
141 | foreach ($this->rows as &$row)
142 | {
143 | if ($row['type']==='column')
144 | {
145 | if ($row['data']!==null)
146 | {
147 | $row['data1'] = $row['data']->getTypeInfo1();
148 | $row['data2'] = $row['data']->getTypeInfo2();
149 | }
150 | else
151 | {
152 | $row['data1'] = null;
153 | $row['data2'] = null;
154 | }
155 |
156 | if ($row['audit']!==null)
157 | {
158 | $row['audit1'] = $row['audit']->getTypeInfo1();
159 | $row['audit2'] = $row['audit']->getTypeInfo2();
160 | }
161 | else
162 | {
163 | $row['audit1'] = null;
164 | $row['audit2'] = null;
165 | }
166 |
167 | $row['rowspan'] = ($row['data2']===null && $row['audit2']===null) ? 1 : 2;
168 | }
169 | }
170 | }
171 |
172 | //--------------------------------------------------------------------------------------------------------------------
173 | /**
174 | * Enhances rows with diff indicator.
175 | */
176 | private function rowsEnhanceWithDiffIndicator(): void
177 | {
178 | foreach ($this->rows as &$row)
179 | {
180 | switch ($row['type'])
181 | {
182 | case 'column':
183 | $row['diff'] = (isset($row['audit'])!==isset($row['data']) ||
184 | $row['audit']->getColumnDefinition()!==$row['data']->getColumnDefinition());
185 | break;
186 |
187 | case 'option':
188 | $row['diff'] = ($row['audit1']!==$row['data1']);
189 | break;
190 |
191 | default:
192 | throw new FallenException('type', $row['type']);
193 | }
194 | }
195 | }
196 |
197 | //--------------------------------------------------------------------------------------------------------------------
198 | /**
199 | * Enhances rows text with formatting.
200 | */
201 | private function rowsEnhanceWithFormatting(): void
202 | {
203 | foreach ($this->rows as &$row)
204 | {
205 | if ($row['diff'])
206 | {
207 | $row['name'] = sprintf('%s', $row['name']);
208 | }
209 |
210 | if ($row['audit1']!==$row['data1'])
211 | {
212 | $row['audit1'] = sprintf('%s', $row['audit1']);
213 | $row['data1'] = sprintf('%s', $row['data1']);
214 | }
215 |
216 | if ($row['rowspan']===2 && ($row['audit2']!==$row['data2']))
217 | {
218 | $row['audit2'] = sprintf('%s', $row['audit2']);
219 | $row['data2'] = sprintf('%s', $row['data2']);
220 | }
221 | }
222 | }
223 |
224 | //--------------------------------------------------------------------------------------------------------------------
225 | /**
226 | * Computes the joins columns of the audit and data table.
227 | */
228 | private function rowsEnhanceWithTableColumns(): void
229 | {
230 | $auditColumns = $this->auditTable->getColumns()
231 | ->getColumnNames();
232 | $dataColumns = $this->dataTable->getColumns()
233 | ->getColumnNames();
234 |
235 | $this->rows = [];
236 | foreach ($dataColumns as $column)
237 | {
238 | if (in_array($column, $auditColumns))
239 | {
240 | $this->rows[] = ['name' => $column,
241 | 'audit' => $this->auditTable->getColumns()
242 | ->getColumn($column),
243 | 'data' => $this->dataTable->getColumns()
244 | ->getColumn($column),
245 | 'type' => 'column',
246 | 'new' => false,
247 | 'obsolete' => false];
248 | }
249 | else
250 | {
251 | $this->rows[] = ['name' => $column,
252 | 'audit' => null,
253 | 'data' => $this->dataTable->getColumns()
254 | ->getColumn($column),
255 | 'type' => 'column',
256 | 'new' => true,
257 | 'obsolete' => false];
258 | }
259 | }
260 |
261 | foreach ($auditColumns as $column)
262 | {
263 | if (!in_array($column, $dataColumns))
264 | {
265 | $this->rows[] = ['name' => $column,
266 | 'audit' => $this->auditTable->getColumns()
267 | ->getColumn($column),
268 | 'data' => null,
269 | 'type' => 'column',
270 | 'new' => false,
271 | 'obsolete' => true];
272 | }
273 | }
274 | }
275 |
276 | //--------------------------------------------------------------------------------------------------------------------
277 | /**
278 | * Adds table options to the rows.
279 | */
280 | private function rowsEnhanceWithTableOptions(): void
281 | {
282 | $auditOptions = array_keys($this->auditTable->getOptions());
283 | $dataOptions = array_keys($this->dataTable->getOptions());
284 |
285 | foreach ($dataOptions as $option)
286 | {
287 | $this->rows[] = ['name' => $option,
288 | 'audit1' => $this->auditTable->getProperty($option),
289 | 'data1' => $this->dataTable->getProperty($option),
290 | 'type' => 'option',
291 | 'rowspan' => 1];
292 | }
293 |
294 | foreach ($auditOptions as $option)
295 | {
296 | if (!in_array($option, $dataOptions))
297 | {
298 | $this->rows[] = ['name' => $option,
299 | 'audit1' => $this->auditTable->getProperty($option),
300 | 'data2' => null,
301 | 'type' => 'option',
302 | 'rowspan' => 1];
303 | }
304 | }
305 | }
306 |
307 | //--------------------------------------------------------------------------------------------------------------------
308 | }
309 |
310 | //----------------------------------------------------------------------------------------------------------------------
311 |
--------------------------------------------------------------------------------
/src/AuditTable.php:
--------------------------------------------------------------------------------
1 | io = $io;
96 | $this->dataSchemaName = $dataSchemaName;
97 | $this->auditSchemaName = $auditSchemaName;
98 | $this->tableName = $tableName;
99 | $this->dataTableColumnsDatabase = new TableColumnsMetadata($this->getColumnsFromInformationSchema());
100 | $this->additionalAuditColumns = $additionalAuditColumns;
101 | $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 | * @param AuditStyle $io The output decorator.
112 | * @param string $schemaName The name of the table schema.
113 | * @param string $tableName The name of the table.
114 | */
115 | public static function dropAuditTriggers(AuditStyle $io, string $schemaName, string $tableName): void
116 | {
117 | $triggers = AuditDataLayer::$dl->getTableTriggers($schemaName, $tableName);
118 | foreach ($triggers as $trigger)
119 | {
120 | if (preg_match('/^trg_audit_.*_(insert|update|delete)$/', $trigger['trigger_name']))
121 | {
122 | $io->logVerbose('Dropping trigger %s on %s.%s',
123 | $trigger['trigger_name'],
124 | $schemaName,
125 | $tableName);
126 |
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 | //--------------------------------------------------------------------------------------------------------------------
144 | /**
145 | * Creates an audit table for this table.
146 | */
147 | public function createAuditTable(): void
148 | {
149 | $this->io->logInfo('Creating audit table %s.%s', $this->auditSchemaName, $this->tableName);
150 |
151 | // In the audit table all columns from the data table must be nullable.
152 | $dataTableColumnsDatabase = clone($this->dataTableColumnsDatabase);
153 | $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 | * Creates audit triggers on this table.
162 | *
163 | * @param string[] $additionalSql Additional SQL statements to be included in triggers.
164 | */
165 | public function createTriggers(array $additionalSql): void
166 | {
167 | // Lock the table to prevent insert, updates, or deletes between dropping and creating triggers.
168 | $this->lockTable();
169 |
170 | // Drop all triggers, if any.
171 | static::dropAuditTriggers($this->io, $this->dataSchemaName, $this->tableName);
172 |
173 | // Create or recreate the audit triggers.
174 | $this->createTableTrigger('INSERT', $this->skipVariable, $additionalSql);
175 | $this->createTableTrigger('UPDATE', $this->skipVariable, $additionalSql);
176 | $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 | * Main function for work with table.
196 | *
197 | * @param string[] $additionalSql Additional SQL statements to be included in triggers.
198 | */
199 | public function main(array $additionalSql): void
200 | {
201 | $comparedColumns = $this->getTableColumnInfo();
202 | $newColumns = $comparedColumns['new_columns'];
203 |
204 | $this->addNewColumns($newColumns);
205 | $this->createTriggers($additionalSql);
206 | }
207 |
208 | //--------------------------------------------------------------------------------------------------------------------
209 | /**
210 | * Adds new columns to audit table.
211 | *
212 | * @param TableColumnsMetadata $columns TableColumnsMetadata array
213 | */
214 | private function addNewColumns(TableColumnsMetadata $columns): void
215 | {
216 | // Return immediately if there are no columns to add.
217 | if ($columns->getNumberOfColumns()===0)
218 | {
219 | return;
220 | }
221 |
222 | $alterColumns = $this->alterNewColumns($columns);
223 |
224 | AuditDataLayer::$dl->addNewColumns($this->auditSchemaName, $this->tableName, $alterColumns);
225 | }
226 |
227 | //--------------------------------------------------------------------------------------------------------------------
228 | /**
229 | * Returns metadata of new table columns that can be used in a 'alter table ... add column' statement.
230 | *
231 | * @param TableColumnsMetadata $newColumns The metadata new table columns.
232 | *
233 | * @return TableColumnsMetadata
234 | */
235 | private function alterNewColumns(TableColumnsMetadata $newColumns): TableColumnsMetadata
236 | {
237 | $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 | /**
249 | * Creates a triggers for this table.
250 | *
251 | * @param string $action The trigger action (INSERT, DELETE, or UPDATE).
252 | * @param string|null $skipVariable The name of the MySQL user defined variable for skipping triggers.
253 | * @param string[] $additionSql The additional SQL statements to be included in triggers.
254 | */
255 | private function createTableTrigger(string $action, ?string $skipVariable, array $additionSql): void
256 | {
257 | $triggerName = $this->getTriggerName($action);
258 |
259 | $this->io->logVerbose('Creating trigger %s.%s on table %s.%s',
260 | $this->dataSchemaName,
261 | $triggerName,
262 | $this->dataSchemaName,
263 | $this->tableName);
264 |
265 | AuditDataLayer::$dl->createAuditTrigger($this->dataSchemaName,
266 | $this->auditSchemaName,
267 | $this->tableName,
268 | $triggerName,
269 | $action,
270 | $this->additionalAuditColumns,
271 | $this->dataTableColumnsDatabase,
272 | $skipVariable,
273 | $additionSql);
274 | }
275 |
276 | //--------------------------------------------------------------------------------------------------------------------
277 | /**
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 |
287 | //--------------------------------------------------------------------------------------------------------------------
288 | /**
289 | * Compare columns from table in data_schema with columns in config file.
290 | *
291 | * @return array
292 | */
293 | private function getTableColumnInfo(): array
294 | {
295 | $actual = new TableColumnsMetadata(AuditDataLayer::$dl->getTableColumns($this->auditSchemaName, $this->tableName));
296 | $target = TableColumnsMetadata::combine($this->additionalAuditColumns, $this->dataTableColumnsDatabase);
297 | $target->enhanceAfter();
298 |
299 | $new = TableColumnsMetadata::notInOtherSet($target, $actual);
300 | $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 | /**
312 | * Returns the trigger name for a trigger action.
313 | *
314 | * @param string $action Trigger on action (Insert, Update, Delete).
315 | *
316 | * @return string
317 | */
318 | private function getTriggerName(string $action): string
319 | {
320 | return strtolower(sprintf('trg_audit_%s_%s', $this->alias, $action));
321 | }
322 |
323 | //--------------------------------------------------------------------------------------------------------------------
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 | /**
334 | * Logs info about new, obsolete, and altered columns.
335 | *
336 | * @param TableColumnsMetadata $newColumns The metadata of the new columns.
337 | * @param TableColumnsMetadata $obsoleteColumns The metadata of the obsolete columns.
338 | * @param TableColumnsMetadata $alteredColumns The metadata of the altered columns.
339 | */
340 | private function logColumnInfo(TableColumnsMetadata $newColumns,
341 | TableColumnsMetadata $obsoleteColumns,
342 | TableColumnsMetadata $alteredColumns): void
343 | {
344 | foreach ($newColumns->getColumns() as $column)
345 | {
346 | $this->io->logInfo('New column %s.%s',
347 | $this->tableName,
348 | $column->getName());
349 | }
350 |
351 | foreach ($obsoleteColumns->getColumns() as $column)
352 | {
353 | $this->io->logInfo('Obsolete column %s.%s',
354 | $this->tableName,
355 | $column->getName());
356 | }
357 |
358 | foreach ($alteredColumns->getColumns() as $column)
359 | {
360 | $this->io->logInfo('Type of %s.%s has been altered to %s',
361 | $this->tableName,
362 | $column->getName(),
363 | $column->getProperty('column_type'));
364 | }
365 | }
366 |
367 | //--------------------------------------------------------------------------------------------------------------------
368 | /**
369 | * Releases the table lock.
370 | */
371 | private function unlockTable(): void
372 | {
373 | AuditDataLayer::$dl->unlockTables();
374 | }
375 |
376 | //--------------------------------------------------------------------------------------------------------------------
377 | }
378 |
379 | //----------------------------------------------------------------------------------------------------------------------
380 |
--------------------------------------------------------------------------------
/src/MySql/AuditDataLayer.php:
--------------------------------------------------------------------------------
1 | io = $io;
49 | self::$dl = $this;
50 | }
51 |
52 | //--------------------------------------------------------------------------------------------------------------------
53 | /**
54 | * Adds new columns to an audit table.
55 | *
56 | * @param string $auditSchemaName The name of audit schema.
57 | * @param string $tableName The name of the table.
58 | * @param TableColumnsMetadata $columns The metadata of the new columns.
59 | */
60 | public function addNewColumns(string $auditSchemaName, string $tableName, TableColumnsMetadata $columns): void
61 | {
62 | $helper = new AlterAuditTableAddColumns($auditSchemaName, $tableName, $columns);
63 | $sql = $helper->buildStatement();
64 |
65 | $this->executeNone($sql);
66 | }
67 |
68 | //--------------------------------------------------------------------------------------------------------------------
69 | /**
70 | * Creates an audit table.
71 | *
72 | * @param string $dataSchemaName The name of the data schema.
73 | * @param string $auditSchemaName The name of the audit schema.
74 | * @param string $tableName The name of the table.
75 | * @param TableColumnsMetadata $columns The metadata of the columns of the audit table (i.e. the audit
76 | * columns and columns of the data table).
77 | */
78 | public function createAuditTable(string $dataSchemaName,
79 | string $auditSchemaName,
80 | string $tableName,
81 | TableColumnsMetadata $columns): void
82 | {
83 | $helper = new CreateAuditTable($dataSchemaName, $auditSchemaName, $tableName, $columns);
84 | $sql = $helper->buildStatement();
85 |
86 | $this->executeNone($sql);
87 | }
88 |
89 | //--------------------------------------------------------------------------------------------------------------------
90 | /**
91 | * Creates a trigger on a table.
92 | *
93 | * @param string $dataSchemaName The name of the data schema.
94 | * @param string $auditSchemaName The name of the audit schema.
95 | * @param string $tableName The name of the table.
96 | * @param string $triggerAction The trigger action (i.e. INSERT, UPDATE, or DELETE).
97 | * @param string $triggerName The name of the trigger.
98 | * @param TableColumnsMetadata $additionalAuditColumns The metadata of the additional audit columns.
99 | * @param TableColumnsMetadata $tableColumns The metadata of the data table columns.
100 | * @param string|null $skipVariable The name of the MySQL user defined variable for skipping
101 | * triggers.
102 | * @param string[] $additionSql Additional SQL statements.
103 | */
104 | public function createAuditTrigger(string $dataSchemaName,
105 | string $auditSchemaName,
106 | string $tableName,
107 | string $triggerName,
108 | string $triggerAction,
109 | TableColumnsMetadata $additionalAuditColumns,
110 | TableColumnsMetadata $tableColumns,
111 | ?string $skipVariable,
112 | array $additionSql): void
113 | {
114 | $helper = new CreateAuditTrigger($dataSchemaName,
115 | $auditSchemaName,
116 | $tableName,
117 | $triggerName,
118 | $triggerAction,
119 | $additionalAuditColumns,
120 | $tableColumns,
121 | $skipVariable,
122 | $additionSql);
123 | $sql = $helper->buildStatement();
124 |
125 | $this->executeNone($sql);
126 | }
127 |
128 | //--------------------------------------------------------------------------------------------------------------------
129 | /**
130 | * Creates a temporary table for getting column type information for audit columns.
131 | *
132 | * @param string $schemaName The name of the table schema.
133 | * @param string $tableName The table name.
134 | * @param array[] $auditColumns Audit columns from config file.
135 | */
136 | public function createTemporaryTable(string $schemaName, string $tableName, array $auditColumns): void
137 | {
138 | $sql = new MySqlCompoundSyntaxCodeStore();
139 | $sql->append(sprintf('create table `%s`.`%s` (', $schemaName, $tableName));
140 | foreach ($auditColumns as $column)
141 | {
142 | $sql->append(sprintf('%s %s', $column['column_name'], $column['column_type']));
143 | if (end($auditColumns)!==$column)
144 | {
145 | $sql->appendToLastLine(',');
146 | }
147 | }
148 | $sql->append(')');
149 |
150 | $this->executeNone($sql->getCode());
151 | }
152 |
153 | //--------------------------------------------------------------------------------------------------------------------
154 | /**
155 | * Drops a temporary table.
156 | *
157 | * @param string $schemaName The name of the table schema.
158 | * @param string $tableName The name of the table.
159 | */
160 | public function dropTemporaryTable(string $schemaName, string $tableName): void
161 | {
162 | $sql = sprintf('drop table `%s`.`%s`', $schemaName, $tableName);
163 |
164 | $this->executeNone($sql);
165 | }
166 |
167 | //--------------------------------------------------------------------------------------------------------------------
168 | /**
169 | * Drops a trigger.
170 | *
171 | * @param string $triggerSchema The name of the trigger schema.
172 | * @param string $triggerName The mame of trigger.
173 | */
174 | public function dropTrigger(string $triggerSchema, string $triggerName): void
175 | {
176 | $sql = sprintf('drop trigger `%s`.`%s`', $triggerSchema, $triggerName);
177 |
178 | $this->executeNone($sql);
179 | }
180 |
181 | //--------------------------------------------------------------------------------------------------------------------
182 | /**
183 | * @inheritdoc
184 | */
185 | public function executeBulk(BulkHandler $bulkHandler, string $query): void
186 | {
187 | $this->logQuery($query);
188 |
189 | parent::executeBulk($bulkHandler, $query);
190 | }
191 |
192 | //--------------------------------------------------------------------------------------------------------------------
193 | /**
194 | * @inheritdoc
195 | */
196 | public function executeMulti(string $queries): array
197 | {
198 | $this->logQuery($queries);
199 |
200 | return parent::executeMulti($queries);
201 | }
202 |
203 | //--------------------------------------------------------------------------------------------------------------------
204 | /**
205 | * @inheritdoc
206 | */
207 | public function executeNone(string $query): int
208 | {
209 | $this->logQuery($query);
210 |
211 | return parent::executeNone($query);
212 | }
213 |
214 | //--------------------------------------------------------------------------------------------------------------------
215 | /**
216 | * @inheritdoc
217 | */
218 | public function executeRow0(string $query): ?array
219 | {
220 | $this->logQuery($query);
221 |
222 | return parent::executeRow0($query);
223 | }
224 |
225 | //--------------------------------------------------------------------------------------------------------------------
226 | /**
227 | * @inheritdoc
228 | */
229 | public function executeRow1(string $query): array
230 | {
231 | $this->logQuery($query);
232 |
233 | return parent::executeRow1($query);
234 | }
235 |
236 | //--------------------------------------------------------------------------------------------------------------------
237 | /**
238 | * @inheritdoc
239 | */
240 | public function executeRows(string $query): array
241 | {
242 | $this->logQuery($query);
243 |
244 | return parent::executeRows($query);
245 | }
246 |
247 | //--------------------------------------------------------------------------------------------------------------------
248 | /**
249 | * @inheritdoc
250 | */
251 | public function executeSingleton0(string $query): mixed
252 | {
253 | $this->logQuery($query);
254 |
255 | return parent::executeSingleton0($query);
256 | }
257 |
258 | //--------------------------------------------------------------------------------------------------------------------
259 | /**
260 | * @inheritdoc
261 | */
262 | public function executeSingleton1(string $query): mixed
263 | {
264 | $this->logQuery($query);
265 |
266 | return parent::executeSingleton1($query);
267 | }
268 |
269 | //--------------------------------------------------------------------------------------------------------------------
270 | /**
271 | * @inheritdoc
272 | */
273 | public function executeTable(string $query): int
274 | {
275 | $this->logQuery($query);
276 |
277 | return parent::executeTable($query);
278 | }
279 |
280 | //--------------------------------------------------------------------------------------------------------------------
281 | /**
282 | * Selects metadata of all columns of table.
283 | *
284 | * @param string $schemaName The name of the table schema.
285 | * @param string $tableName The name of the table.
286 | *
287 | * @return array[]
288 | */
289 | public function getTableColumns(string $schemaName, string $tableName): array
290 | {
291 | // When a column has no default prior to MariaDB 10.2.7 column_default is null from MariaDB 10.2.7
292 | // column_default = 'NULL' (string(4)).
293 | $sql = sprintf("
294 | select COLUMN_NAME as column_name
295 | , COLUMN_TYPE as column_type
296 | , ifnull(COLUMN_DEFAULT, 'NULL') as column_default
297 | , IS_NULLABLE as is_nullable
298 | , CHARACTER_SET_NAME as character_set_name
299 | , COLLATION_NAME as collation_name
300 | from information_schema.COLUMNS
301 | where TABLE_SCHEMA = %s
302 | and TABLE_NAME = %s
303 | order by ORDINAL_POSITION",
304 | $this->quoteString($schemaName),
305 | $this->quoteString($tableName));
306 |
307 | return $this->executeRows($sql);
308 | }
309 |
310 | //--------------------------------------------------------------------------------------------------------------------
311 | /**
312 | * Selects table engine, character_set_name and table_collation.
313 | *
314 | * @param string $schemaName The name of the table schema.
315 | * @param string $tableName The name of the table.
316 | *
317 | * @return array
318 | */
319 | public function getTableOptions(string $schemaName, string $tableName): array
320 | {
321 | $sql = sprintf('
322 | select t1.TABLE_SCHEMA as table_schema
323 | , t1.TABLE_NAME as table_name
324 | , t1.TABLE_COLLATION as table_collation
325 | , t1.ENGINE as engine
326 | , t2.CHARACTER_SET_NAME as character_set_name
327 | from information_schema.TABLES t1
328 | inner join information_schema.COLLATION_CHARACTER_SET_APPLICABILITY t2 on t2.COLLATION_NAME = t1.TABLE_COLLATION
329 | where t1.TABLE_SCHEMA = %s
330 | and t1.TABLE_NAME = %s',
331 | $this->quoteString($schemaName),
332 | $this->quoteString($tableName));
333 |
334 | return $this->executeRow1($sql);
335 | }
336 |
337 | //--------------------------------------------------------------------------------------------------------------------
338 | /**
339 | * Selects all triggers on a table.
340 | *
341 | * @param string $schemaName The name of the table schema.
342 | * @param string $tableName The name of the table.
343 | *
344 | * @return array[]
345 | */
346 | public function getTableTriggers(string $schemaName, string $tableName): array
347 | {
348 | $sql = sprintf('
349 | select TRIGGER_NAME as trigger_name
350 | from information_schema.TRIGGERS
351 | where TRIGGER_SCHEMA = %s
352 | and EVENT_OBJECT_TABLE = %s
353 | order by Trigger_Name',
354 | $this->quoteString($schemaName),
355 | $this->quoteString($tableName));
356 |
357 | return $this->executeRows($sql);
358 | }
359 |
360 | //--------------------------------------------------------------------------------------------------------------------
361 | /**
362 | * Selects all table names in a schema.
363 | *
364 | * @param string $schemaName The name of the schema.
365 | *
366 | * @return array[]
367 | */
368 | public function getTablesNames(string $schemaName): array
369 | {
370 | $sql = sprintf("
371 | select TABLE_NAME as table_name
372 | from information_schema.TABLES
373 | where TABLE_SCHEMA = %s
374 | and TABLE_TYPE = 'BASE TABLE'
375 | order by TABLE_NAME",
376 | $this->quoteString($schemaName));
377 |
378 | return $this->executeRows($sql);
379 | }
380 |
381 | //--------------------------------------------------------------------------------------------------------------------
382 | /**
383 | * Selects all triggers in a schema
384 | *
385 | * @param string $schemaName The name of the table schema.
386 | *
387 | * @return array[]
388 | */
389 | public function getTriggers(string $schemaName): array
390 | {
391 | $sql = sprintf('
392 | select EVENT_OBJECT_TABLE as table_name
393 | , TRIGGER_NAME as trigger_name
394 | from information_schema.TRIGGERS
395 | where TRIGGER_SCHEMA = %s
396 | order by EVENT_OBJECT_TABLE
397 | , TRIGGER_NAME',
398 | $this->quoteString($schemaName));
399 |
400 | return $this->executeRows($sql);
401 | }
402 |
403 | //--------------------------------------------------------------------------------------------------------------------
404 | /**
405 | * Acquires a write lock on a table.
406 | *
407 | * @param string $schemaName The schema of the table.
408 | * @param string $tableName The table name.
409 | */
410 | public function lockTable(string $schemaName, string $tableName): void
411 | {
412 | $sql = sprintf('lock tables `%s`.`%s` write', $schemaName, $tableName);
413 |
414 | $this->executeNone($sql);
415 | }
416 |
417 | //--------------------------------------------------------------------------------------------------------------------
418 | /**
419 | * Resolves the canonical column types of the additional audit columns.
420 | *
421 | * @param string $auditSchema The name of the audit schema.
422 | * @param array[] $additionalAuditColumns The metadata of the additional audit columns.
423 | *
424 | * @return TableColumnsMetadata
425 | */
426 | public function resolveCanonicalAdditionalAuditColumns(string $auditSchema,
427 | array $additionalAuditColumns): TableColumnsMetadata
428 | {
429 | if (empty($additionalAuditColumns))
430 | {
431 | return new TableColumnsMetadata([], 'AuditColumnMetadata');
432 | }
433 |
434 | $tableName = '_TMP_'.uniqid();
435 | $this->createTemporaryTable($auditSchema, $tableName, $additionalAuditColumns);
436 | $columns = AuditDataLayer::$dl->getTableColumns($auditSchema, $tableName);
437 | $this->dropTemporaryTable($auditSchema, $tableName);
438 |
439 | foreach ($additionalAuditColumns as $column)
440 | {
441 | $key = RowSetHelper::findInRowSet($columns, 'column_name', $column['column_name']);
442 |
443 | if (isset($column['value_type']))
444 | {
445 | $columns[$key]['value_type'] = $column['value_type'];
446 | }
447 | if (isset($column['expression']))
448 | {
449 | $columns[$key]['expression'] = $column['expression'];
450 | }
451 | }
452 |
453 | return new TableColumnsMetadata($columns, 'AuditColumnMetadata');
454 | }
455 |
456 | //--------------------------------------------------------------------------------------------------------------------
457 | /**
458 | * Releases all table locks.
459 | */
460 | public function unlockTables(): void
461 | {
462 | $sql = 'unlock tables';
463 |
464 | $this->executeNone($sql);
465 | }
466 |
467 | //--------------------------------------------------------------------------------------------------------------------
468 | /**
469 | * Logs the query on the console.
470 | *
471 | * @param string $query The query.
472 | */
473 | private function logQuery(string $query): void
474 | {
475 | $query = trim($query);
476 |
477 | if (str_contains($query, PHP_EOL))
478 | {
479 | // Query is a multi line query.
480 | $this->io->logVeryVerbose('Executing query:');
481 | $this->io->logVeryVerbose('%s', $query);
482 | }
483 | else
484 | {
485 | // Query is a single line query.
486 | $this->io->logVeryVerbose('Executing query: %s', $query);
487 | }
488 | }
489 |
490 | //--------------------------------------------------------------------------------------------------------------------
491 | }
492 |
493 | //----------------------------------------------------------------------------------------------------------------------
494 |
--------------------------------------------------------------------------------