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(); | |
} | |
} | |