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