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