<?php
declare(strict_types=1);
namespace DI\Compiler;
use DI\Definition\Exception\InvalidDefinition;
use DI\Definition\ObjectDefinition;
use DI\Definition\ObjectDefinition\MethodInjection;
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
/**
* Compiles an object definition into native PHP code that, when executed, creates the object.
*
* @author Matthieu Napoli <[email protected]>
*/
class ObjectCreationCompiler
{
/**
* @var Compiler
*/
private $compiler;
public function __construct(Compiler $compiler)
{
$this->compiler = $compiler;
}
public function compile(ObjectDefinition $definition) : string
{
$this->assertClassIsNotAnonymous($definition);
$this->assertClassIsInstantiable($definition);
// Lazy?
if ($definition->isLazy()) {
return $this->compileLazyDefinition($definition);
}
try {
$classReflection = new ReflectionClass($definition->getClassName());
$constructorArguments = $this->resolveParameters($definition->getConstructorInjection(), $classReflection->getConstructor());
$dumpedConstructorArguments = array_map(function ($value) {
return $this->compiler->compileValue($value);
}, $constructorArguments);
$code = [];
$code[] = sprintf(
'$object = new %s(%s);',
$definition->getClassName(),
implode(', ', $dumpedConstructorArguments)
);
// Property injections
foreach ($definition->getPropertyInjections() as $propertyInjection) {
$value = $propertyInjection->getValue();
$value = $this->compiler->compileValue($value);
$className = $propertyInjection->getClassName() ?: $definition->getClassName();
$property = new ReflectionProperty($className, $propertyInjection->getPropertyName());
if ($property->isPublic()) {
$code[] = sprintf('$object->%s = %s;', $propertyInjection->getPropertyName(), $value);
} else {
// Private/protected property
$code[] = sprintf(
'\DI\Definition\Resolver\ObjectCreator::setPrivatePropertyValue(%s, $object, \'%s\', %s);',
var_export($propertyInjection->getClassName(), true),
$propertyInjection->getPropertyName(),
$value
);
}
}
// Method injections
foreach ($definition->getMethodInjections() as $methodInjection) {
$methodReflection = new \ReflectionMethod($definition->getClassName(), $methodInjection->getMethodName());
$parameters = $this->resolveParameters($methodInjection, $methodReflection);
$dumpedParameters = array_map(function ($value) {
return $this->compiler->compileValue($value);
}, $parameters);
$code[] = sprintf(
'$object->%s(%s);',
$methodInjection->getMethodName(),
implode(', ', $dumpedParameters)
);
}
} catch (InvalidDefinition $e) {
throw InvalidDefinition::create($definition, sprintf(
'Entry "%s" cannot be compiled: %s',
$definition->getName(),
$e->getMessage()
));
}
return implode("\n ", $code);
}
public function resolveParameters(MethodInjection $definition = null, ReflectionMethod $method = null) : array
{
$args = [];
if (! $method) {
return $args;
}
$definitionParameters = $definition ? $definition->getParameters() : [];
foreach ($method->getParameters() as $index => $parameter) {
if (array_key_exists($index, $definitionParameters)) {
// Look in the definition
$value = &$definitionParameters[$index];
} elseif ($parameter->isOptional()) {
// If the parameter is optional and wasn't specified, we take its default value
$args[] = $this->getParameterDefaultValue($parameter, $method);
continue;
} else {
throw new InvalidDefinition(sprintf(
'Parameter $%s of %s has no value defined or guessable',
$parameter->getName(),
$this->getFunctionName($method)
));
}
$args[] = &$value;
}
return $args;
}
private function compileLazyDefinition(ObjectDefinition $definition) : string
{
$subDefinition = clone $definition;
$subDefinition->setLazy(false);
$subDefinition = $this->compiler->compileValue($subDefinition);
$this->compiler->getProxyFactory()->generateProxyClass($definition->getClassName());
return <<<PHP
\$object = \$this->proxyFactory->createProxy(
'{$definition->getClassName()}',
function (&\$wrappedObject, \$proxy, \$method, \$params, &\$initializer) {
\$wrappedObject = $subDefinition;
\$initializer = null; // turning off further lazy initialization
return true;
}
);
PHP;
}
/**
* Returns the default value of a function parameter.
*
* @throws InvalidDefinition Can't get default values from PHP internal classes and functions
* @return mixed
*/
private function getParameterDefaultValue(ReflectionParameter $parameter, ReflectionMethod $function)
{
try {
return $parameter->getDefaultValue();
} catch (\ReflectionException $e) {
throw new InvalidDefinition(sprintf(
'The parameter "%s" of %s has no type defined or guessable. It has a default value, '
. 'but the default value can\'t be read through Reflection because it is a PHP internal class.',
$parameter->getName(),
$this->getFunctionName($function)
));
}
}
private function getFunctionName(ReflectionMethod $method) : string
{
return $method->getName() . '()';
}
private function assertClassIsNotAnonymous(ObjectDefinition $definition)
{
if (strpos($definition->getClassName(), '@') !== false) {
throw InvalidDefinition::create($definition, sprintf(
'Entry "%s" cannot be compiled: anonymous classes cannot be compiled',
$definition->getName()
));
}
}
private function assertClassIsInstantiable(ObjectDefinition $definition)
{
if ($definition->isInstantiable()) {
return;
}
$message = ! $definition->classExists()
? 'Entry "%s" cannot be compiled: the class doesn\'t exist'
: 'Entry "%s" cannot be compiled: the class is not instantiable';
throw InvalidDefinition::create($definition, sprintf($message, $definition->getName()));
}
}