Passed
Push — master ( dd24f6...e23e40 )
by Mr
02:33
created

ConfigProvider::__invoke()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 5
ccs 0
cts 4
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
/**
3
 * This file is part of the daikon-cqrs/config project.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
namespace Daikon\Config;
10
11
use Daikon\Interop\Assertion;
12
13
final class ConfigProvider implements ConfigProviderInterface
14
{
15
    private const INTERPOLATION_PATTERN = '/(\$\{(.*?)\})/';
16
17
    private array $config;
18
19
    private ConfigProviderParamsInterface $params;
20
21
    private array $paramInterpolations;
22
23 3
    public function __construct(ConfigProviderParamsInterface $params)
24
    {
25 3
        $this->params = $params;
26 3
        $this->config = [];
27 3
        $this->paramInterpolations = [];
28 3
    }
29
30 7
    public function get(string $path, $default = null)
31
    {
32 7
        $path = ConfigPath::fromString($path);
33 7
        $scope = $path->getScope();
34 7
        Assertion::keyNotExists(
35 7
            $this->paramInterpolations,
36
            $scope,
37
            "Recursive interpolations are not allowed when interpolating 'locations' or 'sources'. ".
38 7
            "Trying to recurse into scope '$scope'"
39
        );
40
41 7
        if (!isset($this->config[$scope]) && $this->params->hasScope($scope)) {
42 7
            $this->config[$scope] = $this->loadScope($scope);
43 5
        } elseif (!isset($this->config[$scope])) {
44 2
            return $default;
45
        }
46
47 6
        return $this->evaluatePath(
48 6
            $path->getParts(),
49 6
            $this->config[$path->getScope()],
50 6
            $path->getSeparator()
51 6
        ) ?? $default;
52
    }
53
54 1
    public function has(string $path): bool
55
    {
56 1
        return $this->get($path) !== null;
57
    }
58
59
60
    public function __invoke(string $path, $default = null)
61
    {
62
        $value = $this->get($path, $default);
63
        Assertion::allNotNull([$value, $default], "Missing required config value at path '$path'");
64
        return $value;
65
    }
66
67 7
    private function loadScope(string $scope): array
68
    {
69 7
        $this->paramInterpolations[$scope] = true;
70 7
        $locations = $this->params->getLocations($scope);
71 7
        $sources = $this->params->getSources($scope);
72 7
        $loader = $this->params->getLoader($scope);
73 7
        if (!$loader instanceof ArrayConfigLoader) {
74 2
            $sources = $this->interpolateConfigValues($sources);
75
        }
76 6
        $locations = $this->interpolateConfigValues($locations);
77 6
        unset($this->paramInterpolations[$scope]);
78
79 6
        $this->config[$scope] = $loader->load($locations, $sources);
80 6
        return $this->interpolateConfigValues($this->config[$scope]);
81
    }
82
83
    /** @return mixed */
84 6
    private function evaluatePath(array $parts, array $values, string $separator)
85
    {
86 6
        if (empty($values)) {
87 1
            return null;
88
        }
89 6
        $pos = 0;
90 6
        $length = count($parts);
91 6
        $value = &$values;
92 6
        while (!empty($parts)) {
93 6
            $pos++;
94 6
            $part = array_shift($parts);
95 6
            Assertion::isArray(
96 6
                $value,
97 6
                sprintf("Trying to traverse non-array value with path part '%s'", join($separator, $parts))
98
            );
99 6
            if ($part === ConfigPathInterface::WILDCARD_TOKEN) {
100 1
                return $this->expandWildcard($parts, $value, $separator);
101 6
            } elseif (!isset($value[$part]) && $pos === $length) {
102 1
                return null;
103 6
            } elseif (!isset($value[$part])) {
104 2
                array_unshift($parts, $part.$separator.array_shift($parts));
105 2
                continue;
106
            }
107 6
            $value = &$value[$part];
108
        }
109 6
        return $value;
110
    }
111
112 1
    private function expandWildcard(array $parts, array $context, string $separator): array
113
    {
114 1
        return array_merge(...array_reduce(
115 1
            $context,
116
            function (array $collected, array $ctx) use ($parts, $separator): array {
117 1
                $expandedValue = $this->evaluatePath($parts, $ctx, $separator);
118 1
                if (!is_null($expandedValue)) {
119 1
                    $collected[] =  (array)$expandedValue;
120
                }
121 1
                return $collected;
122 1
            },
123 1
            []
124
        ));
125
    }
126
127 7
    private function interpolateConfigValues(array $config): array
128
    {
129 7
        return array_map([$this, 'mapInterpolation'], $config);
130
    }
131
132
    /**
133
     * @param mixed $value
134
     * @return mixed
135
     */
136 7
    private function mapInterpolation($value)
137
    {
138 7
        if (is_array($value)) {
139 6
            return $this->interpolateConfigValues($value);
140 7
        } elseif (is_string($value) && preg_match_all(self::INTERPOLATION_PATTERN, $value, $matches)) {
141 3
            return $this->interpolateConfigValue($value, $matches[0], $matches[2]);
142
        }
143 6
        return $value;
144
    }
145
146
    /** @return mixed */
147 3
    private function interpolateConfigValue(string $value, array $valueParts, array $interpolations)
148
    {
149 3
        $interpolatedValues = array_map([$this, 'get'], $interpolations);
150 2
        return array_filter($interpolatedValues, 'is_string') === $interpolatedValues
151 2
            ? str_replace($valueParts, $interpolatedValues, $value)
152 2
            : $interpolatedValues[0];
153
    }
154
}
155