Namespaces

  • 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

  • Container
  • ContainerBuilder
  • ServiceDefinition
  • Statement

Interfaces

  • IContainer

Exceptions

  • MissingServiceException
  • ServiceCreationException
  • Overview
  • Namespace
  • 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:  */
  7: 
  8: namespace Nette\DI;
  9: 
 10: use Nette,
 11:     Nette\Utils\Validators,
 12:     Nette\Utils\Strings,
 13:     Nette\Reflection,
 14:     Nette\Utils\PhpGenerator\Helpers as PhpHelpers,
 15:     Nette\Utils\PhpGenerator\PhpLiteral;
 16: 
 17: 
 18: /**
 19:  * Basic container builder.
 20:  *
 21:  * @author     David Grudl
 22:  * @property-read ServiceDefinition[] $definitions
 23:  * @property-read array $dependencies
 24:  */
 25: class ContainerBuilder extends Nette\Object
 26: {
 27:     const CREATED_SERVICE = 'self',
 28:         THIS_CONTAINER = 'container';
 29: 
 30:     /** @var array  %param% will be expanded */
 31:     public $parameters = array();
 32: 
 33:     /** @var ServiceDefinition[] */
 34:     private $definitions = array();
 35: 
 36:     /** @var array for auto-wiring */
 37:     private $classes;
 38: 
 39:     /** @var array of file names */
 40:     private $dependencies = array();
 41: 
 42: 
 43:     /**
 44:      * Adds new service definition. The expressions %param% and @service will be expanded.
 45:      * @param  string
 46:      * @return ServiceDefinition
 47:      */
 48:     public function addDefinition($name)
 49:     {
 50:         if (!is_string($name) || !$name) { // builder is not ready for falsy names such as '0'
 51:             throw new Nette\InvalidArgumentException("Service name must be a non-empty string, " . gettype($name) . " given.");
 52: 
 53:         } elseif (isset($this->definitions[$name])) {
 54:             throw new Nette\InvalidStateException("Service '$name' has already been added.");
 55:         }
 56:         return $this->definitions[$name] = new ServiceDefinition;
 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:      * Gets the service definition.
 73:      * @param  string
 74:      * @return ServiceDefinition
 75:      */
 76:     public function getDefinition($name)
 77:     {
 78:         if (!isset($this->definitions[$name])) {
 79:             throw new MissingServiceException("Service '$name' not found.");
 80:         }
 81:         return $this->definitions[$name];
 82:     }
 83: 
 84: 
 85:     /**
 86:      * Gets all service definitions.
 87:      * @return array
 88:      */
 89:     public function getDefinitions()
 90:     {
 91:         return $this->definitions;
 92:     }
 93: 
 94: 
 95:     /**
 96:      * Does the service definition exist?
 97:      * @param  string
 98:      * @return bool
 99:      */
100:     public function hasDefinition($name)
101:     {
102:         return isset($this->definitions[$name]);
103:     }
104: 
105: 
106:     /********************* class resolving ****************d*g**/
107: 
108: 
109:     /**
110:      * Resolves service name by type.
111:      * @param  string  class or interface
112:      * @return string  service name or NULL
113:      * @throws ServiceCreationException
114:      */
115:     public function getByType($class)
116:     {
117:         $lower = ltrim(strtolower($class), '\\');
118:         if (!isset($this->classes[$lower])) {
119:             return;
120: 
121:         } elseif (count($this->classes[$lower]) === 1) {
122:             return $this->classes[$lower][0];
123: 
124:         } else {
125:             throw new ServiceCreationException("Multiple services of type $class found: " . implode(', ', $this->classes[$lower]));
126:         }
127:     }
128: 
129: 
130:     /**
131:      * Gets the service objects of the specified tag.
132:      * @param  string
133:      * @return array of [service name => tag attributes]
134:      */
135:     public function findByTag($tag)
136:     {
137:         $found = array();
138:         foreach ($this->definitions as $name => $def) {
139:             if (isset($def->tags[$tag]) && $def->shared) {
140:                 $found[$name] = $def->tags[$tag];
141:             }
142:         }
143:         return $found;
144:     }
145: 
146: 
147:     /**
148:      * Creates a list of arguments using autowiring.
149:      * @return array
150:      */
151:     public function autowireArguments($class, $method, array $arguments)
152:     {
153:         $rc = Reflection\ClassType::from($class);
154:         if (!$rc->hasMethod($method)) {
155:             if (!Nette\Utils\Validators::isList($arguments)) {
156:                 throw new ServiceCreationException("Unable to pass specified arguments to $class::$method().");
157:             }
158:             return $arguments;
159:         }
160: 
161:         $rm = $rc->getMethod($method);
162:         if ($rm->isAbstract() || !$rm->isPublic()) {
163:             throw new ServiceCreationException("$rm is not callable.");
164:         }
165:         $this->addDependency($rm->getFileName());
166:         return Helpers::autowireArguments($rm, $arguments, $this);
167:     }
168: 
169: 
170:     /**
171:      * Generates $dependencies, $classes and expands and normalize class names.
172:      * @return array
173:      * @internal
174:      */
175:     public function prepareClassList()
176:     {
177:         // complete class-factory pairs; expand classes
178:         foreach ($this->definitions as $name => $def) {
179:             if ($def->class === self::CREATED_SERVICE || ($def->factory && $def->factory->entity === self::CREATED_SERVICE)) {
180:                 $def->class = $name;
181:                 $def->internal = TRUE;
182:                 if ($def->factory && $def->factory->entity === self::CREATED_SERVICE) {
183:                     $def->factory->entity = $def->class;
184:                 }
185:                 unset($this->definitions[$name]);
186:                 $this->definitions['_anonymous_' . str_replace('\\', '_', strtolower(trim($name, '\\')))] = $def;
187:             }
188: 
189:             if ($def->class) {
190:                 $def->class = $this->expand($def->class);
191:                 if (!$def->factory) {
192:                     $def->factory = new Statement($def->class);
193:                 }
194:             } elseif (!$def->factory) {
195:                 throw new ServiceCreationException("Class and factory are missing in service '$name' definition.");
196:             }
197:         }
198: 
199:         // check if services are instantiable
200:         foreach ($this->definitions as $name => $def) {
201:             $factory = $this->normalizeEntity($this->expand($def->factory->entity));
202:             if (is_string($factory) && preg_match('#^[\w\\\\]+\z#', $factory) && $factory !== self::CREATED_SERVICE) {
203:                 if (!class_exists($factory) || !Reflection\ClassType::from($factory)->isInstantiable()) {
204:                     throw new Nette\InvalidStateException("Class $factory used in service '$name' has not been found or is not instantiable.");
205:                 }
206:             }
207:         }
208: 
209:         // complete classes
210:         $this->classes = FALSE;
211:         foreach ($this->definitions as $name => $def) {
212:             $this->resolveClass($name);
213:         }
214: 
215:         //  build auto-wiring list
216:         $this->classes = array();
217:         foreach ($this->definitions as $name => $def) {
218:             if (!$def->class) {
219:                 continue;
220:             }
221:             if (!class_exists($def->class) && !interface_exists($def->class)) {
222:                 throw new Nette\InvalidStateException("Class $def->class has not been found.");
223:             }
224:             $def->class = Reflection\ClassType::from($def->class)->getName();
225:             if ($def->autowired) {
226:                 foreach (class_parents($def->class) + class_implements($def->class) + array($def->class) as $parent) {
227:                     $this->classes[strtolower($parent)][] = (string) $name;
228:                 }
229:             }
230:         }
231: 
232:         foreach ($this->classes as $class => $foo) {
233:             $this->addDependency(Reflection\ClassType::from($class)->getFileName());
234:         }
235:     }
236: 
237: 
238:     private function resolveClass($name, $recursive = array())
239:     {
240:         if (isset($recursive[$name])) {
241:             throw new Nette\InvalidArgumentException('Circular reference detected for services: ' . implode(', ', array_keys($recursive)) . '.');
242:         }
243:         $recursive[$name] = TRUE;
244: 
245:         $def = $this->definitions[$name];
246:         $factory = $this->normalizeEntity($this->expand($def->factory->entity));
247: 
248:         if ($def->class) {
249:             return $def->class;
250: 
251:         } elseif (is_array($factory)) { // method calling
252:             if ($service = $this->getServiceName($factory[0])) {
253:                 if (Strings::contains($service, '\\')) { // @\Class
254:                     throw new ServiceCreationException("Unable resolve class name for service '$name'.");
255:                 }
256:                 $factory[0] = $this->resolveClass($service, $recursive);
257:                 if (!$factory[0]) {
258:                     return;
259:                 }
260:             }
261:             $factory = new Nette\Callback($factory);
262:             try {
263:                 $reflection = $factory->toReflection();
264:             } catch (\ReflectionException $e) {
265:             }
266:             if (isset($e) || !$factory->isCallable()) {
267:                 throw new ServiceCreationException("Factory '$factory' used in service '$name' is not callable.");
268:             }
269:             $def->class = preg_replace('#[|\s].*#', '', $reflection->getAnnotation('return'));
270:             if ($def->class && !class_exists($def->class) && $def->class[0] !== '\\' && $reflection instanceof \ReflectionMethod) {
271:                 $def->class = $reflection->getDeclaringClass()->getNamespaceName() . '\\' . $def->class;
272:             }
273: 
274:         } elseif ($service = $this->getServiceName($factory)) { // alias or factory
275:             if (Strings::contains($service, '\\')) { // @\Class
276:                 $def->autowired = FALSE;
277:                 return $def->class = $service;
278:             }
279:             if ($this->definitions[$service]->shared) {
280:                 $def->autowired = FALSE;
281:             }
282:             return $def->class = $this->resolveClass($service, $recursive);
283: 
284:         } else {
285:             return $def->class = $factory; // class name
286:         }
287:     }
288: 
289: 
290:     /**
291:      * Adds a file to the list of dependencies.
292:      * @return self
293:      */
294:     public function addDependency($file)
295:     {
296:         $this->dependencies[$file] = TRUE;
297:         return $this;
298:     }
299: 
300: 
301:     /**
302:      * Returns the list of dependent files.
303:      * @return array
304:      */
305:     public function getDependencies()
306:     {
307:         unset($this->dependencies[FALSE]);
308:         return array_keys($this->dependencies);
309:     }
310: 
311: 
312:     /********************* code generator ****************d*g**/
313: 
314: 
315:     /**
316:      * Generates PHP class.
317:      * @return Nette\Utils\PhpGenerator\ClassType
318:      */
319:     public function generateClass($parentClass = 'Nette\DI\Container')
320:     {
321:         unset($this->definitions[self::THIS_CONTAINER]);
322:         $this->addDefinition(self::THIS_CONTAINER)->setClass($parentClass);
323: 
324:         $this->prepareClassList();
325: 
326:         $class = new Nette\Utils\PhpGenerator\ClassType('Container');
327:         $class->addExtend($parentClass);
328:         $class->addMethod('__construct')
329:             ->addBody('parent::__construct(?);', array($this->expand($this->parameters)));
330: 
331:         $classes = $class->addProperty('classes', array());
332:         foreach ($this->classes as $name => $foo) {
333:             try {
334:                 $classes->value[$name] = $this->getByType($name);
335:             } catch (ServiceCreationException $e) {
336:                 $classes->value[$name] = new PhpLiteral('FALSE, //' . strstr($e->getMessage(), ':'));
337:             }
338:         }
339: 
340:         $definitions = $this->definitions;
341:         ksort($definitions);
342: 
343:         $meta = $class->addProperty('meta', array());
344:         foreach ($definitions as $name => $def) {
345:             if ($def->shared) {
346:                 foreach ($this->expand($def->tags) as $tag => $value) {
347:                     $meta->value[$name][Container::TAGS][$tag] = $value;
348:                 }
349:             }
350:         }
351: 
352:         foreach ($definitions as $name => $def) {
353:             try {
354:                 $name = (string) $name;
355:                 $type = $def->class ?: 'object';
356:                 $methodName = Container::getMethodName($name, $def->shared);
357:                 if (!PhpHelpers::isIdentifier($methodName)) {
358:                     throw new ServiceCreationException('Name contains invalid characters.');
359:                 }
360:                 if ($def->shared && !$def->internal && PhpHelpers::isIdentifier($name)) {
361:                     $class->addDocument("@property $type \$$name");
362:                 }
363:                 $method = $class->addMethod($methodName)
364:                     ->addDocument("@return $type")
365:                     ->setVisibility($def->shared || $def->internal ? 'protected' : 'public')
366:                     ->setBody($name === self::THIS_CONTAINER ? 'return $this;' : $this->generateService($name));
367: 
368:                 foreach ($this->expand($def->parameters) as $k => $v) {
369:                     $tmp = explode(' ', is_int($k) ? $v : $k);
370:                     $param = is_int($k) ? $method->addParameter(end($tmp)) : $method->addParameter(end($tmp), $v);
371:                     if (isset($tmp[1])) {
372:                         $param->setTypeHint($tmp[0]);
373:                     }
374:                 }
375:             } catch (\Exception $e) {
376:                 throw new ServiceCreationException("Service '$name': " . $e->getMessage(), NULL, $e);
377:             }
378:         }
379: 
380:         return $class;
381:     }
382: 
383: 
384:     /**
385:      * Generates body of service method.
386:      * @return string
387:      */
388:     private function generateService($name)
389:     {
390:         $def = $this->definitions[$name];
391:         $parameters = $this->parameters;
392:         foreach ($this->expand($def->parameters) as $k => $v) {
393:             $v = explode(' ', is_int($k) ? $v : $k);
394:             $parameters[end($v)] = new PhpLiteral('$' . end($v));
395:         }
396: 
397:         $code = '$service = ' . $this->formatStatement(Helpers::expand($def->factory, $parameters, TRUE)) . ";\n";
398: 
399:         $entity = $this->normalizeEntity($def->factory->entity);
400:         if ($def->class && $def->class !== $entity && !$this->getServiceName($entity)) {
401:             $code .= PhpHelpers::formatArgs("if (!\$service instanceof $def->class) {\n"
402:                 . "\tthrow new Nette\\UnexpectedValueException(?);\n}\n",
403:                 array("Unable to create service '$name', value returned by factory is not $def->class type.")
404:             );
405:         }
406: 
407:         foreach ((array) $def->setup as $setup) {
408:             $setup = Helpers::expand($setup, $parameters, TRUE);
409:             if (is_string($setup->entity) && strpbrk($setup->entity, ':@?') === FALSE) { // auto-prepend @self
410:                 $setup->entity = array("@$name", $setup->entity);
411:             }
412:             $code .= $this->formatStatement($setup, $name) . ";\n";
413:         }
414: 
415:         return $code .= 'return $service;';
416:     }
417: 
418: 
419:     /**
420:      * Formats PHP code for class instantiating, function calling or property setting in PHP.
421:      * @return string
422:      * @internal
423:      */
424:     public function formatStatement(Statement $statement, $self = NULL)
425:     {
426:         $entity = $this->normalizeEntity($statement->entity);
427:         $arguments = $statement->arguments;
428: 
429:         if (is_string($entity) && Strings::contains($entity, '?')) { // PHP literal
430:             return $this->formatPhp($entity, $arguments, $self);
431: 
432:         } elseif ($service = $this->getServiceName($entity)) { // factory calling or service retrieving
433:             if ($this->definitions[$service]->shared) {
434:                 if ($arguments) {
435:                     throw new ServiceCreationException("Unable to call service '$entity'.");
436:                 }
437:                 return $this->formatPhp('$this->getService(?)', array($service));
438:             }
439:             $params = array();
440:             foreach ($this->definitions[$service]->parameters as $k => $v) {
441:                 $params[] = preg_replace('#\w+\z#', '\$$0', (is_int($k) ? $v : $k)) . (is_int($k) ? '' : ' = ' . PhpHelpers::dump($v));
442:             }
443:             $rm = new Reflection\GlobalFunction(create_function(implode(', ', $params), ''));
444:             $arguments = Helpers::autowireArguments($rm, $arguments, $this);
445:             return $this->formatPhp('$this->?(?*)', array(Container::getMethodName($service, FALSE), $arguments), $self);
446: 
447:         } elseif ($entity === 'not') { // operator
448:             return $this->formatPhp('!?', array($arguments[0]));
449: 
450:         } elseif (is_string($entity)) { // class name
451:             if ($constructor = Reflection\ClassType::from($entity)->getConstructor()) {
452:                 $this->addDependency($constructor->getFileName());
453:                 $arguments = Helpers::autowireArguments($constructor, $arguments, $this);
454:             } elseif ($arguments) {
455:                 throw new ServiceCreationException("Unable to pass arguments, class $entity has no constructor.");
456:             }
457:             return $this->formatPhp("new $entity" . ($arguments ? '(?*)' : ''), array($arguments), $self);
458: 
459:         } elseif (!Validators::isList($entity) || count($entity) !== 2) {
460:             throw new Nette\InvalidStateException("Expected class, method or property, " . PhpHelpers::dump($entity) . " given.");
461: 
462:         } elseif ($entity[0] === '') { // globalFunc
463:             return $this->formatPhp("$entity[1](?*)", array($arguments), $self);
464: 
465:         } elseif (Strings::contains($entity[1], '$')) { // property setter
466:             Validators::assert($arguments, 'list:1', "setup arguments for '" . Nette\Callback::create($entity) . "'");
467:             if ($this->getServiceName($entity[0], $self)) {
468:                 return $this->formatPhp('?->? = ?', array($entity[0], substr($entity[1], 1), $arguments[0]), $self);
469:             } else {
470:                 return $this->formatPhp($entity[0] . '::$? = ?', array(substr($entity[1], 1), $arguments[0]), $self);
471:             }
472: 
473:         } elseif ($service = $this->getServiceName($entity[0], $self)) { // service method
474:             if ($this->definitions[$service]->class) {
475:                 $arguments = $this->autowireArguments($this->definitions[$service]->class, $entity[1], $arguments);
476:             }
477:             return $this->formatPhp('?->?(?*)', array($entity[0], $entity[1], $arguments), $self);
478: 
479:         } else { // static method
480:             $arguments = $this->autowireArguments($entity[0], $entity[1], $arguments);
481:             return $this->formatPhp("$entity[0]::$entity[1](?*)", array($arguments), $self);
482:         }
483:     }
484: 
485: 
486:     /**
487:      * Formats PHP statement.
488:      * @return string
489:      * @internal
490:      */
491:     public function formatPhp($statement, $args, $self = NULL)
492:     {
493:         $that = $this;
494:         array_walk_recursive($args, function(& $val) use ($self, $that) {
495:             list($val) = $that->normalizeEntity(array($val));
496: 
497:             if ($val instanceof Statement) {
498:                 $val = new PhpLiteral($that->formatStatement($val, $self));
499: 
500:             } elseif ($val === '@' . ContainerBuilder::THIS_CONTAINER) {
501:                 $val = new PhpLiteral('$this');
502: 
503:             } elseif ($service = $that->getServiceName($val, $self)) {
504:                 $val = $service === $self ? '$service' : $that->formatStatement(new Statement($val));
505:                 $val = new PhpLiteral($val);
506:             }
507:         });
508:         return PhpHelpers::formatArgs($statement, $args);
509:     }
510: 
511: 
512:     /**
513:      * Expands %placeholders% in strings (recursive).
514:      * @return mixed
515:      */
516:     public function expand($value)
517:     {
518:         return Helpers::expand($value, $this->parameters, TRUE);
519:     }
520: 
521: 
522:     /** @internal */
523:     public function normalizeEntity($entity)
524:     {
525:         if (is_string($entity) && Strings::contains($entity, '::') && !Strings::contains($entity, '?')) { // Class::method -> [Class, method]
526:             $entity = explode('::', $entity);
527:         }
528: 
529:         if (is_array($entity) && $entity[0] instanceof ServiceDefinition) { // [ServiceDefinition, ...] -> [@serviceName, ...]
530:             $tmp = array_keys($this->definitions, $entity[0], TRUE);
531:             $entity[0] = "@$tmp[0]";
532: 
533:         } elseif ($entity instanceof ServiceDefinition) { // ServiceDefinition -> @serviceName
534:             $tmp = array_keys($this->definitions, $entity, TRUE);
535:             $entity = "@$tmp[0]";
536: 
537:         } elseif (is_array($entity) && $entity[0] === $this) { // [$this, ...] -> [@container, ...]
538:             $entity[0] = '@' . ContainerBuilder::THIS_CONTAINER;
539:         }
540:         return $entity; // Class, @service, [Class, member], [@service, member], [, globalFunc]
541:     }
542: 
543: 
544:     /**
545:      * Converts @service or @\Class -> service name and checks its existence.
546:      * @return string  of FALSE, if argument is not service name
547:      * @internal
548:      */
549:     public function getServiceName($arg, $self = NULL)
550:     {
551:         if (!is_string($arg) || !preg_match('#^@[\w\\\\.].*\z#', $arg)) {
552:             return FALSE;
553:         }
554:         $service = substr($arg, 1);
555:         if ($service === self::CREATED_SERVICE) {
556:             $service = $self;
557:         }
558:         if (Strings::contains($service, '\\')) {
559:             if ($this->classes === FALSE) { // may be disabled by prepareClassList
560:                 return $service;
561:             }
562:             $res = $this->getByType($service);
563:             if (!$res) {
564:                 throw new ServiceCreationException("Reference to missing service of type $service.");
565:             }
566:             return $res;
567:         }
568:         if (!isset($this->definitions[$service])) {
569:             throw new ServiceCreationException("Reference to missing service '$service'.");
570:         }
571:         return $service;
572:     }
573: 
574: }
575: 
Nette 2.0 API documentation generated by ApiGen 2.8.0