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