Files
kupshop/class/class.Upgrade.php
2025-08-02 16:30:27 +02:00

950 lines
28 KiB
PHP

<?php
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Database\QueryHint;
class Upgrade
{
use DatabaseCommunication;
public const VERBOSE_YES = true;
public const VERBOSE_NO = false;
public const LOCAL_UPGRADES_YES = true;
public const LOCAL_UPGRADES_NO = false;
public $path = [];
protected $priority;
// aktualni nastaveni vybraneho upgrade
public $upgradeIni = [];
// loguje chyby
public $errors = [];
// loguje zpravy
public $notes = [];
protected $verbose = false;
protected static $instanceCache = [];
private $useLocalUpgrades;
// ///////////////////////////////////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function __construct($verbose = self::VERBOSE_NO, $useLocalUpgrades = self::LOCAL_UPGRADES_YES)
{
global $cfg;
$this->path['upgrades_dir'] = dirname(__DIR__).'/upgrade/list/';
$this->path['local_upgrades_dir'] = $cfg['Path']['web_root'].'upgrade/';
$this->verbose = $verbose;
$this->useLocalUpgrades = $useLocalUpgrades;
return true;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// prohleda slozku a najde vsechny dostupne upgrade
public function loadFileNamesFromFolder($folder)
{
$files = [];
$dp = @opendir($folder);
if ($dp) {
while ($fileThis = readdir($dp)) {
if (!is_dir($fileThis)) {
// pokud odpovida jmeno souboru tvaru upgradu
if (preg_match('/^(.+)\\.php$/i', $fileThis, $matches)) {
$files[] = [
'file' => $folder.$fileThis,
'date' => $matches[1],
];
}
}
}
}
sort($files);
return $files;
}
public function getUpgrades()
{
$shared_files = $this->loadFileNamesFromFolder($this->path['upgrades_dir']);
if ($this->useLocalUpgrades) {
$local_files = $this->loadFileNamesFromFolder($this->path['local_upgrades_dir']);
$shared_files = array_merge($shared_files, $local_files);
}
try {
$finder = ServiceContainer::getService(\KupShop\KupShopBundle\Util\System\BundleFinder::class);
$files = $finder->getBundlesPath('Resources/upgrade/');
foreach ($files as $file) {
if ($this->loadFileNamesFromFolder($file) != null) {
$shared_files = array_merge($shared_files, $this->loadFileNamesFromFolder($file));
}
}
} catch (Throwable $e) {
echo 'Failed loading bundle upgrades: '.$e->getMessage();
}
return $shared_files;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function checkTableExists($tableName, $dbName = null)
{
$change = true;
$db = $dbName ? 'IN '.$dbName : '';
$SQL = sqlQuery("SHOW TABLES {$db} LIKE '".getTableName($tableName, false)."'");
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
public function checkDatabaseExists($tableName)
{
$change = true;
$SQL = sqlQuery("SHOW DATABASES LIKE '".getTableName($tableName, false)."'");
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
public function checkTableIsEmpty($tableName)
{
$change = true;
if ($this->checkTableExists($tableName)) {
return true;
}
$SQL = sqlQuery('SELECT * FROM '.getTableName($tableName, false));
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
public function checkColumnExists($table, $column)
{
$change = true;
try {
$SQL = sqlQuery('SHOW FIELDS FROM '.getTableName($table));
while (($row = sqlFetchArray($SQL)) !== false) {
if ($row['Field'] == $column) {
$change = false;
break;
}
}
} catch (Exception $e) {
// pass
}
return $change;
}
public function checkColumnIsEmpty($tableName, $columnName)
{
$change = true;
$SQL = sqlQuery('SELECT * FROM '.getTableName($tableName).' WHERE '.$columnName);
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
public function checkHasPrimaryKey(string $table)
{
return !$this->checkConstraintExists($table, 'PRIMARY');
}
public function checkConstraintExists($table, $name)
{
$change = true;
// $SQL = sqlQuery("SELECT *
// FROM information_schema.KEY_COLUMN_USAGE
// WHERE CONSTRAINT_SCHEMA='".$GLOBALS['cfg']['Connection']['database']."' AND CONSTRAINT_NAME='$name' AND TABLE_NAME='".getTableName($table, false)."'");
$SQL = sqlQuery("SHOW KEYS FROM {$table} WHERE key_name='{$name}'");
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
public function checkIsInnoDB($tableName)
{
$change = true;
$SQL = sqlFetchAssoc(sqlQuery("SHOW TABLE STATUS WHERE Name = '".getTableName($tableName, false)."'"));
if ($SQL['Engine'] == 'InnoDB') {
$change = false;
}
return $change;
}
public function checkFulltextConstraintExists($table, $name)
{
$change = true;
$SQL = sqlQuery("SELECT 1
FROM information_schema.STATISTICS
WHERE table_schema='".$GLOBALS['cfg']['Connection']['database']."' AND INDEX_NAME='{$name}' AND index_type='FULLTEXT' AND TABLE_NAME='".getTableName($table, false)."'");
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
public function checkConstraintTable($table, $name)
{
$change = true;
$query = "SELECT 1
FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA='".$GLOBALS['cfg']['Connection']['database']."' AND CONSTRAINT_NAME='{$name}' AND TABLE_NAME='".getTableName($table, false)."'";
if (sqlNumRows(sqlQuery($query)) > 0) {
$change = false;
}
return $change;
}
public function checkConstraintRule($table, $name, $delete = null, $update = null)
{
$change = true;
$query = "SELECT 1
FROM information_schema.REFERENTIAL_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA='".$GLOBALS['cfg']['Connection']['database']."' AND CONSTRAINT_NAME='{$name}' AND TABLE_NAME='".getTableName($table, false)."'";
if ($delete) {
$query .= "AND DELETE_RULE='{$delete}'";
}
if ($update) {
$query .= "AND UPDATE_RULE='{$update}'";
}
if (sqlNumRows(sqlQuery($query)) > 0) {
$change = false;
}
return $change;
}
public function checkConstraintWithColumnExists($table, $name, $column)
{
$change = true;
$SQL = sqlQuery("SELECT 1
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA='".$GLOBALS['cfg']['Connection']['database']."'
AND CONSTRAINT_NAME='{$name}'
AND TABLE_NAME='".getTableName($table, false)."'
AND COLUMN_NAME='{$column}'");
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
public function checkForeignKeyExists($table, $column)
{
$change = true;
$SQL = sqlQuery("SELECT 1
FROM information_schema.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA='{$GLOBALS['cfg']['Connection']['database']}'
AND TABLE_NAME='".getTableName($table, false)."'
AND COLUMN_NAME='{$column}'
AND REFERENCED_COLUMN_NAME IS NOT NULL");
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
public function checkIfTriggerExists($triggerName, $table = null)
{
$change = true;
if (!$table) {
$table = str_replace('trigger_', '', $triggerName);
}
$SQL = sqlQuery('SELECT 1
FROM INFORMATION_SCHEMA.TRIGGERS
WHERE TRIGGER_NAME = :trigger
AND EVENT_OBJECT_SCHEMA = DATABASE()
AND EVENT_OBJECT_TABLE = :table',
['table' => $table, 'trigger' => $triggerName]);
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
public function checkColumnType($table, $column, $type, $dbName = null)
{
$change = true;
$type = sqlFormatInput($type);
$dbName = $dbName ?? $GLOBALS['cfg']['Connection']['database'];
$SQL = sqlQuery("SELECT *
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='".$dbName."'
AND TABLE_NAME='".getTableName($table, false)."'
AND COLUMN_NAME='{$column}'
AND COLUMN_TYPE COLLATE UTF8_GENERAL_CI LIKE '{$type}'");
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
/**
* @deprecated Use self::checkEnumOptions
*/
public function checkEnumExists($table, $column, $value, &$values = '')
{
$change = true;
$type = returnSqlResult("SELECT COLUMN_TYPE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='".$GLOBALS['cfg']['Connection']['database']."'
AND TABLE_NAME='".getTableName($table, false)."'
AND COLUMN_NAME='{$column}'");
if (empty($type)) {
throw new \Exception("Can not find column {$table}:{$column}");
}
if (strstr($type, "'{$value}'") !== false) {
$change = false;
}
$values = $type;
return $change;
}
public function checkEnumOptions(string $table, string $column, array $values)
{
$enums = '';
if (empty($values)) {
throw new \Exception('No value');
}
$this->checkEnumExists($table, $column, 'bžet', $enums);
foreach ($values as $value) {
if (strstr($enums, "'{$value}'") === false) {
return true;
}
}
return false;
}
public function updateEnumOptions($table, $column, $values, $default = '')
{
$enums = '';
$this->checkEnumExists($table, $column, 'bžet', $enums);
foreach ($values as $value) {
if (strstr($enums, "'{$value}'") === false) {
$enums = str_replace(')', ",'{$value}')", $enums);
}
}
if ($default) {
$default = " DEFAULT '{$default}'";
}
sqlQuery("ALTER TABLE {$table} CHANGE `{$column}` `{$column}` {$enums} NOT NULL {$default};");
}
public function checkColumnIsNull($table, $column, $null)
{
$change = true;
$SQL = sqlQuery("SELECT 1
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='".$GLOBALS['cfg']['Connection']['database']."'
AND TABLE_NAME='".getTableName($table, false)."'
AND COLUMN_NAME='{$column}'
AND IS_NULLABLE='".($null ? 'YES' : 'NO')."'");
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
public function checkColumnCollation($table, $column, $collation)
{
$change = true;
$SQL = sqlQuery("SELECT 1
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='".$GLOBALS['cfg']['Connection']['database']."'
AND TABLE_NAME='".getTableName($table, false)."'
AND COLUMN_NAME='{$column}'
AND COLLATION_NAME='{$collation}'");
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
public function checkColumnDefault($table, $column, $default)
{
$change = true;
$SQL = sqlQuery("SHOW FIELDS FROM {$table}
WHERE Field=:column
AND `Default`=:default", ['default' => $default, 'column' => $column]);
if (sqlNumRows($SQL) > 0) {
$change = false;
}
return $change;
}
public function checkIndexNameExists($table, $name)
{
return sqlNumRows(sqlQuery("SHOW INDEXES FROM {$table} WHERE key_name=:name", ['name' => $name])) == 0;
}
public function checkIndexOnColumnExists($table, $column)
{
return sqlNumRows(sqlQuery("SHOW INDEXES FROM {$table} WHERE column_name=:column", ['column' => $column])) == 0;
}
public function checkModule($module, $submodule = null)
{
return findModule($module, $submodule);
}
public function checkDatabaseEncoding($collation)
{
return $collation != returnSQLResult('SELECT default_character_set_name FROM information_schema.SCHEMATA
WHERE schema_name = :database', ['database' => $GLOBALS['cfg']['Connection']['database']]);
}
public function checkTableEncoding($table, $collation)
{
return $collation != returnSQLResult('SELECT CCSA.character_set_name FROM information_schema.`TABLES` T,
information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` CCSA
WHERE CCSA.collation_name = T.table_collation
AND T.table_schema = :database
AND T.table_name = :table', ['database' => $GLOBALS['cfg']['Connection']['database'], 'table' => $table]);
}
public function checkFunctionExists($name)
{
return sqlNumRows(sqlQuery('SHOW FUNCTION STATUS WHERE name=:name AND db=:db', ['name' => $name, 'db' => Config::get()['Connection']['database']])) === 0;
}
public function checkProcedureExists($name)
{
return sqlNumRows(sqlQuery('SHOW PROCEDURE STATUS WHERE name=:name AND db=:db', ['name' => $name, 'db' => Config::get()['Connection']['database']])) === 0;
}
public function getTableInformationSchema(string $tableName): array|false
{
$database = Config::get()['Connection']['database'];
return sqlQueryBuilder()->select('*')
->from('information_schema.TABLES')
->andWhere(\Query\Operator::equals([
'TABLE_SCHEMA' => $database,
'TABLE_NAME' => $tableName,
]))->execute()->fetchAssociative();
}
// Posledni pouzita verze = 38
public function checkDataMigration($version)
{
return Settings::getDefault()->user_rights_version == $version - 1;
}
public function checkPrimaryKey($table, $column)
{
$return = sqlQueryBuilder()->select('COLUMN_KEY')->from('information_schema.COLUMNS')
->where(\Query\Operator::equals([
'TABLE_SCHEMA' => $GLOBALS['cfg']['Connection']['database'],
'COLUMN_NAME' => $column,
'TABLE_NAME' => $table,
'COLUMN_KEY' => 'PRI',
]))
->execute()->fetchOne();
return $return === false;
}
public function commitDataMigration($version)
{
$settings = Settings::getDefault();
$settings->user_rights_version = $version;
$settings->saveValue('user_rights_version', $version);
\Settings::clearCache();
}
public function loadIniFile($upgradeName)
{
// sestavit cestu k souboru
$src = $this->path['upgrades_dir'].'/'.$upgradeName;
if (!file_exists($src)) {
return false;
}
$this->upgradeIni = @parse_ini_file($src, true);
// kdyz neexistuje nastaveni souboru, pouzit defaukt
if (!array_key_exists('file', $this->upgradeIni['UpgradeSettings'])) {
$this->upgradeIni['UpgradeSettings']['file'] = str_replace('.ini', '', $upgradeName);
}
return $this->upgradeIni;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function includeUpgradeScript($upgradeName)
{
// sestavit cestu k souboru
$src = $this->path['upgrades_dir'].'/'.$upgradeName;
// ukoncit, kdy soubor neexistuje
// ukoncit, kdy neni nalezeno nejdrive nastaveni
if (!file_exists($src)
|| count($this->upgradeIni['UpgradeSettings']) == 0
) {
return false;
}
include_once $src;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function addError($msg)
{
$this->errors[] = $msg;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function addNote($msg)
{
$this->notes[] = $msg;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function noChanges()
{
$this->notes[] = 'Nebylo zapotrebi zmen';
}
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function upgradeOK()
{
echo ' + '.get_class($this).'::??????: OK';
}
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
public function printResult($global = true)
{
if (count($this->errors) > 0) {
echo '<strong>Behem upgradu vznikly nasledujici chyby</strong>';
for ($i = 0; $i < count($this->errors); $i++) {
echo '<li>'.$this->errors[$i].'</li>';
}
$this->errors = [];
}
if (!$global && count($this->notes) > 0) {
echo join(', ', $this->notes);
$this->notes = [];
}
}
public function run()
{
// Sorry za tohle, musím kvůli tomu že v buildu se spouští bez databáze ale musí proběhnout alespoň prvních pár migrací bez DB
try {
QueryHint::routeToMaster();
} catch (\Throwable) {
// pass
}
// nacte seznam ini souboru
$upgradesList = $this->getUpgrades();
// kontrola delky zpracovani skriptu
controlTimeLimit();
ini_set('memory_limit', '1G');
if ($this->verbose) {
echo '<pre>';
}
$this->loadUpgrades($upgradesList);
$this->orderUpgrades($upgradesList);
// -----------------------------------------------------
// projet a spustit vsechny nalezene upgrady
foreach ($upgradesList as $index => $upgrade) {
$className = get_class($upgrade['instance']);
if ($this->verbose) {
if ($index > 0) {
echo '---------------------------------------------------------'."\n";
}
echo 'Upgrade #'.($index + 1)." - {$className} ({$upgrade['date']})\n";
}
// inicializuje tridu
$time = microtime(true);
/** @var Upgrade $upgrade */
$upgrade = $upgrade['instance'];
$upgrade->setVerbose($this->verbose);
try {
$upgrade->upgrade();
} catch (Exception $exception) {
echo "Upgrade error: {$className}\n";
throw $exception;
}
if ($this->verbose) {
echo ' => '.round((microtime(true) - $time) * 1000).'ms'."\n";
}
// kontrola delky zpracovani skriptu
controlTimeLimit();
}
if ($this->verbose) {
echo '</pre><div id="end"></div>'."\n";
}
}
public function setVerbose($verbose)
{
$this->verbose = $verbose;
}
public function upgrade()
{
throw new LogicException('class Upgrade: method upgrade() not defined');
}
public function getShopDir()
{
global $cfg;
return realpath($cfg['Path']['web_root']).'/';
}
public function loadUpgrades(&$upgradesList)
{
foreach ($upgradesList as $index => &$upgrade) {
// Load class if not already loaded
if (!isset(static::$instanceCache[$upgrade['file']])) {
// nacist upgrade - trik jak ziskat nactenou classu
$classes = get_declared_classes();
include_once $upgrade['file'];
$diff = array_diff(get_declared_classes(), $classes);
$className = reset($diff);
if (!class_exists($className)) {
echo '!!! Chyba - chybi trida pro upgrade: '.$upgrade['file'];
continue;
}
/* @var Upgrade $u */
static::$instanceCache[$upgrade['file']] = new $className();
}
$u = static::$instanceCache[$upgrade['file']];
$upgrade['priority'] = $u->getPriority();
$upgrade['instance'] = $u;
// Upgrades from egnine/upgrade/list has higher priority
if ($upgrade['priority'] == null) {
if (strpos($upgrade['file'], 'upgrade/list') !== false) {
$upgrade['priority'] = -100;
} else {
$upgrade['priority'] = 0;
}
}
}
}
public function orderUpgrades(&$upgradesList)
{
usort($upgradesList, ['Upgrade', 'prioritySort']);
}
// sort by priority and date
public function prioritySort($a, $b)
{
$order = ['priority' => 'asc', 'date' => 'asc'];
$t = [true => -1, false => 1];
$r = true;
$k = 1;
foreach ($order as $key => $value) {
$k = ($value === 'asc') ? 1 : -1;
$r = ($a[$key] < $b[$key]);
if ($a[$key] !== $b[$key]) {
return $t[$r] * $k;
}
}
return $t[$r] * $k;
}
/**
* @return int
*/
public function getPriority()
{
return $this->priority;
}
/**
* @throws ReflectionException
*/
public function ensureDbExists(string $db): bool
{
try {
sqlGetConnection()->connect();
sqlQuery("USE `{$db}`");
return false;
} catch (Exception $e) {
sqlGetConnection()->close();
}
$reflectionClass = new ReflectionClass(\Doctrine\DBAL\Connection::class);
try {
$reflectionProperty = $reflectionClass->getProperty('_params');
} catch (ReflectionException $e) {
$reflectionProperty = $reflectionClass->getProperty('params');
}
$reflectionProperty->setAccessible(true);
$params = $reflectionProperty->getValue(sqlGetConnection());
unset($params['dbname']);
$reflectionProperty->setValue(sqlGetConnection(), $params);
sqlGetConnection()->connect();
$params['dbname'] = $db;
$reflectionProperty->setValue(sqlGetConnection(), $params);
sqlQuery("CREATE DATABASE IF NOT EXISTS `{$db}`");
sqlQuery("USE `{$db}`");
return true;
}
protected function isAllowed()
{
return true;
}
protected function addPrivilegeToPrivilegedAdmins(?string $conditionPrivilege = null, string $newPrivilege)
{
$qb = sqlQueryBuilder()->select('id, privilege')->from('admins');
if (isset($conditionPrivilege)) {
$qb->where('privilege LIKE :privilege')
->setParameter('privilege', '%'.$conditionPrivilege.'%');
}
foreach ($qb->execute() as $user) {
$privilegeArr = explode('|', $user['privilege']);
if (isset($conditionPrivilege) && !in_array($conditionPrivilege, $privilegeArr)) {
continue;
}
// pridavam pravo vsem krom adminum z pravem ALL_RIGHTS
if (!isset($conditionPrivilege) && in_array('ALL_RIGHTS', $privilegeArr)) {
continue;
}
$privilegeArr[] = $newPrivilege;
$privilege = join('|', $privilegeArr);
$this->updateSQL('admins', ['privilege' => $privilege], ['id' => $user['id']]);
}
}
}
class UpgradeNew extends Upgrade
{
public function upgrade()
{
$changed = false;
$methods = get_class_methods($this);
if (!$this->isAllowed()) {
$this->addNote('Preskoceno');
$methods = [];
}
foreach ($methods as $method) {
if (!preg_match('/^check(Rightfulness)?_(.+)$/i', $method, $matches)) {
continue;
}
$name = $matches[2];
$checkRightfulness = $matches[0];
if (method_exists($this, "makeChanges_{$name}")) {
$makeChanges = "makeChanges_{$name}";
} elseif (method_exists($this, "upgrade_{$name}")) {
$makeChanges = "upgrade_{$name}";
} else {
continue;
}
// jestlize je opravnene udelat upgrade, tak provest
$time = microtime(true);
$change = $this->$checkRightfulness();
if (is_null($change)) {
echo 'Method "'.$checkRightfulness.'" in class "'.get_class($this).'" returns NULL instead of boolean! You might have missed the word "return".'.PHP_EOL;
}
if ($this->verbose && round((microtime(true) - $time) * 1000) > 10) {
echo ' ! '.$name.': '.$this->getComment($makeChanges).' checking took '.round((microtime(true) - $time) * 1000).'ms)'."\n";
}
if ($change) {
$changed = true;
$comment = $this->getComment($makeChanges) ?: get_class($this).'::'.$makeChanges;
echo ' + '.$comment.': ';
flush();
$time = microtime(true);
$this->$makeChanges();
$this->printResult(false);
echo ' ('.round((microtime(true) - $time) * 1000).'ms)';
echo "\n";
}
}
if (!$changed) {
$this->noChanges();
$this->printResult();
}
}
public function getComment($method)
{
$c = new ReflectionClass($this);
$m = $c->getMethod($method);
$s = $m->getDocComment();
$s = str_replace('/*', '', $s);
$s = str_replace('*/', '', $s);
$s = str_replace('*', '', $s);
return trim($s);
}
public function checkSetOptions($table, $column, $values)
{
$this->checkEnumExists($table, $column, 'bžet', $set);
$set = preg_match_all("/'([^']+)'/", $set, $matches);
if (array_diff($values, $matches[1])) {
return true;
}
return false;
}
public function updateSetOptions($table, $column, $values)
{
$typesString = array_map(fn ($value) => "'{$value}'", $values);
sqlQuery("ALTER TABLE {$table} MODIFY COLUMN {$column} SET(".join(',', $typesString).');');
}
public function upgradeOK()
{
$this->notes[] = 'OK';
}
}