Spaces:
No application file
No application file
| namespace Mautic\InstallBundle\Helper; | |
| use Doctrine\DBAL\Connection; | |
| use Doctrine\DBAL\DriverManager; | |
| use Doctrine\DBAL\Platforms\AbstractPlatform; | |
| use Doctrine\DBAL\Platforms\SqlitePlatform; | |
| use Doctrine\DBAL\Schema\AbstractSchemaManager; | |
| use Doctrine\DBAL\Schema\ForeignKeyConstraint; | |
| use Doctrine\DBAL\Schema\Index; | |
| use Doctrine\ORM\EntityManager; | |
| use Doctrine\ORM\ORMException; | |
| use Doctrine\ORM\Tools\SchemaTool; | |
| use Mautic\CoreBundle\Release\ThisRelease; | |
| use Mautic\InstallBundle\Exception\DatabaseVersionTooOldException; | |
| class SchemaHelper | |
| { | |
| protected Connection $db; | |
| /** | |
| * @var EntityManager | |
| */ | |
| protected $em; | |
| /** | |
| * @var AbstractPlatform | |
| */ | |
| protected $platform; | |
| protected array $dbParams; | |
| /** | |
| * @var AbstractSchemaManager<AbstractPlatform>|null | |
| */ | |
| private ?AbstractSchemaManager $schemaManager = null; | |
| /** | |
| * @throws \Doctrine\DBAL\Exception | |
| */ | |
| public function __construct(array $dbParams) | |
| { | |
| // suppress display of errors as we know its going to happen while testing the connection | |
| ini_set('display_errors', '0'); | |
| // Support for env variables | |
| foreach ($dbParams as &$v) { | |
| if (!empty($v) && is_string($v) && preg_match('/getenv\((.*?)\)/', $v, $match)) { | |
| $v = (string) getenv($match[1]); | |
| } | |
| } | |
| $dbParams['charset'] = 'utf8mb4'; | |
| if (isset($dbParams['name'])) { | |
| $dbParams['dbname'] = $dbParams['name']; | |
| unset($dbParams['name']); | |
| } | |
| $this->db = DriverManager::getConnection($dbParams); | |
| $this->dbParams = $dbParams; | |
| } | |
| public function setEntityManager(EntityManager $em): void | |
| { | |
| $this->em = $em; | |
| } | |
| /** | |
| * Test db connection. | |
| */ | |
| public function testConnection(): void | |
| { | |
| if (isset($this->dbParams['dbname'])) { | |
| // Test connection credentials | |
| $dbParams = $this->dbParams; | |
| unset($dbParams['dbname']); | |
| $db = DriverManager::getConnection($dbParams); | |
| $db->connect(); | |
| $db->close(); | |
| } else { | |
| $this->db->connect(); | |
| $this->db->close(); | |
| } | |
| } | |
| /** | |
| * @throws \Doctrine\DBAL\Exception | |
| */ | |
| public function createDatabase(): bool | |
| { | |
| try { | |
| $this->db->connect(); | |
| } catch (\Exception) { | |
| // it failed to connect so remove the dbname and try to create it | |
| $dbName = $this->dbParams['dbname']; | |
| $this->dbParams['dbname'] = null; | |
| try { | |
| // database does not exist so try to create it | |
| $this->getSchemaManager()->createDatabase($dbName); | |
| // close the connection and reconnect with the new database name | |
| $this->db->close(); | |
| $this->dbParams['dbname'] = $dbName; | |
| $this->db = DriverManager::getConnection($this->dbParams); | |
| $this->db->close(); | |
| } catch (\Exception) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| /** | |
| * Generates SQL for installation. | |
| * | |
| * @throws \Doctrine\DBAL\Exception | |
| * @throws ORMException | |
| */ | |
| public function installSchema(): bool | |
| { | |
| $sm = $this->getSchemaManager(); | |
| try { | |
| // check to see if the table already exist | |
| $tables = $sm->listTableNames(); | |
| } catch (\Exception $e) { | |
| $this->db->close(); | |
| throw $e; | |
| } | |
| $this->platform = $this->db->getDatabasePlatform(); | |
| $backupPrefix = (!empty($this->dbParams['backup_prefix'])) ? $this->dbParams['backup_prefix'] : 'bak_'; | |
| $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); | |
| if (empty($metadatas)) { | |
| $this->db->close(); | |
| return false; | |
| } | |
| $schemaTool = new SchemaTool($this->em); | |
| $installSchema = $schemaTool->getSchemaFromMetadata($metadatas); | |
| $mauticTables = []; | |
| foreach ($installSchema->getTables() as $m) { | |
| $tableName = $m->getName(); | |
| $mauticTables[$tableName] = $this->generateBackupName($this->dbParams['table_prefix'], $backupPrefix, $tableName); | |
| } | |
| $isSqlite = $this->em->getConnection()->getDatabasePlatform() instanceof SqlitePlatform; | |
| $sql = $isSqlite ? [] : ['SET foreign_key_checks = 0;']; | |
| if ($this->dbParams['backup_tables']) { | |
| $sql = array_merge($sql, $this->backupExistingSchema($tables, $mauticTables, $backupPrefix)); | |
| } else { | |
| $sql = array_merge($sql, $this->dropExistingSchema($tables, $mauticTables)); | |
| } | |
| $sql = array_merge($sql, $installSchema->toSql($this->platform)); | |
| // Execute drop queries | |
| foreach ($sql as $q) { | |
| try { | |
| $this->db->executeQuery($q); | |
| } catch (\Exception $exception) { | |
| $this->db->close(); | |
| throw $exception; | |
| } | |
| } | |
| $this->db->close(); | |
| return true; | |
| } | |
| public function validateDatabaseVersion(): void | |
| { | |
| // Version strings are in the format 10.3.30-MariaDB-1:10.3.30+maria~focal-log | |
| $version = $this->db->executeQuery('SELECT VERSION()')->fetchOne(); | |
| // Platform class names are in the format Doctrine\DBAL\Platforms\MariaDb1027Platform | |
| $platform = strtolower($this->db->getDatabasePlatform()::class); | |
| $metadata = ThisRelease::getMetadata(); | |
| /** | |
| * The second case is for MariaDB < 10.2, where Doctrine reports it as MySQLPlatform. Here we can use a little | |
| * help from the version string, which contains "MariaDB" in that case: 10.1.48-MariaDB-1~bionic. | |
| */ | |
| if (str_contains($platform, 'mariadb') || str_contains(strtolower($version), 'mariadb')) { | |
| $minSupported = $metadata->getMinSupportedMariaDbVersion(); | |
| } elseif (str_contains($platform, 'mysql')) { | |
| $minSupported = $metadata->getMinSupportedMySqlVersion(); | |
| } else { | |
| throw new \Exception('Invalid database platform '.$platform.'. Mautic only supports MySQL and MariaDB!'); | |
| } | |
| if (true !== version_compare($version, $minSupported, 'gt')) { | |
| throw new DatabaseVersionTooOldException($version); | |
| } | |
| } | |
| /** | |
| * @throws \Doctrine\DBAL\Exception | |
| */ | |
| protected function backupExistingSchema($tables, $mauticTables, $backupPrefix): array | |
| { | |
| $sql = []; | |
| $sm = $this->getSchemaManager(); | |
| // backup existing tables | |
| $backupRestraints = $backupSequences = $backupIndexes = $backupTables = $dropSequences = $dropTables = []; | |
| // cycle through the first time to drop all the foreign keys | |
| foreach ($tables as $t) { | |
| if (!isset($mauticTables[$t]) && !in_array($t, $mauticTables)) { | |
| // Not an applicable table | |
| continue; | |
| } | |
| $restraints = $sm->listTableForeignKeys($t); | |
| if (isset($mauticTables[$t])) { | |
| // to be backed up | |
| $backupRestraints[$mauticTables[$t]] = $restraints; | |
| $backupTables[$t] = $mauticTables[$t]; | |
| $backupIndexes[$t] = $sm->listTableIndexes($t); | |
| } else { | |
| // existing backup to be dropped | |
| $dropTables[] = $t; | |
| } | |
| foreach ($restraints as $restraint) { | |
| $sql[] = $this->platform->getDropForeignKeySQL($restraint, $t); | |
| } | |
| } | |
| // now drop all the backup tables | |
| foreach ($dropTables as $t) { | |
| $sql[] = $this->platform->getDropTableSQL($t); | |
| } | |
| // now backup tables | |
| foreach ($backupTables as $t => $backup) { | |
| // drop old indexes | |
| /** @var Index $oldIndex */ | |
| foreach ($backupIndexes[$t] as $indexName => $oldIndex) { | |
| if ('primary' == $indexName) { | |
| continue; | |
| } | |
| $oldName = $oldIndex->getName(); | |
| $newName = $this->generateBackupName($this->dbParams['table_prefix'], $backupPrefix, $oldName); | |
| $newIndex = new Index( | |
| $newName, | |
| $oldIndex->getColumns(), | |
| $oldIndex->isUnique(), | |
| $oldIndex->isPrimary(), | |
| $oldIndex->getFlags(), | |
| $oldIndex->getOptions() | |
| ); | |
| $newIndexes[] = $newIndex; | |
| $sql[] = $this->platform->getDropIndexSQL($oldIndex, $t); | |
| } | |
| // rename table | |
| $queries = $this->platform->getRenameTableSQL($t, $backup); | |
| $sql = array_merge($sql, $queries); | |
| // create new index | |
| if (!empty($newIndexes)) { | |
| foreach ($newIndexes as $newIndex) { | |
| $sql[] = $this->platform->getCreateIndexSQL($newIndex, $backup); | |
| } | |
| unset($newIndexes); | |
| } | |
| } | |
| // apply foreign keys to backup tables | |
| foreach ($backupRestraints as $table => $oldRestraints) { | |
| foreach ($oldRestraints as $or) { | |
| $foreignTable = $or->getForeignTableName(); | |
| $foreignTableName = $this->generateBackupName($this->dbParams['table_prefix'], $backupPrefix, $foreignTable); | |
| $r = new ForeignKeyConstraint( | |
| $or->getLocalColumns(), | |
| $foreignTableName, | |
| $or->getForeignColumns(), | |
| $backupPrefix.$or->getName(), | |
| $or->getOptions() | |
| ); | |
| $sql[] = $this->platform->getCreateForeignKeySQL($r, $table); | |
| } | |
| } | |
| return $sql; | |
| } | |
| protected function dropExistingSchema($tables, $mauticTables): array | |
| { | |
| $sql = []; | |
| // drop tables | |
| foreach ($tables as $t) { | |
| if (isset($mauticTables[$t])) { | |
| $sql[] = $this->platform->getDropTableSQL($t); | |
| } | |
| } | |
| return $sql; | |
| } | |
| /** | |
| * @return mixed|string | |
| */ | |
| protected function generateBackupName($prefix, $backupPrefix, $name) | |
| { | |
| if (empty($prefix) || !str_contains($name, $prefix)) { | |
| return $backupPrefix.$name; | |
| } else { | |
| return str_replace($prefix, $backupPrefix, $name); | |
| } | |
| } | |
| /** | |
| * @return AbstractSchemaManager<AbstractPlatform> | |
| */ | |
| private function getSchemaManager(): AbstractSchemaManager | |
| { | |
| if (null !== $this->schemaManager) { | |
| return $this->schemaManager; | |
| } | |
| return $this->schemaManager = $this->db->createSchemaManager(); | |
| } | |
| } | |