Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.65% covered (success)
92.65%
63 / 68
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
YamlReader
92.65% covered (success)
92.65%
63 / 68
40.00% covered (danger)
40.00%
2 / 5
31.38
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
7.01
 getFromPaths
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
 parseYaml
90.00% covered (success)
90.00%
27 / 30
0.00% covered (danger)
0.00%
0 / 1
12.14
 getArrayElemRefByPath
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
1<?php
2
3/**
4 * VuFind YAML Configuration Reader
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
9 * Copyright (C) The National Library of Finland 2022.
10 *
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License version 2,
13 * as published by the Free Software Foundation.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
23 *
24 * @category VuFind
25 * @package  Config
26 * @author   Demian Katz <demian.katz@villanova.edu>
27 * @author   Ere Maijala <ere.maijala@helsinki.fi>
28 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
29 * @link     https://vufind.org Main Site
30 */
31
32namespace VuFind\Config;
33
34use Symfony\Component\Yaml\Yaml;
35
36use function array_key_exists;
37use function dirname;
38use function is_array;
39
40/**
41 * VuFind YAML Configuration Reader
42 *
43 * @category VuFind
44 * @package  Config
45 * @author   Demian Katz <demian.katz@villanova.edu>
46 * @author   Ere Maijala <ere.maijala@helsinki.fi>
47 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
48 * @link     https://vufind.org Main Site
49 */
50class YamlReader
51{
52    use \VuFind\Feature\MergeRecursiveTrait;
53
54    /**
55     * Cache directory name
56     *
57     * @var string
58     */
59    protected $cacheName = 'yaml';
60
61    /**
62     * Cache manager
63     *
64     * @var \VuFind\Cache\Manager
65     */
66    protected $cacheManager;
67
68    /**
69     * Config file path resolver
70     *
71     * @var PathResolver
72     */
73    protected $pathResolver;
74
75    /**
76     * Cache of loaded files.
77     *
78     * @var array
79     */
80    protected $files = [];
81
82    /**
83     * Constructor
84     *
85     * @param \VuFind\Cache\Manager $cacheManager Cache manager (optional)
86     * @param PathResolver          $pathResolver Config file path resolver
87     * (optional; defaults to \VuFind\Config\Locator)
88     */
89    public function __construct(
90        \VuFind\Cache\Manager $cacheManager = null,
91        PathResolver $pathResolver = null
92    ) {
93        $this->cacheManager = $cacheManager;
94        $this->pathResolver = $pathResolver;
95    }
96
97    /**
98     * Return a configuration
99     *
100     * @param string  $filename       Config file name
101     * @param boolean $useLocalConfig Use local configuration if available
102     * @param boolean $forceReload    Reload even if config has been internally
103     * cached in the class.
104     *
105     * @return array
106     */
107    public function get($filename, $useLocalConfig = true, $forceReload = false)
108    {
109        // Load data if it is not already in the object's cache (note that, because
110        // the disk-based cache is keyed based on modification time, we don't need
111        // to pass $forceReload down another level to load an updated file if
112        // something has changed -- it's enough to force a cache recheck).
113        if ($forceReload || !isset($this->files[$filename])) {
114            if ($this->pathResolver) {
115                $localConfigPath = $useLocalConfig
116                    ? $this->pathResolver->getLocalConfigPath($filename)
117                    : null;
118            } else {
119                $localConfigPath = $useLocalConfig
120                    ? Locator::getLocalConfigPath($filename)
121                    : null;
122            }
123            $baseConfigPath = $this->pathResolver
124                ? $this->pathResolver->getBaseConfigPath($filename)
125                : Locator::getBaseConfigPath($filename);
126            $this->files[$filename] = $this->getFromPaths(
127                $baseConfigPath,
128                $localConfigPath
129            );
130        }
131
132        return $this->files[$filename];
133    }
134
135    /**
136     * Given core and local filenames, retrieve the configuration data.
137     *
138     * @param string $defaultFile Full path to file containing default YAML
139     * @param string $customFile  Full path to file containing local customizations
140     * (may be null if no local file exists).
141     *
142     * @return array
143     */
144    protected function getFromPaths($defaultFile, $customFile = null)
145    {
146        // Connect to the cache:
147        $cache = (null !== $this->cacheManager)
148            ? $this->cacheManager->getCache($this->cacheName) : false;
149
150        // Generate cache key:
151        $cacheKey = $defaultFile . '-'
152            . (file_exists($defaultFile) ? filemtime($defaultFile) : 0);
153        if (!empty($customFile)) {
154            $cacheKey .= '-local-' . filemtime($customFile);
155        }
156        $cacheKey = md5($cacheKey);
157
158        // Generate data if not found in cache:
159        if ($cache === false || !($results = $cache->getItem($cacheKey))) {
160            $results = $this->parseYaml($customFile, $defaultFile);
161            if ($cache !== false) {
162                $cache->setItem($cacheKey, $results);
163            }
164        }
165
166        return $results;
167    }
168
169    /**
170     * Process a YAML file (and its parent, if necessary).
171     *
172     * @param string $file          YAML file to load (will evaluate to null
173     * if file does not exist).
174     * @param string $defaultParent Parent YAML file from which $file should
175     * inherit (unless overridden by a specific directive in $file). None by
176     * default.
177     *
178     * @return array
179     */
180    protected function parseYaml($file, $defaultParent = null)
181    {
182        // First load current file:
183        $results = (!empty($file) && file_exists($file))
184            ? Yaml::parse(file_get_contents($file)) : [];
185
186        // Override default parent with explicitly-defined parent, if present:
187        if (isset($results['@parent_yaml'])) {
188            // First try parent as absolute path, then as relative:
189            $defaultParent = file_exists($results['@parent_yaml'])
190                ? $results['@parent_yaml']
191                : dirname($file) . '/' . $results['@parent_yaml'];
192            if (!file_exists($defaultParent)) {
193                $defaultParent = null;
194                error_log('Cannot find parent file: ' . $results['@parent_yaml']);
195            }
196            // Swallow the directive after processing it:
197            unset($results['@parent_yaml']);
198        }
199        // Check for sections to merge instead of overriding:
200        $mergedSections = [];
201        if (isset($results['@merge_sections'])) {
202            $mergedSections = $results['@merge_sections'];
203            // Swallow the directive after processing it:
204            unset($results['@merge_sections']);
205        }
206
207        // Now load in merged or missing sections from parent, if applicable:
208        if (null !== $defaultParent) {
209            $parentSections = $this->parseYaml($defaultParent);
210            // Process merged sections:
211            foreach ($mergedSections as $path) {
212                $parentElem
213                    = $this->getArrayElemRefByPath($parentSections, $path);
214                if (is_array($parentElem)) {
215                    $resultElemRef
216                        = &$this->getArrayElemRefByPath($results, $path, true);
217                    $resultElemRef
218                        = $this->mergeRecursive($parentElem, $resultElemRef);
219                    unset($parentElem);
220                    unset($resultElemRef);
221                }
222            }
223            // Add missing sections:
224            foreach ($parentSections as $section => $contents) {
225                if (!isset($results[$section])) {
226                    $results[$section] = $contents;
227                }
228            }
229        }
230
231        return $results;
232    }
233
234    /**
235     * Return array element reference by path
236     *
237     * @param array $arr    Array to access
238     * @param array $path   Path to retrieve
239     * @param bool  $create Whether to create the path if it doesn't exist. Default
240     * is false.
241     *
242     * @return mixed
243     */
244    protected function &getArrayElemRefByPath(
245        array &$arr,
246        array $path,
247        bool $create = false
248    ) {
249        $result = &$arr;
250        foreach ($path as $pathPart) {
251            if (!array_key_exists($pathPart, $result)) {
252                if (!$create) {
253                    return null;
254                }
255                $result[$pathPart] = [];
256            }
257            $result = &$result[$pathPart];
258        }
259        return $result;
260    }
261}