1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\DI;
9:
10: use Nette;
11: use Nette\Utils\Validators;
12:
13:
14: 15: 16:
17: class Compiler
18: {
19: use Nette\SmartObject;
20:
21:
22: private $extensions = [];
23:
24:
25: private $builder;
26:
27:
28: private $config = [];
29:
30:
31: private $dependencies;
32:
33:
34: private $className = 'Container';
35:
36:
37: private $dynamicParams = [];
38:
39:
40: private static $reserved = ['services' => 1, 'parameters' => 1];
41:
42:
43: public function __construct(ContainerBuilder $builder = null)
44: {
45: $this->builder = $builder ?: new ContainerBuilder;
46: $this->dependencies = new DependencyChecker;
47: }
48:
49:
50: 51: 52: 53: 54:
55: public function addExtension($name, CompilerExtension $extension)
56: {
57: if ($name === null) {
58: $name = '_' . count($this->extensions);
59: } elseif (isset($this->extensions[$name]) || isset(self::$reserved[$name])) {
60: throw new Nette\InvalidArgumentException("Name '$name' is already used or reserved.");
61: }
62: $this->extensions[$name] = $extension->setCompiler($this, $name);
63: return $this;
64: }
65:
66:
67: 68: 69:
70: public function getExtensions($type = null)
71: {
72: return $type
73: ? array_filter($this->extensions, function ($item) use ($type) { return $item instanceof $type; })
74: : $this->extensions;
75: }
76:
77:
78: 79: 80:
81: public function getContainerBuilder()
82: {
83: return $this->builder;
84: }
85:
86:
87: 88: 89:
90: public function setClassName($className)
91: {
92: $this->className = $className;
93: return $this;
94: }
95:
96:
97: 98: 99: 100:
101: public function addConfig(array $config)
102: {
103: $this->config = Config\Helpers::merge($config, $this->config);
104: return $this;
105: }
106:
107:
108: 109: 110: 111:
112: public function loadConfig($file)
113: {
114: $loader = new Config\Loader;
115: $this->addConfig($loader->load($file));
116: $this->dependencies->add($loader->getDependencies());
117: return $this;
118: }
119:
120:
121: 122: 123: 124:
125: public function getConfig()
126: {
127: return $this->config;
128: }
129:
130:
131: 132: 133: 134:
135: public function setDynamicParameterNames(array $names)
136: {
137: $this->dynamicParams = $names;
138: return $this;
139: }
140:
141:
142: 143: 144: 145: 146:
147: public function addDependencies(array $deps)
148: {
149: $this->dependencies->add(array_filter($deps));
150: return $this;
151: }
152:
153:
154: 155: 156: 157:
158: public function exportDependencies()
159: {
160: return $this->dependencies->export();
161: }
162:
163:
164: 165: 166:
167: public function compile()
168: {
169: if (func_num_args()) {
170: trigger_error(__METHOD__ . ' arguments are deprecated, use Compiler::addConfig() and Compiler::setClassName().', E_USER_DEPRECATED);
171: $this->config = func_get_arg(0) ?: $this->config;
172: $this->className = @func_get_arg(1) ?: $this->className;
173: }
174: $this->processParameters();
175: $this->processExtensions();
176: $this->processServices();
177: $classes = $this->generateCode();
178: return implode("\n\n\n", $classes);
179: }
180:
181:
182:
183: public function processParameters()
184: {
185: $params = isset($this->config['parameters']) ? $this->config['parameters'] : [];
186: foreach ($this->dynamicParams as $key) {
187: $params[$key] = array_key_exists($key, $params)
188: ? ContainerBuilder::literal('current([isset($this->parameters[?]) \? $this->parameters[?] : ?])', [$key, $key, $params[$key]])
189: : ContainerBuilder::literal('$this->parameters[?]', [$key]);
190: }
191: $this->builder->parameters = Helpers::expand($params, $params, true);
192: }
193:
194:
195:
196: public function processExtensions()
197: {
198: $this->config = Helpers::expand(array_diff_key($this->config, self::$reserved), $this->builder->parameters)
199: + array_intersect_key($this->config, self::$reserved);
200:
201: foreach ($first = $this->getExtensions(Extensions\ExtensionsExtension::class) as $name => $extension) {
202: $extension->setConfig(isset($this->config[$name]) ? $this->config[$name] : []);
203: $extension->loadConfiguration();
204: }
205:
206: $last = $this->getExtensions(Extensions\InjectExtension::class);
207: $this->extensions = array_merge(array_diff_key($this->extensions, $last), $last);
208:
209: $extensions = array_diff_key($this->extensions, $first);
210: foreach (array_intersect_key($extensions, $this->config) as $name => $extension) {
211: $extension->setConfig($this->config[$name] ?: []);
212: }
213:
214: foreach ($extensions as $extension) {
215: $extension->loadConfiguration();
216: }
217:
218: if ($extra = array_diff_key($this->extensions, $extensions, $first)) {
219: $extra = implode("', '", array_keys($extra));
220: throw new Nette\DeprecatedException("Extensions '$extra' were added while container was being compiled.");
221:
222: } elseif ($extra = key(array_diff_key($this->config, self::$reserved, $this->extensions))) {
223: $hint = Nette\Utils\ObjectHelpers::getSuggestion(array_keys(self::$reserved + $this->extensions), $extra);
224: throw new Nette\InvalidStateException(
225: "Found section '$extra' in configuration, but corresponding extension is missing"
226: . ($hint ? ", did you mean '$hint'?" : '.')
227: );
228: }
229: }
230:
231:
232:
233: public function processServices()
234: {
235: if (isset($this->config['services'])) {
236: self::loadDefinitions($this->builder, $this->config['services']);
237: }
238: }
239:
240:
241:
242: public function generateCode()
243: {
244: if (func_num_args()) {
245: trigger_error(__METHOD__ . ' arguments are deprecated, use Compiler::setClassName().', E_USER_DEPRECATED);
246: $this->className = func_get_arg(0) ?: $this->className;
247: }
248:
249: $this->builder->prepareClassList();
250:
251: foreach ($this->extensions as $extension) {
252: $extension->beforeCompile();
253: $this->dependencies->add([(new \ReflectionClass($extension))->getFileName()]);
254: }
255:
256: $generator = new PhpGenerator($this->builder);
257: $classes = $generator->generate($this->className);
258: $classes[0]->addMethod('initialize');
259: $this->dependencies->add($this->builder->getDependencies());
260:
261: foreach ($this->extensions as $extension) {
262: $extension->afterCompile($classes[0]);
263: }
264: return $classes;
265: }
266:
267:
268:
269:
270:
271: 272: 273: 274:
275: public static function loadDefinitions(ContainerBuilder $builder, array $services, $namespace = null)
276: {
277: $depths = [];
278: $sort = false;
279: foreach ($services as $name => $def) {
280: $path = [];
281: while (Config\Helpers::isInheriting($def)) {
282: $sort = true;
283: $path[] = $def;
284: $def = isset($services[$def[Config\Helpers::EXTENDS_KEY]]) ? $services[$def[Config\Helpers::EXTENDS_KEY]] : [];
285: if (in_array($def, $path, true)) {
286: throw new ServiceCreationException("Circular reference detected for service '$name'.");
287: }
288: }
289: $depths[$name] = count($path) + preg_match('#^@[\w\\\\]+\z#', $name);
290: }
291: if ($sort) {
292: @array_multisort($depths, $services);
293: }
294:
295: foreach ($services as $name => $def) {
296: if (is_int($name)) {
297: $counter = 1;
298: do {
299: $name = (string) $counter++;
300: } while ($builder->hasDefinition($name));
301: } elseif (preg_match('#^@[\w\\\\]+\z#', $name)) {
302: $name = $builder->getByType(substr($name, 1), true);
303: } elseif ($namespace) {
304: $name = $namespace . '.' . $name;
305: }
306:
307: if ($def === false) {
308: $builder->removeDefinition($name);
309: continue;
310: }
311: if ($namespace) {
312: $def = Helpers::prefixServiceName($def, $namespace);
313: }
314:
315: $params = $builder->parameters;
316: if (is_array($def) && isset($def['parameters'])) {
317: foreach ((array) $def['parameters'] as $k => $v) {
318: $v = explode(' ', is_int($k) ? $v : $k);
319: $params[end($v)] = $builder::literal('$' . end($v));
320: }
321: }
322: $def = Helpers::expand($def, $params);
323:
324: if (is_array($def) && !empty($def['alteration']) && !$builder->hasDefinition($name)) {
325: throw new ServiceCreationException("Service '$name': missing original definition for alteration.");
326: }
327:
328: if (($parent = Config\Helpers::takeParent($def)) && $parent !== $name) {
329: if ($parent !== Config\Helpers::OVERWRITE) {
330: trigger_error("Section inheritance $name < $parent is deprecated.", E_USER_DEPRECATED);
331: }
332: $builder->removeDefinition($name);
333: $definition = $builder->addDefinition(
334: $name,
335: $parent === Config\Helpers::OVERWRITE ? null : clone $builder->getDefinition($parent)
336: );
337: } elseif ($builder->hasDefinition($name)) {
338: $definition = $builder->getDefinition($name);
339: } else {
340: $definition = $builder->addDefinition($name);
341: }
342:
343: try {
344: static::loadDefinition($definition, $def);
345: } catch (\Exception $e) {
346: throw new ServiceCreationException("Service '$name': " . $e->getMessage(), 0, $e);
347: }
348: }
349: }
350:
351:
352: 353: 354: 355:
356: public static function loadDefinition(ServiceDefinition $definition, $config)
357: {
358: if ($config === null) {
359: return;
360:
361: } elseif (is_string($config) && interface_exists($config)) {
362: $config = ['class' => null, 'implement' => $config];
363:
364: } elseif ($config instanceof Statement && is_string($config->getEntity()) && interface_exists($config->getEntity())) {
365: $config = ['class' => null, 'implement' => $config->getEntity(), 'factory' => array_shift($config->arguments)];
366:
367: } elseif (!is_array($config) || isset($config[0], $config[1])) {
368: $config = ['class' => null, 'factory' => $config];
369: }
370:
371: if (array_key_exists('create', $config)) {
372: trigger_error("Key 'create' is deprecated, use 'factory' or 'type' in configuration.", E_USER_DEPRECATED);
373: $config['factory'] = $config['create'];
374: unset($config['create']);
375: }
376:
377: $known = ['type', 'class', 'factory', 'arguments', 'setup', 'autowired', 'dynamic', 'imported', 'inject', 'parameters', 'implement', 'run', 'tags', 'alteration'];
378: if ($error = array_diff(array_keys($config), $known)) {
379: $hints = array_filter(array_map(function ($error) use ($known) {
380: return Nette\Utils\ObjectHelpers::getSuggestion($known, $error);
381: }, $error));
382: $hint = $hints ? ", did you mean '" . implode("', '", $hints) . "'?" : '.';
383: throw new Nette\InvalidStateException(sprintf("Unknown key '%s' in definition of service$hint", implode("', '", $error)));
384: }
385:
386: $config = Helpers::filterArguments($config);
387:
388: if (array_key_exists('class', $config) || array_key_exists('factory', $config)) {
389: $definition->setType(null);
390: $definition->setFactory(null);
391: }
392:
393: if (array_key_exists('type', $config)) {
394: Validators::assertField($config, 'type', 'string|null');
395: $definition->setType($config['type']);
396: if (array_key_exists('class', $config)) {
397: throw new Nette\InvalidStateException("Unexpected 'class' when 'type' is used.");
398: }
399: }
400:
401: if (array_key_exists('class', $config)) {
402: Validators::assertField($config, 'class', 'string|Nette\DI\Statement|null');
403: if (!$config['class'] instanceof Statement) {
404: $definition->setType($config['class']);
405: }
406: $definition->setFactory($config['class']);
407: }
408:
409: if (array_key_exists('factory', $config)) {
410: Validators::assertField($config, 'factory', 'callable|Nette\DI\Statement|null');
411: $definition->setFactory($config['factory']);
412: }
413:
414: if (array_key_exists('arguments', $config)) {
415: Validators::assertField($config, 'arguments', 'array');
416: $arguments = $config['arguments'];
417: if (!Config\Helpers::takeParent($arguments) && !Nette\Utils\Arrays::isList($arguments) && $definition->getFactory()) {
418: $arguments += $definition->getFactory()->arguments;
419: }
420: $definition->setArguments($arguments);
421: }
422:
423: if (isset($config['setup'])) {
424: if (Config\Helpers::takeParent($config['setup'])) {
425: $definition->setSetup([]);
426: }
427: Validators::assertField($config, 'setup', 'list');
428: foreach ($config['setup'] as $id => $setup) {
429: Validators::assert($setup, 'callable|Nette\DI\Statement|array:1', "setup item #$id");
430: if (is_array($setup)) {
431: $setup = new Statement(key($setup), array_values($setup));
432: }
433: $definition->addSetup($setup);
434: }
435: }
436:
437: if (isset($config['parameters'])) {
438: Validators::assertField($config, 'parameters', 'array');
439: $definition->setParameters($config['parameters']);
440: }
441:
442: if (isset($config['implement'])) {
443: Validators::assertField($config, 'implement', 'string');
444: $definition->setImplement($config['implement']);
445: $definition->setAutowired(true);
446: }
447:
448: if (isset($config['autowired'])) {
449: Validators::assertField($config, 'autowired', 'bool|string|array');
450: $definition->setAutowired($config['autowired']);
451: }
452:
453: if (isset($config['imported'])) {
454: $config['dynamic'] = $config['imported'];
455: }
456:
457: if (isset($config['dynamic'])) {
458: Validators::assertField($config, 'dynamic', 'bool');
459: $definition->setDynamic($config['dynamic']);
460: }
461:
462: if (isset($config['inject'])) {
463: Validators::assertField($config, 'inject', 'bool');
464: $definition->addTag(Extensions\InjectExtension::TAG_INJECT, $config['inject']);
465: }
466:
467: if (isset($config['run'])) {
468: trigger_error("Option 'run' is deprecated, use 'run' as tag.", E_USER_DEPRECATED);
469: $config['tags']['run'] = (bool) $config['run'];
470: }
471:
472: if (isset($config['tags'])) {
473: Validators::assertField($config, 'tags', 'array');
474: if (Config\Helpers::takeParent($config['tags'])) {
475: $definition->setTags([]);
476: }
477: foreach ($config['tags'] as $tag => $attrs) {
478: if (is_int($tag) && is_string($attrs)) {
479: $definition->addTag($attrs);
480: } else {
481: $definition->addTag($tag, $attrs);
482: }
483: }
484: }
485: }
486:
487:
488:
489: public static function filterArguments(array $args)
490: {
491: return Helpers::filterArguments($args);
492: }
493:
494:
495:
496: public static function parseServices(ContainerBuilder $builder, array $config, $namespace = null)
497: {
498: self::loadDefinitions($builder, isset($config['services']) ? $config['services'] : [], $namespace);
499: }
500:
501:
502:
503: public static function parseService(ServiceDefinition $definition, $config)
504: {
505: self::loadDefinition($definition, $config);
506: }
507: }
508: