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

Classes

Interfaces

Exceptions

  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  • Other releases
  • Nette homepage
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Nette Framework (https://nette.org)
  5:  * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
  6:  * @package Nette\DI
  7:  */
  8: 
  9: 
 10: 
 11: /**
 12:  * Basic container builder.
 13:  *
 14:  * @author     David Grudl
 15:  * @property-read DIServiceDefinition[] $definitions
 16:  * @property-read array $dependencies
 17:  * @package Nette\DI
 18:  */
 19: class DIContainerBuilder extends Object
 20: {
 21:     const CREATED_SERVICE = 'self',
 22:         THIS_CONTAINER = 'container';
 23: 
 24:     /** @var array  %param% will be expanded */
 25:     public $parameters = array();
 26: 
 27:     /** @var DIServiceDefinition[] */
 28:     private $definitions = array();
 29: 
 30:     /** @var array for auto-wiring */
 31:     private $classes;
 32: 
 33:     /** @var array of file names */
 34:     private $dependencies = array();
 35: 
 36: 
 37:     /**
 38:      * Adds new service definition. The expressions %param% and @service will be expanded.
 39:      * @param  string
 40:      * @return DIServiceDefinition
 41:      */
 42:     public function addDefinition($name)
 43:     {
 44:         if (!is_string($name) || !$name) { // builder is not ready for falsy names such as '0'
 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 DIServiceDefinition;
 51:     }
 52: 
 53: 
 54:     /**
 55:      * Removes the specified service definition.
 56:      * @param  string
 57:      * @return void
 58:      */
 59:     public function removeDefinition($name)
 60:     {
 61:         unset($this->definitions[$name]);
 62:     }
 63: 
 64: 
 65:     /**
 66:      * Gets the service definition.
 67:      * @param  string
 68:      * @return DIServiceDefinition
 69:      */
 70:     public function getDefinition($name)
 71:     {
 72:         if (!isset($this->definitions[$name])) {
 73:             throw new MissingServiceException("Service '$name' not found.");
 74:         }
 75:         return $this->definitions[$name];
 76:     }
 77: 
 78: 
 79:     /**
 80:      * Gets all service definitions.
 81:      * @return array
 82:      */
 83:     public function getDefinitions()
 84:     {
 85:         return $this->definitions;
 86:     }
 87: 
 88: 
 89:     /**
 90:      * Does the service definition exist?
 91:      * @param  string
 92:      * @return bool
 93:      */
 94:     public function hasDefinition($name)
 95:     {
 96:         return isset($this->definitions[$name]);
 97:     }
 98: 
 99: 
100:     /********************* class resolving ****************d*g**/
101: 
102: 
103:     /**
104:      * Resolves service name by type.
105:      * @param  string  class or interface
106:      * @return string  service name or NULL
107:      * @throws ServiceCreationException
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 ServiceCreationException("Multiple services of type $class found: " . implode(', ', $this->classes[$lower]));
120:         }
121:     }
122: 
123: 
124:     /**
125:      * Gets the service objects of the specified tag.
126:      * @param  string
127:      * @return array of [service name => tag attributes]
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:      * Creates a list of arguments using autowiring.
143:      * @return array
144:      */
145:     public function autowireArguments($class, $method, array $arguments)
146:     {
147:         $rc = ClassReflection::from($class);
148:         if (!$rc->hasMethod($method)) {
149:             if (!Validators::isList($arguments)) {
150:                 throw new ServiceCreationException("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 ServiceCreationException("$rm is not callable.");
158:         }
159:         $this->addDependency($rm->getFileName());
160:         return DIHelpers::autowireArguments($rm, $arguments, $this);
161:     }
162: 
163: 
164:     /**
165:      * Generates $dependencies, $classes and expands and normalize class names.
166:      * @return array
167:      * @internal
168:      */
169:     public function prepareClassList()
170:     {
171:         // complete class-factory pairs; expand classes
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 DIStatement($def->class);
187:                 }
188:             } elseif (!$def->factory) {
189:                 throw new ServiceCreationException("Class and factory are missing in service '$name' definition.");
190:             }
191:         }
192: 
193:         // check if services are instantiable
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) || !ClassReflection::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:         // complete classes
204:         $this->classes = FALSE;
205:         foreach ($this->definitions as $name => $def) {
206:             $this->resolveClass($name);
207:         }
208: 
209:         //  build auto-wiring list
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 = ClassReflection::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(ClassReflection::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)) { // method calling
246:             if ($service = $this->getServiceName($factory[0])) {
247:                 if (Strings::contains($service, '\\')) { // @Class
248:                     throw new ServiceCreationException("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 Callback($factory);
256:             try {
257:                 $reflection = $factory->toReflection();
258:             } catch (ReflectionException $e) {
259:             }
260:             if (isset($e) || !$factory->isCallable()) {
261:                 throw new ServiceCreationException("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)) { // alias or factory
268:             if (Strings::contains($service, '\\')) { // @Class
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; // class name
280:         }
281:     }
282: 
283: 
284:     /**
285:      * Adds a file to the list of dependencies.
286:      * @return self
287:      */
288:     public function addDependency($file)
289:     {
290:         $this->dependencies[$file] = TRUE;
291:         return $this;
292:     }
293: 
294: 
295:     /**
296:      * Returns the list of dependent files.
297:      * @return array
298:      */
299:     public function getDependencies()
300:     {
301:         unset($this->dependencies[FALSE]);
302:         return array_keys($this->dependencies);
303:     }
304: 
305: 
306:     /********************* code generator ****************d*g**/
307: 
308: 
309:     /**
310:      * Generates PHP class.
311:      * @return PhpClassType
312:      */
313:     public function generateClass($parentClass = 'DIContainer')
314:     {
315:         unset($this->definitions[self::THIS_CONTAINER]);
316:         $this->addDefinition(self::THIS_CONTAINER)->setClass($parentClass);
317: 
318:         $this->prepareClassList();
319: 
320:         $class = new PhpClassType('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 (ServiceCreationException $e) {
330:                 $classes->value[$name] = new PhpLiteral('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][DIContainer::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 = DIContainer::getMethodName($name, $def->shared);
351:                 if (!PhpHelpers::isIdentifier($methodName)) {
352:                     throw new ServiceCreationException('Name contains invalid characters.');
353:                 }
354:                 if ($def->shared && !$def->internal && PhpHelpers::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 ServiceCreationException("Service '$name': " . $e->getMessage(), NULL, $e);
371:             }
372:         }
373: 
374:         return $class;
375:     }
376: 
377: 
378:     /**
379:      * Generates body of service method.
380:      * @return string
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 PhpLiteral('$' . end($v));
389:         }
390: 
391:         $code = '$service = ' . $this->formatStatement(DIHelpers::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 .= PhpHelpers::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 = DIHelpers::expand($setup, $parameters, TRUE);
403:             if (is_string($setup->entity) && strpbrk($setup->entity, ':@?') === FALSE) { // auto-prepend @self
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:      * Formats PHP code for class instantiating, function calling or property setting in PHP.
415:      * @return string
416:      * @internal
417:      */
418:     public function formatStatement(DIStatement $statement, $self = NULL)
419:     {
420:         $entity = $this->normalizeEntity($statement->entity);
421:         $arguments = $statement->arguments;
422: 
423:         if (is_string($entity) && Strings::contains($entity, '?')) { // PHP literal
424:             return $this->formatPhp($entity, $arguments, $self);
425: 
426:         } elseif ($service = $this->getServiceName($entity)) { // factory calling or service retrieving
427:             if ($this->definitions[$service]->shared) {
428:                 if ($arguments) {
429:                     throw new ServiceCreationException("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) ? '' : ' = ' . PhpHelpers::dump($v));
436:             }
437:             $rm = new FunctionReflection(create_function(implode(', ', $params), ''));
438:             $arguments = DIHelpers::autowireArguments($rm, $arguments, $this);
439:             return $this->formatPhp('$this->?(?*)', array(DIContainer::getMethodName($service, FALSE), $arguments), $self);
440: 
441:         } elseif ($entity === 'not') { // operator
442:             return $this->formatPhp('!?', array($arguments[0]));
443: 
444:         } elseif (is_string($entity)) { // class name
445:             if ($constructor = ClassReflection::from($entity)->getConstructor()) {
446:                 $this->addDependency($constructor->getFileName());
447:                 $arguments = DIHelpers::autowireArguments($constructor, $arguments, $this);
448:             } elseif ($arguments) {
449:                 throw new ServiceCreationException("Unable to pass arguments, class $entity has no constructor.");
450:             }
451:             return $this->formatPhp("new $entity" . ($arguments ? '(?*)' : ''), array($arguments), $self);
452: 
453:         } elseif (!Validators::isList($entity) || count($entity) !== 2) {
454:             throw new InvalidStateException("Expected class, method or property, " . PhpHelpers::dump($entity) . " given.");
455: 
456:         } elseif ($entity[0] === '') { // globalFunc
457:             return $this->formatPhp("$entity[1](?*)", array($arguments), $self);
458: 
459:         } elseif (Strings::contains($entity[1], '$')) { // property setter
460:             Validators::assert($arguments, 'list:1', "setup arguments for '" . Callback::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)) { // service method
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 { // static method
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:      * Formats PHP statement.
482:      * @return string
483:      * @internal
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 DIStatement) {
492:                 $val = new PhpLiteral($that->formatStatement($val, $self));
493: 
494:             } elseif ($val === \'@\' . DIContainerBuilder::THIS_CONTAINER) {
495:                 $val = new PhpLiteral(\'$this\');
496: 
497:             } elseif ($service = $that->getServiceName($val, $self)) {
498:                 $val = $service === $self ? \'$service\' : $that->formatStatement(new DIStatement($val));
499:                 $val = new PhpLiteral($val);
500:             }
501:         '));
502:         return PhpHelpers::formatArgs($statement, $args);
503:     }
504: 
505: 
506:     /**
507:      * Expands %placeholders% in strings (recursive).
508:      * @return mixed
509:      */
510:     public function expand($value)
511:     {
512:         return DIHelpers::expand($value, $this->parameters, TRUE);
513:     }
514: 
515: 
516:     /** @internal */
517:     public function normalizeEntity($entity)
518:     {
519:         if (is_string($entity) && Strings::contains($entity, '::') && !Strings::contains($entity, '?')) { // Class::method -> [Class, method]
520:             $entity = explode('::', $entity);
521:         }
522: 
523:         if (is_array($entity) && $entity[0] instanceof DIServiceDefinition) { // [ServiceDefinition, ...] -> [@serviceName, ...]
524:             $tmp = array_keys($this->definitions, $entity[0], TRUE);
525:             $entity[0] = "@$tmp[0]";
526: 
527:         } elseif ($entity instanceof DIServiceDefinition) { // ServiceDefinition -> @serviceName
528:             $tmp = array_keys($this->definitions, $entity, TRUE);
529:             $entity = "@$tmp[0]";
530: 
531:         } elseif (is_array($entity) && $entity[0] === $this) { // [$this, ...] -> [@container, ...]
532:             $entity[0] = '@' . DIContainerBuilder::THIS_CONTAINER;
533:         }
534:         return $entity; // Class, @service, [Class, member], [@service, member], [, globalFunc]
535:     }
536: 
537: 
538:     /**
539:      * Converts @service or @Class -> service name and checks its existence.
540:      * @return string  of FALSE, if argument is not service name
541:      * @internal
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 (Strings::contains($service, '\\')) {
553:             if ($this->classes === FALSE) { // may be disabled by prepareClassList
554:                 return $service;
555:             }
556:             $res = $this->getByType($service);
557:             if (!$res) {
558:                 throw new ServiceCreationException("Reference to missing service of type $service.");
559:             }
560:             return $res;
561:         }
562:         if (!isset($this->definitions[$service])) {
563:             throw new ServiceCreationException("Reference to missing service '$service'.");
564:         }
565:         return $service;
566:     }
567: 
568: }
569: 
Nette Framework 2.0.18 (for PHP 5.2, un-prefixed) API documentation generated by ApiGen 2.8.0