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('isset($this->parameters[?]) ? $this->parameters[?] : ?', [$key, ContainerBuilder::literal('?'), $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\ObjectMixin::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: foreach ($services as $name => $def) {
279: $path = [];
280: while (Config\Helpers::isInheriting($def)) {
281: $path[] = $def;
282: $def = isset($services[$def[Config\Helpers::EXTENDS_KEY]]) ? $services[$def[Config\Helpers::EXTENDS_KEY]] : [];
283: if (in_array($def, $path, TRUE)) {
284: throw new ServiceCreationException("Circular reference detected for service '$name'.");
285: }
286: }
287: $depths[$name] = count($path);
288: }
289: array_multisort($depths, $services);
290:
291: foreach ($services as $name => $def) {
292: if ((string) (int) $name === (string) $name) {
293: $postfix = $def instanceof Statement && is_string($def->getEntity()) ? '.' . $def->getEntity() : (is_scalar($def) ? ".$def" : '');
294: $name = (count($builder->getDefinitions()) + 1) . preg_replace('#\W+#', '_', $postfix);
295: } elseif ($namespace) {
296: $name = $namespace . '.' . $name;
297: }
298:
299: if ($def === FALSE) {
300: $builder->removeDefinition($name);
301: continue;
302: }
303: if ($namespace) {
304: $def = Helpers::prefixServiceName($def, $namespace);
305: }
306:
307: $params = $builder->parameters;
308: if (is_array($def) && isset($def['parameters'])) {
309: foreach ((array) $def['parameters'] as $k => $v) {
310: $v = explode(' ', is_int($k) ? $v : $k);
311: $params[end($v)] = $builder::literal('$' . end($v));
312: }
313: }
314: $def = Helpers::expand($def, $params);
315:
316: if (is_array($def) && !empty($def['alteration']) && !$builder->hasDefinition($name)) {
317: throw new ServiceCreationException("Service '$name': missing original definition for alteration.");
318: }
319:
320: if (($parent = Config\Helpers::takeParent($def)) && $parent !== $name) {
321: if ($parent !== Config\Helpers::OVERWRITE) {
322: trigger_error("Section inheritance $name < $parent is deprecated.", E_USER_DEPRECATED);
323: }
324: $builder->removeDefinition($name);
325: $definition = $builder->addDefinition(
326: $name,
327: $parent === Config\Helpers::OVERWRITE ? NULL : clone $builder->getDefinition($parent)
328: );
329: } elseif ($builder->hasDefinition($name)) {
330: $definition = $builder->getDefinition($name);
331: } else {
332: $definition = $builder->addDefinition($name);
333: }
334:
335: try {
336: static::loadDefinition($definition, $def);
337: } catch (\Exception $e) {
338: throw new ServiceCreationException("Service '$name': " . $e->getMessage(), 0, $e);
339: }
340: }
341: }
342:
343:
344: 345: 346: 347:
348: public static function loadDefinition(ServiceDefinition $definition, $config)
349: {
350: if ($config === NULL) {
351: return;
352:
353: } elseif (is_string($config) && interface_exists($config)) {
354: $config = ['class' => NULL, 'implement' => $config];
355:
356: } elseif ($config instanceof Statement && is_string($config->getEntity()) && interface_exists($config->getEntity())) {
357: $config = ['class' => NULL, 'implement' => $config->getEntity(), 'factory' => array_shift($config->arguments)];
358:
359: } elseif (!is_array($config) || isset($config[0], $config[1])) {
360: $config = ['class' => NULL, 'factory' => $config];
361: }
362:
363: if (array_key_exists('create', $config)) {
364: trigger_error("Key 'create' is deprecated, use 'factory' or 'class' in configuration.", E_USER_DEPRECATED);
365: $config['factory'] = $config['create'];
366: unset($config['create']);
367: }
368:
369: $known = ['class', 'factory', 'arguments', 'setup', 'autowired', 'dynamic', 'inject', 'parameters', 'implement', 'run', 'tags', 'alteration'];
370: if ($error = array_diff(array_keys($config), $known)) {
371: $hints = array_filter(array_map(function ($error) use ($known) {
372: return Nette\Utils\ObjectMixin::getSuggestion($known, $error);
373: }, $error));
374: $hint = $hints ? ", did you mean '" . implode("', '", $hints) . "'?" : '.';
375: throw new Nette\InvalidStateException(sprintf("Unknown key '%s' in definition of service$hint", implode("', '", $error)));
376: }
377:
378: $config = Helpers::filterArguments($config);
379:
380: if (array_key_exists('class', $config) || array_key_exists('factory', $config)) {
381: $definition->setClass(NULL);
382: $definition->setFactory(NULL);
383: }
384:
385: if (array_key_exists('class', $config)) {
386: Validators::assertField($config, 'class', 'string|Nette\DI\Statement|null');
387: if (!$config['class'] instanceof Statement) {
388: $definition->setClass($config['class']);
389: }
390: $definition->setFactory($config['class']);
391: }
392:
393: if (array_key_exists('factory', $config)) {
394: Validators::assertField($config, 'factory', 'callable|Nette\DI\Statement|null');
395: $definition->setFactory($config['factory']);
396: }
397:
398: if (array_key_exists('arguments', $config)) {
399: Validators::assertField($config, 'arguments', 'array');
400: $arguments = $config['arguments'];
401: if (!Config\Helpers::takeParent($arguments) && !Nette\Utils\Arrays::isList($arguments) && $definition->getFactory()) {
402: $arguments += $definition->getFactory()->arguments;
403: }
404: $definition->setArguments($arguments);
405: }
406:
407: if (isset($config['setup'])) {
408: if (Config\Helpers::takeParent($config['setup'])) {
409: $definition->setSetup([]);
410: }
411: Validators::assertField($config, 'setup', 'list');
412: foreach ($config['setup'] as $id => $setup) {
413: Validators::assert($setup, 'callable|Nette\DI\Statement|array:1', "setup item #$id");
414: if (is_array($setup)) {
415: $setup = new Statement(key($setup), array_values($setup));
416: }
417: $definition->addSetup($setup);
418: }
419: }
420:
421: if (isset($config['parameters'])) {
422: Validators::assertField($config, 'parameters', 'array');
423: $definition->setParameters($config['parameters']);
424: }
425:
426: if (isset($config['implement'])) {
427: Validators::assertField($config, 'implement', 'string');
428: $definition->setImplement($config['implement']);
429: $definition->setAutowired(TRUE);
430: }
431:
432: if (isset($config['autowired'])) {
433: Validators::assertField($config, 'autowired', 'bool|string|array');
434: $definition->setAutowired($config['autowired']);
435: }
436:
437: if (isset($config['dynamic'])) {
438: Validators::assertField($config, 'dynamic', 'bool');
439: $definition->setDynamic($config['dynamic']);
440: }
441:
442: if (isset($config['inject'])) {
443: Validators::assertField($config, 'inject', 'bool');
444: $definition->addTag(Extensions\InjectExtension::TAG_INJECT, $config['inject']);
445: }
446:
447: if (isset($config['run'])) {
448: trigger_error("Option 'run' is deprecated, use 'run' as tag.", E_USER_DEPRECATED);
449: $config['tags']['run'] = (bool) $config['run'];
450: }
451:
452: if (isset($config['tags'])) {
453: Validators::assertField($config, 'tags', 'array');
454: if (Config\Helpers::takeParent($config['tags'])) {
455: $definition->setTags([]);
456: }
457: foreach ($config['tags'] as $tag => $attrs) {
458: if (is_int($tag) && is_string($attrs)) {
459: $definition->addTag($attrs);
460: } else {
461: $definition->addTag($tag, $attrs);
462: }
463: }
464: }
465: }
466:
467:
468:
469: public static function filterArguments(array $args)
470: {
471: return Helpers::filterArguments($args);
472: }
473:
474:
475:
476: public static function parseServices(ContainerBuilder $builder, array $config, $namespace = NULL)
477: {
478: self::loadDefinitions($builder, isset($config['services']) ? $config['services'] : [], $namespace);
479: }
480:
481:
482:
483: public static function parseService(ServiceDefinition $definition, $config)
484: {
485: self::loadDefinition($definition, $config);
486: }
487:
488: }
489: