341 lines
11 KiB
PHP
Executable File
341 lines
11 KiB
PHP
Executable File
<?php
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
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->executeStatement($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();
|
|
}
|
|
}
|