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