Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
declare(strict_types=1);
4
 
5
/*
6
 * This file is part of the league/config package.
7
 *
8
 * (c) Colin O'Dell <colinodell@gmail.com>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
 
14
namespace League\Config;
15
 
16
use Dflydev\DotAccessData\Data;
17
use Dflydev\DotAccessData\DataInterface;
18
use Dflydev\DotAccessData\Exception\DataException;
19
use Dflydev\DotAccessData\Exception\InvalidPathException;
20
use Dflydev\DotAccessData\Exception\MissingPathException;
21
use League\Config\Exception\UnknownOptionException;
22
use League\Config\Exception\ValidationException;
23
use Nette\Schema\Expect;
24
use Nette\Schema\Processor;
25
use Nette\Schema\Schema;
26
use Nette\Schema\ValidationException as NetteValidationException;
27
 
28
final class Configuration implements ConfigurationBuilderInterface, ConfigurationInterface
29
{
30
    /** @psalm-readonly */
31
    private Data $userConfig;
32
 
33
    /**
34
     * @var array<string, Schema>
35
     *
36
     * @psalm-allow-private-mutation
37
     */
38
    private array $configSchemas = [];
39
 
40
    /** @psalm-allow-private-mutation */
41
    private Data $finalConfig;
42
 
43
    /**
44
     * @var array<string, mixed>
45
     *
46
     * @psalm-allow-private-mutation
47
     */
48
    private array $cache = [];
49
 
50
    /** @psalm-readonly */
51
    private ConfigurationInterface $reader;
52
 
53
    /**
54
     * @param array<string, Schema> $baseSchemas
55
     */
56
    public function __construct(array $baseSchemas = [])
57
    {
58
        $this->configSchemas = $baseSchemas;
59
        $this->userConfig    = new Data();
60
        $this->finalConfig   = new Data();
61
 
62
        $this->reader = new ReadOnlyConfiguration($this);
63
    }
64
 
65
    /**
66
     * Registers a new configuration schema at the given top-level key
67
     *
68
     * @psalm-allow-private-mutation
69
     */
70
    public function addSchema(string $key, Schema $schema): void
71
    {
72
        $this->invalidate();
73
 
74
        $this->configSchemas[$key] = $schema;
75
    }
76
 
77
    /**
78
     * {@inheritDoc}
79
     *
80
     * @psalm-allow-private-mutation
81
     */
82
    public function merge(array $config = []): void
83
    {
84
        $this->invalidate();
85
 
86
        $this->userConfig->import($config, DataInterface::REPLACE);
87
    }
88
 
89
    /**
90
     * {@inheritDoc}
91
     *
92
     * @psalm-allow-private-mutation
93
     */
94
    public function set(string $key, $value): void
95
    {
96
        $this->invalidate();
97
 
98
        try {
99
            $this->userConfig->set($key, $value);
100
        } catch (DataException $ex) {
101
            throw new UnknownOptionException($ex->getMessage(), $key, (int) $ex->getCode(), $ex);
102
        }
103
    }
104
 
105
    /**
106
     * {@inheritDoc}
107
     *
108
     * @psalm-external-mutation-free
109
     */
110
    public function get(string $key)
111
    {
112
        if (\array_key_exists($key, $this->cache)) {
113
            return $this->cache[$key];
114
        }
115
 
116
        try {
117
            $this->build(self::getTopLevelKey($key));
118
 
119
            return $this->cache[$key] = $this->finalConfig->get($key);
120
        } catch (InvalidPathException | MissingPathException $ex) {
121
            throw new UnknownOptionException($ex->getMessage(), $key, (int) $ex->getCode(), $ex);
122
        }
123
    }
124
 
125
    /**
126
     * {@inheritDoc}
127
     *
128
     * @psalm-external-mutation-free
129
     */
130
    public function exists(string $key): bool
131
    {
132
        if (\array_key_exists($key, $this->cache)) {
133
            return true;
134
        }
135
 
136
        try {
137
            $this->build(self::getTopLevelKey($key));
138
 
139
            return $this->finalConfig->has($key);
140
        } catch (InvalidPathException | UnknownOptionException $ex) {
141
            return false;
142
        }
143
    }
144
 
145
    /**
146
     * @psalm-mutation-free
147
     */
148
    public function reader(): ConfigurationInterface
149
    {
150
        return $this->reader;
151
    }
152
 
153
    /**
154
     * @psalm-external-mutation-free
155
     */
156
    private function invalidate(): void
157
    {
158
        $this->cache       = [];
159
        $this->finalConfig = new Data();
160
    }
161
 
162
    /**
163
     * Applies the schema against the configuration to return the final configuration
164
     *
165
     * @throws ValidationException|UnknownOptionException|InvalidPathException
166
     *
167
     * @psalm-allow-private-mutation
168
     */
169
    private function build(string $topLevelKey): void
170
    {
171
        if ($this->finalConfig->has($topLevelKey)) {
172
            return;
173
        }
174
 
175
        if (! isset($this->configSchemas[$topLevelKey])) {
176
            throw new UnknownOptionException(\sprintf('Missing config schema for "%s"', $topLevelKey), $topLevelKey);
177
        }
178
 
179
        try {
180
            $userData = [$topLevelKey => $this->userConfig->get($topLevelKey)];
181
        } catch (DataException $ex) {
182
            $userData = [];
183
        }
184
 
185
        try {
186
            $schema    = $this->configSchemas[$topLevelKey];
187
            $processor = new Processor();
188
 
189
            $processed = $processor->process(Expect::structure([$topLevelKey => $schema]), $userData);
190
 
191
            $this->raiseAnyDeprecationNotices($processor->getWarnings());
192
 
193
            $this->finalConfig->import((array) self::convertStdClassesToArrays($processed));
194
        } catch (NetteValidationException $ex) {
195
            throw new ValidationException($ex);
196
        }
197
    }
198
 
199
    /**
200
     * Recursively converts stdClass instances to arrays
201
     *
202
     * @phpstan-template T
203
     *
204
     * @param T $data
205
     *
206
     * @return mixed
207
     *
208
     * @phpstan-return ($data is \stdClass ? array<string, mixed> : T)
209
     *
210
     * @psalm-pure
211
     */
212
    private static function convertStdClassesToArrays($data)
213
    {
214
        if ($data instanceof \stdClass) {
215
            $data = (array) $data;
216
        }
217
 
218
        if (\is_array($data)) {
219
            foreach ($data as $k => $v) {
220
                $data[$k] = self::convertStdClassesToArrays($v);
221
            }
222
        }
223
 
224
        return $data;
225
    }
226
 
227
    /**
228
     * @param string[] $warnings
229
     */
230
    private function raiseAnyDeprecationNotices(array $warnings): void
231
    {
232
        foreach ($warnings as $warning) {
233
            @\trigger_error($warning, \E_USER_DEPRECATED);
234
        }
235
    }
236
 
237
    /**
238
     * @throws InvalidPathException
239
     */
240
    private static function getTopLevelKey(string $path): string
241
    {
242
        if (\strlen($path) === 0) {
243
            throw new InvalidPathException('Path cannot be an empty string');
244
        }
245
 
246
        $path = \str_replace(['.', '/'], '.', $path);
247
 
248
        $firstDelimiter = \strpos($path, '.');
249
        if ($firstDelimiter === false) {
250
            return $path;
251
        }
252
 
253
        return \substr($path, 0, $firstDelimiter);
254
    }
255
}