1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\DI;
9:
10: use Nette;
11: use Nette\Utils\Validators;
12: use Nette\Utils\Strings;
13: use Nette\Reflection;
14: use Nette\PhpGenerator\Helpers as PhpHelpers;
15:
16:
17: 18: 19: 20: 21: 22: 23:
24: class ContainerBuilder extends Nette\Object
25: {
26: const THIS_SERVICE = 'self',
27: THIS_CONTAINER = 'container';
28:
29:
30: public $parameters = array();
31:
32:
33: private $definitions = array();
34:
35:
36: private $classes;
37:
38:
39: private $dependencies = array();
40:
41:
42: private $generatedClasses = array();
43:
44:
45: public $currentService;
46:
47:
48: 49: 50: 51: 52:
53: public function addDefinition($name, ServiceDefinition $definition = NULL)
54: {
55: if (!is_string($name) || !$name) {
56: throw new Nette\InvalidArgumentException(sprintf('Service name must be a non-empty string, %s given.', gettype($name)));
57:
58: } elseif (isset($this->definitions[$name])) {
59: throw new Nette\InvalidStateException("Service '$name' has already been added.");
60: }
61: return $this->definitions[$name] = $definition ?: new ServiceDefinition;
62: }
63:
64:
65: 66: 67: 68: 69:
70: public function removeDefinition($name)
71: {
72: unset($this->definitions[$name]);
73: }
74:
75:
76: 77: 78: 79: 80:
81: public function getDefinition($name)
82: {
83: if (!isset($this->definitions[$name])) {
84: throw new MissingServiceException("Service '$name' not found.");
85: }
86: return $this->definitions[$name];
87: }
88:
89:
90: 91: 92: 93:
94: public function getDefinitions()
95: {
96: return $this->definitions;
97: }
98:
99:
100: 101: 102: 103: 104:
105: public function hasDefinition($name)
106: {
107: return isset($this->definitions[$name]);
108: }
109:
110:
111:
112:
113:
114: 115: 116: 117: 118: 119:
120: public function getByType($class)
121: {
122: if ($this->currentService !== NULL && Reflection\ClassType::from($this->definitions[$this->currentService]->class)->is($class)) {
123: return $this->currentService;
124: }
125:
126: $lower = ltrim(strtolower($class), '\\');
127: if (!isset($this->classes[$lower])) {
128: return;
129:
130: } elseif (count($this->classes[$lower]) === 1) {
131: return $this->classes[$lower][0];
132:
133: } else {
134: throw new ServiceCreationException("Multiple services of type $class found: " . implode(', ', $this->classes[$lower]));
135: }
136: }
137:
138:
139: 140: 141: 142: 143:
144: public function findByTag($tag)
145: {
146: $found = array();
147: foreach ($this->definitions as $name => $def) {
148: if (isset($def->tags[$tag])) {
149: $found[$name] = $def->tags[$tag];
150: }
151: }
152: return $found;
153: }
154:
155:
156: 157: 158: 159:
160: public function autowireArguments($class, $method, array $arguments)
161: {
162: $rc = Reflection\ClassType::from($class);
163: if (!$rc->hasMethod($method)) {
164: if (!Nette\Utils\Arrays::isList($arguments)) {
165: throw new ServiceCreationException("Unable to pass specified arguments to $class::$method().");
166: }
167: return $arguments;
168: }
169:
170: $rm = $rc->getMethod($method);
171: if (!$rm->isPublic()) {
172: throw new ServiceCreationException("$rm is not callable.");
173: }
174: $this->addDependency($rm->getFileName());
175: return Helpers::autowireArguments($rm, $arguments, $this);
176: }
177:
178:
179: 180: 181: 182: 183:
184: public function prepareClassList()
185: {
186: $this->classes = FALSE;
187:
188:
189: foreach ($this->definitions as $name => $def) {
190: if (!$def->implement) {
191: continue;
192: }
193:
194: if (!interface_exists($def->implement)) {
195: throw new ServiceCreationException("Interface $def->implement used in service '$name' not found.");
196: }
197: $rc = Reflection\ClassType::from($def->implement);
198: $method = $rc->hasMethod('create') ? $rc->getMethod('create') : ($rc->hasMethod('get') ? $rc->getMethod('get') : NULL);
199: if (count($rc->getMethods()) !== 1 || !$method || $method->isStatic()) {
200: throw new ServiceCreationException("Interface $def->implement used in service '$name' must have just one non-static method create() or get().");
201: }
202: $def->implement = $rc->getName();
203: $def->implementType = $rc->hasMethod('create') ? 'create' : 'get';
204:
205: if (!$def->class && empty($def->factory->entity)) {
206: $returnType = $method->getAnnotation('return');
207: if (!$returnType) {
208: throw new ServiceCreationException("Method $method used in service '$name' has no @return annotation.");
209: }
210:
211: $returnType = Reflection\AnnotationsParser::expandClassName(preg_replace('#[|\s].*#', '', $returnType), $rc);
212: if (!class_exists($returnType)) {
213: throw new ServiceCreationException("Please check a @return annotation of the $method method used in service '$name'. Class '$returnType' cannot be found.");
214: }
215: $def->setClass($returnType);
216: }
217:
218: if ($method->getName() === 'get') {
219: if ($method->getParameters()) {
220: throw new ServiceCreationException("Method $method used in service '$name' must have no arguments.");
221: }
222: if (empty($def->factory->entity)) {
223: $def->setFactory('@\\' . ltrim($def->class, '\\'));
224: } elseif (!$this->getServiceName($def->factory->entity)) {
225: throw new ServiceCreationException("Invalid factory in service '$name' definition.");
226: }
227: }
228:
229: if (!$def->parameters) {
230: foreach ($method->getParameters() as $param) {
231: $paramDef = ($param->isArray() ? 'array' : $param->getClassName()) . ' ' . $param->getName();
232: if ($param->isOptional()) {
233: $def->parameters[$paramDef] = $param->getDefaultValue();
234: } else {
235: $def->parameters[] = $paramDef;
236: }
237: }
238: }
239: }
240:
241:
242: foreach ($this->definitions as $name => $def) {
243: if (!$def->factory || !$def->factory->entity) {
244: if (!$def->class) {
245: throw new ServiceCreationException("Class and factory are missing in service '$name' definition.");
246: }
247: if ($def->factory) {
248: $def->factory->entity = $def->class;
249: } else {
250: $def->factory = new Statement($def->class);
251: }
252: }
253: }
254:
255:
256: foreach ($this->definitions as $name => $def) {
257: $factory = $def->factory->entity = $this->normalizeEntity($def->factory->entity);
258:
259: if (is_string($factory) && preg_match('#^[\w\\\\]+\z#', $factory) && $factory !== self::THIS_SERVICE) {
260: if (!class_exists($factory) || !Reflection\ClassType::from($factory)->isInstantiable()) {
261: throw new ServiceCreationException("Class $factory used in service '$name' not found or is not instantiable.");
262: }
263: }
264: }
265:
266:
267: foreach ($this->definitions as $name => $def) {
268: $this->resolveClass($name);
269:
270: if (!$def->class) {
271: continue;
272: } elseif (!class_exists($def->class) && !interface_exists($def->class)) {
273: throw new ServiceCreationException("Class or interface $def->class used in service '$name' not found.");
274: } else {
275: $def->class = Reflection\ClassType::from($def->class)->getName();
276: }
277: }
278:
279:
280: $this->classes = array();
281: foreach ($this->definitions as $name => $def) {
282: $class = $def->implement ?: $def->class;
283: if ($def->autowired && $class) {
284: foreach (class_parents($class) + class_implements($class) + array($class) as $parent) {
285: $this->classes[strtolower($parent)][] = (string) $name;
286: }
287: }
288: }
289:
290: foreach ($this->classes as $class => $foo) {
291: $this->addDependency(Reflection\ClassType::from($class)->getFileName());
292: }
293: }
294:
295:
296: private function resolveClass($name, $recursive = array())
297: {
298: if (isset($recursive[$name])) {
299: throw new ServiceCreationException(sprintf('Circular reference detected for services: %s.', implode(', ', array_keys($recursive))));
300: }
301: $recursive[$name] = TRUE;
302:
303: $def = $this->definitions[$name];
304: $factory = $def->factory->entity;
305:
306: if ($def->class) {
307: return $def->class;
308:
309: } elseif (is_array($factory)) {
310: if ($service = $this->getServiceName($factory[0])) {
311: if (Strings::contains($service, '\\')) {
312: $factory[0] = $service;
313: } else {
314: $factory[0] = $this->resolveClass($service, $recursive);
315: if (!$factory[0]) {
316: return;
317: }
318: if ($this->definitions[$service]->implement && $factory[1] === 'create') {
319: return $def->class = $factory[0];
320: }
321: }
322: }
323: try {
324: $reflection = Nette\Utils\Callback::toReflection($factory);
325: } catch (\ReflectionException $e) {
326: }
327: if (isset($e) || !is_callable($factory)) {
328: throw new ServiceCreationException(sprintf("Factory '%s' used in service '%s' is not callable.", Nette\Utils\Callback::toString($factory), $name));
329: }
330: $def->class = preg_replace('#[|\s].*#', '', $reflection->getAnnotation('return'));
331: if ($def->class && $reflection instanceof \ReflectionMethod) {
332: $def->class = Reflection\AnnotationsParser::expandClassName($tmp = $def->class, $reflection->getDeclaringClass());
333: if ($tmp !== $def->class && $tmp[0] !== '\\' && class_exists($tmp)) {
334: $def->class = $tmp;
335: trigger_error("You should use @return \\$tmp' in $reflection.", E_USER_WARNING);
336: }
337: }
338:
339: } elseif ($service = $this->getServiceName($factory)) {
340: if (!$def->implement) {
341: $def->autowired = FALSE;
342: }
343: if (Strings::contains($service, '\\')) {
344: return $def->class = $service;
345: }
346: if ($this->definitions[$service]->implement) {
347: $def->autowired = FALSE;
348: }
349: return $def->class = $this->definitions[$service]->implement ?: $this->resolveClass($service, $recursive);
350:
351: } else {
352: return $def->class = $factory;
353: }
354: }
355:
356:
357: 358: 359: 360:
361: public function addDependency($file)
362: {
363: $this->dependencies[$file] = TRUE;
364: return $this;
365: }
366:
367:
368: 369: 370: 371:
372: public function getDependencies()
373: {
374: unset($this->dependencies[FALSE]);
375: return array_keys($this->dependencies);
376: }
377:
378:
379:
380:
381:
382: 383: 384: 385:
386: public function generateClasses($className = 'Container', $parentName = 'Nette\DI\Container')
387: {
388: unset($this->definitions[self::THIS_CONTAINER]);
389: $this->addDefinition(self::THIS_CONTAINER)->setClass('Nette\DI\Container');
390:
391: $this->generatedClasses = array();
392: $this->prepareClassList();
393:
394: $containerClass = $this->generatedClasses[] = new Nette\PhpGenerator\ClassType($className);
395: $containerClass->setExtends($parentName);
396: $containerClass->addMethod('__construct')
397: ->addBody('parent::__construct(?);', array($this->parameters));
398:
399: $definitions = $this->definitions;
400: ksort($definitions);
401:
402: $meta = $containerClass->addProperty('meta', array())
403: ->setVisibility('protected')
404: ->setValue(array(Container::TYPES => $this->classes));
405:
406: foreach ($definitions as $name => $def) {
407: foreach ($def->tags as $tag => $value) {
408: $meta->value[Container::TAGS][$tag][$name] = $value;
409: }
410: }
411:
412: foreach ($definitions as $name => $def) {
413: try {
414: $name = (string) $name;
415: $methodName = Container::getMethodName($name);
416: if (!PhpHelpers::isIdentifier($methodName)) {
417: throw new ServiceCreationException('Name contains invalid characters.');
418: }
419: $containerClass->addMethod($methodName)
420: ->addDocument('@return ' . ($def->implement ?: $def->class))
421: ->setBody($name === self::THIS_CONTAINER ? 'return $this;' : $this->generateService($name))
422: ->setParameters($def->implement ? array() : $this->convertParameters($def->parameters));
423: } catch (\Exception $e) {
424: throw new ServiceCreationException("Service '$name': " . $e->getMessage(), NULL, $e);
425: }
426: }
427:
428: return $this->generatedClasses;
429: }
430:
431:
432: 433: 434: 435:
436: private function generateService($name)
437: {
438: $this->currentService = NULL;
439: $def = $this->definitions[$name];
440:
441: $serviceRef = $this->getServiceName($def->factory->entity);
442: $factory = $serviceRef && !$def->factory->arguments && !$def->setup && $def->implementType !== 'create'
443: ? new Statement(array('@' . ContainerBuilder::THIS_CONTAINER, 'getService'), array($serviceRef))
444: : $def->factory;
445:
446: $code = '$service = ' . $this->formatStatement($factory) . ";\n";
447: $this->currentService = $name;
448:
449: if ($def->class && $def->class !== $def->factory->entity && !$serviceRef) {
450: $code .= PhpHelpers::formatArgs("if (!\$service instanceof $def->class) {\n"
451: . "\tthrow new Nette\\UnexpectedValueException(?);\n}\n",
452: array("Unable to create service '$name', value returned by factory is not $def->class type.")
453: );
454: }
455:
456: $setups = (array) $def->setup;
457: if ($def->inject && $def->class) {
458: $injects = array();
459: foreach (Helpers::getInjectProperties(Reflection\ClassType::from($def->class), $this) as $property => $type) {
460: $injects[] = new Statement('$' . $property, array('@\\' . ltrim($type, '\\')));
461: }
462:
463: foreach (get_class_methods($def->class) as $method) {
464: if (substr($method, 0, 6) === 'inject') {
465: $injects[] = new Statement($method);
466: }
467: }
468:
469: foreach ($injects as $inject) {
470: foreach ($setups as $key => $setup) {
471: if ($setup->entity === $inject->entity) {
472: $inject = $setup;
473: unset($setups[$key]);
474: }
475: }
476: array_unshift($setups, $inject);
477: }
478: }
479:
480: foreach ($setups as $setup) {
481: if (is_string($setup->entity) && strpbrk($setup->entity, ':@?') === FALSE) {
482: $setup->entity = array('@self', $setup->entity);
483: }
484: $code .= $this->formatStatement($setup) . ";\n";
485: }
486: $this->currentService = NULL;
487:
488: $code .= 'return $service;';
489:
490: if (!$def->implement) {
491: return $code;
492: }
493:
494: $factoryClass = $this->generatedClasses[] = new Nette\PhpGenerator\ClassType;
495: $factoryClass->setName(str_replace(array('\\', '.'), '_', "{$this->generatedClasses[0]->name}_{$def->implement}Impl_{$name}"))
496: ->addImplement($def->implement)
497: ->setFinal(TRUE);
498:
499: $factoryClass->addProperty('container')
500: ->setVisibility('private');
501:
502: $factoryClass->addMethod('__construct')
503: ->addBody('$this->container = $container;')
504: ->addParameter('container')
505: ->setTypeHint('Nette\DI\Container');
506:
507: $factoryClass->addMethod($def->implementType)
508: ->setParameters($this->convertParameters($def->parameters))
509: ->setBody(str_replace('$this', '$this->container', $code));
510:
511: return "return new {$factoryClass->name}(\$this);";
512: }
513:
514:
515: 516: 517: 518:
519: private function convertParameters(array $parameters)
520: {
521: $res = array();
522: foreach ($parameters as $k => $v) {
523: $tmp = explode(' ', is_int($k) ? $v : $k);
524: $param = $res[] = new Nette\PhpGenerator\Parameter;
525: $param->setName(end($tmp));
526: if (!is_int($k)) {
527: $param = $param->setOptional(TRUE)->setDefaultValue($v);
528: }
529: if (isset($tmp[1])) {
530: $param->setTypeHint($tmp[0]);
531: }
532: }
533: return $res;
534: }
535:
536:
537: 538: 539: 540: 541:
542: public function formatStatement(Statement $statement)
543: {
544: $entity = $this->normalizeEntity($statement->entity);
545: $arguments = $statement->arguments;
546:
547: if (is_string($entity) && Strings::contains($entity, '?')) {
548: return $this->formatPhp($entity, $arguments);
549:
550: } elseif ($service = $this->getServiceName($entity)) {
551: $params = array();
552: foreach ($this->definitions[$service]->parameters as $k => $v) {
553: $params[] = preg_replace('#\w+\z#', '\$$0', (is_int($k) ? $v : $k)) . (is_int($k) ? '' : ' = ' . PhpHelpers::dump($v));
554: }
555: $rm = new Reflection\GlobalFunction(create_function(implode(', ', $params), ''));
556: $arguments = Helpers::autowireArguments($rm, $arguments, $this);
557: return $this->formatPhp('$this->?(?*)', array(Container::getMethodName($service), $arguments));
558:
559: } elseif ($entity === 'not') {
560: return $this->formatPhp('!?', array($arguments[0]));
561:
562: } elseif (is_string($entity)) {
563: if ($constructor = Reflection\ClassType::from($entity)->getConstructor()) {
564: $this->addDependency($constructor->getFileName());
565: $arguments = Helpers::autowireArguments($constructor, $arguments, $this);
566: } elseif ($arguments) {
567: throw new ServiceCreationException("Unable to pass arguments, class $entity has no constructor.");
568: }
569: return $this->formatPhp("new $entity" . ($arguments ? '(?*)' : ''), array($arguments));
570:
571: } elseif (!Nette\Utils\Arrays::isList($entity) || count($entity) !== 2) {
572: throw new ServiceCreationException(sprintf('Expected class, method or property, %s given.', PhpHelpers::dump($entity)));
573:
574: } elseif ($entity[0] === '') {
575: return $this->formatPhp("$entity[1](?*)", array($arguments));
576:
577: } elseif (Strings::contains($entity[1], '$')) {
578: Validators::assert($arguments, 'list:1', "setup arguments for '" . Nette\Utils\Callback::toString($entity) . "'");
579: if ($this->getServiceName($entity[0])) {
580: return $this->formatPhp('?->? = ?', array($entity[0], substr($entity[1], 1), $arguments[0]));
581: } else {
582: return $this->formatPhp($entity[0] . '::$? = ?', array(substr($entity[1], 1), $arguments[0]));
583: }
584:
585: } elseif ($service = $this->getServiceName($entity[0])) {
586: $class = $this->definitions[$service]->implement;
587: if (!$class || !method_exists($class, $entity[1])) {
588: $class = $this->definitions[$service]->class;
589: }
590: if ($class) {
591: $arguments = $this->autowireArguments($class, $entity[1], $arguments);
592: }
593: return $this->formatPhp('?->?(?*)', array($entity[0], $entity[1], $arguments));
594:
595: } else {
596: $arguments = $this->autowireArguments($entity[0], $entity[1], $arguments);
597: return $this->formatPhp("$entity[0]::$entity[1](?*)", array($arguments));
598: }
599: }
600:
601:
602: 603: 604: 605: 606:
607: public function formatPhp($statement, $args)
608: {
609: $that = $this;
610: array_walk_recursive($args, function (& $val) use ($that) {
611: if ($val instanceof Statement) {
612: $val = ContainerBuilder::literal($that->formatStatement($val));
613:
614: } elseif ($val === $that) {
615: $val = ContainerBuilder::literal('$this');
616:
617: } elseif ($val instanceof ServiceDefinition) {
618: $val = '@' . current(array_keys($that->definitions, $val, TRUE));
619:
620: } elseif (is_string($val) && preg_match('#^[\w\\\\]*::[A-Z][A-Z0-9_]*\z#', $val, $m)) {
621: $val = ContainerBuilder::literal(ltrim($val, ':'));
622: }
623:
624: if (is_string($val) && substr($val, 0, 1) === '@') {
625: $pair = explode('::', $val, 2);
626: $name = $that->getServiceName($pair[0]);
627: if (isset($pair[1]) && preg_match('#^[A-Z][A-Z0-9_]*\z#', $pair[1], $m)) {
628: $val = $that->definitions[$name]->class . '::' . $pair[1];
629: } else {
630: if ($name === ContainerBuilder::THIS_CONTAINER) {
631: $val = '$this';
632: } elseif ($name === $that->currentService) {
633: $val = '$service';
634: } else {
635: $val = $that->formatStatement(new Statement(array('@' . ContainerBuilder::THIS_CONTAINER, 'getService'), array($name)));
636: }
637: $val .= (isset($pair[1]) ? PhpHelpers::formatArgs('->?', array($pair[1])) : '');
638: }
639: $val = ContainerBuilder::literal($val);
640: }
641: });
642: return PhpHelpers::formatArgs($statement, $args);
643: }
644:
645:
646: 647: 648: 649:
650: public function expand($value)
651: {
652: return Helpers::expand($value, $this->parameters);
653: }
654:
655:
656: 657: 658:
659: public static function literal($phpCode)
660: {
661: return new Nette\PhpGenerator\PhpLiteral($phpCode);
662: }
663:
664:
665:
666: public function normalizeEntity($entity)
667: {
668: if (is_string($entity) && Strings::contains($entity, '::') && !Strings::contains($entity, '?')) {
669: $entity = explode('::', $entity);
670: }
671:
672: if (is_array($entity) && $entity[0] instanceof ServiceDefinition) {
673: $entity[0] = '@' . current(array_keys($this->definitions, $entity[0], TRUE));
674:
675: } elseif ($entity instanceof ServiceDefinition) {
676: $entity = '@' . current(array_keys($this->definitions, $entity, TRUE));
677:
678: } elseif (is_array($entity) && $entity[0] === $this) {
679: $entity[0] = '@' . ContainerBuilder::THIS_CONTAINER;
680: }
681: return $entity;
682: }
683:
684:
685: 686: 687: 688: 689:
690: public function getServiceName($arg)
691: {
692: if (!is_string($arg) || !preg_match('#^@[\w\\\\.].*\z#', $arg)) {
693: return FALSE;
694: }
695: $service = substr($arg, 1);
696: if ($service === self::THIS_SERVICE) {
697: $service = $this->currentService;
698: }
699: if (Strings::contains($service, '\\')) {
700: if ($this->classes === FALSE) {
701: return $service;
702: }
703: $res = $this->getByType($service);
704: if (!$res) {
705: throw new ServiceCreationException("Reference to missing service of type $service.");
706: }
707: return $res;
708: }
709: if (!isset($this->definitions[$service])) {
710: throw new ServiceCreationException("Reference to missing service '$service'.");
711: }
712: return $service;
713: }
714:
715:
716:
717: function generateClass()
718: {
719: throw new Nette\DeprecatedException(__METHOD__ . '() is deprecated; use generateClasses()[0] instead.');
720: }
721:
722: }
723: