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