Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiosmosis <benaka@piwik.pro>2015-02-27 07:19:06 +0300
committerdiosmosis <benaka@piwik.pro>2015-03-02 00:34:43 +0300
commit1ee16073ae884711f01ef6dcdc2f715885fb7e47 (patch)
treeb02f27fbbd975328c3b55e6aa46727d6f382c68d /core/Config
parentd556d333e4fcfccfbe9298c778670f548bd56862 (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.php298
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