Beispiel #1
0
 public function key()
 {
     /** @var \DirectoryIterator $this */
     return Helper::getClassnameFromFilepath($this->getFilename());
 }
Beispiel #2
0
 /**
  * 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;
 }
Beispiel #3
0
 /**
  * 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.");
         }
     }
 }
Beispiel #4
0
 /**
  * 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 .= " [Large]";
                 }
                 $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 .= " [Large]";
                 }
                 $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();
 }