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