Projekt

Obecné

Profil

Stáhnout (16.1 KB) Statistiky
| Větev: | Tag: | Revize:
<?php

/*
* This file is part of open source system FreenetIS
* and it is release under GPLv3 licence.
*
* More info about licence can be found:
* http://www.gnu.org/licenses/gpl-3.0.html
*
* More info about project can be found:
* http://www.freenetis.org/
*/

namespace freenetis\service\core;

use AbstractService;
use Config;
use Database;
use Log;
use Settings;
use Version;

/**
* Service that handles database initialization/upgrade procedures.
*
* @author Ondřej Fibich <ondrej.fibich@gmail.com>
* @since 1.2
*/
class DatabaseInitService extends AbstractService
{
/**
* Creates service.
*
* @param \ServiceFactory $factory
*/
public function __construct(\ServiceFactory $factory)
{
parent::__construct($factory);
}

/**
* DB midpoint constant for before life cycle callback fail.
*/
const DBU_MIDPOINT_BEFORE = -1;

/**
* DB midpoint constant for after life cycle callback fail.
* Must be higher than count of SQL queries in each DB upgrade.
*/
const DBU_MIDPOINT_AFTER = \PHP_INT_MAX;

/**
* Makes database to be up-to-day or skip if it already is. This method
* is synchronized using lock file which path is provided as parameter.
* If initialization or upgrade of database is performed than optional
* after function callback is called.
*
* @param string $lock_file_name Lock mutex file path
* @param Closure $after_callback Optional closure PHP callback function
* that is called after database upgrade is done (but synchronization
* lock is still on).
* @throws \InvalidArgumentException On not writable lock file
* @throws DowngrateDbUpgradeException On not allowed database downgrade.
* @throws OldMechanismDbUpgradeException On old upgrade mechanism
* @throws NotEnabledDbUpgradeException On not enabled upgrade
* @throws \Exception On any other error
*/
public function make($lock_file_name, $after_callback = NULL)
{
// change database encoding if incorect, TODO: collation by locale
$this->set_db_encoding('utf8', 'utf8_czech_ci');

// try to open mutex file that prevents for multiple running of DB
// upgrades by parallel init method calls.
if (($mutex_lock_file = @fopen($lock_file_name, 'w')) === FALSE)
{
throw new \InvalidArgumentException('lock file not writable: '
. $lock_file_name);
}

// acquire an exclusive access to file
// wait while database is being updated
if (flock($mutex_lock_file, LOCK_EX))
{
// first lock access - update db
// other lock access - skip (request accepted during upgrade)
if (!Version::is_db_up_to_date())
{
$this->execute_applicable_db_upgrades();
// callback
if (is_callable($after_callback))
{
try
{
$after_callback();
}
catch (\Exception $ex)
{
throw new \Exception('after callback failed', NULL, $ex);
}
}
}

// unlock mutex file
flock($mutex_lock_file, LOCK_UN);
}

// close mutex file
fclose($mutex_lock_file);
}

/**
* Set DB encoding and collation by arguments if not already in these values.
*
* @param string $encoding
* @param string $collation
* @throws \Exception on set error
*/
public function set_db_encoding($encoding, $collation)
{
$enc = mb_strtolower($encoding);
$coll = mb_strtolower($collation);
try
{
$db = Database::instance();

if ($db->get_variable_value('character_set_database') != $enc ||
$db->get_variable_value('collation_database') != $coll)
{
$db->alter_db_character_set(Config::get('db_name'), $enc, $coll);
}
}
catch (\Exception $e)
{
Log::add_exception($e);
$m = __('Cannot set database character set to %s@%s',
array($encoding, $collation));
throw new \Exception($m, NULL, $e);
}
}

/**
* Init/upgrade database by execution of applicable database upgrades.
* Appplicable upgrades are all available upgrades that versions are higner
* than the current database version.
*
* Upgrades files are located at /db_upgrades directory. Each upgrade has
* a name that consist of 'upgrade' and version (similar to version in
* /version.php). Each file contains whole definitions that are required
* for performing the upgrade (life cycle callback, SQL queries, etc.).
*
* @throws DowngrateDbUpgradeException On not allowed database downgrade.
* (e.g. FN: 1.0.0 - DB: 1.1.0)
* @throws OldMechanismDbUpgradeException On old upgrade mechanism
* @throws NotEnabledDbUpgradeException On not enabled upgrade between DB
* versions
* @throws \Exception On any other error
*/
private function execute_applicable_db_upgrades()
{
// detect downgrade (not on invalid DB verion - possibility of upgrade
// from old system)
if (Version::is_valid_version(Version::get_db_version()) &&
Version::fn_version_compare() < 0)
{
throw new DowngrateDbUpgradeException();
}

// get all available DB update versions from files in /db_upgrades
$versions = $this->scan_applicable_db_upgrades('db_upgrades');

// sort files according to version (we eant to make DB upgrades in order)
usort($versions, 'Version::compare');

// make upgrades
if (!empty($versions))
{
// check if old style is in use
if (is_numeric(Version::get_db_version()) &&
Version::get_db_version() > 0)
{
// not possible, inform user
throw new OldMechanismDbUpgradeException();
}
// execute each DB upgrade
foreach ($versions as $version)
{
$this->execute_db_upgrade($version);
}
}

// set current version (optimalization if version has no DB update)
Settings::set('db_schema_version', Version::get_version());
}

/**
* Scan directory with given name for upgrade DB files and returns
* all applicable DB upgrade versions. Applicable upgrade means that
* its version is higher than the current database version.
*
* @param string $scan_directory directory for scanning name
* @return array list of applicable DB upgrade versions
*/
private function scan_applicable_db_upgrades($scan_directory)
{
// array of available verisons
$versions = array();
// gets all files in scan directory dir
$files = \scandir($scan_directory);
// regex for file: upgrade_VERSION.php
$regex = '^upgrade_' . rtrim(ltrim(Version::VERSION_REGEX, '^'), '$')
. '\.php$';
// filter files
foreach ($files as $file)
{
$matches = array();
// remove invalid files (wrong name) and value replace by version
if (!mb_eregi($regex, $file, $matches))
{
continue;
}
// get version
$version = $matches[1];
// remove old already installed upgrades and future upgrades
if (Version::compare($version, Version::get_version()) > 0 || (
!is_numeric(Version::get_db_version()) &&
Version::compare($version, Version::get_db_version(FALSE)) <= 0
))
{
continue;
}
// add to available versions
$versions[] = $version;
}

return $versions;
}

/**
* Loads DB upgrade files and executes DB upgrade by its definitions
* in following order:
*
* 1) Load DB upgrade file
* 2) Check if upgrade is not disabled by "upgrade_enabled_only_from"
* 3) Skip upgrade if it is equivalent to previous version that
* may be defined by "upgrade_equal_to"
* 4) Execute before life cycle callback if it exists
* 5) Executes SQL upgrade queries
* 6) Execute fter life cycle callback if it exists
* 7) Set new DB version
*
* If any error occures in 5, 6 or 7 than error position in upgrade
* is marked using DB upgrade midpoint that prevents from reexecuting
* of already executed upgrade parts during another upgrade attempt.
*
* @param string $version upgrade version
* @return boolean was executed or just skiped because it was equivalent
* to previous DB version
* @throws NotEnabledDbUpgradeException if upgrade restricted via
* $upgrade_enabled_only_from take affect
* @throws \Exception if DB upgrade fails
*/
private function execute_db_upgrade($version)
{
Log::add('debug', 'Starting upgrade ' . $version);
// include upgrade file
require 'db_upgrades/upgrade_' . $version . '.php';

// check if the upgrade is allowed from the current DB version
if (isset($upgrade_enabled_only_from[$version]) &&
!Version::is_db_version_in($upgrade_enabled_only_from[$version]))
{
throw new NotEnabledDbUpgradeException(__(
'Database upgrade %s not allowed from version %s',
array($version, Version::get_db_version())
));
}

// check if the upgrade is equivalent to the current DB version
if (isset($upgrade_equal_to[$version]) &&
Version::is_db_version_in($upgrade_equal_to[$version]))
{
Log::add('debug', 'Upgrade ' . $version . ' skipping ('
. Version::get_db_version() . ')');
// it is => so skip it
$this->set_db_version($version);
return FALSE; // exit
}

// get upgrade midpoint (partial upgrade pointer)
$midpoint = $this->get_db_upgrade_midpoint();

// make update
try
{
// upgrade function before
if (!is_numeric($midpoint) ||
$midpoint == self::DBU_MIDPOINT_BEFORE)
{
try
{
$this->call_db_upgrade_method($version, 'before');
}
catch (\Exception $ex)
{
$this->set_db_upgrade_midpoint(self::DBU_MIDPOINT_BEFORE);
throw $ex;
}
$midpoint = NULL;
}

// upgrade SQL
if (isset($upgrade_sql[$version]))
{
$from_index = is_numeric($midpoint) ? intval($midpoint) : 0;
$this->execute_sql_queries($upgrade_sql[$version], $from_index);
}

// upgrade function after
try
{
$this->call_db_upgrade_method($version, 'after');
}
catch (\Exception $ex)
{
$this->set_db_upgrade_midpoint(self::DBU_MIDPOINT_AFTER);
throw $ex;
}

// set up db schema
$this->set_db_version($version);

Log::add('debug', 'Upgrade ' . $version . ' complete');
}
catch (\Exception $e)
{
$message = 'Upgrade DB: ' . $version . ' failed<br /><br />'
. 'Cause: ' . $e->getMessage();
throw new \Exception($message, NULL, $e);
}

return TRUE;
}

/**
* Set DB FreenetIS version and clean DB upgrade midpoint.
*
* @param string $version new DB version
*/
private function set_db_version($version)
{
Settings::set('db_schema_version', $version);
$this->clear_db_upgrade_midpoint();
}

/**
* Get DB upgrade midpoint which defines from which point failed database
* upgrade should be started (in order to not make same upgrade parts
* again).
*
* @param mixed $value
*/
private function get_db_upgrade_midpoint()
{
return Settings::get('upgrade_midpoint_error', FALSE);
}

/**
* Set DB upgrade midpoint to given value that should be numeric.
*
* Value equal to DBU_MIDPOINT_BEFORE means midpoint on: before life cycle
* Value equal to DBU_MIDPOINT_AFTER means midpoint on: after life cycle
* Values between previous two values means that midpoint is index to
* uprade queries.
*
* @param mixed $value
*/
private function set_db_upgrade_midpoint($value)
{
Settings::set('upgrade_midpoint_error', $value);
}

/**
* Clears DB upgrade midpoint to value whe it do not prevent from execution
* of all upgrade.
*/
private function clear_db_upgrade_midpoint()
{
$this->set_db_upgrade_midpoint('');
}

/**
* Calls upgrade life cycle before or update method for upgrade with
* given version if callback function is defined.
*
* DB upgrade midpoint is cleared after sucessfull call.
*
* @param string $version
* @param string $phase life cycle phase (before or after)
* @return boolean callback exists and was called?
* @throws \Exception on callback fail (returns FALSE or throw any Exception)
*/
private function call_db_upgrade_method($version, $phase) {
// check phase
if (!in_array($phase, array('before', 'after')))
{
throw new \InvalidArgumentException('invalid phase: ' . $phase);
}
// transform version to version which may be used at PHP functions
$f_version = str_replace(array('~', '.'), array('_', '_'), $version);
// function name
$function_name = 'upgrade_' . $f_version . '_' . $phase;
// call function only if it exists
if (!function_exists($function_name))
{
return FALSE;
}
// log action
Log::add('debug', 'Upgrade ' . $version . ' [' . $phase . '] trigger');
// call
try {
$result = call_user_func($function_name);
}
catch (\Exception $e)
{
throw new \Exception($function_name . ' throwed exception', NULL, $e);
}
if (!$result)
{
throw new \Exception($function_name . ' call failed');
}
// call may be invoked after last error
$this->clear_db_upgrade_midpoint();
// called sucessfully
return TRUE;
}

/**
* Executes given array of queries that has index higher than value
* of from argument.
*
* If an exception is thrown fromm method than DB upgrade midpoint is also
* set to query index on which the error occured.
*
* @param array $queries array of SQL queries
* @param integer $from start index for queries array [optional: default 0]
* @throws \Exception on SQL query execition fail
*/
private function execute_sql_queries($queries, $from = 0)
{
$query_index = intval($from);
// each item of array (SQL)
for (; $query_index < count($queries); $query_index++)
{
$query = $queries[$query_index];
try
{
Log::add('debug', 'Upgrade SQL command [' . $query_index
. ']: ' . $query);
Database::instance()->query($query);
}
catch (\Exception $ex)
{
$this->set_db_upgrade_midpoint($query_index);
throw new \Exception('SQL query failed: ' . $query, NULL, $ex);
}
}
}

}

/**
* Exception that reflects state of not allowed downgration of database.
*/
class DowngrateDbUpgradeException extends \Exception
{
}

/**
* Exception that reflects state of old mechanism for updating of the database
* structure that cannot be automatically turned to new mechanism.
*/
class OldMechanismDbUpgradeException extends \Exception
{
}

/**
* Exception that reflects state of not enabled upgrade by using field
* upgrade_enabled_only_from in the current database upgrade.
*
* It occures when a new upgrade should be performed, but current version
* is not listed in upgrade_enabled_only_from.
*/
class NotEnabledDbUpgradeException extends \Exception
{
}
(2-2/4)