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