1<?php 2 3declare(strict_types=1); 4 5namespace DI\Definition\Source; 6 7use DI\Annotation\Inject; 8use DI\Annotation\Injectable; 9use DI\Definition\Exception\InvalidAnnotation; 10use DI\Definition\ObjectDefinition; 11use DI\Definition\ObjectDefinition\MethodInjection; 12use DI\Definition\ObjectDefinition\PropertyInjection; 13use DI\Definition\Reference; 14use Doctrine\Common\Annotations\AnnotationRegistry; 15use Doctrine\Common\Annotations\Reader; 16use Doctrine\Common\Annotations\SimpleAnnotationReader; 17use InvalidArgumentException; 18use PhpDocReader\PhpDocReader; 19use ReflectionClass; 20use ReflectionMethod; 21use ReflectionNamedType; 22use ReflectionParameter; 23use ReflectionProperty; 24use UnexpectedValueException; 25 26/** 27 * Provides DI definitions by reading annotations such as @ Inject and @ var annotations. 28 * 29 * Uses Autowiring, Doctrine's Annotations and regex docblock parsing. 30 * This source automatically includes the reflection source. 31 * 32 * @author Matthieu Napoli <matthieu@mnapoli.fr> 33 */ 34class AnnotationBasedAutowiring implements DefinitionSource, Autowiring 35{ 36 /** 37 * @var Reader 38 */ 39 private $annotationReader; 40 41 /** 42 * @var PhpDocReader 43 */ 44 private $phpDocReader; 45 46 /** 47 * @var bool 48 */ 49 private $ignorePhpDocErrors; 50 51 public function __construct($ignorePhpDocErrors = false) 52 { 53 $this->ignorePhpDocErrors = (bool) $ignorePhpDocErrors; 54 } 55 56 public function autowire(string $name, ObjectDefinition $definition = null) 57 { 58 $className = $definition ? $definition->getClassName() : $name; 59 60 if (!class_exists($className) && !interface_exists($className)) { 61 return $definition; 62 } 63 64 $definition = $definition ?: new ObjectDefinition($name); 65 66 $class = new ReflectionClass($className); 67 68 $this->readInjectableAnnotation($class, $definition); 69 70 // Browse the class properties looking for annotated properties 71 $this->readProperties($class, $definition); 72 73 // Browse the object's methods looking for annotated methods 74 $this->readMethods($class, $definition); 75 76 return $definition; 77 } 78 79 /** 80 * {@inheritdoc} 81 * @throws InvalidAnnotation 82 * @throws InvalidArgumentException The class doesn't exist 83 */ 84 public function getDefinition(string $name) 85 { 86 return $this->autowire($name); 87 } 88 89 /** 90 * Autowiring cannot guess all existing definitions. 91 */ 92 public function getDefinitions() : array 93 { 94 return []; 95 } 96 97 /** 98 * Browse the class properties looking for annotated properties. 99 */ 100 private function readProperties(ReflectionClass $class, ObjectDefinition $definition) 101 { 102 foreach ($class->getProperties() as $property) { 103 if ($property->isStatic()) { 104 continue; 105 } 106 $this->readProperty($property, $definition); 107 } 108 109 // Read also the *private* properties of the parent classes 110 /** @noinspection PhpAssignmentInConditionInspection */ 111 while ($class = $class->getParentClass()) { 112 foreach ($class->getProperties(ReflectionProperty::IS_PRIVATE) as $property) { 113 if ($property->isStatic()) { 114 continue; 115 } 116 $this->readProperty($property, $definition, $class->getName()); 117 } 118 } 119 } 120 121 private function readProperty(ReflectionProperty $property, ObjectDefinition $definition, $classname = null) 122 { 123 // Look for @Inject annotation 124 $annotation = $this->getAnnotationReader()->getPropertyAnnotation($property, 'DI\Annotation\Inject'); 125 if (!$annotation instanceof Inject) { 126 return; 127 } 128 129 // Try to @Inject("name") or look for @var content 130 $entryName = $annotation->getName() ?: $this->getPhpDocReader()->getPropertyClass($property); 131 132 // Try using PHP7.4 typed properties 133 if (\PHP_VERSION_ID > 70400 134 && $entryName === null 135 && $property->getType() instanceof ReflectionNamedType 136 && (class_exists($property->getType()->getName()) || interface_exists($property->getType()->getName())) 137 ) { 138 $entryName = $property->getType()->getName(); 139 } 140 141 if ($entryName === null) { 142 throw new InvalidAnnotation(sprintf( 143 '@Inject found on property %s::%s but unable to guess what to inject, use a @var annotation', 144 $property->getDeclaringClass()->getName(), 145 $property->getName() 146 )); 147 } 148 149 $definition->addPropertyInjection( 150 new PropertyInjection($property->getName(), new Reference($entryName), $classname) 151 ); 152 } 153 154 /** 155 * Browse the object's methods looking for annotated methods. 156 */ 157 private function readMethods(ReflectionClass $class, ObjectDefinition $objectDefinition) 158 { 159 // This will look in all the methods, including those of the parent classes 160 foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { 161 if ($method->isStatic()) { 162 continue; 163 } 164 165 $methodInjection = $this->getMethodInjection($method); 166 167 if (! $methodInjection) { 168 continue; 169 } 170 171 if ($method->isConstructor()) { 172 $objectDefinition->completeConstructorInjection($methodInjection); 173 } else { 174 $objectDefinition->completeFirstMethodInjection($methodInjection); 175 } 176 } 177 } 178 179 /** 180 * @return MethodInjection|null 181 */ 182 private function getMethodInjection(ReflectionMethod $method) 183 { 184 // Look for @Inject annotation 185 try { 186 $annotation = $this->getAnnotationReader()->getMethodAnnotation($method, 'DI\Annotation\Inject'); 187 } catch (InvalidAnnotation $e) { 188 throw new InvalidAnnotation(sprintf( 189 '@Inject annotation on %s::%s is malformed. %s', 190 $method->getDeclaringClass()->getName(), 191 $method->getName(), 192 $e->getMessage() 193 ), 0, $e); 194 } 195 196 // @Inject on constructor is implicit 197 if (! ($annotation || $method->isConstructor())) { 198 return null; 199 } 200 201 $annotationParameters = $annotation instanceof Inject ? $annotation->getParameters() : []; 202 203 $parameters = []; 204 foreach ($method->getParameters() as $index => $parameter) { 205 $entryName = $this->getMethodParameter($index, $parameter, $annotationParameters); 206 207 if ($entryName !== null) { 208 $parameters[$index] = new Reference($entryName); 209 } 210 } 211 212 if ($method->isConstructor()) { 213 return MethodInjection::constructor($parameters); 214 } 215 216 return new MethodInjection($method->getName(), $parameters); 217 } 218 219 /** 220 * @param int $parameterIndex 221 * 222 * @return string|null Entry name or null if not found. 223 */ 224 private function getMethodParameter($parameterIndex, ReflectionParameter $parameter, array $annotationParameters) 225 { 226 // @Inject has definition for this parameter (by index, or by name) 227 if (isset($annotationParameters[$parameterIndex])) { 228 return $annotationParameters[$parameterIndex]; 229 } 230 if (isset($annotationParameters[$parameter->getName()])) { 231 return $annotationParameters[$parameter->getName()]; 232 } 233 234 // Skip optional parameters if not explicitly defined 235 if ($parameter->isOptional()) { 236 return null; 237 } 238 239 // Try to use the type-hinting 240 $parameterType = $parameter->getType(); 241 if ($parameterType && $parameterType instanceof ReflectionNamedType && !$parameterType->isBuiltin()) { 242 return $parameterType->getName(); 243 } 244 245 // Last resort, look for @param tag 246 return $this->getPhpDocReader()->getParameterClass($parameter); 247 } 248 249 /** 250 * @return Reader The annotation reader 251 */ 252 public function getAnnotationReader() 253 { 254 if ($this->annotationReader === null) { 255 AnnotationRegistry::registerLoader('class_exists'); 256 $this->annotationReader = new SimpleAnnotationReader(); 257 $this->annotationReader->addNamespace('DI\Annotation'); 258 } 259 260 return $this->annotationReader; 261 } 262 263 /** 264 * @return PhpDocReader 265 */ 266 private function getPhpDocReader() 267 { 268 if ($this->phpDocReader === null) { 269 $this->phpDocReader = new PhpDocReader($this->ignorePhpDocErrors); 270 } 271 272 return $this->phpDocReader; 273 } 274 275 private function readInjectableAnnotation(ReflectionClass $class, ObjectDefinition $definition) 276 { 277 try { 278 /** @var Injectable|null $annotation */ 279 $annotation = $this->getAnnotationReader() 280 ->getClassAnnotation($class, 'DI\Annotation\Injectable'); 281 } catch (UnexpectedValueException $e) { 282 throw new InvalidAnnotation(sprintf( 283 'Error while reading @Injectable on %s: %s', 284 $class->getName(), 285 $e->getMessage() 286 ), 0, $e); 287 } 288 289 if (! $annotation) { 290 return; 291 } 292 293 if ($annotation->isLazy() !== null) { 294 $definition->setLazy($annotation->isLazy()); 295 } 296 } 297} 298