Conn->qstr( $this->moduleName ); $revisions = $this->Conn->GetOne($sql); $this->appliedRevisions = $revisions ? explode(',', $revisions) : Array (); } private function saveAppliedRevisions() { // maybe optimize sort($this->appliedRevisions); $fields_hash = Array ( 'AppliedDBRevisions' => implode(',', $this->appliedRevisions), ); $this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'Modules', '`Name` = ' . $this->Conn->qstr($this->moduleName)); } public function deployAll($dry_run = false) { $this->dryRun = $dry_run; $this->lineEnding = $dry_run ? '
' . PHP_EOL : PHP_EOL; $ret = true; foreach ($this->Application->ModuleInfo as $module_name => $module_info) { $this->moduleName = $module_name; if ( !file_exists($this->getModuleFile('project_upgrades.sql')) ) { continue; } $ret = $ret && $this->deploy($module_name); } if (!$this->dryRun) { $this->resetCaches(); $this->refreshThemes(); } return $ret; } /** * Deploys pending changes to a site * */ private function deploy($module_name) { echo 'Deploying Module "' . $module_name . '":' . $this->lineEnding; echo 'Upgrading Database ... '; if ( !$this->upgradeDatabase() ) { return false; } echo 'OK' . $this->lineEnding; $this->importLanguagePack(); echo 'Done.' . $this->lineEnding; return true; } /** * Import latest languagepack (without overwrite) * */ private function importLanguagePack() { $language_import_helper =& $this->Application->recallObject('LanguageImportHelper'); /* @var $language_import_helper LanguageImportHelper */ echo 'Importing LanguagePack ... '; $filename = $this->getModuleFile('english.lang'); $language_import_helper->performImport($filename, '|0|1|2|', $this->moduleName, LANG_SKIP_EXISTING); echo 'OK' . $this->lineEnding; } /** * Resets unit and section cache * */ private function resetCaches() { // 2. reset unit config cache (so new classes get auto-registered) echo 'Resetting Unit Config Cache ... '; $admin_event = new kEvent('adm:OnResetConfigsCache'); $this->Application->HandleEvent($admin_event); echo 'OK' . $this->lineEnding; // 3. reset sections cache echo 'Resetting Sections Cache ... '; $admin_event = new kEvent('adm:OnResetSections'); $this->Application->HandleEvent($admin_event); echo 'OK' . $this->lineEnding; } /** * Rebuild theme files * */ private function refreshThemes() { echo 'Rebuilding Theme Files ... '; $admin_event = new kEvent('adm:OnRebuildThemes'); $this->Application->HandleEvent($admin_event); echo 'OK' . $this->lineEnding; } /** * Runs database upgrade script * * @return bool */ private function upgradeDatabase() { $this->loadAppliedRevisions(); $this->Conn->errorHandler = Array(&$this, 'handleSqlError'); if ( !$this->collectDatabaseRevisions() || !$this->checkRevisionDependencies() ) { return false; } $applied = $this->applyRevisions(); $this->saveAppliedRevisions(); return $applied; } /** * Collects database revisions from "project_upgrades.sql" file. * * @return bool */ private function collectDatabaseRevisions() { $filename = $this->getModuleFile('project_upgrades.sql'); if ( !file_exists($filename) ) { return true; } $sqls = file_get_contents($filename); preg_match_all("/# r([\d]+)([^\:]*):.*?(\n|$)/s", $sqls, $matches, PREG_SET_ORDER + PREG_OFFSET_CAPTURE); if (!$matches) { echo 'No Database Revisions Found' . $this->lineEnding; return false; } foreach ($matches as $index => $match) { $revision = $match[1][0]; if ( $this->revisionApplied($revision) ) { // skip applied revisions continue; } if ( isset($this->revisionSqls[$revision]) ) { // duplicate revision among non-applied ones echo 'Duplicate revision ' . $revision . ' found' . $this->lineEnding; return false; } // get revision sqls $start_pos = $match[0][1] + strlen($match[0][0]); $end_pos = isset($matches[$index + 1]) ? $matches[$index + 1][0][1] : strlen($sqls); $revision_sqls = substr($sqls, $start_pos, $end_pos - $start_pos); if (!$revision_sqls) { // resision without sqls continue; } $this->revisionSqls[$revision] = $revision_sqls; $revision_lependencies = $this->parseRevisionDependencies($match[2][0]); if ($revision_lependencies) { $this->revisionDependencies[$revision] = $revision_lependencies; } } ksort($this->revisionSqls); ksort($this->revisionDependencies); return true; } /** * Checks that all dependent revisions are either present now OR were applied before * * @return bool */ private function checkRevisionDependencies() { foreach ($this->revisionDependencies as $revision => $revision_dependencies) { foreach ($revision_dependencies as $revision_dependency) { if ( $this->revisionApplied($revision_dependency) ) { // revision dependend upon already applied -> depencency fulfilled continue; } if ($revision_dependency >= $revision) { echo 'Revision ' . $revision . ' has incorrect dependency to revision ' . $revision_dependency . '. Only dependencies to older revisions are allowed!' . $this->lineEnding; return false; } if ( !isset($this->revisionSqls[$revision_dependency]) ) { echo 'Revision ' . $revision . ' depends on missing revision ' . $revision_dependency . '!' . $this->lineEnding; return false; } } } return true; } /** * Runs all pending sqls * * @return bool */ private function applyRevisions() { if (!$this->revisionSqls) { return true; } if ($this->dryRun) { $this->appliedRevisions = array_merge($this->appliedRevisions, array_keys($this->revisionSqls)); return true; } echo $this->lineEnding; foreach ($this->revisionSqls as $revision => $sqls) { echo 'Processing DB Revision: #' . $revision . ' ... '; $sqls = str_replace("\r\n", "\n", $sqls); // convert to linux line endings $no_comment_sqls = preg_replace("/#\s([^;]*?)\n/is", '', $sqls); // remove all comments "#" on new lines $sqls = explode(";\n", $no_comment_sqls . "\n"); // ensures that last sql won't have ";" in it $sqls = array_map('trim', $sqls); foreach ($sqls as $index => $sql) { if (!$sql || (substr($sql, 0, 1) == '#')) { continue; // usually last line } $this->Conn->Query($sql); if ( $this->Conn->hasError() ) { // consider revisions with errors applied $this->appliedRevisions[] = $revision; return false; } } $this->appliedRevisions[] = $revision; echo 'OK' . $this->lineEnding; } return true; } /** * Error handler for sql errors * * @param int $code * @param string $msg * @param string $sql * @return bool */ public function handleSqlError($code, $msg, $sql) { echo 'Error (#' . $code . ': ' . $msg . ') during SQL processing:' . $this->lineEnding . $sql . $this->lineEnding; echo 'Please execute rest of sqls in this revision by hand and run deployment script again.' . $this->lineEnding; return true; } /** * Checks if given revision was already applied * * @param int $revision * @return bool */ private function revisionApplied($revision) { foreach ($this->appliedRevisions as $applied_revision) { // revision range $applied_revision = explode('-', $applied_revision, 2); if ( !isset($applied_revision[1]) ) { // convert single revision to revision range $applied_revision[1] = $applied_revision[0]; } if ( $revision >= $applied_revision[0] && $revision <= $applied_revision[1] ) { return true; } } return false; } /** * Returns path to given file in current module install folder * * @param string $filename * @return string */ private function getModuleFile($filename) { $module_folder = $this->Application->findModule('Name', $this->moduleName, 'Path'); return FULL_PATH . DIRECTORY_SEPARATOR . $module_folder . 'install/' . $filename; } /** * Extracts revisions from string in format "(1,3,5464,23342,3243)" * * @param string $string * @return Array */ private function parseRevisionDependencies($string) { if (!$string) { return Array (); } $string = explode(',', substr($string, 1, -1)); return array_map('trim', $string); } }