1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette\DI;
13:
14: use Nette,
15: Nette\Utils\Validators,
16: Nette\Utils\Strings,
17: Nette\Reflection,
18: Nette\Utils\PhpGenerator\Helpers as PhpHelpers,
19: Nette\Utils\PhpGenerator\PhpLiteral;
20:
21:
22:
23: 24: 25: 26: 27: 28: 29:
30: class ContainerBuilder extends Nette\Object
31: {
32: const CREATED_SERVICE = 'self',
33: THIS_CONTAINER = 'container';
34:
35:
36: public $parameters = array();
37:
38:
39: private $definitions = array();
40:
41:
42: private $classes;
43:
44:
45: private $dependencies = array();
46:
47:
48:
49: 50: 51: 52: 53:
54: public function addDefinition($name)
55: {
56: if (!is_string($name) || !$name) {
57: throw new Nette\InvalidArgumentException("Service name must be a non-empty string, " . gettype($name) . " given.");
58:
59: } elseif (isset($this->definitions[$name])) {
60: throw new Nette\InvalidStateException("Service '$name' has already been added.");
61: }
62: return $this->definitions[$name] = new ServiceDefinition;
63: }
64:
65:
66:
67: 68: 69: 70: 71:
72: public function removeDefinition($name)
73: {
74: unset($this->definitions[$name]);
75: }
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:
98: public function getDefinitions()
99: {
100: return $this->definitions;
101: }
102:
103:
104:
105: 106: 107: 108: 109:
110: public function hasDefinition($name)
111: {
112: return isset($this->definitions[$name]);
113: }
114:
115:
116:
117:
118:
119:
120:
121: 122: 123: 124: 125: 126:
127: public function getByType($class)
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:
148: public function findByTag($tag)
149: {
150: $found = array();
151: foreach ($this->definitions as $name => $def) {
152: if (isset($def->tags[$tag]) && $def->shared) {
153: $found[$name] = $def->tags[$tag];
154: }
155: }
156: return $found;
157: }
158:
159:
160:
161: 162: 163: 164:
165: public function autowireArguments($class, $method, array $arguments)
166: {
167: $rc = Reflection\ClassType::from($class);
168: if (!$rc->hasMethod($method)) {
169: if (!Nette\Utils\Validators::isList($arguments)) {
170: throw new ServiceCreationException("Unable to pass specified arguments to $class::$method().");
171: }
172: return $arguments;
173: }
174:
175: $rm = $rc->getMethod($method);
176: if ($rm->isAbstract() || !$rm->isPublic()) {
177: throw new ServiceCreationException("$rm is not callable.");
178: }
179: $this->addDependency($rm->getFileName());
180: return Helpers::autowireArguments($rm, $arguments, $this);
181: }
182:
183:
184:
185: 186: 187: 188:
189: public function prepareClassList()
190: {
191:
192: foreach ($this->definitions as $name => $def) {
193: if ($def->class === self::CREATED_SERVICE || ($def->factory && $def->factory->entity === self::CREATED_SERVICE)) {
194: $def->class = $name;
195: $def->internal = TRUE;
196: if ($def->factory && $def->factory->entity === self::CREATED_SERVICE) {
197: $def->factory->entity = $def->class;
198: }
199: unset($this->definitions[$name]);
200: $this->definitions['_anonymous_' . str_replace('\\', '_', strtolower(trim($name, '\\')))] = $def;
201: }
202:
203: if ($def->class) {
204: $def->class = $this->expand($def->class);
205: if (!$def->factory) {
206: $def->factory = new Statement($def->class);
207: }
208: } elseif (!$def->factory) {
209: throw new ServiceCreationException("Class and factory are missing in service '$name' definition.");
210: }
211: }
212:
213:
214: foreach ($this->definitions as $name => $def) {
215: $factory = $this->normalizeEntity($this->expand($def->factory->entity));
216: if (is_string($factory) && preg_match('#^[\w\\\\]+\z#', $factory) && $factory !== self::CREATED_SERVICE) {
217: if (!class_exists($factory) || !Reflection\ClassType::from($factory)->isInstantiable()) {
218: throw new Nette\InvalidStateException("Class $factory used in service '$name' has not been found or is not instantiable.");
219: }
220: }
221: }
222:
223:
224: $this->classes = FALSE;
225: foreach ($this->definitions as $name => $def) {
226: $this->resolveClass($name);
227: }
228:
229:
230: $this->classes = array();
231: foreach ($this->definitions as $name => $def) {
232: if (!$def->class) {
233: continue;
234: }
235: if (!class_exists($def->class) && !interface_exists($def->class)) {
236: throw new Nette\InvalidStateException("Class $def->class has not been found.");
237: }
238: $def->class = Reflection\ClassType::from($def->class)->getName();
239: if ($def->autowired) {
240: foreach (class_parents($def->class) + class_implements($def->class) + array($def->class) as $parent) {
241: $this->classes[strtolower($parent)][] = (string) $name;
242: }
243: }
244: }
245:
246: foreach ($this->classes as $class => $foo) {
247: $this->addDependency(Reflection\ClassType::from($class)->getFileName());
248: }
249: }
250:
251:
252:
253: private function resolveClass($name, $recursive = array())
254: {
255: if (isset($recursive[$name])) {
256: throw new Nette\InvalidArgumentException('Circular reference detected for services: ' . implode(', ', array_keys($recursive)) . '.');
257: }
258: $recursive[$name] = TRUE;
259:
260: $def = $this->definitions[$name];
261: $factory = $this->normalizeEntity($this->expand($def->factory->entity));
262:
263: if ($def->class) {
264: return $def->class;
265:
266: } elseif (is_array($factory)) {
267: if ($service = $this->getServiceName($factory[0])) {
268: if (Strings::contains($service, '\\')) {
269: throw new ServiceCreationException("Unable resolve class name for service '$name'.");
270: }
271: $factory[0] = $this->resolveClass($service, $recursive);
272: if (!$factory[0]) {
273: return;
274: }
275: }
276: $factory = new Nette\Callback($factory);
277: if (!$factory->isCallable()) {
278: throw new Nette\InvalidStateException("Factory '$factory' is not callable.");
279: }
280: try {
281: $reflection = $factory->toReflection();
282: $def->class = preg_replace('#[|\s].*#', '', $reflection->getAnnotation('return'));
283: if ($def->class && !class_exists($def->class) && $def->class[0] !== '\\' && $reflection instanceof \ReflectionMethod) {
284: $def->class = $reflection->getDeclaringClass()->getNamespaceName() . '\\' . $def->class;
285: }
286: } catch (\ReflectionException $e) {
287: }
288:
289: } elseif ($service = $this->getServiceName($factory)) {
290: if (Strings::contains($service, '\\')) {
291: $def->autowired = FALSE;
292: return $def->class = $service;
293: }
294: if ($this->definitions[$service]->shared) {
295: $def->autowired = FALSE;
296: }
297: return $def->class = $this->resolveClass($service, $recursive);
298:
299: } else {
300: return $def->class = $factory;
301: }
302: }
303:
304:
305:
306: 307: 308: 309:
310: public function addDependency($file)
311: {
312: $this->dependencies[$file] = TRUE;
313: return $this;
314: }
315:
316:
317:
318: 319: 320: 321:
322: public function getDependencies()
323: {
324: unset($this->dependencies[FALSE]);
325: return array_keys($this->dependencies);
326: }
327:
328:
329:
330:
331:
332:
333:
334: 335: 336: 337:
338: public function generateClass($parentClass = 'Nette\DI\Container')
339: {
340: unset($this->definitions[self::THIS_CONTAINER]);
341: $this->addDefinition(self::THIS_CONTAINER)->setClass($parentClass);
342:
343: $this->prepareClassList();
344:
345: $class = new Nette\Utils\PhpGenerator\ClassType('Container');
346: $class->addExtend($parentClass);
347: $class->addMethod('__construct')
348: ->addBody('parent::__construct(?);', array($this->expand($this->parameters)));
349:
350: $classes = $class->addProperty('classes', array());
351: foreach ($this->classes as $name => $foo) {
352: try {
353: $classes->value[$name] = $this->getByType($name);
354: } catch (ServiceCreationException $e) {
355: $classes->value[$name] = new PhpLiteral('FALSE, //' . strstr($e->getMessage(), ':'));
356: }
357: }
358:
359: $definitions = $this->definitions;
360: ksort($definitions);
361:
362: $meta = $class->addProperty('meta', array());
363: foreach ($definitions as $name => $def) {
364: if ($def->shared) {
365: foreach ($this->expand($def->tags) as $tag => $value) {
366: $meta->value[$name][Container::TAGS][$tag] = $value;
367: }
368: }
369: }
370:
371: foreach ($definitions as $name => $def) {
372: try {
373: $name = (string) $name;
374: $type = $def->class ?: 'object';
375: $methodName = Container::getMethodName($name, $def->shared);
376: if (!PhpHelpers::isIdentifier($methodName)) {
377: throw new ServiceCreationException('Name contains invalid characters.');
378: }
379: if ($def->shared && !$def->internal && PhpHelpers::isIdentifier($name)) {
380: $class->addDocument("@property $type \$$name");
381: }
382: $method = $class->addMethod($methodName)
383: ->addDocument("@return $type")
384: ->setVisibility($def->shared || $def->internal ? 'protected' : 'public')
385: ->setBody($name === self::THIS_CONTAINER ? 'return $this;' : $this->generateService($name));
386:
387: foreach ($this->expand($def->parameters) as $k => $v) {
388: $tmp = explode(' ', is_int($k) ? $v : $k);
389: $param = is_int($k) ? $method->addParameter(end($tmp)) : $method->addParameter(end($tmp), $v);
390: if (isset($tmp[1])) {
391: $param->setTypeHint($tmp[0]);
392: }
393: }
394: } catch (\Exception $e) {
395: throw new ServiceCreationException("Service '$name': " . $e->getMessage(), NULL, $e);
396: }
397: }
398:
399: return $class;
400: }
401:
402:
403:
404: 405: 406: 407:
408: private function generateService($name)
409: {
410: $def = $this->definitions[$name];
411: $parameters = $this->parameters;
412: foreach ($this->expand($def->parameters) as $k => $v) {
413: $v = explode(' ', is_int($k) ? $v : $k);
414: $parameters[end($v)] = new PhpLiteral('$' . end($v));
415: }
416:
417: $code = '$service = ' . $this->formatStatement(Helpers::expand($def->factory, $parameters, TRUE)) . ";\n";
418:
419: $entity = $this->normalizeEntity($def->factory->entity);
420: if ($def->class && $def->class !== $entity && !$this->getServiceName($entity)) {
421: $code .= PhpHelpers::formatArgs("if (!\$service instanceof $def->class) {\n"
422: . "\tthrow new Nette\\UnexpectedValueException(?);\n}\n",
423: array("Unable to create service '$name', value returned by factory is not $def->class type.")
424: );
425: }
426:
427: foreach ((array) $def->setup as $setup) {
428: $setup = Helpers::expand($setup, $parameters, TRUE);
429: if (is_string($setup->entity) && strpbrk($setup->entity, ':@?') === FALSE) {
430: $setup->entity = array("@$name", $setup->entity);
431: }
432: $code .= $this->formatStatement($setup, $name) . ";\n";
433: }
434:
435: return $code .= 'return $service;';
436: }
437:
438:
439:
440: 441: 442: 443: 444:
445: public function formatStatement(Statement $statement, $self = NULL)
446: {
447: $entity = $this->normalizeEntity($statement->entity);
448: $arguments = $statement->arguments;
449:
450: if (is_string($entity) && Strings::contains($entity, '?')) {
451: return $this->formatPhp($entity, $arguments, $self);
452:
453: } elseif ($service = $this->getServiceName($entity)) {
454: if ($this->definitions[$service]->shared) {
455: if ($arguments) {
456: throw new ServiceCreationException("Unable to call service '$entity'.");
457: }
458: return $this->formatPhp('$this->getService(?)', array($service));
459: }
460: $params = array();
461: foreach ($this->definitions[$service]->parameters as $k => $v) {
462: $params[] = preg_replace('#\w+\z#', '\$$0', (is_int($k) ? $v : $k)) . (is_int($k) ? '' : ' = ' . PhpHelpers::dump($v));
463: }
464: $rm = new Reflection\GlobalFunction(create_function(implode(', ', $params), ''));
465: $arguments = Helpers::autowireArguments($rm, $arguments, $this);
466: return $this->formatPhp('$this->?(?*)', array(Container::getMethodName($service, FALSE), $arguments), $self);
467:
468: } elseif ($entity === 'not') {
469: return $this->formatPhp('!?', array($arguments[0]));
470:
471: } elseif (is_string($entity)) {
472: if ($constructor = Reflection\ClassType::from($entity)->getConstructor()) {
473: $this->addDependency($constructor->getFileName());
474: $arguments = Helpers::autowireArguments($constructor, $arguments, $this);
475: } elseif ($arguments) {
476: throw new ServiceCreationException("Unable to pass arguments, class $entity has no constructor.");
477: }
478: return $this->formatPhp("new $entity" . ($arguments ? '(?*)' : ''), array($arguments), $self);
479:
480: } elseif (!Validators::isList($entity) || count($entity) !== 2) {
481: throw new Nette\InvalidStateException("Expected class, method or property, " . PhpHelpers::dump($entity) . " given.");
482:
483: } elseif ($entity[0] === '') {
484: return $this->formatPhp("$entity[1](?*)", array($arguments), $self);
485:
486: } elseif (Strings::contains($entity[1], '$')) {
487: Validators::assert($arguments, 'list:1', "setup arguments for '" . Nette\Callback::create($entity) . "'");
488: if ($this->getServiceName($entity[0], $self)) {
489: return $this->formatPhp('?->? = ?', array($entity[0], substr($entity[1], 1), $arguments[0]), $self);
490: } else {
491: return $this->formatPhp($entity[0] . '::$? = ?', array(substr($entity[1], 1), $arguments[0]), $self);
492: }
493:
494: } elseif ($service = $this->getServiceName($entity[0], $self)) {
495: if ($this->definitions[$service]->class) {
496: $arguments = $this->autowireArguments($this->definitions[$service]->class, $entity[1], $arguments);
497: }
498: return $this->formatPhp('?->?(?*)', array($entity[0], $entity[1], $arguments), $self);
499:
500: } else {
501: $arguments = $this->autowireArguments($entity[0], $entity[1], $arguments);
502: return $this->formatPhp("$entity[0]::$entity[1](?*)", array($arguments), $self);
503: }
504: }
505:
506:
507:
508: 509: 510: 511:
512: public function formatPhp($statement, $args, $self = NULL)
513: {
514: $that = $this;
515: array_walk_recursive($args, function(&$val) use ($self, $that) {
516: list($val) = $that->normalizeEntity(array($val));
517:
518: if ($val instanceof Statement) {
519: $val = new PhpLiteral($that->formatStatement($val, $self));
520:
521: } elseif ($val === '@' . ContainerBuilder::THIS_CONTAINER) {
522: $val = new PhpLiteral('$this');
523:
524: } elseif ($service = $that->getServiceName($val, $self)) {
525: $val = $service === $self ? '$service' : $that->formatStatement(new Statement($val));
526: $val = new PhpLiteral($val);
527: }
528: });
529: return PhpHelpers::formatArgs($statement, $args);
530: }
531:
532:
533:
534: 535: 536: 537: 538:
539: public function expand($value)
540: {
541: return Helpers::expand($value, $this->parameters, TRUE);
542: }
543:
544:
545:
546:
547: public function normalizeEntity($entity)
548: {
549: if (is_string($entity) && Strings::contains($entity, '::') && !Strings::contains($entity, '?')) {
550: $entity = explode('::', $entity);
551: }
552:
553: if (is_array($entity) && $entity[0] instanceof ServiceDefinition) {
554: $tmp = array_keys($this->definitions, $entity[0], TRUE);
555: $entity[0] = "@$tmp[0]";
556:
557: } elseif ($entity instanceof ServiceDefinition) {
558: $tmp = array_keys($this->definitions, $entity, TRUE);
559: $entity = "@$tmp[0]";
560:
561: } elseif (is_array($entity) && $entity[0] === $this) {
562: $entity[0] = '@' . ContainerBuilder::THIS_CONTAINER;
563: }
564: return $entity;
565: }
566:
567:
568:
569: 570: 571: 572: 573:
574: public function getServiceName($arg, $self = NULL)
575: {
576: if (!is_string($arg) || !preg_match('#^@[\w\\\\.].*\z#', $arg)) {
577: return FALSE;
578: }
579: $service = substr($arg, 1);
580: if ($service === self::CREATED_SERVICE) {
581: $service = $self;
582: }
583: if (Strings::contains($service, '\\')) {
584: if ($this->classes === FALSE) {
585: return $service;
586: }
587: $res = $this->getByType($service);
588: if (!$res) {
589: throw new ServiceCreationException("Reference to missing service of type $service.");
590: }
591: return $res;
592: }
593: if (!isset($this->definitions[$service])) {
594: throw new ServiceCreationException("Reference to missing service '$service'.");
595: }
596: return $service;
597: }
598:
599: }
600: