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:      * Adds new service definition. The expressions %param% and @service will be expanded.
 43:      * @param  string
 44:      * @return DIServiceDefinition
 45:      */
 46:     public function addDefinition($name)
 47:     {
 48:         if (!is_string($name) || !$name) { // builder is not ready for falsy names such as '0'
 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 DIServiceDefinition;
 55:     }
 56: 
 57: 
 58:     /**
 59:      * Removes the specified service definition.
 60:      * @param  string
 61:      * @return void
 62:      */
 63:     public function removeDefinition($name)
 64:     {
 65:         unset($this->definitions[$name]);
 66:     }
 67: 
 68: 
 69:     /**
 70:      * Gets the service definition.
 71:      * @param  string
 72:      * @return DIServiceDefinition
 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:      * Gets all service definitions.
 85:      * @return array
 86:      */
 87:     public function getDefinitions()
 88:     {
 89:         return $this->definitions;
 90:     }
 91: 
 92: 
 93:     /**
 94:      * Does the service definition exist?
 95:      * @param  string
 96:      * @return bool
 97:      */
 98:     public function hasDefinition($name)
 99:     {
100:         return isset($this->definitions[$name]);
101:     }
102: 
103: 
104:     /********************* class resolving ****************d*g**/
105: 
106: 
107:     /**
108:      * Resolves service name by type.
109:      * @param  string  class or interface
110:      * @return string  service name or NULL
111:      * @throws ServiceCreationException
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 ServiceCreationException("Multiple services of type $class found: " . implode(', ', $this->classes[$lower]));
124:         }
125:     }
126: 
127: 
128:     /**
129:      * Gets the service objects of the specified tag.
130:      * @param  string
131:      * @return array of [service name => tag attributes]
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:      * Creates a list of arguments using autowiring.
147:      * @return array
148:      */
149:     public function autowireArguments($class, $method, array $arguments)
150:     {
151:         $rc = ClassReflection::from($class);
152:         if (!$rc->hasMethod($method)) {
153:             if (!Validators::isList($arguments)) {
154:                 throw new ServiceCreationException("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 ServiceCreationException("$rm is not callable.");
162:         }
163:         $this->addDependency($rm->getFileName());
164:         return DIHelpers::autowireArguments($rm, $arguments, $this);
165:     }
166: 
167: 
168:     /**
169:      * Generates $dependencies, $classes and expands and normalize class names.
170:      * @return array
171:      */
172:     public function prepareClassList()
173:     {
174:         // complete class-factory pairs; expand classes
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 DIStatement($def->class);
190:                 }
191:             } elseif (!$def->factory) {
192:                 throw new ServiceCreationException("Class and factory are missing in service '$name' definition.");
193:             }
194:         }
195: 
196:         // check if services are instantiable
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) || !ClassReflection::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:         // complete classes
207:         $this->classes = FALSE;
208:         foreach ($this->definitions as $name => $def) {
209:             $this->resolveClass($name);
210:         }
211: 
212:         //  build auto-wiring list
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 = ClassReflection::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(ClassReflection::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)) { // method calling
249:             if ($service = $this->getServiceName($factory[0])) {
250:                 if (Strings::contains($service, '\\')) { // @Class
251:                     throw new ServiceCreationException("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 Callback($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)) { // alias or factory
272:             if (Strings::contains($service, '\\')) { // @Class
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; // class name
284:         }
285:     }
286: 
287: 
288:     /**
289:      * Adds a file to the list of dependencies.
290:      * @return self
291:      */
292:     public function addDependency($file)
293:     {
294:         $this->dependencies[$file] = TRUE;
295:         return $this;
296:     }
297: 
298: 
299:     /**
300:      * Returns the list of dependent files.
301:      * @return array
302:      */
303:     public function getDependencies()
304:     {
305:         unset($this->dependencies[FALSE]);
306:         return array_keys($this->dependencies);
307:     }
308: 
309: 
310:     /********************* code generator ****************d*g**/
311: 
312: 
313:     /**
314:      * Generates PHP class.
315:      * @return PhpClassType
316:      */
317:     public function generateClass($parentClass = 'DIContainer')
318:     {
319:         unset($this->definitions[self::THIS_CONTAINER]);
320:         $this->addDefinition(self::THIS_CONTAINER)->setClass($parentClass);
321: 
322:         $this->prepareClassList();
323: 
324:         $class = new PhpClassType('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 (ServiceCreationException $e) {
334:                 $classes->value[$name] = new PhpLiteral('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][DIContainer::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 = 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(), NULL, $e);
375:             }
376:         }
377: 
378:         return $class;
379:     }
380: 
381: 
382:     /**
383:      * Generates body of service method.
384:      * @return string
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 PhpLiteral('$' . end($v));
393:         }
394: 
395:         $code = '$service = ' . $this->formatStatement(DIHelpers::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 .= PhpHelpers::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 = DIHelpers::expand($setup, $parameters, TRUE);
407:             if (is_string($setup->entity) && strpbrk($setup->entity, ':@?') === FALSE) { // auto-prepend @self
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:      * Formats PHP code for class instantiating, function calling or property setting in PHP.
419:      * @return string
420:      * @internal
421:      */
422:     public function formatStatement(DIStatement $statement, $self = NULL)
423:     {
424:         $entity = $this->normalizeEntity($statement->entity);
425:         $arguments = $statement->arguments;
426: 
427:         if (is_string($entity) && Strings::contains($entity, '?')) { // PHP literal
428:             return $this->formatPhp($entity, $arguments, $self);
429: 
430:         } elseif ($service = $this->getServiceName($entity)) { // factory calling or service retrieving
431:             if ($this->definitions[$service]->shared) {
432:                 if ($arguments) {
433:                     throw new ServiceCreationException("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) ? '' : ' = ' . PhpHelpers::dump($v));
440:             }
441:             $rm = new FunctionReflection(create_function(implode(', ', $params), ''));
442:             $arguments = DIHelpers::autowireArguments($rm, $arguments, $this);
443:             return $this->formatPhp('$this->?(?*)', array(DIContainer::getMethodName($service, FALSE), $arguments), $self);
444: 
445:         } elseif ($entity === 'not') { // operator
446:             return $this->formatPhp('!?', array($arguments[0]));
447: 
448:         } elseif (is_string($entity)) { // class name
449:             if ($constructor = ClassReflection::from($entity)->getConstructor()) {
450:                 $this->addDependency($constructor->getFileName());
451:                 $arguments = DIHelpers::autowireArguments($constructor, $arguments, $this);
452:             } elseif ($arguments) {
453:                 throw new ServiceCreationException("Unable to pass arguments, class $entity has no constructor.");
454:             }
455:             return $this->formatPhp("new $entity" . ($arguments ? '(?*)' : ''), array($arguments), $self);
456: 
457:         } elseif (!Validators::isList($entity) || count($entity) !== 2) {
458:             throw new InvalidStateException("Expected class, method or property, " . PhpHelpers::dump($entity) . " given.");
459: 
460:         } elseif ($entity[0] === '') { // globalFunc
461:             return $this->formatPhp("$entity[1](?*)", array($arguments), $self);
462: 
463:         } elseif (Strings::contains($entity[1], '$')) { // property setter
464:             Validators::assert($arguments, 'list:1', "setup arguments for '" . Callback::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)) { // service method
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 { // static method
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:      * Formats PHP statement.
486:      * @return string
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 DIStatement) {
495:                 $val = new PhpLiteral($that->formatStatement($val, $self));
496: 
497:             } elseif ($val === \'@\' . DIContainerBuilder::THIS_CONTAINER) {
498:                 $val = new PhpLiteral(\'$this\');
499: 
500:             } elseif ($service = $that->getServiceName($val, $self)) {
501:                 $val = $service === $self ? \'$service\' : $that->formatStatement(new DIStatement($val));
502:                 $val = new PhpLiteral($val);
503:             }
504:         '));
505:         return PhpHelpers::formatArgs($statement, $args);
506:     }
507: 
508: 
509:     /**
510:      * Expands %placeholders% in strings (recursive).
511:      * @return mixed
512:      */
513:     public function expand($value)
514:     {
515:         return DIHelpers::expand($value, $this->parameters, TRUE);
516:     }
517: 
518: 
519:     /** @internal */
520:     public function normalizeEntity($entity)
521:     {
522:         if (is_string($entity) && Strings::contains($entity, '::') && !Strings::contains($entity, '?')) { // Class::method -> [Class, method]
523:             $entity = explode('::', $entity);
524:         }
525: 
526:         if (is_array($entity) && $entity[0] instanceof DIServiceDefinition) { // [ServiceDefinition, ...] -> [@serviceName, ...]
527:             $tmp = array_keys($this->definitions, $entity[0], TRUE);
528:             $entity[0] = "@$tmp[0]";
529: 
530:         } elseif ($entity instanceof DIServiceDefinition) { // ServiceDefinition -> @serviceName
531:             $tmp = array_keys($this->definitions, $entity, TRUE);
532:             $entity = "@$tmp[0]";
533: 
534:         } elseif (is_array($entity) && $entity[0] === $this) { // [$this, ...] -> [@container, ...]
535:             $entity[0] = '@' . DIContainerBuilder::THIS_CONTAINER;
536:         }
537:         return $entity; // Class, @service, [Class, member], [@service, member], [, globalFunc]
538:     }
539: 
540: 
541:     /**
542:      * Converts @service or @Class -> service name and checks its existence.
543:      * @return string  of FALSE, if argument is not service name
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 (Strings::contains($service, '\\')) {
555:             if ($this->classes === FALSE) { // may be disabled by prepareClassList
556:                 return $service;
557:             }
558:             $res = $this->getByType($service);
559:             if (!$res) {
560:                 throw new ServiceCreationException("Reference to missing service of type $service.");
561:             }
562:             return $res;
563:         }
564:         if (!isset($this->definitions[$service])) {
565:             throw new ServiceCreationException("Reference to missing service '$service'.");
566:         }
567:         return $service;
568:     }
569: 
570: }
571: 
Nette Framework 2.0.11 (for PHP 5.2, un-prefixed) API API documentation generated by ApiGen 2.8.0