| 1 | efrain | 1 | <?php
 | 
        
           |  |  | 2 |   | 
        
           |  |  | 3 | declare(strict_types=1);
 | 
        
           |  |  | 4 |   | 
        
           |  |  | 5 | namespace DI\Definition\Resolver;
 | 
        
           |  |  | 6 |   | 
        
           |  |  | 7 | use DI\Definition\Definition;
 | 
        
           |  |  | 8 | use DI\Definition\Exception\InvalidDefinition;
 | 
        
           |  |  | 9 | use DI\Definition\ObjectDefinition;
 | 
        
           |  |  | 10 | use DI\Definition\ObjectDefinition\PropertyInjection;
 | 
        
           |  |  | 11 | use DI\DependencyException;
 | 
        
           |  |  | 12 | use DI\Proxy\ProxyFactory;
 | 
        
           |  |  | 13 | use Exception;
 | 
        
           |  |  | 14 | use ProxyManager\Proxy\LazyLoadingInterface;
 | 
        
           |  |  | 15 | use Psr\Container\NotFoundExceptionInterface;
 | 
        
           |  |  | 16 | use ReflectionClass;
 | 
        
           |  |  | 17 | use ReflectionProperty;
 | 
        
           |  |  | 18 |   | 
        
           |  |  | 19 | /**
 | 
        
           |  |  | 20 |  * Create objects based on an object definition.
 | 
        
           |  |  | 21 |  *
 | 
        
           |  |  | 22 |  * @template-implements DefinitionResolver<ObjectDefinition>
 | 
        
           |  |  | 23 |  *
 | 
        
           |  |  | 24 |  * @since 4.0
 | 
        
           |  |  | 25 |  * @author Matthieu Napoli <matthieu@mnapoli.fr>
 | 
        
           |  |  | 26 |  */
 | 
        
           |  |  | 27 | class ObjectCreator implements DefinitionResolver
 | 
        
           |  |  | 28 | {
 | 
        
           |  |  | 29 |     private ParameterResolver $parameterResolver;
 | 
        
           |  |  | 30 |   | 
        
           |  |  | 31 |     /**
 | 
        
           |  |  | 32 |      * @param DefinitionResolver $definitionResolver Used to resolve nested definitions.
 | 
        
           |  |  | 33 |      * @param ProxyFactory       $proxyFactory       Used to create proxies for lazy injections.
 | 
        
           |  |  | 34 |      */
 | 
        
           |  |  | 35 |     public function __construct(
 | 
        
           |  |  | 36 |         private DefinitionResolver $definitionResolver,
 | 
        
           |  |  | 37 |         private ProxyFactory $proxyFactory
 | 
        
           |  |  | 38 |     ) {
 | 
        
           |  |  | 39 |         $this->parameterResolver = new ParameterResolver($definitionResolver);
 | 
        
           |  |  | 40 |     }
 | 
        
           |  |  | 41 |   | 
        
           |  |  | 42 |     /**
 | 
        
           |  |  | 43 |      * Resolve a class definition to a value.
 | 
        
           |  |  | 44 |      *
 | 
        
           |  |  | 45 |      * This will create a new instance of the class using the injections points defined.
 | 
        
           |  |  | 46 |      *
 | 
        
           |  |  | 47 |      * @param ObjectDefinition $definition
 | 
        
           |  |  | 48 |      */
 | 
        
           |  |  | 49 |     public function resolve(Definition $definition, array $parameters = []) : ?object
 | 
        
           |  |  | 50 |     {
 | 
        
           |  |  | 51 |         // Lazy?
 | 
        
           |  |  | 52 |         if ($definition->isLazy()) {
 | 
        
           |  |  | 53 |             return $this->createProxy($definition, $parameters);
 | 
        
           |  |  | 54 |         }
 | 
        
           |  |  | 55 |   | 
        
           |  |  | 56 |         return $this->createInstance($definition, $parameters);
 | 
        
           |  |  | 57 |     }
 | 
        
           |  |  | 58 |   | 
        
           |  |  | 59 |     /**
 | 
        
           |  |  | 60 |      * The definition is not resolvable if the class is not instantiable (interface or abstract)
 | 
        
           |  |  | 61 |      * or if the class doesn't exist.
 | 
        
           |  |  | 62 |      *
 | 
        
           |  |  | 63 |      * @param ObjectDefinition $definition
 | 
        
           |  |  | 64 |      */
 | 
        
           |  |  | 65 |     public function isResolvable(Definition $definition, array $parameters = []) : bool
 | 
        
           |  |  | 66 |     {
 | 
        
           |  |  | 67 |         return $definition->isInstantiable();
 | 
        
           |  |  | 68 |     }
 | 
        
           |  |  | 69 |   | 
        
           |  |  | 70 |     /**
 | 
        
           |  |  | 71 |      * Returns a proxy instance.
 | 
        
           |  |  | 72 |      */
 | 
        
           |  |  | 73 |     private function createProxy(ObjectDefinition $definition, array $parameters) : LazyLoadingInterface
 | 
        
           |  |  | 74 |     {
 | 
        
           |  |  | 75 |         /** @var class-string $className */
 | 
        
           |  |  | 76 |         $className = $definition->getClassName();
 | 
        
           |  |  | 77 |   | 
        
           |  |  | 78 |         return $this->proxyFactory->createProxy(
 | 
        
           |  |  | 79 |             $className,
 | 
        
           |  |  | 80 |             function (& $wrappedObject, $proxy, $method, $params, & $initializer) use ($definition, $parameters) {
 | 
        
           |  |  | 81 |                 $wrappedObject = $this->createInstance($definition, $parameters);
 | 
        
           |  |  | 82 |                 $initializer = null; // turning off further lazy initialization
 | 
        
           |  |  | 83 |   | 
        
           |  |  | 84 |                 return true;
 | 
        
           |  |  | 85 |             }
 | 
        
           |  |  | 86 |         );
 | 
        
           |  |  | 87 |     }
 | 
        
           |  |  | 88 |   | 
        
           |  |  | 89 |     /**
 | 
        
           |  |  | 90 |      * Creates an instance of the class and injects dependencies..
 | 
        
           |  |  | 91 |      *
 | 
        
           |  |  | 92 |      * @param array $parameters Optional parameters to use to create the instance.
 | 
        
           |  |  | 93 |      *
 | 
        
           |  |  | 94 |      * @throws DependencyException
 | 
        
           |  |  | 95 |      * @throws InvalidDefinition
 | 
        
           |  |  | 96 |      */
 | 
        
           |  |  | 97 |     private function createInstance(ObjectDefinition $definition, array $parameters) : object
 | 
        
           |  |  | 98 |     {
 | 
        
           |  |  | 99 |         // Check that the class is instantiable
 | 
        
           |  |  | 100 |         if (! $definition->isInstantiable()) {
 | 
        
           |  |  | 101 |             // Check that the class exists
 | 
        
           |  |  | 102 |             if (! $definition->classExists()) {
 | 
        
           |  |  | 103 |                 throw InvalidDefinition::create($definition, sprintf(
 | 
        
           |  |  | 104 |                     'Entry "%s" cannot be resolved: the class doesn\'t exist',
 | 
        
           |  |  | 105 |                     $definition->getName()
 | 
        
           |  |  | 106 |                 ));
 | 
        
           |  |  | 107 |             }
 | 
        
           |  |  | 108 |   | 
        
           |  |  | 109 |             throw InvalidDefinition::create($definition, sprintf(
 | 
        
           |  |  | 110 |                 'Entry "%s" cannot be resolved: the class is not instantiable',
 | 
        
           |  |  | 111 |                 $definition->getName()
 | 
        
           |  |  | 112 |             ));
 | 
        
           |  |  | 113 |         }
 | 
        
           |  |  | 114 |   | 
        
           |  |  | 115 |         /** @psalm-var class-string $classname */
 | 
        
           |  |  | 116 |         $classname = $definition->getClassName();
 | 
        
           |  |  | 117 |         $classReflection = new ReflectionClass($classname);
 | 
        
           |  |  | 118 |   | 
        
           |  |  | 119 |         $constructorInjection = $definition->getConstructorInjection();
 | 
        
           |  |  | 120 |   | 
        
           |  |  | 121 |         /** @psalm-suppress InvalidCatch */
 | 
        
           |  |  | 122 |         try {
 | 
        
           |  |  | 123 |             $args = $this->parameterResolver->resolveParameters(
 | 
        
           |  |  | 124 |                 $constructorInjection,
 | 
        
           |  |  | 125 |                 $classReflection->getConstructor(),
 | 
        
           |  |  | 126 |                 $parameters
 | 
        
           |  |  | 127 |             );
 | 
        
           |  |  | 128 |   | 
        
           |  |  | 129 |             $object = new $classname(...$args);
 | 
        
           |  |  | 130 |   | 
        
           |  |  | 131 |             $this->injectMethodsAndProperties($object, $definition);
 | 
        
           |  |  | 132 |         } catch (NotFoundExceptionInterface $e) {
 | 
        
           |  |  | 133 |             throw new DependencyException(sprintf(
 | 
        
           |  |  | 134 |                 'Error while injecting dependencies into %s: %s',
 | 
        
           |  |  | 135 |                 $classReflection->getName(),
 | 
        
           |  |  | 136 |                 $e->getMessage()
 | 
        
           |  |  | 137 |             ), 0, $e);
 | 
        
           |  |  | 138 |         } catch (InvalidDefinition $e) {
 | 
        
           |  |  | 139 |             throw InvalidDefinition::create($definition, sprintf(
 | 
        
           |  |  | 140 |                 'Entry "%s" cannot be resolved: %s',
 | 
        
           |  |  | 141 |                 $definition->getName(),
 | 
        
           |  |  | 142 |                 $e->getMessage()
 | 
        
           |  |  | 143 |             ));
 | 
        
           |  |  | 144 |         }
 | 
        
           |  |  | 145 |   | 
        
           |  |  | 146 |         return $object;
 | 
        
           |  |  | 147 |     }
 | 
        
           |  |  | 148 |   | 
        
           |  |  | 149 |     protected function injectMethodsAndProperties(object $object, ObjectDefinition $objectDefinition) : void
 | 
        
           |  |  | 150 |     {
 | 
        
           |  |  | 151 |         // Property injections
 | 
        
           |  |  | 152 |         foreach ($objectDefinition->getPropertyInjections() as $propertyInjection) {
 | 
        
           |  |  | 153 |             $this->injectProperty($object, $propertyInjection);
 | 
        
           |  |  | 154 |         }
 | 
        
           |  |  | 155 |   | 
        
           |  |  | 156 |         // Method injections
 | 
        
           |  |  | 157 |         foreach ($objectDefinition->getMethodInjections() as $methodInjection) {
 | 
        
           |  |  | 158 |             $methodReflection = new \ReflectionMethod($object, $methodInjection->getMethodName());
 | 
        
           |  |  | 159 |             $args = $this->parameterResolver->resolveParameters($methodInjection, $methodReflection);
 | 
        
           |  |  | 160 |   | 
        
           |  |  | 161 |             $methodReflection->invokeArgs($object, $args);
 | 
        
           |  |  | 162 |         }
 | 
        
           |  |  | 163 |     }
 | 
        
           |  |  | 164 |   | 
        
           |  |  | 165 |     /**
 | 
        
           |  |  | 166 |      * Inject dependencies into properties.
 | 
        
           |  |  | 167 |      *
 | 
        
           |  |  | 168 |      * @param object            $object            Object to inject dependencies into
 | 
        
           |  |  | 169 |      * @param PropertyInjection $propertyInjection Property injection definition
 | 
        
           |  |  | 170 |      *
 | 
        
           |  |  | 171 |      * @throws DependencyException
 | 
        
           |  |  | 172 |      */
 | 
        
           |  |  | 173 |     private function injectProperty(object $object, PropertyInjection $propertyInjection) : void
 | 
        
           |  |  | 174 |     {
 | 
        
           |  |  | 175 |         $propertyName = $propertyInjection->getPropertyName();
 | 
        
           |  |  | 176 |   | 
        
           |  |  | 177 |         $value = $propertyInjection->getValue();
 | 
        
           |  |  | 178 |   | 
        
           |  |  | 179 |         if ($value instanceof Definition) {
 | 
        
           |  |  | 180 |             try {
 | 
        
           |  |  | 181 |                 $value = $this->definitionResolver->resolve($value);
 | 
        
           |  |  | 182 |             } catch (DependencyException $e) {
 | 
        
           |  |  | 183 |                 throw $e;
 | 
        
           |  |  | 184 |             } catch (Exception $e) {
 | 
        
           |  |  | 185 |                 throw new DependencyException(sprintf(
 | 
        
           |  |  | 186 |                     'Error while injecting in %s::%s. %s',
 | 
        
           |  |  | 187 |                     $object::class,
 | 
        
           |  |  | 188 |                     $propertyName,
 | 
        
           |  |  | 189 |                     $e->getMessage()
 | 
        
           |  |  | 190 |                 ), 0, $e);
 | 
        
           |  |  | 191 |             }
 | 
        
           |  |  | 192 |         }
 | 
        
           |  |  | 193 |   | 
        
           |  |  | 194 |         self::setPrivatePropertyValue($propertyInjection->getClassName(), $object, $propertyName, $value);
 | 
        
           |  |  | 195 |     }
 | 
        
           |  |  | 196 |   | 
        
           |  |  | 197 |     public static function setPrivatePropertyValue(?string $className, $object, string $propertyName, mixed $propertyValue) : void
 | 
        
           |  |  | 198 |     {
 | 
        
           |  |  | 199 |         $className = $className ?: $object::class;
 | 
        
           |  |  | 200 |   | 
        
           |  |  | 201 |         $property = new ReflectionProperty($className, $propertyName);
 | 
        
           |  |  | 202 |         if (! $property->isPublic() && \PHP_VERSION_ID < 80100) {
 | 
        
           |  |  | 203 |             $property->setAccessible(true);
 | 
        
           |  |  | 204 |         }
 | 
        
           |  |  | 205 |         $property->setValue($object, $propertyValue);
 | 
        
           |  |  | 206 |     }
 | 
        
           |  |  | 207 | }
 |