Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

641 lines (517 sloc) 19.343 kb
<?php
/**
* This file is part of the Nette Framework (http://nette.org)
*
* Copyright (c) 2004 David Grudl (http://davidgrudl.com)
*
* For the full copyright and license information, please view
* the file license.txt that was distributed with this source code.
*/
namespace Nette\DI;
use Nette,
Nette\Utils\Validators,
Nette\Utils\Strings,
Nette\Utils\PhpGenerator\Helpers as PhpHelpers,
Nette\Utils\PhpGenerator\PhpLiteral;
/**
* Basic container builder.
*
* @author David Grudl
* @property-read ServiceDefinition[] $definitions
* @property-read array $dependencies
*/
class ContainerBuilder extends Nette\Object
{
const CREATED_SERVICE = 'self',
THIS_CONTAINER = 'container';
/** @var array %param% will be expanded */
public $parameters = array();
/** @var ServiceDefinition[] */
private $definitions = array();
/** @var array for auto-wiring */
private $classes;
/** @var array of file names */
private $dependencies = array();
/**
* Adds new service definition. The expressions %param% and @service will be expanded.
* @param string
* @return ServiceDefinition
*/
public function addDefinition($name)
{
if (isset($this->definitions[$name])) {
throw new Nette\InvalidStateException("Service '$name' has already been added.");
}
return $this->definitions[$name] = new ServiceDefinition;
}
/**
* Removes the specified service definition.
* @param string
* @return void
*/
public function removeDefinition($name)
{
unset($this->definitions[$name]);
}
/**
* Gets the service definition.
* @param string
* @return ServiceDefinition
*/
public function getDefinition($name)
{
if (!isset($this->definitions[$name])) {
throw new MissingServiceException("Service '$name' not found.");
}
return $this->definitions[$name];
}
/**
* Gets all service definitions.
* @return array
*/
public function getDefinitions()
{
return $this->definitions;
}
/**
* Does the service definition exist?
* @param string
* @return bool
*/
public function hasDefinition($name)
{
return isset($this->definitions[$name]);
}
/********************* class resolving ****************d*g**/
/**
* Resolves service name by type.
* @param string class or interface
* @return string service name or NULL
* @throws ServiceCreationException
*/
public function getByType($class)
{
$lower = ltrim(strtolower($class), '\\');
if (!isset($this->classes[$lower])) {
return;
} elseif (count($this->classes[$lower]) === 1) {
return $this->classes[$lower][0];
} else {
throw new ServiceCreationException("Multiple services of type $class found: " . implode(', ', $this->classes[$lower]));
}
}
/**
* Gets the service objects of the specified tag.
* @param string
* @return array of [service name => tag attributes]
*/
public function findByTag($tag)
{
$found = array();
foreach ($this->definitions as $name => $def) {
if (isset($def->tags[$tag]) && $def->shared) {
$found[$name] = $def->tags[$tag];
}
}
return $found;
}
/**
* Creates a list of arguments using autowiring.
* @return array
*/
public function autowireArguments($class, $method, array $arguments)
{
$rc = Nette\Reflection\ClassType::from($class);
if (!$rc->hasMethod($method)) {
if (!Nette\Utils\Validators::isList($arguments)) {
throw new ServiceCreationException("Unable to pass specified arguments to $class::$method().");
}
return $arguments;
}
$rm = $rc->getMethod($method);
if ($rm->isAbstract() || !$rm->isPublic()) {
throw new ServiceCreationException("$rm is not callable.");
}
$this->addDependency($rm->getFileName());
return Helpers::autowireArguments($rm, $arguments, $this);
}
/**
* Generates $dependencies, $classes and expands and normalize class names.
* @return array
*/
public function prepareClassList()
{
// complete class-factory pairs; expand classes
foreach ($this->definitions as $name => /** @var ServiceDefinition $def */ $def) {
if ($def->class === self::CREATED_SERVICE || ($def->factory && $def->factory->entity === self::CREATED_SERVICE)) {
$def->class = $name;
$def->internal = TRUE;
if ($def->factory && $def->factory->entity === self::CREATED_SERVICE) {
$def->factory->entity = $def->class;
}
unset($this->definitions[$name]);
$this->definitions['_anonymous_' . str_replace('\\', '_', strtolower(trim($name, '\\')))] = $def;
}
if ($def->class) {
$def->class = $this->expand($def->class);
if (!$def->factory) {
$def->factory = new Statement($def->class);
}
} elseif (!$def->factory) {
throw new ServiceCreationException("Class and factory are missing in service '$name' definition.");
}
}
// complete classes
$this->classes = FALSE;
foreach ($this->definitions as $name => $def) {
$this->resolveClass($name);
}
// build auto-wiring list
$this->classes = array();
foreach ($this->definitions as $name => $def) {
if (!$def->class) {
continue;
}
if (!class_exists($def->class) && !interface_exists($def->class)) {
throw new Nette\InvalidStateException("Class $def->class has not been found.");
}
$def->class = Nette\Reflection\ClassType::from($def->class)->getName();
if ($def->autowired) {
foreach (class_parents($def->class) + class_implements($def->class) + array($def->class) as $parent) {
$this->classes[strtolower($parent)][] = $name;
}
}
}
foreach ($this->classes as $class => $foo) {
$this->addDependency(Nette\Reflection\ClassType::from($class)->getFileName());
}
}
private function resolveClass($name, $recursive = array())
{
if (isset($recursive[$name])) {
throw new Nette\InvalidArgumentException('Circular reference detected for services: ' . implode(', ', array_keys($recursive)) . '.');
}
$recursive[$name] = TRUE;
$def = $this->definitions[$name];
$factory = $this->normalizeEntity($this->expand($def->factory->entity));
if ($def->class) {
return $def->class;
} elseif (is_array($factory)) { // method calling
if ($service = $this->getServiceName($factory[0])) {
if (Strings::contains($service, '\\')) { // @\Class
throw new ServiceCreationException("Unable resolve class name for service '$name'.");
}
$factory[0] = $this->resolveClass($service, $recursive);
if (!$factory[0]) {
return;
}
}
$factory = new Nette\Callback($factory);
if (!$factory->isCallable()) {
throw new Nette\InvalidStateException("Factory '$factory' is not callable.");
}
try {
$reflection = $factory->toReflection();
$def->class = preg_replace('#[|\s].*#', '', $reflection->getAnnotation('return'));
if ($def->class && !class_exists($def->class) && $def->class[0] !== '\\' && $reflection instanceof \ReflectionMethod) {
/**/$def->class = $reflection->getDeclaringClass()->getNamespaceName() . '\\' . $def->class;/**/
}
} catch (\ReflectionException $e) {
}
} elseif ($service = $this->getServiceName($factory)) { // alias or factory
if (Strings::contains($service, '\\')) { // @\Class
/*5.2* $service = ltrim($service, '\\');*/
$def->autowired = FALSE;
return $def->class = $service;
}
if ($this->definitions[$service]->shared) {
$def->autowired = FALSE;
}
return $def->class = $this->resolveClass($service, $recursive);
} else {
return $def->class = $factory; // class name
}
}
/**
* Adds a file to the list of dependencies.
* @return ContainerBuilder provides a fluent interface
*/
public function addDependency($file)
{
$this->dependencies[$file] = TRUE;
return $this;
}
/**
* Returns the list of dependent files.
* @return array
*/
public function getDependencies()
{
unset($this->dependencies[FALSE]);
return array_keys($this->dependencies);
}
/********************* code generator ****************d*g**/
/**
* Generates PHP class.
* @return Nette\Utils\PhpGenerator\ClassType
*/
public function generateClass($parentClass = 'Nette\DI\Container')
{
unset($this->definitions[self::THIS_CONTAINER]);
$this->addDefinition(self::THIS_CONTAINER)->setClass($parentClass);
$this->prepareClassList();
$class = new Nette\Utils\PhpGenerator\ClassType('Container');
$class->addExtend($parentClass);
$class->addMethod('__construct')
->addBody('parent::__construct(?);', array($this->expand($this->parameters)));
$classes = $class->addProperty('classes', array());
foreach ($this->classes as $name => $foo) {
try {
$classes->value[$name] = $this->getByType($name);
} catch (ServiceCreationException $e) {
$classes->value[$name] = new PhpLiteral('FALSE, //' . strstr($e->getMessage(), ':'));
}
}
$definitions = $this->definitions;
ksort($definitions);
$meta = $class->addProperty('meta', array());
foreach ($definitions as $name => $def) {
if ($def->shared) {
foreach ($this->expand($def->tags) as $tag => $value) {
$meta->value[$name][Container::TAGS][$tag] = $value;
}
}
}
foreach ($definitions as $name => $def) {
try {
$type = $def->class ?: 'object';
$methodName = Container::getMethodName($name, $def->shared);
if (!PhpHelpers::isIdentifier($methodName)) {
throw new ServiceCreationException('Name contains invalid characters.');
}
if ($def->shared && !$def->internal && PhpHelpers::isIdentifier($name)) {
$class->addDocument("@property $type \$$name");
}
$method = $class->addMethod($methodName)
->addDocument("@return $type")
->setVisibility($def->shared || $def->internal ? 'protected' : 'public')
->setBody($name === self::THIS_CONTAINER ? 'return $this;' : $this->generateService($name));
foreach ($this->expand($def->parameters) as $k => $v) {
$tmp = explode(' ', is_int($k) ? $v : $k);
$param = is_int($k) ? $method->addParameter(end($tmp)) : $method->addParameter(end($tmp), $v);
if (isset($tmp[1])) {
$param->setTypeHint($tmp[0]);
}
}
} catch (\Exception $e) {
throw new ServiceCreationException("Service '$name': " . $e->getMessage(), NULL, $e);
}
}
return $class;
}
/**
* Generates body of service method.
* @return string
*/
private function generateService($name)
{
$def = $this->definitions[$name];
$parameters = $this->parameters;
foreach ($this->expand($def->parameters) as $k => $v) {
$v = explode(' ', is_int($k) ? $v : $k);
$parameters[end($v)] = new PhpLiteral('$' . end($v));
}
$code = '$service = ' . $this->formatStatement(Helpers::expand($def->factory, $parameters, TRUE)) . ";\n";
$entity = $this->normalizeEntity($def->factory->entity);
if ($def->class && $def->class !== $entity && !$this->getServiceName($entity)) {
$code .= PhpHelpers::formatArgs("if (!\$service instanceof $def->class) {\n"
. "\tthrow new Nette\\UnexpectedValueException(?);\n}\n",
array("Unable to create service '$name', value returned by factory is not $def->class type.")
);
}
if ($def->inject) {
$code .= "\$this->callInjects(\$service);\n";
}
foreach ((array) $def->setup as $setup) {
$setup = Helpers::expand($setup, $parameters, TRUE);
if (is_string($setup->entity) && strpbrk($setup->entity, ':@?') === FALSE) { // auto-prepend @self
$setup->entity = array("@$name", $setup->entity);
}
$code .= $this->formatStatement($setup, $name) . ";\n";
}
// Injected methods and properties
if ($def->class && !$this->getServiceName($entity)) {
foreach (array_reverse(array_merge(array($def->class), class_parents($def->class))) as $className) {
$rc = new \Nette\Reflection\ClassType($className);
foreach ($rc->getMethods() as $rm) {
if ($rm->hasAnnotation('inject')) {
if (!$rm->isPublic()) throw new ServiceCreationException("Injection method $rc->name::$rm->name is not public");
$annot = $rm->getAnnotation('inject');
if ($annot === TRUE) $args = array();
elseif (is_string($annot)) $args = array($annot);
elseif ($annot instanceof \ArrayObject) $args = iterator_to_array($annot);
else throw new ServiceCreationException("Unknown parameters of annotation");
$setup = new Statement(array("@$name", $rm->name), $args);
$setup = Helpers::expand($setup, $parameters, TRUE);
$code .= $this->formatStatement($setup, $name) . ";\n";
}
}
foreach ($rc->getProperties() as $rp) {
if ($rp->hasAnnotation('inject')) {
// what is supposed to be injected
$annot = $rp->getAnnotation('inject');
if ($annot === TRUE) {
if ($annot = $rp->getAnnotation('var')) {
$value = "@$annot";
} else {
throw new ServiceCreationException("Type of parameter $rc->name::\$$rp->name is not known");
}
}
elseif (is_string($annot)) $value = $annot;
elseif ($annot instanceof \ArrayObject) throw new ServiceCreationException("Cam have only one value!");
else throw new ServiceCreationException("Unknown parameters of annotation");
if ($rp->isPublic()) {
$setup = new Statement(array("@$name", '$' . $rp->name), array($value));
} else {
$setup = new Statement(array('@container', 'injectProperty'), array("@$name", $rc->name, $rp->name, $value));
}
$setup = Helpers::expand($setup, $parameters, TRUE);
$code .= $this->formatStatement($setup, $name) . ";\n";
}
}
}
}
return $code .= 'return $service;';
}
/**
* Formats PHP code for class instantiating, function calling or property setting in PHP.
* @return string
* @internal
*/
public function formatStatement(Statement $statement, $self = NULL)
{
$entity = $this->normalizeEntity($statement->entity);
$arguments = $statement->arguments;
if (is_string($entity) && Strings::contains($entity, '?')) { // PHP literal
return $this->formatPhp($entity, $arguments, $self);
} elseif ($service = $this->getServiceName($entity)) { // factory calling or service retrieving
if ($this->definitions[$service]->shared) {
if ($arguments) {
throw new ServiceCreationException("Unable to call service '$entity'.");
}
return $this->formatPhp('$this->getService(?)', array($service));
}
$params = array();
foreach ($this->definitions[$service]->parameters as $k => $v) {
$params[] = preg_replace('#\w+$#', '\$$0', (is_int($k) ? $v : $k)) . (is_int($k) ? '' : ' = ' . PhpHelpers::dump($v));
}
$rm = new Nette\Reflection\GlobalFunction(create_function(implode(', ', $params), ''));
$arguments = Helpers::autowireArguments($rm, $arguments, $this);
return $this->formatPhp('$this->?(?*)', array(Container::getMethodName($service, FALSE), $arguments), $self);
} elseif ($entity === 'not') { // operator
return $this->formatPhp('!?', array($arguments[0]));
} elseif (is_string($entity)) { // class name
if ($constructor = Nette\Reflection\ClassType::from($entity)->getConstructor()) {
$this->addDependency($constructor->getFileName());
$arguments = Helpers::autowireArguments($constructor, $arguments, $this);
} elseif ($arguments) {
throw new ServiceCreationException("Unable to pass arguments, class $entity has no constructor.");
}
return $this->formatPhp("new $entity" . ($arguments ? '(?*)' : ''), array($arguments), $self);
} elseif (!Validators::isList($entity) || count($entity) !== 2) {
throw new Nette\InvalidStateException("Expected class, method or property, " . PhpHelpers::dump($entity) . " given.");
} elseif ($entity[0] === '') { // globalFunc
return $this->formatPhp("$entity[1](?*)", array($arguments), $self);
} elseif (Strings::contains($entity[1], '$')) { // property setter
Validators::assert($arguments, 'list:1', "setup arguments for '" . Nette\Callback::create($entity) . "'");
if ($this->getServiceName($entity[0], $self)) {
return $this->formatPhp('?->? = ?', array($entity[0], substr($entity[1], 1), $arguments[0]), $self);
} else {
return $this->formatPhp($entity[0] . '::$? = ?', array(substr($entity[1], 1), $arguments[0]), $self);
}
} elseif ($service = $this->getServiceName($entity[0], $self)) { // service method
if ($this->definitions[$service]->class) {
$arguments = $this->autowireArguments($this->definitions[$service]->class, $entity[1], $arguments);
}
return $this->formatPhp('?->?(?*)', array($entity[0], $entity[1], $arguments), $self);
} else { // static method
$arguments = $this->autowireArguments($entity[0], $entity[1], $arguments);
return $this->formatPhp("$entity[0]::$entity[1](?*)", array($arguments), $self);
}
}
/**
* Formats PHP statement.
* @return string
*/
public function formatPhp($statement, $args, $self = NULL)
{
$that = $this;
array_walk_recursive($args, function(&$val) use ($self, $that) {
list($val) = $that->normalizeEntity(array($val));
if ($val instanceof Statement) {
$val = new PhpLiteral($that->formatStatement($val, $self));
} elseif ($val === '@' . ContainerBuilder::THIS_CONTAINER) {
$val = new PhpLiteral('$this');
} elseif ($service = $that->getServiceName($val, $self)) {
$val = $service === $self ? '$service' : $that->formatStatement(new Statement($val));
$val = new PhpLiteral($val);
}
});
return PhpHelpers::formatArgs($statement, $args);
}
/**
* Expands %placeholders% in strings (recursive).
* @param mixed
* @return mixed
*/
public function expand($value)
{
return Helpers::expand($value, $this->parameters, TRUE);
}
/** @internal */
public function normalizeEntity($entity)
{
if (is_string($entity) && Strings::contains($entity, '::') && !Strings::contains($entity, '?')) { // Class::method -> [Class, method]
$entity = explode('::', $entity);
}
if (is_array($entity) && $entity[0] instanceof ServiceDefinition) { // [ServiceDefinition, ...] -> [@serviceName, ...]
$tmp = array_keys($this->definitions, $entity[0], TRUE);
$entity[0] = "@$tmp[0]";
} elseif ($entity instanceof ServiceDefinition) { // ServiceDefinition -> @serviceName
$tmp = array_keys($this->definitions, $entity, TRUE);
$entity = "@$tmp[0]";
} elseif (is_array($entity) && $entity[0] === $this) { // [$this, ...] -> [@container, ...]
$entity[0] = '@' . ContainerBuilder::THIS_CONTAINER;
}
return $entity; // Class, @service, [Class, member], [@service, member], [, globalFunc]
}
/**
* Converts @service or @\Class -> service name and checks its existence.
* @param mixed
* @return string of FALSE, if argument is not service name
*/
public function getServiceName($arg, $self = NULL)
{
if (!is_string($arg) || !preg_match('#^@[\w\\\\.].+$#', $arg)) {
return FALSE;
}
$service = substr($arg, 1);
if ($service === self::CREATED_SERVICE) {
$service = $self;
}
if (Strings::contains($service, '\\')) {
if ($this->classes === FALSE) { // may be disabled by prepareClassList
return $service;
}
$res = $this->getByType($service);
if (!$res) {
throw new ServiceCreationException("Reference to missing service of type $service.");
}
return $res;
}
if (!isset($this->definitions[$service])) {
throw new ServiceCreationException("Reference to missing service '$service'.");
}
return $service;
}
}
Jump to Line
Something went wrong with that request. Please try again.