public function key() { /** @var \DirectoryIterator $this */ return Helper::getClassnameFromFilepath($this->getFilename()); }
/** * Removes patches from the array that cannot be performed because of missing dependencies. * * @param array $patches { timestamp: filename, ... } * @param array $performed_patches { timestamp: applied_at, ... } * @return array */ protected function checkDependencies(array $patches, array $performed_patches) { if (empty($patches)) { return $patches; } $checked_patches = array(); foreach ($patches as $patch_name => $filename) { $classname = Helper::getClassnameFromFilepath($filename); /** @var SqlUpdateInterface $patch */ $patch = new $classname(array('charset' => $this->database_charset)); $patch_dependencies = $patch->getDependencies(); if (empty($patch_dependencies)) { $checked_patches[$patch_name] = $filename; continue; } $allow_patch = true; $available_dependencies = array_keys($checked_patches + $performed_patches); foreach ($patch_dependencies as $dependency_classname) { if (!in_array($dependency_classname, $available_dependencies)) { $this->logger->log("Can't apply patch '{$classname}', missing dependency '{$dependency_classname}'."); $allow_patch = false; continue 2; } } if ($allow_patch) { $checked_patches[$patch_name] = $filename; } } return $checked_patches; }
/** * Applies a series of SQL patches to the database and registers them in the db_patches table. * * @param bool $register_only Only register the patches as done, don't run their code * @throws \LemonWeb\Deployer\Exceptions\DatabaseException */ public function update($register_only = false) { $this->logger->log('Database update: ' . implode(', ', array_keys($this->sql_patch_objects)), LOG_DEBUG, true); if (!count($this->sql_patch_objects)) { return; } foreach ($this->sql_patch_objects as $filename => $sql_patch_object) { $patch_name = DatabaseHelper::getClassnameFromFilepath($filename); $patch_timestamp = DatabaseHelper::convertFilenameToDateTime($filename); // Register the patch in the db_patches table (except for the db_patches table patch itself, that wouldn't be possible yet). // Add the revert (down) code to the record so the update can be reverted when the file doesn't exist, which can happen when code is rolled back. // Also add the patch' dependencies list so depending patches won't be reverted before it is reverted first. if ('19700101000000' != $patch_timestamp) { $this->driver->query("\n INSERT INTO db_patches (\n patch_name,\n patch_timestamp,\n down_sql,\n dependencies\n )\n VALUES (\n '" . $this->driver->escape($patch_name) . "',\n '" . $this->driver->escape($patch_timestamp) . "',\n " . (trim($sql_patch_object->down()) != '' ? "'" . $this->driver->escape(trim($sql_patch_object->down())) . "'" : 'null') . ",\n " . (count($sql_patch_object->getDependencies()) > 0 ? "'" . $this->driver->escape(implode("\n", $sql_patch_object->getDependencies())) . "'" : 'null') . "\n );\n "); } // apply the patch if (!$register_only) { $this->driver->startTransaction(); $result = $this->driver->multiQuery($sql_patch_object->up()); if (false === $result) { throw new DatabaseException('Error applying patch ' . $patch_name . ': ' . $this->driver->getLastError(), 1); } $this->driver->doCommit(); } // if there were no errors, mark the patch as applied if ('19700101000000' != $patch_timestamp) { $this->driver->query("\n UPDATE db_patches\n SET applied_at = '" . $this->driver->escape($this->timestamp) . "'\n WHERE patch_name = '" . $this->driver->escape($patch_name) . "';\n "); if ($register_only) { $this->logger->log("Patch '{$filename}' registered."); } else { $this->logger->log("Patch '{$filename}' succeeded."); } } else { // the db_patches patch has no record set, insert it now $this->driver->query("\n INSERT INTO db_patches (\n patch_name,\n patch_timestamp,\n applied_at\n )\n VALUES (\n '" . $this->driver->escape($patch_name) . "',\n '" . $this->driver->escape($patch_timestamp) . "',\n '" . $this->driver->escape($this->timestamp) . "'\n );\n "); $this->logger->log("Patch '{$filename}' succeeded."); } } }
/** * Check if the db_patches table exists, compare it to the locally available patches and ask the user what he wants to do if there's a difference. * * @param string $action update of rollback * @throws \LemonWeb\Deployer\Exceptions\DeployException */ public function check($action) { $this->logger->log('Check for database updates:', LOG_INFO, true); if (empty($this->database_dirs)) { return; } // collect and verify the database login information so the db_patches table can be checked $this->getDatabaseLogin(true); // make a list of all available patchfiles in de project $available_patches = $this->findSQLFiles($action); $patches_to_apply = array(); $patches_to_revert = array(); $patches_to_register_as_done = array(); $performed_patches = array(); $dependencies = array(); if ($this->patches_table_exists = $this->checkIfPatchTableExists()) { // get the list of all performed patches from the database list($performed_patches, $dependencies) = $this->findPerformedSQLPatches(); if (Deploy::UPDATE == $action) { // list the patches that have not yet been applied $patches_to_apply = array_diff_key($available_patches, $performed_patches); // list the patches that have been removed from the project and may need to be reverted $patches_to_revert = array_diff_key($performed_patches, $available_patches); } elseif (Deploy::ROLLBACK == $action) { // find the patches that have been performed on the previous deploy foreach ($performed_patches as $datetime => $applied_at) { if (($timestamp = strtotime($applied_at)) && $timestamp > $this->previous_timestamp && $timestamp <= $this->last_timestamp) { $patches_to_revert[$datetime] = $datetime; } } } } else { if (Deploy::UPDATE == $action) { // make a list of all patches that could be considered as already applied $patches_to_apply = $available_patches; } } // nothing needs to be done if (empty($patches_to_apply) && empty($patches_to_revert) && empty($patches_to_register_as_done)) { $this->logger->log('Database is up to date !'); return; } ksort($patches_to_apply, SORT_STRING); krsort($patches_to_revert, SORT_STRING); ksort($patches_to_register_as_done, SORT_STRING); // check if the files all contain SQL patches and filter out inactive patches $patches_to_apply = array_intersect($patches_to_apply, array_keys(Helper::checkFiles($this->basedir, $patches_to_apply, $this->patchOptions))); $patches_to_apply = $this->checkDependencies($patches_to_apply, $performed_patches); $patches_to_revert = $this->checkRevertDependencies($patches_to_revert, $dependencies); if (!empty($patches_to_revert)) { if (!empty($patches_to_revert)) { $this->logger->log('Database patches to revert (' . count($patches_to_revert) . '): ' . PHP_EOL . implode(PHP_EOL, array_keys($patches_to_revert))); if (count($patches_to_revert) > 1) { $choice = $this->local_shell->inputPrompt('Revert ? (Y/p/n): ', 'y', false, array('y', 'p', 'n')); } else { $choice = $this->local_shell->inputPrompt('Revert ? (Y/n): ', 'y', false, array('y', 'n')); } if ('y' == $choice) { $this->patches_to_revert += $patches_to_revert; } elseif ('p' == $choice) { list($chosen_patches_to_revert) = $this->pickPatches($patches_to_revert, array('y', 'n')); // if the hand-chosen list introduced dependency problems, prompt the user $checked_patches_to_revert = $this->checkRevertDependencies($chosen_patches_to_revert, $dependencies); if (count($checked_patches_to_revert) > 0 && count($checked_patches_to_revert) != count($chosen_patches_to_revert)) { if ('y' == $this->local_shell->inputPrompt('Are you sure ? (y/N): ', 'n', false, array('y', 'n'))) { $this->patches_to_revert += $checked_patches_to_revert; } } else { $this->patches_to_revert += $checked_patches_to_revert; } } } } if (!empty($patches_to_apply)) { if (!empty($patches_to_apply)) { $patches_list = 'Database patches to apply (' . count($patches_to_apply) . '): ' . PHP_EOL; foreach ($patches_to_apply as $patch_filename) { $patches_list .= $patch_filename; $patch_classname = Helper::getClassnameFromFilepath($patch_filename); /** @var AbstractSqlUpdate $patch */ $patch = new $patch_classname($this->patchOptions); if ($patch->getType() == SqlUpdateInterface::TYPE_LARGE) { $patches_list .= " [01;31m[Large][0m"; } $patches_list .= PHP_EOL; } $this->logger->log($patches_list); // only offer to register patches as done if the patches table exists if ($this->patches_table_exists) { if (count($patches_to_apply) > 1) { $choice = $this->local_shell->inputPrompt('[a]pply, [r]egister as done, [p]ick, [i]gnore (A/r/p/i): ', 'a', false, array('a', 'r', 'p', 'i')); } else { $choice = $this->local_shell->inputPrompt('[a]pply, [r]egister as done, [i]gnore (A/r/i): ', 'a', false, array('a', 'r', 'i')); } } else { if (count($patches_to_apply) > 1) { $choice = $this->local_shell->inputPrompt('[a]pply, [p]ick, [i]gnore (A/p/i): ', 'a', false, array('a', 'p', 'i')); } else { $choice = $this->local_shell->inputPrompt('[a]pply, [i]gnore (A/i): ', 'a', false, array('a', 'i')); } } if ('a' == $choice) { $this->patches_to_apply += $patches_to_apply; } elseif ('r' == $choice) { $this->patches_to_register_as_done += $patches_to_apply; } elseif ('p' == $choice) { list($picked_apply, $picked_register) = $this->pickPatches($patches_to_apply, array('a', 'r', 'i'), 'a'); // if the hand-chosen list introduced dependency problems, prompt the user $checked_patches_to_apply = $this->checkDependencies($picked_apply, $performed_patches + $picked_register); if (count($checked_patches_to_apply) > 0 && count($checked_patches_to_apply) != count($picked_apply)) { if ('y' == $this->local_shell->inputPrompt('Are you sure ? (y/N)', 'n', false, array('y', 'n'))) { $this->patches_to_apply += $picked_apply; $this->patches_to_register_as_done += $picked_register; } } else { $this->patches_to_apply += $checked_patches_to_apply; $this->patches_to_register_as_done += $picked_register; } } } } if (!empty($patches_to_register_as_done)) { $patches_to_register_as_done = $this->checkDependencies($patches_to_register_as_done, array_keys($this->patches_to_apply) + array_keys($performed_patches)); if (!empty($patches_to_register_as_done)) { $patches_list = 'Other patches found (' . count($patches_to_register_as_done) . '): ' . PHP_EOL; foreach ($patches_to_register_as_done as $patch_filename) { $patches_list .= $patch_filename; $patch_classname = Helper::getClassnameFromFilepath($patch_filename); /** @var AbstractSqlUpdate $patch */ $patch = new $patch_classname($this->patchOptions); if ($patch->getType() == SqlUpdateInterface::TYPE_LARGE) { $patches_list .= " [01;31m[Large][0m"; } $patches_list .= PHP_EOL; } $this->logger->log($patches_list); if (count($patches_to_register_as_done) > 1) { $choice = $this->local_shell->inputPrompt('[a]pply, [r]egister as done, [p]ick, [i]gnore (a/r/p/I): ', 'i', false, array('a', 'r', 'p', 'i')); } else { $choice = $this->local_shell->inputPrompt('[a]pply, [r]egister as done, [i]gnore (a/r/I): ', 'i', false, array('a', 'r', 'i')); } if ('a' == $choice) { $this->patches_to_apply += $patches_to_register_as_done; } elseif ('r' == $choice) { $this->patches_to_register_as_done += $patches_to_register_as_done; } elseif ('p' == $choice) { list($picked_apply, $picked_register) = $this->pickPatches($patches_to_register_as_done, array('a', 'r', 'i'), 'i'); $this->patches_to_apply += $picked_apply; $this->patches_to_register_as_done += $picked_register; } } } if (empty($this->patches_to_apply) && empty($this->patches_to_register_as_done) && empty($this->patches_to_revert)) { return; } $this->getDatabaseLogin(); }