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 === self::CREATED_SERVICE || ($def->factory && $def->factory->entity === self::CREATED_SERVICE)) {
184: $def->class = $name;
185: $def->internal = TRUE;
186: if ($def->factory && $def->factory->entity === self::CREATED_SERVICE) {
187: $def->factory->entity = $def->class;
188: }
189: unset($this->definitions[$name]);
190: $this->definitions['_anonymous_' . str_replace('\\', '_', strtolower(trim($name, '\\')))] = $def;
191: }
192:
193: if ($def->class) {
194: $def->class = $this->expand($def->class);
195: if (!$def->factory) {
196: $def->factory = new DIStatement($def->class);
197: }
198: } elseif (!$def->factory) {
199: throw new ServiceCreationException("Class and factory are missing in service '$name' definition.");
200: }
201: }
202:
203:
204: $this->classes = FALSE;
205: foreach ($this->definitions as $name => $def) {
206: $this->resolveClass($name);
207: }
208:
209:
210: $this->classes = array();
211: foreach ($this->definitions as $name => $def) {
212: if (!$def->class) {
213: continue;
214: }
215: if (!class_exists($def->class) && !interface_exists($def->class)) {
216: throw new InvalidStateException("Class $def->class has not been found.");
217: }
218: $def->class = ClassReflection::from($def->class)->getName();
219: if ($def->autowired) {
220: foreach (class_parents($def->class) + class_implements($def->class) + array($def->class) as $parent) {
221: $this->classes[strtolower($parent)][] = $name;
222: }
223: }
224: }
225:
226: foreach ($this->classes as $class => $foo) {
227: $this->addDependency(ClassReflection::from($class)->getFileName());
228: }
229: }
230:
231:
232:
233: private function resolveClass($name, $recursive = array())
234: {
235: if (isset($recursive[$name])) {
236: throw new InvalidArgumentException('Circular reference detected for services: ' . implode(', ', array_keys($recursive)) . '.');
237: }
238: $recursive[$name] = TRUE;
239:
240: $def = $this->definitions[$name];
241: $factory = $this->normalizeEntity($this->expand($def->factory->entity));
242:
243: if ($def->class) {
244: return $def->class;
245:
246: } elseif (is_array($factory)) {
247: if ($service = $this->getServiceName($factory[0])) {
248: if (Strings::contains($service, '\\')) {
249: throw new ServiceCreationException("Unable resolve class name for service '$name'.");
250: }
251: $factory[0] = $this->resolveClass($service, $recursive);
252: if (!$factory[0]) {
253: return;
254: }
255: }
256: $factory = callback($factory);
257: if (!$factory->isCallable()) {
258: throw new InvalidStateException("Factory '$factory' is not callable.");
259: }
260: try {
261: $reflection = $factory->toReflection();
262: $def->class = preg_replace('#[|\s].*#', '', $reflection->getAnnotation('return'));
263: if ($def->class && !class_exists($def->class) && $def->class[0] !== '\\' && $reflection instanceof ReflectionMethod) {
264: }
265: } catch (ReflectionException $e) {
266: }
267:
268: } elseif ($service = $this->getServiceName($factory)) {
269: if (Strings::contains($service, '\\')) {
270: $service = ltrim($service, '\\');
271: $def->autowired = FALSE;
272: return $def->class = $service;
273: }
274: if ($this->definitions[$service]->shared) {
275: $def->autowired = FALSE;
276: }
277: return $def->class = $this->resolveClass($service, $recursive);
278:
279: } else {
280: return $def->class = $factory;
281: }
282: }
283:
284:
285:
286: 287: 288: 289:
290: public function addDependency($file)
291: {
292: $this->dependencies[$file] = TRUE;
293: return $this;
294: }
295:
296:
297:
298: 299: 300: 301:
302: public function getDependencies()
303: {
304: unset($this->dependencies[FALSE]);
305: return array_keys($this->dependencies);
306: }
307:
308:
309:
310:
311:
312:
313:
314: 315: 316: 317:
318: public function generateClass($parentClass = 'DIContainer')
319: {
320: unset($this->definitions[self::THIS_CONTAINER]);
321: $this->addDefinition(self::THIS_CONTAINER)->setClass($parentClass);
322:
323: $this->prepareClassList();
324:
325: $class = new PhpClassType('Container');
326: $class->addExtend($parentClass);
327: $class->addMethod('__construct')
328: ->addBody('parent::__construct(?);', array($this->expand($this->parameters)));
329:
330: $classes = $class->addProperty('classes', array());
331: foreach ($this->classes as $name => $foo) {
332: try {
333: $classes->value[$name] = $this->getByType($name);
334: } catch (ServiceCreationException $e) {
335: $classes->value[$name] = new PhpLiteral('FALSE, //' . strstr($e->getMessage(), ':'));
336: }
337: }
338:
339: $definitions = $this->definitions;
340: ksort($definitions);
341:
342: $meta = $class->addProperty('meta', array());
343: foreach ($definitions as $name => $def) {
344: if ($def->shared) {
345: foreach ($this->expand($def->tags) as $tag => $value) {
346: $meta->value[$name][DIContainer::TAGS][$tag] = $value;
347: }
348: }
349: }
350:
351: foreach ($definitions as $name => $def) {
352: try {
353: $type = ($tmp=$def->class) ? $tmp : 'object';
354: $methodName = DIContainer::getMethodName($name, $def->shared);
355: if (!PhpHelpers::isIdentifier($methodName)) {
356: throw new ServiceCreationException('Name contains invalid characters.');
357: }
358: if ($def->shared && !$def->internal && PhpHelpers::isIdentifier($name)) {
359: $class->addDocument("@property $type \$$name");
360: }
361: $method = $class->addMethod($methodName)
362: ->addDocument("@return $type")
363: ->setVisibility($def->shared || $def->internal ? 'protected' : 'public')
364: ->setBody($name === self::THIS_CONTAINER ? 'return $this;' : $this->generateService($name));
365:
366: foreach ($this->expand($def->parameters) as $k => $v) {
367: $tmp = explode(' ', is_int($k) ? $v : $k);
368: $param = is_int($k) ? $method->addParameter(end($tmp)) : $method->addParameter(end($tmp), $v);
369: if (isset($tmp[1])) {
370: $param->setTypeHint($tmp[0]);
371: }
372: }
373: } catch (Exception $e) {
374: throw new ServiceCreationException("Service '$name': " . $e->getMessage());
375: }
376: }
377:
378: return $class;
379: }
380:
381:
382:
383: 384: 385: 386:
387: private function generateService($name)
388: {
389: $def = $this->definitions[$name];
390: $parameters = $this->parameters;
391: foreach ($this->expand($def->parameters) as $k => $v) {
392: $v = explode(' ', is_int($k) ? $v : $k);
393: $parameters[end($v)] = new PhpLiteral('$' . end($v));
394: }
395:
396: $code = '$service = ' . $this->formatStatement(DIHelpers::expand($def->factory, $parameters, TRUE)) . ";\n";
397:
398: $entity = $this->normalizeEntity($def->factory->entity);
399: if ($def->class && $def->class !== $entity && !$this->getServiceName($entity)) {
400: $code .= PhpHelpers::formatArgs("if (!\$service instanceof $def->class) {\n"
401: . "\tthrow new UnexpectedValueException(?);\n}\n",
402: array("Unable to create service '$name', value returned by factory is not $def->class type.")
403: );
404: }
405:
406: foreach ((array) $def->setup as $setup) {
407: $setup = DIHelpers::expand($setup, $parameters, TRUE);
408: if (is_string($setup->entity) && strpbrk($setup->entity, ':@?') === FALSE) {
409: $setup->entity = array("@$name", $setup->entity);
410: }
411: $code .= $this->formatStatement($setup, $name) . ";\n";
412: }
413:
414: return $code .= 'return $service;';
415: }
416:
417:
418:
419: 420: 421: 422: 423:
424: public function formatStatement(DIStatement $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->?', array($service));
438: }
439: $params = array();
440: foreach ($this->definitions[$service]->parameters as $k => $v) {
441: $params[] = preg_replace('#\w+$#', '\$$0', (is_int($k) ? $v : $k)) . (is_int($k) ? '' : ' = ' . PhpHelpers::dump($v));
442: }
443: $rm = new FunctionReflection(create_function(implode(', ', $params), ''));
444: $arguments = DIHelpers::autowireArguments($rm, $arguments, $this);
445: return $this->formatPhp('$this->?(?*)', array(DIContainer::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 = ClassReflection::from($entity)->getConstructor()) {
452: $this->addDependency($constructor->getFileName());
453: $arguments = DIHelpers::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 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 '" . callback($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, create_function('&$val', 'extract(NCFix::$vars['.NCFix::uses(array('self'=>$self,'that'=> $that)).'], EXTR_REFS);
495: list($val) = $that->normalizeEntity(array($val));
496:
497: if ($val instanceof DIStatement) {
498: $val = new PhpLiteral($that->formatStatement($val, $self));
499:
500: } elseif ($val === \'@\' . DIContainerBuilder::THIS_CONTAINER) {
501: $val = new PhpLiteral(\'$this\');
502:
503: } elseif ($service = $that->getServiceName($val, $self)) {
504: $val = $service === $self ? \'$service\' : $that->formatStatement(new DIStatement($val));
505: $val = new PhpLiteral($val, $self);
506: }
507: '));
508: return PhpHelpers::formatArgs($statement, $args);
509: }
510:
511:
512:
513: 514: 515: 516: 517:
518: public function expand($value)
519: {
520: return DIHelpers::expand($value, $this->parameters, TRUE);
521: }
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 DIServiceDefinition) {
533: $tmp = array_keys($this->definitions, $entity[0], TRUE);
534: $entity[0] = "@$tmp[0]";
535:
536: } elseif ($entity instanceof DIServiceDefinition) {
537: $tmp = array_keys($this->definitions, $entity, TRUE);
538: $entity = "@$tmp[0]";
539:
540: } elseif (is_array($entity) && $entity[0] === $this) {
541: $entity[0] = '@' . DIContainerBuilder::THIS_CONTAINER;
542: }
543: return $entity;
544: }
545:
546:
547:
548: 549: 550: 551: 552:
553: public function getServiceName($arg, $self = NULL)
554: {
555: if (!is_string($arg) || !preg_match('#^@[\w\\\\.].+$#', $arg)) {
556: return FALSE;
557: }
558: $service = substr($arg, 1);
559: if ($service === self::CREATED_SERVICE) {
560: $service = $self;
561: }
562: if (Strings::contains($service, '\\')) {
563: if ($this->classes === FALSE) {
564: return $service;
565: }
566: $res = $this->getByType($service);
567: if (!$res) {
568: throw new ServiceCreationException("Reference to missing service of type $service.");
569: }
570: return $res;
571: }
572: if (!isset($this->definitions[$service])) {
573: throw new ServiceCreationException("Reference to missing service '$service'.");
574: }
575: return $service;
576: }
577:
578: }
579: