diff options
author | diosmosis <benaka@piwik.pro> | 2015-02-27 07:19:06 +0300 |
---|---|---|
committer | diosmosis <benaka@piwik.pro> | 2015-03-02 00:34:43 +0300 |
commit | 1ee16073ae884711f01ef6dcdc2f715885fb7e47 (patch) | |
tree | b02f27fbbd975328c3b55e6aa46727d6f382c68d /core/Config | |
parent | d556d333e4fcfccfbe9298c778670f548bd56862 (diff) |
Extract INI file merging logic in Config class and move to new IniFileChain class. This is an intermediate step in allowing Config/Plugin\Manager to exist in DI.
Diffstat (limited to 'core/Config')
-rw-r--r-- | core/Config/IniFileChain.php | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/core/Config/IniFileChain.php b/core/Config/IniFileChain.php new file mode 100644 index 0000000000..014cc0c9fb --- /dev/null +++ b/core/Config/IniFileChain.php @@ -0,0 +1,298 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +namespace Piwik\Config; + +use Piwik\Ini\IniReader; +use Piwik\Ini\IniWriter; + +/** + * Manages a list of INI files where the settings in each INI file merge with or override the + * settings in the previous INI file. + * + * The IniFileChain class manages two types of INI files: multiple default setting files and one + * user settings file. + * + * The default setting files (for example, global.ini.php & common.ini.php) hold the default setting values. + * The settings in these files are merged recursively, so array settings in one file will add elements to + * the same settings in the previous file. Default settings files cannot be modified through the IniFileChain + * class. + * + * The user settings file (for example, config.ini.php) holds the actual setting values. Settings in the + * user settings files overwrite other settings. So array settings will not merge w/ previous values. + */ +class IniFileChain +{ + /** + * Maps INI file names with their parsed contents. The order of the files signifies the order + * in the chain. Files with lower index are overwritten/merged with files w/ a higher index. + * + * @var array + */ + protected $settingsChain = array(); + + /** + * The merged INI settings. + * + * @var array + */ + protected $mergedSettings = array(); + + /** + * Constructor. + * + * @param string[] $defaultSettingsFiles The list of paths to INI files w/ the default setting values. + * @param string $userSettingsFile The path to the user settings file. + */ + public function __construct($defaultSettingsFiles, $userSettingsFile) + { + foreach ($defaultSettingsFiles as $file) { + $this->settingsChain[$file] = null; + } + $this->settingsChain[$userSettingsFile] = null; + + $this->reload(); + } + + /** + * Return setting value by reference. + * + * @param string $name + * @return mixed + */ + public function &get($name) + { + $result = array(); + if (isset($this->mergedSettings[$name])) { + $result =& $this->mergedSettings[$name]; + } + return $result; + } + + /** + * Sets a setting value. + * + * @param string $name + * @param mixed $value + */ + public function set($name, $value) + { + $this->mergedSettings[$name] = $value; + } + + /** + * Saves the current in-memory setting values to the specified file. + * + * @param string $toPath The path to save the contents to. + * @param string $header The header of the output INI file. + */ + public function dump($toPath, $header = '') + { + $writer = new IniWriter(); + $writer->writeToFile($toPath, $this->mergedSettings, $header); + } + + /** + * Writes the difference of the in-memory setting values and the on-disk user settings file setting + * values to a string in INI format, and returns it. + * + * If a config section is identical to the default settings section (as computed by merging + * all default setting files), it is not written to the user settings file. + * + * @param string $header The header of the INI output. + * @return string The dumped INI contents. + */ + public function dumpChanges($header = '') + { + $userSettingsFile = $this->getUserSettingsFile(); + + $defaultSettings = $this->getMergedDefaultSettings(); + $existingMutableSettings = $this->settingsChain[$userSettingsFile]; + + $dirty = false; + + $configToWrite = array(); + foreach ($this->mergedSettings as $sectionName => $changedSection) { + $existingMutableSection = @$existingMutableSettings[$sectionName] ?: array(); + + // remove default values from both (they should not get written to local) + if (isset($defaultSettings[$sectionName])) { + $changedSection = $this->array_unmerge($defaultSettings[$sectionName], $changedSection); + $existingMutableSection = $this->array_unmerge($defaultSettings[$sectionName], $existingMutableSection); + } + + // if either local/config have non-default values and the other doesn't, + // OR both have values, but different values, we must write to config.ini.php + if (empty($changedSection) xor empty($existingMutableSection) + || (!empty($changedSection) + && !empty($existingMutableSection) + && self::compareElements($changedSection, $existingMutableSection)) + ) { + $dirty = true; + } + + $configToWrite[$sectionName] = $changedSection; + } + + if ($dirty) { + @ksort($configToWrite); + + $writer = new IniWriter(); + return $writer->writeToString($configToWrite, $header); + } else { + return null; + } + } + + /** + * Reloads settings from disk. + */ + public function reload() + { + $reader = new IniReader(); + + foreach ($this->settingsChain as $file => $ignore) { + if (is_readable($file)) { + $this->settingsChain[$file] = $reader->readFile($file); + } + } + + $this->mergedSettings = $this->mergeFileSettings(); + } + + protected function mergeFileSettings() + { + $mergedSettings = $this->getMergedDefaultSettings(); + + $userSettings = end($this->settingsChain) ?: array(); + foreach ($userSettings as $sectionName => $section) { + if (!isset($mergedSettings[$sectionName])) { + $mergedSettings[$sectionName] = $section; + } else { + // the last user settings file completely overwrites INI sections. the other files in the chain + // can add to array options + $mergedSettings[$sectionName] = array_merge($mergedSettings[$sectionName], $section); + } + } + + return $mergedSettings; + } + + protected function getMergedDefaultSettings() + { + $userSettingsFile = $this->getUserSettingsFile(); + + $mergedSettings = array(); + foreach ($this->settingsChain as $file => $settings) { + if ($file == $userSettingsFile + || empty($settings) + ) { + continue; + } + + foreach ($settings as $sectionName => $section) { + if (!isset($mergedSettings[$sectionName])) { + $mergedSettings[$sectionName] = $section; + } else { + $mergedSettings[$sectionName] = $this->array_merge_recursive_distinct($mergedSettings[$sectionName], $section); + } + } + } + return $mergedSettings; + } + + protected function getUserSettingsFile() + { + // the user settings file is the last key in $settingsChain + end($this->settingsChain); + return key($this->settingsChain); + } + + /** + * Comparison function + * + * @param mixed $elem1 + * @param mixed $elem2 + * @return int; + */ + public static function compareElements($elem1, $elem2) + { + if (is_array($elem1)) { + if (is_array($elem2)) { + return strcmp(serialize($elem1), serialize($elem2)); + } + + return 1; + } + + if (is_array($elem2)) { + return -1; + } + + if ((string)$elem1 === (string)$elem2) { + return 0; + } + + return ((string)$elem1 > (string)$elem2) ? 1 : -1; + } + + /** + * Compare arrays and return difference, such that: + * + * $modified = array_merge($original, $difference); + * + * @param array $original original array + * @param array $modified modified array + * @return array differences between original and modified + */ + public function array_unmerge($original, $modified) + { + // return key/value pairs for keys in $modified but not in $original + // return key/value pairs for keys in both $modified and $original, but values differ + // ignore keys that are in $original but not in $modified + + return array_udiff_assoc($modified, $original, array(__CLASS__, 'compareElements')); + } + + /** + * array_merge_recursive does indeed merge arrays, but it converts values with duplicate + * keys to arrays rather than overwriting the value in the first array with the duplicate + * value in the second array, as array_merge does. I.e., with array_merge_recursive, + * this happens (documented behavior): + * + * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value')); + * => array('key' => array('org value', 'new value')); + * + * array_merge_recursive_distinct does not change the datatypes of the values in the arrays. + * Matching keys' values in the second array overwrite those in the first array, as is the + * case with array_merge, i.e.: + * + * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value')); + * => array('key' => array('new value')); + * + * Parameters are passed by reference, though only for performance reasons. They're not + * altered by this function. + * + * @param array $array1 + * @param array $array2 + * @return array + * @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk> + * @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com> + */ + private function array_merge_recursive_distinct ( array &$array1, array &$array2 ) + { + $merged = $array1; + foreach ( $array2 as $key => &$value ) { + if ( is_array ( $value ) && isset ( $merged [$key] ) && is_array ( $merged [$key] ) ) { + $merged [$key] = $this->array_merge_recursive_distinct ( $merged [$key], $value ); + } else { + $merged [$key] = $value; + } + } + return $merged; + } +}
\ No newline at end of file |