Packages

  • Nette
    • Application
      • Diagnostics
      • Responses
      • Routers
      • UI
    • Caching
      • Storages
    • ComponentModel
    • Config
      • Adapters
      • Extensions
    • Database
      • Diagnostics
      • Drivers
      • Reflection
      • Table
    • DI
      • Diagnostics
    • Diagnostics
    • Forms
      • Controls
      • Rendering
    • Http
    • Iterators
    • Latte
      • Macros
    • Loaders
    • Localization
    • Mail
    • Reflection
    • Security
      • Diagnostics
    • Templating
    • Utils
      • PhpGenerator
  • NetteModule
  • None
  • PHP

Classes

  • DIContainer
  • DIContainerBuilder
  • DIHelpers
  • DIServiceDefinition
  • DIStatement

Interfaces

  • IDIContainer

Exceptions

  • MissingServiceException
  • ServiceCreationException
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Nette Framework (http://nette.org)
  5:  *
  6:  * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
  7:  *
  8:  * For the full copyright and license information, please view
  9:  * the file license.txt that was distributed with this source code.
 10:  * @package Nette\DI
 11:  */
 12: 
 13: 
 14: 
 15: /**
 16:  * Basic container builder.
 17:  *
 18:  * @author     David Grudl
 19:  * @property-read DIServiceDefinition[] $definitions
 20:  * @property-read array $dependencies
 21:  * @package Nette\DI
 22:  */
 23: class DIContainerBuilder extends Object
 24: {
 25:     const CREATED_SERVICE = 'self',
 26:         THIS_CONTAINER = 'container';
 27: 
 28:     /** @var array  %param% will be expanded */
 29:     public $parameters = array();
 30: 
 31:     /** @var DIServiceDefinition[] */
 32:     private $definitions = array();
 33: 
 34:     /** @var array for auto-wiring */
 35:     private $classes;
 36: 
 37:     /** @var array of file names */
 38:     private $dependencies = array();
 39: 
 40: 
 41: 
 42:     /**
 43:      * Adds new service definition. The expressions %param% and @service will be expanded.
 44:      * @param  string
 45:      * @return DIServiceDefinition
 46:      */
 47:     public function addDefinition($name)
 48:     {
 49:         if (!is_string($name) || !$name) { // builder is not ready for falsy names such as '0'
 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 DIServiceDefinition;
 56:     }
 57: 
 58: 
 59: 
 60:     /**
 61:      * Removes the specified service definition.
 62:      * @param  string
 63:      * @return void
 64:      */
 65:     public function removeDefinition($name)
 66:     {
 67:         unset($this->definitions[$name]);
 68:     }
 69: 
 70: 
 71: 
 72:     /**
 73:      * Gets the service definition.
 74:      * @param  string
 75:      * @return DIServiceDefinition
 76:      */
 77:     public function getDefinition($name)
 78:     {
 79:         if (!isset($this->definitions[$name])) {
 80:             throw new MissingServiceException("Service '$name' not found.");
 81:         }
 82:         return $this->definitions[$name];
 83:     }
 84: 
 85: 
 86: 
 87:     /**
 88:      * Gets all service definitions.
 89:      * @return array
 90:      */
 91:     public function getDefinitions()
 92:     {
 93:         return $this->definitions;
 94:     }
 95: 
 96: 
 97: 
 98:     /**
 99:      * Does the service definition exist?
100:      * @param  string
101:      * @return bool
102:      */
103:     public function hasDefinition($name)
104:     {
105:         return isset($this->definitions[$name]);
106:     }
107: 
108: 
109: 
110:     /********************* class resolving ****************d*g**/
111: 
112: 
113: 
114:     /**
115:      * Resolves service name by type.
116:      * @param  string  class or interface
117:      * @return string  service name or NULL
118:      * @throws ServiceCreationException
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 ServiceCreationException("Multiple services of type $class found: " . implode(', ', $this->classes[$lower]));
131:         }
132:     }
133: 
134: 
135: 
136:     /**
137:      * Gets the service objects of the specified tag.
138:      * @param  string
139:      * @return array of [service name => tag attributes]
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:      * Creates a list of arguments using autowiring.
156:      * @return array
157:      */
158:     public function autowireArguments($class, $method, array $arguments)
159:     {
160:         $rc = ClassReflection::from($class);
161:         if (!$rc->hasMethod($method)) {
162:             if (!Validators::isList($arguments)) {
163:                 throw new ServiceCreationException("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 ServiceCreationException("$rm is not callable.");
171:         }
172:         $this->addDependency($rm->getFileName());
173:         return DIHelpers::autowireArguments($rm, $arguments, $this);
174:     }
175: 
176: 
177: 
178:     /**
179:      * Generates $dependencies, $classes and expands and normalize class names.
180:      * @return array
181:      */
182:     public function prepareClassList()
183:     {
184:         // complete class-factory pairs; expand classes
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 DIStatement($def->class);
200:                 }
201:             } elseif (!$def->factory) {
202:                 throw new ServiceCreationException("Class and factory are missing in service '$name' definition.");
203:             }
204:         }
205: 
206:         // check if services are instantiable
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) || !ClassReflection::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:         // complete classes
217:         $this->classes = FALSE;
218:         foreach ($this->definitions as $name => $def) {
219:             $this->resolveClass($name);
220:         }
221: 
222:         //  build auto-wiring list
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 = ClassReflection::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(ClassReflection::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)) { // method calling
260:             if ($service = $this->getServiceName($factory[0])) {
261:                 if (Strings::contains($service, '\\')) { // @Class
262:                     throw new ServiceCreationException("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 Callback($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)) { // alias or factory
282:             if (Strings::contains($service, '\\')) { // @Class
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; // class name
294:         }
295:     }
296: 
297: 
298: 
299:     /**
300:      * Adds a file to the list of dependencies.
301:      * @return DIContainerBuilder  provides a fluent interface
302:      */
303:     public function addDependency($file)
304:     {
305:         $this->dependencies[$file] = TRUE;
306:         return $this;
307:     }
308: 
309: 
310: 
311:     /**
312:      * Returns the list of dependent files.
313:      * @return array
314:      */
315:     public function getDependencies()
316:     {
317:         unset($this->dependencies[FALSE]);
318:         return array_keys($this->dependencies);
319:     }
320: 
321: 
322: 
323:     /********************* code generator ****************d*g**/
324: 
325: 
326: 
327:     /**
328:      * Generates PHP class.
329:      * @return PhpClassType
330:      */
331:     public function generateClass($parentClass = 'DIContainer')
332:     {
333:         unset($this->definitions[self::THIS_CONTAINER]);
334:         $this->addDefinition(self::THIS_CONTAINER)->setClass($parentClass);
335: 
336:         $this->prepareClassList();
337: 
338:         $class = new PhpClassType('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 (ServiceCreationException $e) {
348:                 $classes->value[$name] = new PhpLiteral('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][DIContainer::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 = DIContainer::getMethodName($name, $def->shared);
369:                 if (!PhpHelpers::isIdentifier($methodName)) {
370:                     throw new ServiceCreationException('Name contains invalid characters.');
371:                 }
372:                 if ($def->shared && !$def->internal && PhpHelpers::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 ServiceCreationException("Service '$name': " . $e->getMessage(), NULL, $e);
389:             }
390:         }
391: 
392:         return $class;
393:     }
394: 
395: 
396: 
397:     /**
398:      * Generates body of service method.
399:      * @return string
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 PhpLiteral('$' . end($v));
408:         }
409: 
410:         $code = '$service = ' . $this->formatStatement(DIHelpers::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 .= PhpHelpers::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 = DIHelpers::expand($setup, $parameters, TRUE);
422:             if (is_string($setup->entity) && strpbrk($setup->entity, ':@?') === FALSE) { // auto-prepend @self
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:      * Formats PHP code for class instantiating, function calling or property setting in PHP.
435:      * @return string
436:      * @internal
437:      */
438:     public function formatStatement(DIStatement $statement, $self = NULL)
439:     {
440:         $entity = $this->normalizeEntity($statement->entity);
441:         $arguments = $statement->arguments;
442: 
443:         if (is_string($entity) && Strings::contains($entity, '?')) { // PHP literal
444:             return $this->formatPhp($entity, $arguments, $self);
445: 
446:         } elseif ($service = $this->getServiceName($entity)) { // factory calling or service retrieving
447:             if ($this->definitions[$service]->shared) {
448:                 if ($arguments) {
449:                     throw new ServiceCreationException("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) ? '' : ' = ' . PhpHelpers::dump($v));
456:             }
457:             $rm = new FunctionReflection(create_function(implode(', ', $params), ''));
458:             $arguments = DIHelpers::autowireArguments($rm, $arguments, $this);
459:             return $this->formatPhp('$this->?(?*)', array(DIContainer::getMethodName($service, FALSE), $arguments), $self);
460: 
461:         } elseif ($entity === 'not') { // operator
462:             return $this->formatPhp('!?', array($arguments[0]));
463: 
464:         } elseif (is_string($entity)) { // class name
465:             if ($constructor = ClassReflection::from($entity)->getConstructor()) {
466:                 $this->addDependency($constructor->getFileName());
467:                 $arguments = DIHelpers::autowireArguments($constructor, $arguments, $this);
468:             } elseif ($arguments) {
469:                 throw new ServiceCreationException("Unable to pass arguments, class $entity has no constructor.");
470:             }
471:             return $this->formatPhp("new $entity" . ($arguments ? '(?*)' : ''), array($arguments), $self);
472: 
473:         } elseif (!Validators::isList($entity) || count($entity) !== 2) {
474:             throw new InvalidStateException("Expected class, method or property, " . PhpHelpers::dump($entity) . " given.");
475: 
476:         } elseif ($entity[0] === '') { // globalFunc
477:             return $this->formatPhp("$entity[1](?*)", array($arguments), $self);
478: 
479:         } elseif (Strings::contains($entity[1], '$')) { // property setter
480:             Validators::assert($arguments, 'list:1', "setup arguments for '" . Callback::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)) { // service method
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 { // static method
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:      * Formats PHP statement.
503:      * @return string
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 DIStatement) {
512:                 $val = new PhpLiteral($that->formatStatement($val, $self));
513: 
514:             } elseif ($val === \'@\' . DIContainerBuilder::THIS_CONTAINER) {
515:                 $val = new PhpLiteral(\'$this\');
516: 
517:             } elseif ($service = $that->getServiceName($val, $self)) {
518:                 $val = $service === $self ? \'$service\' : $that->formatStatement(new DIStatement($val));
519:                 $val = new PhpLiteral($val);
520:             }
521:         '));
522:         return PhpHelpers::formatArgs($statement, $args);
523:     }
524: 
525: 
526: 
527:     /**
528:      * Expands %placeholders% in strings (recursive).
529:      * @param  mixed
530:      * @return mixed
531:      */
532:     public function expand($value)
533:     {
534:         return DIHelpers::expand($value, $this->parameters, TRUE);
535:     }
536: 
537: 
538: 
539:     /** @internal */
540:     public function normalizeEntity($entity)
541:     {
542:         if (is_string($entity) && Strings::contains($entity, '::') && !Strings::contains($entity, '?')) { // Class::method -> [Class, method]
543:             $entity = explode('::', $entity);
544:         }
545: 
546:         if (is_array($entity) && $entity[0] instanceof DIServiceDefinition) { // [ServiceDefinition, ...] -> [@serviceName, ...]
547:             $tmp = array_keys($this->definitions, $entity[0], TRUE);
548:             $entity[0] = "@$tmp[0]";
549: 
550:         } elseif ($entity instanceof DIServiceDefinition) { // ServiceDefinition -> @serviceName
551:             $tmp = array_keys($this->definitions, $entity, TRUE);
552:             $entity = "@$tmp[0]";
553: 
554:         } elseif (is_array($entity) && $entity[0] === $this) { // [$this, ...] -> [@container, ...]
555:             $entity[0] = '@' . DIContainerBuilder::THIS_CONTAINER;
556:         }
557:         return $entity; // Class, @service, [Class, member], [@service, member], [, globalFunc]
558:     }
559: 
560: 
561: 
562:     /**
563:      * Converts @service or @Class -> service name and checks its existence.
564:      * @param  mixed
565:      * @return string  of FALSE, if argument is not service name
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 (Strings::contains($service, '\\')) {
577:             if ($this->classes === FALSE) { // may be disabled by prepareClassList
578:                 return $service;
579:             }
580:             $res = $this->getByType($service);
581:             if (!$res) {
582:                 throw new ServiceCreationException("Reference to missing service of type $service.");
583:             }
584:             return $res;
585:         }
586:         if (!isset($this->definitions[$service])) {
587:             throw new ServiceCreationException("Reference to missing service '$service'.");
588:         }
589:         return $service;
590:     }
591: 
592: }
593: 
Nette Framework 2.0.7 (for PHP 5.2, un-prefixed) API API documentation generated by ApiGen 2.8.0