1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* This file is part of DivineNii opensource projects. |
7
|
|
|
* |
8
|
|
|
* PHP version 7.4 and above required |
9
|
|
|
* |
10
|
|
|
* @author Divine Niiquaye Ibok <[email protected]> |
11
|
|
|
* @copyright 2021 DivineNii (https://divinenii.com/) |
12
|
|
|
* @license https://opensource.org/licenses/BSD-3-Clause License |
13
|
|
|
* |
14
|
|
|
* For the full copyright and license information, please view the LICENSE |
15
|
|
|
* file that was distributed with this source code. |
16
|
|
|
*/ |
17
|
|
|
|
18
|
|
|
namespace Rade\DI\Loader; |
19
|
|
|
|
20
|
|
|
use Rade\DI\Builder\Reference; |
21
|
|
|
use Rade\DI\Builder\Statement; |
22
|
|
|
use Rade\DI\Config\ConfigurationInterface; |
23
|
|
|
use Rade\DI\Container; |
24
|
|
|
use Rade\DI\ContainerBuilder; |
25
|
|
|
use Rade\DI\Definition; |
26
|
|
|
use Symfony\Component\Config\Exception\LoaderLoadException; |
27
|
|
|
use Symfony\Component\Config\Resource\FileExistenceResource; |
28
|
|
|
use Symfony\Component\Config\Resource\FileResource; |
29
|
|
|
use Symfony\Component\Yaml\Exception\ParseException; |
30
|
|
|
use Symfony\Component\Yaml\Parser as YamlParser; |
31
|
|
|
use Symfony\Component\Yaml\Tag\TaggedValue; |
32
|
|
|
use Symfony\Component\Yaml\Yaml; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* YamlFileLoader loads YAML files service definitions. |
36
|
|
|
* |
37
|
|
|
* @experimental in 1.0 |
38
|
|
|
* |
39
|
|
|
* @author Divine Niiquaye Ibok <[email protected]> |
40
|
|
|
*/ |
41
|
|
|
class YamlFileLoader extends FileLoader |
42
|
|
|
{ |
43
|
|
|
private const DEFAULTS_KEYWORDS = [ |
44
|
|
|
'private' => 'private', |
45
|
|
|
'tags' => 'tags', |
46
|
|
|
'autowire' => 'autowire', |
47
|
|
|
'bind' => 'bind', |
48
|
|
|
'calls' => 'bind', |
49
|
|
|
]; |
50
|
|
|
|
51
|
|
|
private const SERVICE_KEYWORDS = [ |
52
|
|
|
'alias' => 'alias', |
53
|
|
|
'entity' => 'entity', |
54
|
|
|
'class' => 'entity', // backward compatibility for symfony devs, will be dropped in 2.0 |
55
|
|
|
'arguments' => 'arguments', |
56
|
|
|
'lazy' => 'lazy', |
57
|
|
|
'private' => 'private', |
58
|
|
|
'deprecated' => 'deprecated', |
59
|
|
|
'factory' => 'factory', |
60
|
|
|
'tags' => 'tags', |
61
|
|
|
'decorates' => 'decorates', |
62
|
|
|
'autowire' => 'autowire', |
63
|
|
|
'bind' => 'bind', |
64
|
|
|
'calls' => 'bind', |
65
|
|
|
]; |
66
|
|
|
|
67
|
|
|
private const PROTOTYPE_KEYWORDS = [ |
68
|
|
|
'resource' => 'resource', |
69
|
|
|
'namespace' => 'namespace', |
70
|
|
|
'exclude' => 'exclude', |
71
|
|
|
'lazy' => 'lazy', |
72
|
|
|
'private' => 'private', |
73
|
|
|
'deprecated' => 'deprecated', |
74
|
|
|
'factory' => 'factory', |
75
|
|
|
'tags' => 'tags', |
76
|
|
|
'autowire' => 'autowire', |
77
|
|
|
'arguments' => 'arguments', |
78
|
|
|
'bind' => 'bind', |
79
|
|
|
'calls' => 'bind', |
80
|
|
|
]; |
81
|
|
|
|
82
|
|
|
/** @var YamlParser */ |
83
|
|
|
private $yamlParser; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* {@inheritdoc} |
87
|
|
|
*/ |
88
|
68 |
|
public function load($resource, string $type = null): void |
89
|
|
|
{ |
90
|
68 |
|
$path = $this->locator->locate($resource); |
91
|
67 |
|
$content = $this->loadFile($path); |
|
|
|
|
92
|
|
|
|
93
|
66 |
|
if ($this->container instanceof ContainerBuilder) { |
94
|
38 |
|
$this->container->addResource(new FileExistenceResource($path)); |
|
|
|
|
95
|
38 |
|
$this->container->addResource(new FileResource($path)); |
|
|
|
|
96
|
|
|
} |
97
|
|
|
|
98
|
66 |
|
if (!empty($content)) { |
99
|
66 |
|
$this->loadContent($content, $path); |
100
|
|
|
} |
101
|
44 |
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* {@inheritdoc} |
105
|
|
|
*/ |
106
|
5 |
|
public function supports($resource, string $type = null) |
107
|
|
|
{ |
108
|
5 |
|
if (!\is_string($resource)) { |
109
|
2 |
|
return false; |
110
|
|
|
} |
111
|
|
|
|
112
|
5 |
|
if (null === $type && \in_array(\pathinfo($resource, \PATHINFO_EXTENSION), ['yaml', 'yml'], true)) { |
113
|
5 |
|
return true; |
114
|
|
|
} |
115
|
|
|
|
116
|
4 |
|
return \in_array($type, ['yaml', 'yml'], true); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Loads a YAML file. |
121
|
|
|
* |
122
|
|
|
* @param string $file |
123
|
|
|
* |
124
|
|
|
* @throws \InvalidArgumentException when the given file is not a local file or when it does not exist |
125
|
|
|
* |
126
|
|
|
* @return array The file content |
127
|
|
|
*/ |
128
|
71 |
|
protected function loadFile($file) |
129
|
|
|
{ |
130
|
71 |
|
if (!\class_exists(\Symfony\Component\Yaml\Parser::class)) { |
131
|
|
|
throw new \RuntimeException('Unable to load YAML config files as the Symfony Yaml Component is not installed.'); |
132
|
|
|
} |
133
|
|
|
|
134
|
71 |
|
if (!\stream_is_local($file)) { |
135
|
|
|
throw new \InvalidArgumentException(\sprintf('This is not a local file "%s".', $file)); |
136
|
|
|
} |
137
|
|
|
|
138
|
71 |
|
if (!\is_file($file)) { |
139
|
2 |
|
throw new \InvalidArgumentException(\sprintf('The file "%s" does not exist.', $file)); |
140
|
|
|
} |
141
|
|
|
|
142
|
69 |
|
if (null === $this->yamlParser) { |
143
|
69 |
|
$this->yamlParser = new YamlParser(); |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
try { |
147
|
69 |
|
$configuration = $this->yamlParser->parseFile($file, Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS); |
148
|
3 |
|
} catch (ParseException $e) { |
149
|
3 |
|
throw new \InvalidArgumentException(\sprintf('The file "%s" does not contain valid YAML: ', $file) . $e->getMessage(), 0, $e); |
150
|
|
|
} |
151
|
|
|
|
152
|
66 |
|
if (null === $configuration) { |
153
|
|
|
return []; |
154
|
|
|
} |
155
|
|
|
|
156
|
66 |
|
if (!\is_array($configuration)) { |
157
|
|
|
throw new \InvalidArgumentException(\sprintf('The service file "%s" is not valid. It should contain an array. Check your YAML syntax.', $file)); |
158
|
|
|
} |
159
|
|
|
|
160
|
66 |
|
return $configuration; |
161
|
|
|
} |
162
|
|
|
|
163
|
66 |
|
private function parseImports(array $content, string $file): void |
164
|
|
|
{ |
165
|
66 |
|
if (!isset($content['imports'])) { |
166
|
64 |
|
return; |
167
|
|
|
} |
168
|
|
|
|
169
|
5 |
|
if (!\is_array($content['imports'])) { |
170
|
1 |
|
throw new \InvalidArgumentException(\sprintf('The "imports" key should contain an array in "%s". Check your YAML syntax.', $file)); |
171
|
|
|
} |
172
|
|
|
|
173
|
4 |
|
$defaultDirectory = \dirname($file); |
174
|
|
|
|
175
|
4 |
|
foreach ($content['imports'] as $import) { |
176
|
4 |
|
if (!\is_array($import)) { |
177
|
1 |
|
$import = ['resource' => $import]; |
178
|
|
|
} |
179
|
|
|
|
180
|
4 |
|
if (!isset($import['resource'])) { |
181
|
1 |
|
throw new \InvalidArgumentException(\sprintf('An import should provide a resource in "%s". Check your YAML syntax.', $file)); |
182
|
|
|
} |
183
|
|
|
|
184
|
3 |
|
if ('not_found' === $ignoreErrors = $import['ignore_errors'] ?? false) { |
185
|
1 |
|
$notFound = $ignoreErrors = false; // Ignore error on missing file resource. |
186
|
|
|
} |
187
|
|
|
|
188
|
3 |
|
$this->setCurrentDir($defaultDirectory); |
189
|
|
|
|
190
|
|
|
try { |
191
|
3 |
|
$this->import($import['resource'], $import['type'] ?? null, $ignoreErrors, $file); |
192
|
1 |
|
} catch (LoaderLoadException $e) { |
193
|
1 |
|
if (!isset($notFound)) { |
194
|
1 |
|
throw $e; |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
3 |
|
unset($content['imports']); |
200
|
3 |
|
} |
201
|
|
|
|
202
|
66 |
|
private function loadContent($content, $path): void |
203
|
|
|
{ |
204
|
|
|
// imports |
205
|
66 |
|
$this->parseImports($content, $path); |
206
|
|
|
|
207
|
|
|
// parameters |
208
|
64 |
|
if (isset($content['parameters'])) { |
209
|
8 |
|
if (!\is_array($content['parameters'])) { |
210
|
1 |
|
throw new \InvalidArgumentException(\sprintf('The "parameters" key should contain an array in "%s". Check your YAML syntax.', $path)); |
211
|
|
|
} |
212
|
|
|
|
213
|
7 |
|
foreach ($content['parameters'] as $key => $value) { |
214
|
7 |
|
$this->container->parameters[$key] = $this->resolveServices($value, $path, true); |
215
|
|
|
} |
216
|
|
|
|
217
|
6 |
|
unset($content['parameters']); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
// service providers |
221
|
62 |
|
$this->loadServiceProviders($content, $path); |
222
|
|
|
|
223
|
62 |
|
$this->setCurrentDir(\dirname($path)); |
224
|
|
|
|
225
|
|
|
// load definitions |
226
|
62 |
|
$this->parseDefinitions($content, $path); |
227
|
44 |
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* Loads Service Providers. |
231
|
|
|
*/ |
232
|
62 |
|
private function loadServiceProviders(array $content, string $path): void |
233
|
|
|
{ |
234
|
62 |
|
foreach ($content['service_providers'] ?? [] as $k => $provider) { |
235
|
4 |
|
if (\is_string($k)) { |
236
|
|
|
throw new \InvalidArgumentException(\sprintf('Invalid service provider key %s, only list sequence is supported "service_providers: ..." in "%s".', $k, $path)); |
237
|
|
|
} |
238
|
|
|
|
239
|
4 |
|
if ($provider instanceof TaggedValue && 'provider' === $provider->getTag()) { |
240
|
|
|
$value = $provider->getValue(); |
241
|
|
|
|
242
|
|
|
$provider = $value['id'] ?? null; |
243
|
|
|
$args = $this->resolveServices($value['args'] ?? [], $path); |
244
|
|
|
$config = $this->resolveServices($value['config'] ?? [], $path); |
245
|
4 |
|
} elseif (\is_array($provider)) { |
246
|
2 |
|
$provider = \key($value = $provider); |
247
|
2 |
|
$config = $this->resolveServices($value[$provider] ?? [], $path); |
248
|
|
|
} |
249
|
|
|
|
250
|
4 |
|
if (!\is_string($provider)) { |
251
|
|
|
continue; |
252
|
|
|
} |
253
|
|
|
|
254
|
4 |
|
if ($this->container instanceof Container) { |
255
|
2 |
|
$extension = $this->container->resolveClass($provider, $args ?? []); |
256
|
|
|
} else { |
257
|
2 |
|
$extension = (new \ReflectionClass($provider))->newInstanceArgs($args ?? []); |
258
|
|
|
} |
259
|
|
|
|
260
|
4 |
|
if (!\is_array($config ?? null)) { |
261
|
2 |
|
$config = $extension instanceof ConfigurationInterface ? (array) $content[$extension->getId()] ?? [] : []; |
262
|
|
|
} |
263
|
|
|
|
264
|
4 |
|
$this->container->register($extension, $config ?? []); |
265
|
|
|
} |
266
|
|
|
|
267
|
62 |
|
unset($content['service_providers']); |
268
|
62 |
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* Resolves services. |
272
|
|
|
* |
273
|
|
|
* @return array|string|Reference|Statement|object|null |
274
|
|
|
*/ |
275
|
56 |
|
private function resolveServices($value, string $file, bool $isParameter = false) |
276
|
|
|
{ |
277
|
56 |
|
if ($value instanceof TaggedValue) { |
278
|
4 |
|
if ($isParameter) { |
279
|
|
|
throw new \InvalidArgumentException(\sprintf('Using tag "!%s" in a parameter is not allowed in "%s".', $value->getTag(), $file)); |
280
|
|
|
} |
281
|
|
|
|
282
|
4 |
|
$argument = $value->getValue(); |
283
|
|
|
|
284
|
4 |
|
if ('reference' === $value->getTag()) { |
285
|
|
|
if (!\is_string($argument)) { |
286
|
|
|
throw new \InvalidArgumentException(\sprintf('"!reference" tag only accepts string value in "%s".', $file)); |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
if (!$this->container->has($argument)) { |
290
|
|
|
throw new \InvalidArgumentException(\sprintf('Creating an alias using the tag "!reference" is not allowed in "%s".', $file)); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
return $this->container instanceof Container ? $this->container->get($argument) : new Reference($argument); |
294
|
|
|
} |
295
|
|
|
|
296
|
4 |
|
if ('tagged' === $value->getTag()) { |
297
|
2 |
|
if (\is_string($argument) && '' !== $argument) { |
298
|
|
|
return $this->container->tagged($argument); |
299
|
|
|
} |
300
|
|
|
|
301
|
2 |
|
if (\is_array($argument) && (isset($argument['tag']) && '' !== $argument['tag'])) { |
302
|
2 |
|
return $this->container->tagged($argument['tag'], $argument['resolve'] ?? true); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
throw new \InvalidArgumentException(\sprintf('"!%s" tags only accept a non empty string or an array with a key "tag" in "%s".', $value->getTag(), $file)); |
306
|
|
|
} |
307
|
|
|
|
308
|
2 |
|
if ('statement' === $value->getTag()) { |
309
|
2 |
|
if (\is_string($argument)) { |
310
|
2 |
|
return $this->container instanceof Container ? $this->container->call($argument) : new Statement($argument); |
311
|
|
|
} |
312
|
|
|
|
313
|
2 |
|
if (!\is_array($argument)) { |
314
|
|
|
throw new \InvalidArgumentException(\sprintf('"!statement" tag only accepts sequences in "%s".', $file)); |
315
|
|
|
} |
316
|
|
|
|
317
|
2 |
|
if (\array_keys($argument) !== ['value', 'args']) { |
318
|
|
|
throw new \InvalidArgumentException('"!statement" tag only accepts array keys of "value" and "args"'); |
319
|
|
|
} |
320
|
|
|
|
321
|
2 |
|
$argument = $this->resolveServices($argument, $file, $isParameter); |
322
|
|
|
|
323
|
2 |
|
if ($this->container instanceof Container) { |
324
|
1 |
|
return $this->container->call($argument['value'], $argument['args']); |
325
|
|
|
} |
326
|
|
|
|
327
|
1 |
|
return new Statement($argument['value'], $argument['args']); |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
throw new \InvalidArgumentException(\sprintf('Unsupported tag "!%s".', $value->getTag())); |
331
|
|
|
} |
332
|
|
|
|
333
|
56 |
|
if (\is_array($value)) { |
334
|
53 |
|
foreach ($value as $k => $v) { |
335
|
51 |
|
$value[$k] = $this->resolveServices($v, $file, $isParameter); |
336
|
|
|
} |
337
|
56 |
|
} elseif (\is_string($value) && '@' === $value[0]) { |
338
|
10 |
|
$value = \substr($value, 1); |
339
|
|
|
|
340
|
|
|
// double @@ should be escaped |
341
|
10 |
|
if ('@' === $value[0]) { |
342
|
3 |
|
return $value; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
// ignore on invalid reference |
346
|
10 |
|
if ('?' === $value[0]) { |
347
|
|
|
$value = \substr($value, 1); |
348
|
|
|
|
349
|
|
|
if (!$this->container->has($value)) { |
350
|
|
|
return null; |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
|
354
|
10 |
|
return $this->container instanceof Container ? $this->container->get($value) : new Reference($value); |
355
|
|
|
} |
356
|
|
|
|
357
|
56 |
|
if (!\is_string($value)) { |
358
|
53 |
|
return $value; |
359
|
|
|
} |
360
|
|
|
|
361
|
52 |
|
return $this->resolveParameters($value); |
362
|
|
|
} |
363
|
|
|
|
364
|
62 |
|
private function parseDefinitions(array $content, string $file): void |
365
|
|
|
{ |
366
|
62 |
|
if (!isset($content['services'])) { |
367
|
8 |
|
return; |
368
|
|
|
} |
369
|
|
|
|
370
|
56 |
|
if (!\is_array($content['services'])) { |
371
|
1 |
|
throw new \InvalidArgumentException(\sprintf('The "services" key should contain an array in "%s". Check your YAML syntax.', $file)); |
372
|
|
|
} |
373
|
|
|
|
374
|
55 |
|
$defaults = $this->parseDefaults($content, $file); |
375
|
|
|
|
376
|
53 |
|
foreach ($content['services'] as $id => $service) { |
377
|
53 |
|
if (\preg_match('/^_[a-zA-Z0-9_]*$/', $id)) { |
378
|
2 |
|
throw new \InvalidArgumentException(\sprintf('Service names that start with an underscore are reserved. Rename the "%s" service.', $id)); |
379
|
|
|
} |
380
|
|
|
|
381
|
51 |
|
$service = $this->resolveServices($service, $file); |
382
|
|
|
|
383
|
51 |
|
if ($service instanceof Reference) { |
384
|
2 |
|
$this->container->alias($id, (string) $service); |
385
|
|
|
|
386
|
2 |
|
continue; |
387
|
|
|
} |
388
|
|
|
|
389
|
51 |
|
if ($service instanceof Statement) { |
390
|
1 |
|
$this->container->autowire($id, $service); |
|
|
|
|
391
|
|
|
|
392
|
1 |
|
continue; |
393
|
|
|
} |
394
|
|
|
|
395
|
50 |
|
if (\is_object($service)) { |
396
|
3 |
|
$this->container->set($id, $service, true); |
|
|
|
|
397
|
|
|
|
398
|
3 |
|
continue; |
399
|
|
|
} |
400
|
|
|
|
401
|
49 |
|
if (empty($service)) { |
402
|
4 |
|
if ([] === $defaults) { |
403
|
|
|
continue; |
404
|
|
|
} |
405
|
|
|
|
406
|
4 |
|
$service = []; // If $defaults, then a definition creation should be possible. |
407
|
|
|
} |
408
|
|
|
|
409
|
49 |
|
if (!\is_array($service)) { |
410
|
1 |
|
throw new \InvalidArgumentException(\sprintf('A service definition must be an array, a tagged "!statement" or a string starting with "@", but "%s" found for service "%s" in "%s". Check your YAML syntax.', \get_debug_type($service), $id, $file)); |
411
|
|
|
} |
412
|
|
|
|
413
|
48 |
|
if ($this->container->has($id) && $this->container instanceof Container) { |
414
|
2 |
|
$this->container->extend($id, function (Definition $definition) use ($id, $service, $file, $defaults): Definition { |
|
|
|
|
415
|
2 |
|
$this->parseDefinition($id, $service, $file, $defaults, $definition); |
416
|
|
|
|
417
|
2 |
|
return $definition; |
418
|
2 |
|
}); |
419
|
|
|
|
420
|
2 |
|
continue; |
421
|
|
|
} |
422
|
|
|
|
423
|
47 |
|
$this->parseDefinition($id, $service, $file, $defaults); |
424
|
|
|
} |
425
|
38 |
|
} |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* @throws \InvalidArgumentException |
429
|
|
|
*/ |
430
|
55 |
|
private function parseDefaults(array &$content, string $file): array |
431
|
|
|
{ |
432
|
55 |
|
if (!\array_key_exists('_defaults', $content['services'])) { |
433
|
39 |
|
return []; |
434
|
|
|
} |
435
|
16 |
|
$defaults = $content['services']['_defaults']; |
436
|
16 |
|
unset($content['services']['_defaults']); |
437
|
|
|
|
438
|
16 |
|
if (!\is_array($defaults)) { |
439
|
2 |
|
throw new \InvalidArgumentException(\sprintf('Service "_defaults" key must be an array, "%s" given in "%s".', \get_debug_type($defaults), $file)); |
440
|
|
|
} |
441
|
|
|
|
442
|
14 |
|
foreach ($defaults as $key => $default) { |
443
|
14 |
|
if (!isset(self::DEFAULTS_KEYWORDS[$key])) { |
444
|
|
|
throw new \InvalidArgumentException(\sprintf('The configuration key "%s" cannot be used to define a default value in "%s". Allowed keys are "%s".', $key, $file, \implode('", "', self::DEFAULTS_KEYWORDS))); |
445
|
|
|
} |
446
|
|
|
} |
447
|
|
|
|
448
|
14 |
|
if (isset($defaults['tags'])) { |
449
|
4 |
|
if (!\is_array($tags = $defaults['tags'])) { |
450
|
|
|
throw new \InvalidArgumentException(\sprintf('Parameter "tags" in "_defaults" must be an array in "%s". Check your YAML syntax.', $file)); |
451
|
|
|
} |
452
|
|
|
|
453
|
4 |
|
$defaults['tags'] = $this->parseDefinitionTags('in "_defaults"', $tags, $file); |
454
|
|
|
} |
455
|
|
|
|
456
|
14 |
|
if (null !== $bindings = $defaults['bind'] ?? $default['calls'] ?? null) { |
457
|
4 |
|
if (!\is_array($bindings)) { |
458
|
|
|
throw new \InvalidArgumentException(\sprintf('Parameter "bind" in "_defaults" must be an array in "%s". Check your YAML syntax.', $file)); |
459
|
|
|
} |
460
|
|
|
|
461
|
4 |
|
unset($default['calls']); // To avoid conflicts, will be dropped in 1.3 |
462
|
|
|
|
463
|
4 |
|
$defaults['bind'] = $this->parseDefinitionBinds('in "_defaults"', $bindings, $file); |
464
|
|
|
} |
465
|
|
|
|
466
|
14 |
|
return $defaults; |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
/** |
470
|
|
|
* Parses a definition. |
471
|
|
|
* |
472
|
|
|
* @throws \InvalidArgumentException |
473
|
|
|
*/ |
474
|
48 |
|
private function parseDefinition(string $id, array $service, string $file, array $defaults, Definition $definition = null): void |
475
|
|
|
{ |
476
|
48 |
|
$this->checkDefinition($id, $service, $file); |
477
|
|
|
|
478
|
48 |
|
if ($this->container->has($id) && $this->container instanceof ContainerBuilder) { |
479
|
2 |
|
$definition = \array_key_exists($id, $this->container->keys()) ? $this->container->extend($id) : null; |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
// Non existing entity |
483
|
48 |
|
if (!isset($service['entity'])) { |
484
|
25 |
|
$service['entity'] = $service['class'] ?? (\class_exists($id) ? $id : null); |
485
|
|
|
} |
486
|
|
|
|
487
|
48 |
|
$arguments = $this->resolveServices($service['arguments'] ?? [], $file); |
488
|
|
|
|
489
|
48 |
|
if ($definition instanceof Definition) { |
490
|
2 |
|
$hasDefinition = true; |
491
|
|
|
|
492
|
2 |
|
$definition->replace($service['entity'], null !== $service['entity']) |
493
|
2 |
|
->args(\array_merge($definition->get('parameters'), $arguments)); |
494
|
|
|
} else { |
495
|
47 |
|
$definition = new Definition($service['entity'], $arguments); |
496
|
|
|
} |
497
|
|
|
|
498
|
48 |
|
if ($this->container instanceof ContainerBuilder) { |
499
|
25 |
|
$definition->should(Definition::PRIVATE, $service['private'] ?? $defaults['private'] ?? false); |
500
|
|
|
} |
501
|
|
|
|
502
|
48 |
|
$definition->should(Definition::LAZY, $service['lazy'] ?? false); |
503
|
48 |
|
$definition->should(Definition::FACTORY, $service['factory'] ?? false); |
504
|
48 |
|
$this->autowired[$id] = $autowired = $service['autowire'] ?? $defaults['autowire'] ?? false; |
505
|
|
|
|
506
|
48 |
|
if (isset($service['deprecated'])) { |
507
|
2 |
|
$deprecation = \is_array($service['deprecated']) ? $service['deprecated'] : ['message' => $service['deprecated']]; |
508
|
2 |
|
$deprecation = [$deprecation['package'] ?? '', $deprecation['version'] ?? '', $deprecation['message'] ?? null]; |
509
|
|
|
} |
510
|
|
|
|
511
|
48 |
|
if (!\is_array($bindings = $service['bind'] ?? $service['calls'] ?? [])) { |
512
|
|
|
throw new \InvalidArgumentException(\sprintf('Parameter "bind" must be an array for service "%s" in "%s". Check your YAML syntax.', $id, $file)); |
513
|
|
|
} |
514
|
48 |
|
$bindings = \array_merge($defaults['bind'] ?? [], $bindings); |
515
|
|
|
|
516
|
48 |
|
if ([] !== $bindings) { |
517
|
12 |
|
$this->parseDefinitionBinds($id, $bindings, $file, $definition); |
518
|
|
|
} |
519
|
|
|
|
520
|
48 |
|
if (!\is_array($tags = $service['tags'] ?? [])) { |
521
|
4 |
|
throw new \InvalidArgumentException(\sprintf('Parameter "tags" must be an array for service "%s" in "%s". Check your YAML syntax.', $id, $file)); |
522
|
|
|
} |
523
|
|
|
|
524
|
44 |
|
if (isset($defaults['tags'])) { |
525
|
2 |
|
$tags = \array_merge($defaults['tags'], $tags); |
526
|
|
|
} |
527
|
|
|
|
528
|
44 |
|
if (\array_key_exists('namespace', $service) && !\array_key_exists('resource', $service)) { |
529
|
2 |
|
throw new \InvalidArgumentException(\sprintf('A "resource" attribute must be set when the "namespace" attribute is set for service "%s" in "%s". Check your YAML syntax.', $id, $file)); |
530
|
|
|
} |
531
|
|
|
|
532
|
42 |
|
if (\array_key_exists('resource', $service)) { |
533
|
8 |
|
if (!\is_string($service['resource'])) { |
534
|
|
|
throw new \InvalidArgumentException(\sprintf('A "resource" attribute must be of type string for service "%s" in "%s". Check your YAML syntax.', $id, $file)); |
535
|
|
|
} |
536
|
|
|
|
537
|
8 |
|
$namespace = $service['namespace'] ?? $id; |
538
|
|
|
|
539
|
8 |
|
$this->autowired[$namespace] = $autowired; |
540
|
8 |
|
$this->deprecations[$namespace] = $deprecation ?? null; |
541
|
|
|
|
542
|
8 |
|
if ([] !== $tags) { |
543
|
2 |
|
$this->tags[$namespace] = $this->parseDefinitionTags($id, $tags, $file); |
544
|
|
|
} |
545
|
|
|
|
546
|
8 |
|
$this->registerClasses($definition, $namespace, $service['resource'], $service['exclude'] ?? []); |
547
|
|
|
|
548
|
8 |
|
return; |
549
|
|
|
} |
550
|
|
|
|
551
|
34 |
|
if (!isset($hasDefinition)) { |
552
|
33 |
|
$definition = $this->container->set($id, $definition); |
553
|
|
|
} |
554
|
|
|
|
555
|
34 |
|
if (false !== $autowired) { |
556
|
8 |
|
$definition->autowire(\is_array($autowired) ? $autowired : []); |
557
|
|
|
} |
558
|
|
|
|
559
|
34 |
|
if (isset($deprecation)) { |
560
|
2 |
|
[$package, $version, $message] = $deprecation; |
561
|
|
|
|
562
|
2 |
|
$definition->deprecate($package, $version, $message); |
563
|
|
|
} |
564
|
|
|
|
565
|
34 |
|
if ([] !== $tags) { |
566
|
11 |
|
$this->container->tag($id, $this->parseDefinitionTags($id, $tags, $file)); |
567
|
|
|
} |
568
|
30 |
|
} |
569
|
|
|
|
570
|
|
|
/** |
571
|
|
|
* @param array<int,string[]> $bindings |
572
|
|
|
* |
573
|
|
|
* @return array<int,string[]> |
574
|
|
|
*/ |
575
|
12 |
|
private function parseDefinitionBinds(string $id, array $bindings, string $file, Definition $definition = null): array |
576
|
|
|
{ |
577
|
12 |
|
if ('in "_defaults"' !== $id) { |
578
|
12 |
|
$id = \sprintf('for service "%s"', $id); |
579
|
|
|
} |
580
|
|
|
|
581
|
12 |
|
foreach ($bindings as $k => $call) { |
582
|
12 |
|
if (!\is_array($call) && (!\is_string($k) || !$call instanceof TaggedValue)) { |
583
|
|
|
throw new \InvalidArgumentException(\sprintf('Invalid bind call %s: expected map or array, "%s" given in "%s".', $id, $call instanceof TaggedValue ? '!' . $call->getTag() : \get_debug_type($call), $file)); |
584
|
|
|
} |
585
|
|
|
|
586
|
12 |
|
if (\is_string($k)) { |
587
|
|
|
throw new \InvalidArgumentException(\sprintf('Invalid bind call %s, did you forgot a leading dash before "%s: ..." in "%s"?', $id, $k, $file)); |
588
|
|
|
} |
589
|
|
|
|
590
|
12 |
|
if (empty($call)) { |
591
|
|
|
throw new \InvalidArgumentException(\sprintf('Invalid call %s: the bind must be defined as the first index of an array or as the only key of a map in "%s".', $id, $file)); |
592
|
|
|
} |
593
|
|
|
|
594
|
12 |
|
if (1 === \count($call) && \is_string(\key($call))) { |
595
|
9 |
|
$method = \key($call); |
596
|
9 |
|
$args = $this->resolveServices($call[$method], $file); |
597
|
|
|
} elseif (empty($call)) { |
598
|
|
|
throw new \InvalidArgumentException(\sprintf('Invalid call %s: the bind must be defined as the first index of an array or as the only key of a map in "%s".', $id, $file)); |
599
|
|
|
} else { |
600
|
10 |
|
$method = $call[0]; |
601
|
10 |
|
$args = $this->resolveServices($call[1] ?? null, $file); |
602
|
|
|
} |
603
|
|
|
|
604
|
12 |
|
if ($definition instanceof Definition) { |
605
|
12 |
|
$definition->bind($method, $args); |
606
|
|
|
|
607
|
12 |
|
continue; |
608
|
|
|
} |
609
|
|
|
|
610
|
4 |
|
$bindings[$k] = [$method, $args]; |
611
|
|
|
} |
612
|
|
|
|
613
|
12 |
|
return $bindings; |
614
|
|
|
} |
615
|
|
|
|
616
|
15 |
|
private function parseDefinitionTags(string $id, array $tags, string $file): array |
617
|
|
|
{ |
618
|
15 |
|
if ('in "_defaults"' !== $id) { |
619
|
13 |
|
$id = \sprintf('for service "%s"', $id); |
620
|
|
|
} |
621
|
|
|
|
622
|
15 |
|
$serviceTags = []; |
623
|
|
|
|
624
|
15 |
|
foreach ($tags as $k => $tag) { |
625
|
15 |
|
if (\is_string($k)) { |
626
|
2 |
|
throw new \InvalidArgumentException(\sprintf('The "tags" entry %s is invalid, did you forgot a leading dash before "%s: ..." in "%s"?', $id, $k, $file)); |
627
|
|
|
} |
628
|
|
|
|
629
|
13 |
|
if (\is_string($tag)) { |
630
|
8 |
|
$serviceTags[] = $tag; |
631
|
|
|
|
632
|
8 |
|
continue; |
633
|
|
|
} |
634
|
|
|
|
635
|
5 |
|
foreach ((array) $tag as $name => $value) { |
636
|
5 |
|
if (!\is_string($name) || '' === $name) { |
637
|
2 |
|
throw new \InvalidArgumentException(\sprintf('The tag name %s in "%s" must be a non-empty string. Check your YAML syntax.', $id, $file)); |
638
|
|
|
} |
639
|
|
|
|
640
|
3 |
|
$serviceTags[$name] = $value; |
641
|
|
|
} |
642
|
|
|
} |
643
|
|
|
|
644
|
11 |
|
return $serviceTags; |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
/** |
648
|
|
|
* Checks the keywords used to define a service. |
649
|
|
|
*/ |
650
|
48 |
|
private function checkDefinition(string $id, array $definition, string $file): void |
651
|
|
|
{ |
652
|
48 |
|
if (isset($definition['resource']) || isset($definition['namespace'])) { |
653
|
10 |
|
$keywords = self::PROTOTYPE_KEYWORDS; |
654
|
|
|
} else { |
655
|
38 |
|
$keywords = self::SERVICE_KEYWORDS; |
656
|
|
|
} |
657
|
|
|
|
658
|
48 |
|
foreach ($definition as $key => $value) { |
659
|
46 |
|
if (!isset($keywords[$key])) { |
660
|
2 |
|
throw new \InvalidArgumentException(\sprintf('The configuration key "%s" is unsupported for definition "%s" in "%s". Allowed configuration keys are "%s".', $key, $id, $file, \implode('", "', $keywords))); |
661
|
|
|
} |
662
|
|
|
} |
663
|
48 |
|
} |
664
|
|
|
} |
665
|
|
|
|