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