<?php
declare(strict_types=1);
namespace DI\Definition\Resolver;
use DI\Definition\Definition;
use DI\Definition\Exception\InvalidDefinition;
use DI\Definition\ObjectDefinition;
use DI\Definition\ObjectDefinition\PropertyInjection;
use DI\DependencyException;
use DI\Proxy\ProxyFactory;
use Exception;
use ProxyManager\Proxy\LazyLoadingInterface;
use Psr\Container\NotFoundExceptionInterface;
use ReflectionClass;
use ReflectionProperty;
/**
* Create objects based on an object definition.
*
* @template-implements DefinitionResolver<ObjectDefinition>
*
* @since 4.0
* @author Matthieu Napoli <[email protected]>
*/
class ObjectCreator implements DefinitionResolver
{
private ParameterResolver $parameterResolver;
/**
* @param DefinitionResolver $definitionResolver Used to resolve nested definitions.
* @param ProxyFactory $proxyFactory Used to create proxies for lazy injections.
*/
public function __construct(
private DefinitionResolver $definitionResolver,
private ProxyFactory $proxyFactory
) {
$this->parameterResolver = new ParameterResolver($definitionResolver);
}
/**
* Resolve a class definition to a value.
*
* This will create a new instance of the class using the injections points defined.
*
* @param ObjectDefinition $definition
*/
public function resolve(Definition $definition, array $parameters = []) : ?object
{
// Lazy?
if ($definition->isLazy()) {
return $this->createProxy($definition, $parameters);
}
return $this->createInstance($definition, $parameters);
}
/**
* The definition is not resolvable if the class is not instantiable (interface or abstract)
* or if the class doesn't exist.
*
* @param ObjectDefinition $definition
*/
public function isResolvable(Definition $definition, array $parameters = []) : bool
{
return $definition->isInstantiable();
}
/**
* Returns a proxy instance.
*/
private function createProxy(ObjectDefinition $definition, array $parameters) : LazyLoadingInterface
{
/** @var class-string $className */
$className = $definition->getClassName();
return $this->proxyFactory->createProxy(
$className,
function (& $wrappedObject, $proxy, $method, $params, & $initializer) use ($definition, $parameters) {
$wrappedObject = $this->createInstance($definition, $parameters);
$initializer = null; // turning off further lazy initialization
return true;
}
);
}
/**
* Creates an instance of the class and injects dependencies..
*
* @param array $parameters Optional parameters to use to create the instance.
*
* @throws DependencyException
* @throws InvalidDefinition
*/
private function createInstance(ObjectDefinition $definition, array $parameters) : object
{
// Check that the class is instantiable
if (! $definition->isInstantiable()) {
// Check that the class exists
if (! $definition->classExists()) {
throw InvalidDefinition::create($definition, sprintf(
'Entry "%s" cannot be resolved: the class doesn\'t exist',
$definition->getName()
));
}
throw InvalidDefinition::create($definition, sprintf(
'Entry "%s" cannot be resolved: the class is not instantiable',
$definition->getName()
));
}
/** @psalm-var class-string $classname */
$classname = $definition->getClassName();
$classReflection = new ReflectionClass($classname);
$constructorInjection = $definition->getConstructorInjection();
/** @psalm-suppress InvalidCatch */
try {
$args = $this->parameterResolver->resolveParameters(
$constructorInjection,
$classReflection->getConstructor(),
$parameters
);
$object = new $classname(...$args);
$this->injectMethodsAndProperties($object, $definition);
} catch (NotFoundExceptionInterface $e) {
throw new DependencyException(sprintf(
'Error while injecting dependencies into %s: %s',
$classReflection->getName(),
$e->getMessage()
), 0, $e);
} catch (InvalidDefinition $e) {
throw InvalidDefinition::create($definition, sprintf(
'Entry "%s" cannot be resolved: %s',
$definition->getName(),
$e->getMessage()
));
}
return $object;
}
protected function injectMethodsAndProperties(object $object, ObjectDefinition $objectDefinition) : void
{
// Property injections
foreach ($objectDefinition->getPropertyInjections() as $propertyInjection) {
$this->injectProperty($object, $propertyInjection);
}
// Method injections
foreach ($objectDefinition->getMethodInjections() as $methodInjection) {
$methodReflection = new \ReflectionMethod($object, $methodInjection->getMethodName());
$args = $this->parameterResolver->resolveParameters($methodInjection, $methodReflection);
$methodReflection->invokeArgs($object, $args);
}
}
/**
* Inject dependencies into properties.
*
* @param object $object Object to inject dependencies into
* @param PropertyInjection $propertyInjection Property injection definition
*
* @throws DependencyException
*/
private function injectProperty(object $object, PropertyInjection $propertyInjection) : void
{
$propertyName = $propertyInjection->getPropertyName();
$value = $propertyInjection->getValue();
if ($value instanceof Definition) {
try {
$value = $this->definitionResolver->resolve($value);
} catch (DependencyException $e) {
throw $e;
} catch (Exception $e) {
throw new DependencyException(sprintf(
'Error while injecting in %s::%s. %s',
$object::class,
$propertyName,
$e->getMessage()
), 0, $e);
}
}
self::setPrivatePropertyValue($propertyInjection->getClassName(), $object, $propertyName, $value);
}
public static function setPrivatePropertyValue(?string $className, $object, string $propertyName, mixed $propertyValue) : void
{
$className = $className ?: $object::class;
$property = new ReflectionProperty($className, $propertyName);
if (! $property->isPublic()) {
$property->setAccessible(true);
}
$property->setValue($object, $propertyValue);
}
}