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