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) {
190: $def->class = $def->class === self::CREATED_SERVICE ? $name : $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: if ($def->factory && $def->factory->entity === self::CREATED_SERVICE) {
198: $def->factory->entity = $name;
199: }
200: }
201:
202:
203: $this->classes = FALSE;
204: foreach ($this->definitions as $name => $def) {
205: $this->resolveClass($name);
206: }
207:
208:
209: $this->classes = array();
210: foreach ($this->definitions as $name => $def) {
211: if (!$def->class) {
212: continue;
213: }
214: if (!class_exists($def->class) && !interface_exists($def->class)) {
215: throw new Nette\InvalidStateException("Class $def->class has not been found.");
216: }
217: $def->class = Nette\Reflection\ClassType::from($def->class)->getName();
218: if ($def->autowired) {
219: foreach (class_parents($def->class) + class_implements($def->class) + array($def->class) as $parent) {
220: $this->classes[strtolower($parent)][] = $name;
221: }
222: }
223: }
224:
225: foreach ($this->classes as $class => $foo) {
226: $this->addDependency(Nette\Reflection\ClassType::from($class)->getFileName());
227: }
228: }
229:
230:
231:
232: private function resolveClass($name, $recursive = array())
233: {
234: if (isset($recursive[$name])) {
235: throw new Nette\InvalidArgumentException('Circular reference detected for services: ' . implode(', ', array_keys($recursive)) . '.');
236: }
237: $recursive[$name] = TRUE;
238:
239: $def = $this->definitions[$name];
240: $factory = $this->normalizeEntity($this->expand($def->factory->entity));
241:
242: if ($def->class) {
243: return $def->class;
244:
245: } elseif (is_array($factory)) {
246: if ($service = $this->getServiceName($factory[0])) {
247: if (Strings::contains($service, '\\')) {
248: throw new ServiceCreationException("Unable resolve class name for service '$name'.");
249: }
250: $factory[0] = $this->resolveClass($service, $recursive);
251: if (!$factory[0]) {
252: return;
253: }
254: }
255: $factory = callback($factory);
256: if (!$factory->isCallable()) {
257: throw new Nette\InvalidStateException("Factory '$factory' is not callable.");
258: }
259: try {
260: $reflection = $factory->toReflection();
261: $def->class = preg_replace('#[|\s].*#', '', $reflection->getAnnotation('return'));
262: if ($def->class && !class_exists($def->class) && $def->class[0] !== '\\' && $reflection instanceof \ReflectionMethod) {
263: $def->class = $reflection->getDeclaringClass()->getNamespaceName() . '\\' . $def->class;
264: }
265: } catch (\ReflectionException $e) {
266: }
267:
268: } elseif ($service = $this->getServiceName($factory)) {
269: if (Strings::contains($service, '\\')) {
270: $def->autowired = FALSE;
271: return $def->class = $service;
272: }
273: if ($this->definitions[$service]->shared) {
274: $def->autowired = FALSE;
275: }
276: return $def->class = $this->resolveClass($service, $recursive);
277:
278: } else {
279: return $def->class = $factory;
280: }
281: }
282:
283:
284:
285: 286: 287: 288:
289: public function addDependency($file)
290: {
291: $this->dependencies[$file] = TRUE;
292: return $this;
293: }
294:
295:
296:
297: 298: 299: 300:
301: public function getDependencies()
302: {
303: unset($this->dependencies[FALSE]);
304: return array_keys($this->dependencies);
305: }
306:
307:
308:
309:
310:
311:
312:
313: 314: 315: 316:
317: public function generateClass($parentClass = 'Nette\DI\Container')
318: {
319: unset($this->definitions[self::THIS_CONTAINER]);
320: $this->addDefinition(self::THIS_CONTAINER)->setClass($parentClass);
321:
322: $this->prepareClassList();
323:
324: $class = new Nette\Utils\PhpGenerator\ClassType('Container');
325: $class->addExtend($parentClass);
326: $class->addMethod('__construct')
327: ->addBody('parent::__construct(?);', array($this->expand($this->parameters)));
328:
329: $classes = $class->addProperty('classes', array());
330: foreach ($this->classes as $name => $foo) {
331: try {
332: $classes->value[$name] = $this->sanitizeName($this->getByType($name));
333: } catch (ServiceCreationException $e) {
334: $classes->value[$name] = new PhpLiteral('FALSE, //' . strstr($e->getMessage(), ':'));
335: }
336: }
337:
338: $definitions = $this->definitions;
339: ksort($definitions);
340:
341: $meta = $class->addProperty('meta', array());
342: foreach ($definitions as $name => $def) {
343: if ($def->shared) {
344: foreach ($this->expand($def->tags) as $tag => $value) {
345: $meta->value[$name][Container::TAGS][$tag] = $value;
346: }
347: }
348: }
349:
350: foreach ($definitions as $name => $def) {
351: try {
352: $type = $def->class ?: 'object';
353: $sanitized = $this->sanitizeName($name);
354: if (!PhpHelpers::isIdentifier($sanitized)) {
355: throw new ServiceCreationException('Name contains invalid characters.');
356: }
357: if ($def->shared && $name === $sanitized) {
358: $class->addDocument("@property $type \$$name");
359: }
360: $method = $class->addMethod(($def->shared ? 'createService' : 'create') . ucfirst($sanitized))
361: ->addDocument("@return $type")
362: ->setVisibility($def->shared || $def->internal ? 'protected' : 'public')
363: ->setBody($name === self::THIS_CONTAINER ? 'return $this;' : $this->generateService($name));
364:
365: foreach ($this->expand($def->parameters) as $k => $v) {
366: $tmp = explode(' ', is_int($k) ? $v : $k);
367: $param = is_int($k) ? $method->addParameter(end($tmp)) : $method->addParameter(end($tmp), $v);
368: if (isset($tmp[1])) {
369: $param->setTypeHint($tmp[0]);
370: }
371: }
372: } catch (\Exception $e) {
373: throw new ServiceCreationException("Service '$name': " . $e->getMessage(), NULL, $e);
374: }
375: }
376:
377: return $class;
378: }
379:
380:
381:
382: 383: 384: 385:
386: private function generateService($name)
387: {
388: $def = $this->definitions[$name];
389: $parameters = $this->parameters;
390: foreach ($this->expand($def->parameters) as $k => $v) {
391: $v = explode(' ', is_int($k) ? $v : $k);
392: $parameters[end($v)] = new PhpLiteral('$' . end($v));
393: }
394:
395: $code = '$service = ' . $this->formatStatement(Helpers::expand($def->factory, $parameters, TRUE)) . ";\n";
396:
397: if ($def->class && $def->class !== $def->factory->entity) {
398: $code .= PhpHelpers::formatArgs("if (!\$service instanceof $def->class) {\n"
399: . "\tthrow new Nette\\UnexpectedValueException(?);\n}\n",
400: array("Unable to create service '$name', value returned by factory is not $def->class type.")
401: );
402: }
403:
404: foreach ((array) $def->setup as $setup) {
405: $setup = Helpers::expand($setup, $parameters, TRUE);
406: if (is_string($setup->entity) && strpbrk($setup->entity, ':@?') === FALSE) {
407: $setup->entity = array("@$name", $setup->entity);
408: }
409: $code .= $this->formatStatement($setup, $name) . ";\n";
410: }
411:
412: return $code .= 'return $service;';
413: }
414:
415:
416:
417: 418: 419: 420: 421:
422: public function formatStatement(Statement $statement, $self = NULL)
423: {
424: $entity = $this->normalizeEntity($statement->entity);
425: $arguments = (array) $statement->arguments;
426:
427: if (is_string($entity) && Strings::contains($entity, '?')) {
428: return $this->formatPhp($entity, $arguments, $self);
429:
430: } elseif ($service = $this->getServiceName($entity)) {
431: if ($this->definitions[$service]->shared) {
432: if ($arguments) {
433: throw new ServiceCreationException("Unable to call service '$entity'.");
434: }
435: return $this->formatPhp('$this->?', array($this->sanitizeName($service)));
436: }
437: $params = array();
438: foreach ($this->definitions[$service]->parameters as $k => $v) {
439: $params[] = preg_replace('#\w+$#', '\$$0', (is_int($k) ? $v : $k)) . (is_int($k) ? '' : ' = ' . PhpHelpers::dump($v));
440: }
441: $rm = new \ReflectionFunction(create_function(implode(', ', $params), ''));
442: $arguments = Helpers::autowireArguments($rm, $arguments, $this);
443: return $this->formatPhp('$this->?(?*)', array('create' . ucfirst($service), $arguments), $self);
444:
445: } elseif ($entity === 'not') {
446: return $this->formatPhp('!?', array($arguments[0]));
447:
448: } elseif (is_string($entity)) {
449: if ($constructor = Nette\Reflection\ClassType::from($entity)->getConstructor()) {
450: $this->addDependency($constructor->getFileName());
451: $arguments = Helpers::autowireArguments($constructor, $arguments, $this);
452: } elseif ($arguments) {
453: throw new ServiceCreationException("Unable to pass arguments, class $entity has no constructor.");
454: }
455: return $this->formatPhp("new $entity" . ($arguments ? '(?*)' : ''), array($arguments), $self);
456:
457: } elseif (!Validators::isList($entity) || count($entity) !== 2) {
458: throw new Nette\InvalidStateException("Expected class, method or property, " . PhpHelpers::dump($entity) . " given.");
459:
460: } elseif ($entity[0] === '') {
461: return $this->formatPhp("$entity[1](?*)", array($arguments), $self);
462:
463: } elseif (Strings::contains($entity[1], '$')) {
464: if ($this->getServiceName($entity[0], $self)) {
465: return $this->formatPhp('?->? = ?', array($entity[0], substr($entity[1], 1), $statement->arguments), $self);
466: } else {
467: return $this->formatPhp($entity[0] . '::$? = ?', array(substr($entity[1], 1), $statement->arguments), $self);
468: }
469:
470: } elseif ($service = $this->getServiceName($entity[0], $self)) {
471: if ($this->definitions[$service]->class) {
472: $arguments = $this->autowireArguments($this->definitions[$service]->class, $entity[1], $arguments);
473: }
474: return $this->formatPhp('?->?(?*)', array($entity[0], $entity[1], $arguments), $self);
475:
476: } else {
477: $arguments = $this->autowireArguments($entity[0], $entity[1], $arguments);
478: return $this->formatPhp("$entity[0]::$entity[1](?*)", array($arguments), $self);
479: }
480: }
481:
482:
483:
484: 485: 486: 487:
488: private function formatPhp($statement, $args, $self = NULL)
489: {
490: $that = $this;
491: array_walk_recursive($args, function(&$val) use ($self, $that) {
492: list($val) = $that->normalizeEntity(array($val));
493:
494: if ($val instanceof Statement) {
495: $val = new PhpLiteral($that->formatStatement($val, $self));
496:
497: } elseif ($val === '@' . ContainerBuilder::THIS_CONTAINER) {
498: $val = new PhpLiteral('$this');
499:
500: } elseif ($service = $that->getServiceName($val, $self)) {
501: $val = $service === $self ? '$service' : $that->formatStatement(new Statement($val));
502: $val = new PhpLiteral($val, $self);
503: }
504: });
505: return PhpHelpers::formatArgs($statement, $args);
506: }
507:
508:
509:
510: 511: 512: 513: 514:
515: public function expand($value)
516: {
517: return Helpers::expand($value, $this->parameters, TRUE);
518: }
519:
520:
521:
522: private static function sanitizeName($name)
523: {
524: return str_replace('\\', '__', $name);
525: }
526:
527:
528:
529:
530: public function normalizeEntity($entity)
531: {
532: if (is_string($entity) && Strings::contains($entity, '::') && !Strings::contains($entity, '?')) {
533: $entity = explode('::', $entity);
534: }
535:
536: if (is_array($entity) && $entity[0] instanceof ServiceDefinition) {
537: $tmp = array_keys($this->definitions, $entity[0], TRUE);
538: $entity[0] = "@$tmp[0]";
539:
540: } elseif ($entity instanceof ServiceDefinition) {
541: $tmp = array_keys($this->definitions, $entity, TRUE);
542: $entity = "@$tmp[0]";
543:
544: } elseif (is_array($entity) && $entity[0] === $this) {
545: $entity[0] = '@' . ContainerBuilder::THIS_CONTAINER;
546: }
547: return $entity;
548: }
549:
550:
551:
552: 553: 554: 555: 556:
557: public function getServiceName($arg, $self = NULL)
558: {
559: if (!is_string($arg) || !preg_match('#^@[\w\\\\]+$#', $arg)) {
560: return FALSE;
561: }
562: $service = substr($arg, 1);
563: if ($service === self::CREATED_SERVICE) {
564: $service = $self;
565: }
566: if (Strings::contains($service, '\\')) {
567: if ($this->classes === FALSE) {
568: return $service;
569: }
570: $res = $this->getByType($service);
571: if (!$res) {
572: throw new ServiceCreationException("Reference to missing service of type $service.");
573: }
574: return $res;
575: }
576: if (!isset($this->definitions[$service])) {
577: throw new ServiceCreationException("Reference to missing service '$service'.");
578: }
579: return $service;
580: }
581:
582: }
583: