├── 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 | --------------------------------------------------------------------------------