├── .gitattributes ├── LICENSE ├── composer.json └── src ├── AbstractCallableDefinitionCollector.php ├── Annotation ├── AbstractAnnotation.php ├── AbstractMultipleAnnotation.php ├── AnnotationCollector.php ├── AnnotationInterface.php ├── AnnotationReader.php ├── Aspect.php ├── AspectCollector.php ├── AspectLoader.php ├── Debug.php ├── Inject.php ├── InjectAspect.php ├── MultipleAnnotation.php ├── MultipleAnnotationInterface.php ├── RelationCollector.php ├── ScanConfig.php └── Scanner.php ├── Aop ├── AbstractAspect.php ├── AnnotationMetadata.php ├── AroundInterface.php ├── Aspect.php ├── AspectManager.php ├── Ast.php ├── AstVisitorRegistry.php ├── Pipeline.php ├── ProceedingJoinPoint.php ├── PropertyHandlerTrait.php ├── PropertyHandlerVisitor.php ├── ProxyCallVisitor.php ├── ProxyManager.php ├── ProxyTrait.php ├── RegisterInjectPropertyHandler.php ├── RewriteCollection.php └── VisitorMetadata.php ├── ClassLoader.php ├── ClosureDefinitionCollector.php ├── ClosureDefinitionCollectorInterface.php ├── ConfigProvider.php ├── Container.php ├── Definition ├── DefinitionInterface.php ├── DefinitionSource.php ├── DefinitionSourceFactory.php ├── DefinitionSourceInterface.php ├── FactoryDefinition.php ├── MethodInjection.php ├── ObjectDefinition.php ├── PriorityDefinition.php ├── PropertyDefinition.php ├── PropertyHandlerManager.php ├── Reference.php └── SelfResolvingDefinitionInterface.php ├── Exception ├── AnnotationException.php ├── CircularDependencyException.php ├── ConflictAnnotationException.php ├── DependencyException.php ├── DirectoryNotExistException.php ├── Exception.php ├── InvalidArgumentException.php ├── InvalidDefinitionException.php ├── NotCallableException.php └── NotFoundException.php ├── LazyLoader ├── AbstractLazyProxyBuilder.php ├── ClassLazyProxyBuilder.php ├── FallbackLazyProxyBuilder.php ├── InterfaceLazyProxyBuilder.php ├── LazyLoader.php ├── LazyProxyTrait.php └── PublicMethodVisitor.php ├── MetadataCacheCollector.php ├── MetadataCollector.php ├── MetadataCollectorInterface.php ├── MethodDefinitionCollector.php ├── MethodDefinitionCollectorInterface.php ├── ReflectionManager.php ├── ReflectionType.php ├── Resolver ├── DepthGuard.php ├── FactoryResolver.php ├── ObjectResolver.php ├── ParameterResolver.php ├── ResolverDispatcher.php └── ResolverInterface.php └── ScanHandler ├── NullScanHandler.php ├── PcntlScanHandler.php ├── ProcScanHandler.php ├── ScanHandlerInterface.php └── Scanned.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | /.github export-ignore 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Hyperf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/di", 3 | "description": "A DI for Hyperf.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "php", 7 | "swoole", 8 | "hyperf", 9 | "di", 10 | "annotation" 11 | ], 12 | "homepage": "https://hyperf.io", 13 | "support": { 14 | "issues": "https://github.com/hyperf/hyperf/issues", 15 | "source": "https://github.com/hyperf/hyperf", 16 | "docs": "https://hyperf.wiki", 17 | "pull-request": "https://github.com/hyperf/hyperf/pulls" 18 | }, 19 | "require": { 20 | "php": ">=8.1", 21 | "doctrine/instantiator": "^1.0", 22 | "hyperf/code-parser": "~3.1.0", 23 | "hyperf/pipeline": "~3.1.0", 24 | "hyperf/stdlib": "~3.1.0", 25 | "hyperf/support": "~3.1.0", 26 | "nikic/php-parser": "^4.1", 27 | "php-di/phpdoc-reader": "^2.2", 28 | "psr/container": "^1.0 || ^2.0", 29 | "symfony/finder": "^5.0 || ^6.0 || ^7.0", 30 | "vlucas/phpdotenv": "^5.0" 31 | }, 32 | "suggest": { 33 | "ext-pcntl": "Required to scan annotations.", 34 | "hyperf/config": "Require this component for annotation scan progress to retrieve the scan path." 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Hyperf\\Di\\": "src/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "HyperfTest\\Di\\": "tests/" 44 | } 45 | }, 46 | "config": { 47 | "sort-packages": true 48 | }, 49 | "extra": { 50 | "branch-alias": { 51 | "dev-master": "3.1-dev" 52 | }, 53 | "hyperf": { 54 | "config": "Hyperf\\Di\\ConfigProvider" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/AbstractCallableDefinitionCollector.php: -------------------------------------------------------------------------------- 1 | $parameters 24 | */ 25 | protected function getDefinitionsFromParameters(array $parameters): array 26 | { 27 | $definitions = []; 28 | foreach ($parameters as $parameter) { 29 | $definitions[] = $this->createType( 30 | $parameter->getName(), 31 | $parameter->getType(), 32 | $parameter->allowsNull(), 33 | $parameter->isDefaultValueAvailable(), 34 | $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null, 35 | $parameter->getAttributes() 36 | ); 37 | } 38 | return $definitions; 39 | } 40 | 41 | /** 42 | * @param ReflectionAttribute[] $attributes 43 | */ 44 | protected function createType(string $name, ?\ReflectionType $type, bool $allowsNull, bool $hasDefault = false, mixed $defaultValue = null, array $attributes = []): ReflectionType 45 | { 46 | // TODO: Support ReflectionUnionType. 47 | $typeName = match (true) { 48 | $type instanceof ReflectionNamedType => $type->getName(), 49 | $type instanceof ReflectionUnionType => $type->getTypes()[0]->getName(), 50 | default => 'mixed' 51 | }; 52 | return new ReflectionType($typeName, $allowsNull, [ 53 | 'defaultValueAvailable' => $hasDefault, 54 | 'defaultValue' => $defaultValue, 55 | 'name' => $name, 56 | 'attributes' => $attributes, 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Annotation/AbstractAnnotation.php: -------------------------------------------------------------------------------- 1 | getProperties(ReflectionProperty::IS_PUBLIC); 24 | $result = []; 25 | foreach ($properties as $property) { 26 | $result[$property->getName()] = $property->getValue($this); 27 | } 28 | return $result; 29 | } 30 | 31 | public function collectClass(string $className): void 32 | { 33 | AnnotationCollector::collectClass($className, static::class, $this); 34 | } 35 | 36 | public function collectClassConstant(string $className, ?string $target): void 37 | { 38 | AnnotationCollector::collectClassConstant($className, $target, static::class, $this); 39 | } 40 | 41 | public function collectMethod(string $className, ?string $target): void 42 | { 43 | AnnotationCollector::collectMethod($className, $target, static::class, $this); 44 | } 45 | 46 | public function collectProperty(string $className, ?string $target): void 47 | { 48 | AnnotationCollector::collectProperty($className, $target, static::class, $this); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Annotation/AbstractMultipleAnnotation.php: -------------------------------------------------------------------------------- 1 | formatAnnotation($annotation)); 22 | } 23 | 24 | public function collectClassConstant(string $className, ?string $target): void 25 | { 26 | $annotation = AnnotationCollector::getClassConstantAnnotation($className, $target)[static::class] ?? null; 27 | 28 | AnnotationCollector::collectClassConstant($className, $target, static::class, $this->formatAnnotation($annotation)); 29 | } 30 | 31 | public function collectMethod(string $className, ?string $target): void 32 | { 33 | $annotation = AnnotationCollector::getClassMethodAnnotation($className, $target)[static::class] ?? null; 34 | 35 | AnnotationCollector::collectMethod($className, $target, static::class, $this->formatAnnotation($annotation)); 36 | } 37 | 38 | public function collectProperty(string $className, ?string $target): void 39 | { 40 | $annotation = AnnotationCollector::getClassPropertyAnnotation($className, $target)[static::class] ?? null; 41 | 42 | AnnotationCollector::collectProperty($className, $target, static::class, $this->formatAnnotation($annotation)); 43 | } 44 | 45 | protected function formatAnnotation(?MultipleAnnotation $annotation): MultipleAnnotation 46 | { 47 | if ($annotation instanceof MultipleAnnotation) { 48 | return $annotation->insert($this); 49 | } 50 | 51 | return new MultipleAnnotation($this); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Annotation/AnnotationCollector.php: -------------------------------------------------------------------------------- 1 | $metadata) { 54 | foreach ($metadata['_cc'] ?? [] as $constant => $_metadata) { 55 | if ($value = $_metadata[$annotation] ?? null) { 56 | $result[] = ['class' => $class, 'constant' => $constant, 'annotation' => $value]; 57 | } 58 | } 59 | } 60 | return $result; 61 | } 62 | 63 | public static function getClassesByAnnotation(string $annotation): array 64 | { 65 | $result = []; 66 | foreach (static::$container as $class => $metadata) { 67 | if (! isset($metadata['_c'][$annotation])) { 68 | continue; 69 | } 70 | $result[$class] = $metadata['_c'][$annotation]; 71 | } 72 | return $result; 73 | } 74 | 75 | public static function getMethodsByAnnotation(string $annotation): array 76 | { 77 | $result = []; 78 | foreach (static::$container as $class => $metadata) { 79 | foreach ($metadata['_m'] ?? [] as $method => $_metadata) { 80 | if ($value = $_metadata[$annotation] ?? null) { 81 | $result[] = ['class' => $class, 'method' => $method, 'annotation' => $value]; 82 | } 83 | } 84 | } 85 | return $result; 86 | } 87 | 88 | public static function getPropertiesByAnnotation(string $annotation): array 89 | { 90 | $properties = []; 91 | foreach (static::$container as $class => $metadata) { 92 | foreach ($metadata['_p'] ?? [] as $property => $_metadata) { 93 | if ($value = $_metadata[$annotation] ?? null) { 94 | $properties[] = ['class' => $class, 'property' => $property, 'annotation' => $value]; 95 | } 96 | } 97 | } 98 | return $properties; 99 | } 100 | 101 | public static function getClassAnnotation(string $class, string $annotation) 102 | { 103 | return static::get($class . '._c.' . $annotation); 104 | } 105 | 106 | public static function getClassAnnotations(string $class) 107 | { 108 | return static::get($class . '._c'); 109 | } 110 | 111 | public static function getClassConstantAnnotation(string $class, string $constant) 112 | { 113 | return static::get($class . '._cc.' . $constant); 114 | } 115 | 116 | public static function getClassMethodAnnotation(string $class, string $method) 117 | { 118 | return static::get($class . '._m.' . $method); 119 | } 120 | 121 | public static function getClassPropertyAnnotation(string $class, string $property) 122 | { 123 | return static::get($class . '._p.' . $property); 124 | } 125 | 126 | public static function getContainer(): array 127 | { 128 | return static::$container; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Annotation/AnnotationInterface.php: -------------------------------------------------------------------------------- 1 | getAttributes($class); 34 | } 35 | 36 | public function getClassAnnotation(ReflectionClass $class, $annotationName) 37 | { 38 | $annotations = $this->getClassAnnotations($class); 39 | 40 | foreach ($annotations as $annotation) { 41 | if ($annotation instanceof $annotationName) { 42 | return $annotation; 43 | } 44 | } 45 | 46 | return null; 47 | } 48 | 49 | public function getPropertyAnnotations(ReflectionProperty $property) 50 | { 51 | return $this->getAttributes($property); 52 | } 53 | 54 | public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) 55 | { 56 | $annotations = $this->getPropertyAnnotations($property); 57 | 58 | foreach ($annotations as $annotation) { 59 | if ($annotation instanceof $annotationName) { 60 | return $annotation; 61 | } 62 | } 63 | 64 | return null; 65 | } 66 | 67 | public function getMethodAnnotations(ReflectionMethod $method) 68 | { 69 | return $this->getAttributes($method); 70 | } 71 | 72 | public function getMethodAnnotation(ReflectionMethod $method, $annotationName) 73 | { 74 | $annotations = $this->getMethodAnnotations($method); 75 | 76 | foreach ($annotations as $annotation) { 77 | if ($annotation instanceof $annotationName) { 78 | return $annotation; 79 | } 80 | } 81 | 82 | return null; 83 | } 84 | 85 | public function getAttributes(Reflector $reflection): array 86 | { 87 | $result = []; 88 | if (! method_exists($reflection, 'getAttributes')) { 89 | return $result; 90 | } 91 | $attributes = $reflection->getAttributes(); 92 | foreach ($attributes as $attribute) { 93 | if (in_array($attribute->getName(), $this->ignoreAnnotations, true)) { 94 | continue; 95 | } 96 | if (! class_exists($attribute->getName())) { 97 | $className = $methodName = $propertyName = $classConstantName = ''; 98 | if ($reflection instanceof ReflectionClass) { 99 | $className = $reflection->getName(); 100 | } elseif ($reflection instanceof ReflectionMethod) { 101 | $className = $reflection->getDeclaringClass()->getName(); 102 | $methodName = $reflection->getName(); 103 | } elseif ($reflection instanceof ReflectionProperty) { 104 | $className = $reflection->getDeclaringClass()->getName(); 105 | $propertyName = $reflection->getName(); 106 | } elseif ($reflection instanceof ReflectionClassConstant) { 107 | $className = $reflection->getDeclaringClass()->getName(); 108 | $classConstantName = $reflection->getName(); 109 | } 110 | $message = sprintf( 111 | "No attribute class found for '%s' in %s", 112 | $attribute->getName(), 113 | $className 114 | ); 115 | if ($methodName) { 116 | $message .= sprintf('->%s() method', $methodName); 117 | } 118 | if ($propertyName) { 119 | $message .= sprintf('::$%s property', $propertyName); 120 | } 121 | if ($classConstantName) { 122 | $message .= sprintf('::%s class constant', $classConstantName); 123 | } 124 | throw new NotFoundException($message); 125 | } 126 | $result[] = $attribute->newInstance(); 127 | } 128 | return $result; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Annotation/Aspect.php: -------------------------------------------------------------------------------- 1 | collect($className); 29 | } 30 | 31 | protected function collect(string $className) 32 | { 33 | [$instanceClasses, $instanceAnnotations, $instancePriority] = AspectLoader::load($className); 34 | 35 | // Classes 36 | $classes = $this->classes; 37 | $classes = $instanceClasses ? array_merge($classes, $instanceClasses) : $classes; 38 | // Annotations 39 | $annotations = $this->annotations; 40 | $annotations = $instanceAnnotations ? array_merge($annotations, $instanceAnnotations) : $annotations; 41 | // Priority 42 | $annotationPriority = $this->priority; 43 | $propertyPriority = $instancePriority ?: null; 44 | if (! is_null($annotationPriority) && ! is_null($propertyPriority) && $annotationPriority !== $propertyPriority) { 45 | throw new InvalidArgumentException('Cannot define two difference priority of Aspect.'); 46 | } 47 | $priority = $annotationPriority ?? $propertyPriority; 48 | // Save the metadata to AspectCollector 49 | AspectCollector::setAround($className, $classes, $annotations, $priority); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Annotation/AspectCollector.php: -------------------------------------------------------------------------------- 1 | $priority, 39 | 'classes' => array_merge(static::$aspectRules[$aspect]['classes'] ?? [], $classes), 40 | 'annotations' => array_merge(static::$aspectRules[$aspect]['annotations'] ?? [], $annotations), 41 | ]; 42 | } else { 43 | static::$aspectRules[$aspect] = [ 44 | 'priority' => $priority, 45 | 'classes' => $classes, 46 | 'annotations' => $annotations, 47 | ]; 48 | } 49 | } 50 | 51 | public static function clear(?string $key = null): void 52 | { 53 | if ($key) { 54 | unset(static::$container['classes'][$key], static::$container['annotations'][$key], static::$aspectRules[$key]); 55 | } else { 56 | static::$container = []; 57 | static::$aspectRules = []; 58 | } 59 | } 60 | 61 | public static function getRule(string $aspect): array 62 | { 63 | return static::$aspectRules[$aspect] ?? []; 64 | } 65 | 66 | public static function getPriority(string $aspect): int 67 | { 68 | return static::$aspectRules[$aspect]['priority'] ?? static::getDefaultPriority(); 69 | } 70 | 71 | public static function getRules(): array 72 | { 73 | return static::$aspectRules; 74 | } 75 | 76 | public static function getContainer(): array 77 | { 78 | return static::$container; 79 | } 80 | 81 | public static function serialize(): string 82 | { 83 | return serialize([static::$aspectRules, static::$container]); 84 | } 85 | 86 | public static function deserialize(string $metadata): bool 87 | { 88 | [$rules, $container] = unserialize($metadata); 89 | static::$aspectRules = $rules; 90 | static::$container = $container; 91 | return true; 92 | } 93 | 94 | private static function getDefaultPriority(): int 95 | { 96 | return 0; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Annotation/AspectLoader.php: -------------------------------------------------------------------------------- 1 | getProperties(ReflectionProperty::IS_PUBLIC); 27 | $instanceClasses = $instanceAnnotations = []; 28 | $instancePriority = null; 29 | foreach ($properties as $property) { 30 | if ($property->getName() === 'classes') { 31 | $instanceClasses = ReflectionManager::getPropertyDefaultValue($property); 32 | } elseif ($property->getName() === 'annotations') { 33 | $instanceAnnotations = ReflectionManager::getPropertyDefaultValue($property); 34 | } elseif ($property->getName() === 'priority') { 35 | $instancePriority = ReflectionManager::getPropertyDefaultValue($property); 36 | } 37 | } 38 | 39 | return [$instanceClasses, $instanceAnnotations, $instancePriority]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Annotation/Debug.php: -------------------------------------------------------------------------------- 1 | value)) { 33 | $reflectionClass = ReflectionManager::reflectClass($className); 34 | 35 | $reflectionProperty = $reflectionClass->getProperty($target); 36 | 37 | if (method_exists($reflectionProperty, 'hasType') && $reflectionProperty->hasType()) { 38 | /* @phpstan-ignore-next-line */ 39 | $this->value = $reflectionProperty->getType()->getName(); 40 | } else { 41 | $this->value = PhpDocReaderManager::getInstance()->getPropertyClass($reflectionProperty); 42 | } 43 | } 44 | 45 | if (empty($this->value)) { 46 | throw new AnnotationException("The @Inject value is invalid for {$className}->{$target}"); 47 | } 48 | 49 | if ($this->lazy) { 50 | $this->value = 'HyperfLazy\\' . $this->value; 51 | } 52 | AnnotationCollector::collectProperty($className, $target, static::class, $this); 53 | } catch (AnnotationException|DocReaderAnnotationException $exception) { 54 | if ($this->required) { 55 | throw new AnnotationException($exception->getMessage()); 56 | } 57 | $this->value = ''; 58 | } catch (Throwable $exception) { 59 | throw new AnnotationException("The @Inject value is invalid for {$className}->{$target}. Because {$exception->getMessage()}"); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Annotation/InjectAspect.php: -------------------------------------------------------------------------------- 1 | process(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Annotation/MultipleAnnotation.php: -------------------------------------------------------------------------------- 1 | annotations = [$annotation]; 26 | $this->className = get_class($annotation); 27 | } 28 | 29 | public function __get(string $name) 30 | { 31 | if (count($this->annotations) > 1) { 32 | throw new AnnotationException('MultipleAnnotation[' . $this->className() . '] has more than one annotations.'); 33 | } 34 | 35 | return $this->annotations[0]->{$name}; 36 | } 37 | 38 | public function className(): string 39 | { 40 | return $this->className; 41 | } 42 | 43 | public function insert(AnnotationInterface $annotation): static 44 | { 45 | if (! $annotation instanceof $this->className) { 46 | throw new AnnotationException(get_class($annotation) . ' must instanceof ' . $this->className); 47 | } 48 | 49 | $this->annotations[] = $annotation; 50 | return $this; 51 | } 52 | 53 | public function toAnnotations(): array 54 | { 55 | return $this->annotations; 56 | } 57 | 58 | public function collectClass(string $className): void 59 | { 60 | throw new AnnotationException('MultipleAnnotation[' . $this->className() . '] does not support collectClass()'); 61 | } 62 | 63 | public function collectClassConstant(string $className, ?string $target): void 64 | { 65 | throw new AnnotationException('MultipleAnnotation[' . $this->className() . '] does not support collectClassConstant()'); 66 | } 67 | 68 | public function collectMethod(string $className, ?string $target): void 69 | { 70 | throw new AnnotationException('MultipleAnnotation[' . $this->className() . '] does not support collectMethod()'); 71 | } 72 | 73 | public function collectProperty(string $className, ?string $target): void 74 | { 75 | throw new AnnotationException('MultipleAnnotation[' . $this->className() . '] does not support collectProperty()'); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Annotation/MultipleAnnotationInterface.php: -------------------------------------------------------------------------------- 1 | cacheable; 41 | } 42 | 43 | public function getConfigDir(): string 44 | { 45 | return $this->configDir; 46 | } 47 | 48 | public function getPaths(): array 49 | { 50 | return $this->paths; 51 | } 52 | 53 | public function getCollectors(): array 54 | { 55 | return $this->collectors; 56 | } 57 | 58 | public function getIgnoreAnnotations(): array 59 | { 60 | return $this->ignoreAnnotations; 61 | } 62 | 63 | public function getGlobalImports(): array 64 | { 65 | return $this->globalImports; 66 | } 67 | 68 | public function getDependencies(): array 69 | { 70 | return $this->dependencies; 71 | } 72 | 73 | public function getClassMap(): array 74 | { 75 | return $this->classMap; 76 | } 77 | 78 | public static function instance(string $configDir): self 79 | { 80 | if (self::$instance) { 81 | return self::$instance; 82 | } 83 | 84 | $configDir = rtrim($configDir, '/'); 85 | 86 | [$config, $serverDependencies, $cacheable] = static::initConfigByFile($configDir); 87 | 88 | return self::$instance = new self( 89 | $cacheable, 90 | $configDir, 91 | $config['paths'] ?? [], 92 | $serverDependencies ?? [], 93 | $config['ignore_annotations'] ?? [], 94 | $config['global_imports'] ?? [], 95 | $config['collectors'] ?? [], 96 | $config['class_map'] ?? [] 97 | ); 98 | } 99 | 100 | private static function initConfigByFile(string $configDir): array 101 | { 102 | $config = []; 103 | $configFromProviders = []; 104 | $cacheable = false; 105 | if (class_exists(ProviderConfig::class)) { 106 | $configFromProviders = ProviderConfig::load(); 107 | } 108 | 109 | $serverDependencies = $configFromProviders['dependencies'] ?? []; 110 | if (file_exists($configDir . '/autoload/dependencies.php')) { 111 | $definitions = include $configDir . '/autoload/dependencies.php'; 112 | $serverDependencies = array_replace($serverDependencies, $definitions ?? []); 113 | } 114 | 115 | $config = static::allocateConfigValue($configFromProviders['annotations'] ?? [], $config); 116 | 117 | // Load the config/autoload/annotations.php and merge the config 118 | if (file_exists($configDir . '/autoload/annotations.php')) { 119 | $annotations = include $configDir . '/autoload/annotations.php'; 120 | $config = static::allocateConfigValue($annotations, $config); 121 | } 122 | 123 | // Load the config/config.php and merge the config 124 | if (file_exists($configDir . '/config.php')) { 125 | $configContent = include $configDir . '/config.php'; 126 | $appEnv = $configContent['app_env'] ?? 'dev'; 127 | $cacheable = value($configContent['scan_cacheable'] ?? $appEnv === 'prod'); 128 | if (isset($configContent['annotations'])) { 129 | $config = static::allocateConfigValue($configContent['annotations'], $config); 130 | } 131 | } 132 | 133 | return [$config, $serverDependencies, $cacheable]; 134 | } 135 | 136 | private static function allocateConfigValue(array $content, array $config): array 137 | { 138 | if (! isset($content['scan'])) { 139 | return $config; 140 | } 141 | foreach ($content['scan'] as $key => $value) { 142 | if (! isset($config[$key])) { 143 | $config[$key] = []; 144 | } 145 | if (! is_array($value)) { 146 | $value = [$value]; 147 | } 148 | $config[$key] = array_merge($config[$key], $value); 149 | } 150 | return $config; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Annotation/Scanner.php: -------------------------------------------------------------------------------- 1 | filesystem = new Filesystem(); 34 | } 35 | 36 | public function collect(AnnotationReader $reader, ReflectionClass $reflection): void 37 | { 38 | $className = $reflection->getName(); 39 | if ($path = $this->scanConfig->getClassMap()[$className] ?? null) { 40 | if ($reflection->getFileName() !== $path) { 41 | // When the original class is dynamically replaced, the original class should not be collected. 42 | return; 43 | } 44 | } 45 | // Parse class annotations 46 | foreach ($reader->getAttributes($reflection) as $classAnnotation) { 47 | if ($classAnnotation instanceof AnnotationInterface) { 48 | $classAnnotation->collectClass($className); 49 | } 50 | } 51 | // Parse properties annotations 52 | foreach ($reflection->getProperties() as $property) { 53 | foreach ($reader->getAttributes($property) as $propertyAnnotation) { 54 | if ($propertyAnnotation instanceof AnnotationInterface) { 55 | $propertyAnnotation->collectProperty($className, $property->getName()); 56 | } 57 | } 58 | } 59 | // Parse methods annotations 60 | foreach ($reflection->getMethods() as $method) { 61 | foreach ($reader->getAttributes($method) as $methodAnnotation) { 62 | if ($methodAnnotation instanceof AnnotationInterface) { 63 | $methodAnnotation->collectMethod($className, $method->getName()); 64 | } 65 | } 66 | } 67 | // Parse class constants annotations 68 | foreach ($reflection->getReflectionConstants() as $classConstant) { 69 | foreach ($reader->getAttributes($classConstant) as $constantAnnotation) { 70 | if ($constantAnnotation instanceof AnnotationInterface) { 71 | $constantAnnotation->collectClassConstant($className, $classConstant->getName()); 72 | } 73 | } 74 | } 75 | 76 | unset($reflection); 77 | } 78 | 79 | public function scan(array $classMap = [], string $proxyDir = ''): array 80 | { 81 | $paths = $this->scanConfig->getPaths(); 82 | $collectors = $this->scanConfig->getCollectors(); 83 | if (! $paths) { 84 | return []; 85 | } 86 | 87 | $lastCacheModified = file_exists($this->path) ? $this->filesystem->lastModified($this->path) : 0; 88 | if ($lastCacheModified > 0 && $this->scanConfig->isCacheable()) { 89 | return $this->deserializeCachedScanData($collectors); 90 | } 91 | 92 | $scanned = $this->handler->scan(); 93 | if ($scanned->isScanned()) { 94 | return $this->deserializeCachedScanData($collectors); 95 | } 96 | 97 | $this->deserializeCachedScanData($collectors); 98 | 99 | $annotationReader = new AnnotationReader($this->scanConfig->getIgnoreAnnotations()); 100 | 101 | $paths = $this->normalizeDir($paths); 102 | 103 | $classes = ReflectionManager::getAllClasses($paths); 104 | 105 | $this->clearRemovedClasses($collectors, $classes); 106 | 107 | $reflectionClassMap = []; 108 | foreach ($classes as $className => $reflectionClass) { 109 | $reflectionClassMap[$className] = $reflectionClass->getFileName(); 110 | if ($this->filesystem->lastModified($reflectionClass->getFileName()) >= $lastCacheModified) { 111 | /** @var MetadataCollector $collector */ 112 | foreach ($collectors as $collector) { 113 | $collector::clear($className); 114 | } 115 | 116 | $this->collect($annotationReader, $reflectionClass); 117 | } 118 | } 119 | 120 | $this->loadAspects($lastCacheModified); 121 | 122 | $data = []; 123 | /** @var MetadataCollector|string $collector */ 124 | foreach ($collectors as $collector) { 125 | $data[$collector] = $collector::serialize(); 126 | } 127 | 128 | // Get the class map of Composer loader 129 | $classMap = array_merge($reflectionClassMap, $classMap); 130 | $proxyManager = new ProxyManager($classMap, $proxyDir); 131 | $proxies = $proxyManager->getProxies(); 132 | $aspectClasses = $proxyManager->getAspectClasses(); 133 | 134 | $this->putCache($this->path, serialize([$data, $proxies, $aspectClasses])); 135 | exit; 136 | } 137 | 138 | /** 139 | * Normalizes given directory names by removing directory not exist. 140 | * @throws DirectoryNotExistException 141 | */ 142 | public function normalizeDir(array $paths): array 143 | { 144 | $result = []; 145 | foreach ($paths as $path) { 146 | if (is_dir($path)) { 147 | $result[] = $path; 148 | } 149 | } 150 | 151 | if ($paths && ! $result) { 152 | throw new DirectoryNotExistException('The scanned directory does not exist'); 153 | } 154 | 155 | return $result; 156 | } 157 | 158 | protected function deserializeCachedScanData(array $collectors): array 159 | { 160 | if (! file_exists($this->path)) { 161 | return []; 162 | } 163 | 164 | [$data, $proxies] = unserialize(file_get_contents($this->path)); 165 | foreach ($data as $collector => $deserialized) { 166 | /** @var MetadataCollector $collector */ 167 | if (in_array($collector, $collectors)) { 168 | $collector::deserialize($deserialized); 169 | } 170 | } 171 | 172 | return $proxies; 173 | } 174 | 175 | /** 176 | * @param ReflectionClass[] $reflections 177 | */ 178 | protected function clearRemovedClasses(array $collectors, array $reflections): void 179 | { 180 | $path = BASE_PATH . '/runtime/container/classes.cache'; 181 | $classes = array_keys($reflections); 182 | 183 | $data = []; 184 | if ($this->filesystem->exists($path)) { 185 | $data = unserialize($this->filesystem->get($path)); 186 | } 187 | 188 | $this->putCache($path, serialize($classes)); 189 | 190 | $removed = array_diff($data, $classes); 191 | 192 | foreach ($removed as $class) { 193 | /** @var MetadataCollector $collector */ 194 | foreach ($collectors as $collector) { 195 | $collector::clear($class); 196 | } 197 | } 198 | } 199 | 200 | protected function putCache(string $path, $data) 201 | { 202 | if (! $this->filesystem->isDirectory($dir = dirname($path))) { 203 | $this->filesystem->makeDirectory($dir, 0755, true); 204 | } 205 | 206 | $this->filesystem->put($path, $data); 207 | } 208 | 209 | /** 210 | * Load aspects to AspectCollector by configuration files and ConfigProvider. 211 | */ 212 | protected function loadAspects(int $lastCacheModified): void 213 | { 214 | $configDir = $this->scanConfig->getConfigDir(); 215 | if (! $configDir) { 216 | return; 217 | } 218 | 219 | $aspectsPath = $configDir . '/autoload/aspects.php'; 220 | $basePath = $configDir . '/config.php'; 221 | $aspects = file_exists($aspectsPath) ? include $aspectsPath : []; 222 | $baseConfig = file_exists($basePath) ? include $basePath : []; 223 | $providerConfig = []; 224 | if (class_exists(ProviderConfig::class)) { 225 | $providerConfig = ProviderConfig::load(); 226 | } 227 | if (! isset($aspects) || ! is_array($aspects)) { 228 | $aspects = []; 229 | } 230 | if (! isset($baseConfig['aspects']) || ! is_array($baseConfig['aspects'])) { 231 | $baseConfig['aspects'] = []; 232 | } 233 | if (! isset($providerConfig['aspects']) || ! is_array($providerConfig['aspects'])) { 234 | $providerConfig['aspects'] = []; 235 | } 236 | $aspects = array_merge($providerConfig['aspects'], $baseConfig['aspects'], $aspects); 237 | 238 | [$removed, $changed] = $this->getChangedAspects($aspects, $lastCacheModified); 239 | // When the aspect removed from config, it should be removed from AspectCollector. 240 | foreach ($removed as $aspect) { 241 | AspectCollector::clear($aspect); 242 | } 243 | 244 | foreach ($aspects as $key => $value) { 245 | if (is_numeric($key)) { 246 | $aspect = $value; 247 | $priority = null; 248 | } else { 249 | $aspect = $key; 250 | $priority = (int) $value; 251 | } 252 | 253 | if (! in_array($aspect, $changed)) { 254 | continue; 255 | } 256 | 257 | [$instanceClasses, $instanceAnnotations, $instancePriority] = AspectLoader::load($aspect); 258 | 259 | $classes = $instanceClasses ?: []; 260 | // Annotations 261 | $annotations = $instanceAnnotations ?: []; 262 | // Priority 263 | $priority = $priority ?: ($instancePriority ?? null); 264 | // Save the metadata to AspectCollector 265 | AspectCollector::setAround($aspect, $classes, $annotations, $priority); 266 | } 267 | } 268 | 269 | protected function getChangedAspects(array $aspects, int $lastCacheModified): array 270 | { 271 | $path = BASE_PATH . '/runtime/container/aspects.cache'; 272 | $classes = []; 273 | foreach ($aspects as $key => $value) { 274 | if (is_numeric($key)) { 275 | $classes[] = $value; 276 | } else { 277 | $classes[] = $key; 278 | } 279 | } 280 | 281 | $data = []; 282 | if ($this->filesystem->exists($path)) { 283 | $data = unserialize($this->filesystem->get($path)); 284 | } 285 | 286 | $this->putCache($path, serialize($classes)); 287 | 288 | $diff = array_diff($data, $classes); 289 | $changed = array_diff($classes, $data); 290 | $removed = []; 291 | foreach ($diff as $item) { 292 | $annotation = AnnotationCollector::getClassAnnotation($item, Aspect::class); 293 | if (is_null($annotation)) { 294 | $removed[] = $item; 295 | } 296 | } 297 | foreach ($classes as $class) { 298 | $file = Composer::getLoader()->findFile($class); 299 | if ($file === false) { 300 | echo sprintf('Skip class %s, because it does not exist in composer class loader.', $class) . PHP_EOL; 301 | continue; 302 | } 303 | if ($lastCacheModified <= $this->filesystem->lastModified($file)) { 304 | $changed[] = $class; 305 | } 306 | } 307 | 308 | return [ 309 | array_values(array_unique($removed)), 310 | array_values(array_unique($changed)), 311 | ]; 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/Aop/AbstractAspect.php: -------------------------------------------------------------------------------- 1 | $collection) { 30 | if ($type === 'classes') { 31 | static::parseClasses($collection, $class, $rewriteCollection); 32 | } elseif ($type === 'annotations') { 33 | static::parseAnnotations($collection, $class, $rewriteCollection); 34 | } 35 | } 36 | return $rewriteCollection; 37 | } 38 | 39 | /** 40 | * @return array [isMatch, $matchedMethods] 41 | */ 42 | public static function isMatchClassRule(string $target, string $rule): array 43 | { 44 | /* 45 | * e.g. Foo/Bar 46 | * e.g. Foo/B* 47 | * e.g. F*o/Bar 48 | * e.g. Foo/Bar::method 49 | * e.g. Foo/Bar::met* 50 | */ 51 | $ruleMethod = null; 52 | $ruleClass = $rule; 53 | $method = null; 54 | $class = $target; 55 | 56 | if (str_contains($rule, '::')) { 57 | [$ruleClass, $ruleMethod] = explode('::', $rule); 58 | } 59 | if (str_contains($target, '::')) { 60 | [$class, $method] = explode('::', $target); 61 | } 62 | 63 | if ($method == null) { 64 | if (! str_contains($ruleClass, '*')) { 65 | /* 66 | * Match [rule] Foo/Bar::ruleMethod [target] Foo/Bar [return] true,ruleMethod 67 | * Match [rule] Foo/Bar [target] Foo/Bar [return] true,null 68 | * Match [rule] FooBar::rule*Method [target] Foo/Bar [return] true,rule*Method 69 | */ 70 | if ($ruleClass === $class) { 71 | return [true, $ruleMethod]; 72 | } 73 | 74 | return [false, null]; 75 | } 76 | 77 | /** 78 | * Match [rule] Foo*Bar::ruleMethod [target] Foo/Bar [return] true,ruleMethod 79 | * Match [rule] Foo*Bar [target] Foo/Bar [return] true,null. 80 | */ 81 | $preg = str_replace(['*', '\\'], ['.*', '\\\\'], $ruleClass); 82 | $pattern = "#^{$preg}$#"; 83 | 84 | if (preg_match($pattern, $class)) { 85 | return [true, $ruleMethod]; 86 | } 87 | 88 | return [false, null]; 89 | } 90 | 91 | if (! str_contains($rule, '*')) { 92 | /* 93 | * Match [rule] Foo/Bar::ruleMethod [target] Foo/Bar::ruleMethod [return] true,ruleMethod 94 | * Match [rule] Foo/Bar [target] Foo/Bar::ruleMethod [return] false,null 95 | */ 96 | if ($ruleClass === $class && ($ruleMethod === null || $ruleMethod === $method)) { 97 | return [true, $method]; 98 | } 99 | 100 | return [false, null]; 101 | } 102 | 103 | /* 104 | * Match [rule] Foo*Bar::ruleMethod [target] Foo/Bar::ruleMethod [return] true,ruleMethod 105 | * Match [rule] FooBar::rule*Method [target] Foo/Bar::ruleMethod [return] true,rule*Method 106 | */ 107 | if ($ruleMethod) { 108 | $preg = str_replace(['*', '\\'], ['.*', '\\\\'], $rule); 109 | $pattern = "#^{$preg}$#"; 110 | if (preg_match($pattern, $target)) { 111 | return [true, $method]; 112 | } 113 | } else { 114 | /** 115 | * Match [rule] Foo*Bar [target] Foo/Bar::ruleMethod [return] true,null. 116 | */ 117 | $preg = str_replace(['*', '\\'], ['.*', '\\\\'], $rule); 118 | $pattern = "#^{$preg}$#"; 119 | if (preg_match($pattern, $class)) { 120 | return [true, $method]; 121 | } 122 | } 123 | 124 | return [false, null]; 125 | } 126 | 127 | public static function isMatch(string $class, string $method, string $rule): bool 128 | { 129 | [$isMatch] = self::isMatchClassRule($class . '::' . $method, $rule); 130 | 131 | return $isMatch; 132 | } 133 | 134 | private static function parseAnnotations(array $collection, string $class, RewriteCollection $rewriteCollection): void 135 | { 136 | // Get the annotations of class and method. 137 | $annotations = AnnotationCollector::get($class); 138 | $classMapping = $annotations['_c'] ?? []; 139 | $methodMapping = value(function () use ($annotations) { 140 | $mapping = []; 141 | $methodAnnotations = $annotations['_m'] ?? []; 142 | foreach ($methodAnnotations as $method => $targetAnnotations) { 143 | $keys = array_keys($targetAnnotations); 144 | foreach ($keys as $key) { 145 | $mapping[$key][] = $method; 146 | } 147 | } 148 | return $mapping; 149 | }); 150 | $aspects = array_keys($collection); 151 | foreach ($aspects as $aspect) { 152 | $rules = AspectCollector::getRule($aspect); 153 | foreach ($rules['annotations'] ?? [] as $rule) { 154 | // If exist class level annotation, then all methods should rewrite, so return an empty array directly. 155 | if (isset($classMapping[$rule])) { 156 | $rewriteCollection->setLevel(RewriteCollection::CLASS_LEVEL); 157 | return; 158 | } 159 | if (isset($methodMapping[$rule])) { 160 | $rewriteCollection->add($methodMapping[$rule]); 161 | } 162 | } 163 | } 164 | } 165 | 166 | private static function parseClasses(array $collection, string $class, RewriteCollection $rewriteCollection): void 167 | { 168 | $aspects = array_keys($collection); 169 | foreach ($aspects as $aspect) { 170 | $rules = AspectCollector::getRule($aspect); 171 | foreach ($rules['classes'] ?? [] as $rule) { 172 | [$isMatch, $method] = static::isMatchClassRule($class, $rule); 173 | if ($isMatch) { 174 | if ($method === null) { 175 | $rewriteCollection->setLevel(RewriteCollection::CLASS_LEVEL); 176 | return; 177 | } 178 | $rewriteCollection->add($method); 179 | } 180 | } 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Aop/AspectManager.php: -------------------------------------------------------------------------------- 1 | astParser = $parserFactory->create(ParserFactory::ONLY_PHP7); 34 | $this->printer = new Standard(); 35 | } 36 | 37 | public function parse(string $code): ?array 38 | { 39 | return $this->astParser->parse($code); 40 | } 41 | 42 | public function proxy(string $className) 43 | { 44 | $code = $this->getCodeByClassName($className); 45 | $stmts = $this->astParser->parse($code); 46 | $traverser = new NodeTraverser(); 47 | $visitorMetadata = new VisitorMetadata($className); 48 | // User could modify or replace the node visitors by Hyperf\Di\Aop\AstVisitorRegistry. 49 | $queue = clone AstVisitorRegistry::getQueue(); 50 | foreach ($queue as $string) { 51 | $visitor = new $string($visitorMetadata); 52 | $traverser->addVisitor($visitor); 53 | } 54 | $modifiedStmts = $traverser->traverse($stmts); 55 | return $this->printer->prettyPrintFile($modifiedStmts); 56 | } 57 | 58 | public function parseClassByStmts(array $stmts): string 59 | { 60 | $namespace = $className = ''; 61 | foreach ($stmts as $stmt) { 62 | if ($stmt instanceof Namespace_ && $stmt->name) { 63 | $namespace = $stmt->name->toString(); 64 | foreach ($stmt->stmts as $node) { 65 | if (($node instanceof ClassLike) && $node->name) { 66 | $className = $node->name->toString(); 67 | break; 68 | } 69 | } 70 | } 71 | } 72 | return ($namespace && $className) ? $namespace . '\\' . $className : ''; 73 | } 74 | 75 | private function getCodeByClassName(string $className): string 76 | { 77 | $file = Composer::getLoader()->findFile($className); 78 | if (! $file) { 79 | return ''; 80 | } 81 | return file_get_contents($file); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Aop/AstVisitorRegistry.php: -------------------------------------------------------------------------------- 1 | {$name}(...$arguments); 32 | } 33 | throw new InvalidArgumentException('Invalid method for ' . __CLASS__); 34 | } 35 | 36 | public static function insert($value, $priority = 0) 37 | { 38 | static::$values[] = $value; 39 | return static::getQueue()->insert($value, $priority); 40 | } 41 | 42 | public static function exists($value): bool 43 | { 44 | return in_array($value, static::$values); 45 | } 46 | 47 | public static function getQueue(): SplPriorityQueue 48 | { 49 | if (! static::$queue instanceof SplPriorityQueue) { 50 | static::$queue = new SplPriorityQueue(); 51 | } 52 | return static::$queue; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Aop/Pipeline.php: -------------------------------------------------------------------------------- 1 | container->get($pipe); 26 | } 27 | if (! $passable instanceof ProceedingJoinPoint) { 28 | throw new InvalidDefinitionException('$passable must is a ProceedingJoinPoint object.'); 29 | } 30 | $passable->pipe = $stack; 31 | return method_exists($pipe, $this->method) ? $pipe->{$this->method}($passable) : $pipe($passable); 32 | }; 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Aop/ProceedingJoinPoint.php: -------------------------------------------------------------------------------- 1 | pipe; 42 | if (! $closure instanceof Closure) { 43 | throw new Exception('The pipe is not instanceof \Closure'); 44 | } 45 | 46 | return $closure($this); 47 | } 48 | 49 | /** 50 | * Process the original method, this method should trigger by pipeline. 51 | */ 52 | public function processOriginalMethod() 53 | { 54 | $this->pipe = null; 55 | $closure = $this->originalMethod; 56 | $arguments = $this->getArguments(); 57 | return $closure(...$arguments); 58 | } 59 | 60 | public function getAnnotationMetadata(): AnnotationMetadata 61 | { 62 | $metadata = AnnotationCollector::get($this->className); 63 | return new AnnotationMetadata($metadata['_c'] ?? [], $metadata['_m'][$this->methodName] ?? []); 64 | } 65 | 66 | public function getArguments(): array 67 | { 68 | $result = []; 69 | foreach ($this->arguments['order'] ?? [] as $order) { 70 | $result[] = $this->arguments['keys'][$order]; 71 | } 72 | 73 | // Variable arguments are always placed at the end. 74 | if (isset($this->arguments['variadic'], $order) && $order === $this->arguments['variadic']) { 75 | $variadic = array_pop($result); 76 | $result = array_merge($result, $variadic); 77 | } 78 | return $result; 79 | } 80 | 81 | public function getReflectMethod(): ReflectionMethod 82 | { 83 | return ReflectionManager::reflectMethod( 84 | $this->className, 85 | $this->methodName 86 | ); 87 | } 88 | 89 | public function getInstance(): ?object 90 | { 91 | $ref = new ReflectionFunction($this->originalMethod); 92 | 93 | return $ref->getClosureThis(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Aop/PropertyHandlerTrait.php: -------------------------------------------------------------------------------- 1 | __handle($className, $className, $properties); 32 | 33 | // Inject the properties of traits. 34 | // Because the property annotations of trait couldn't be collected by class. 35 | $handled = $this->__handleTrait($reflectionClass, $handled, $className); 36 | 37 | // Inject the properties of parent class. 38 | // It can be used to deal with parent classes whose subclasses have constructor function, but don't execute `parent::__construct()`. 39 | // For example: 40 | // class SubClass extend ParentClass 41 | // { 42 | // public function __construct() { 43 | // } 44 | // } 45 | $parentReflectionClass = $reflectionClass; 46 | while ($parentReflectionClass = $parentReflectionClass->getParentClass()) { 47 | $parentClassProperties = ReflectionManager::reflectPropertyNames($parentReflectionClass->getName()); 48 | $parentClassProperties = array_filter($parentClassProperties, static function ($property) use ($reflectionClass) { 49 | return $reflectionClass->hasProperty($property); 50 | }); 51 | $parentClassProperties = array_diff($parentClassProperties, $handled); 52 | $handled = array_merge( 53 | $handled, 54 | $this->__handle($className, $parentReflectionClass->getName(), $parentClassProperties) 55 | ); 56 | } 57 | } 58 | 59 | protected function __handleTrait(ReflectionClass $reflectionClass, array $handled, string $className): array 60 | { 61 | foreach ($reflectionClass->getTraits() ?? [] as $reflectionTrait) { 62 | if (in_array($reflectionTrait->getName(), [ProxyTrait::class, PropertyHandlerTrait::class])) { 63 | continue; 64 | } 65 | $traitProperties = ReflectionManager::reflectPropertyNames($reflectionTrait->getName()); 66 | $traitProperties = array_diff($traitProperties, $handled); 67 | if (! $traitProperties) { 68 | continue; 69 | } 70 | $handled = array_merge( 71 | $handled, 72 | $this->__handle($className, $reflectionTrait->getName(), $traitProperties) 73 | ); 74 | $handled = $this->__handleTrait($reflectionTrait, $handled, $className); 75 | } 76 | return $handled; 77 | } 78 | 79 | protected function __handle(string $currentClassName, string $targetClassName, array $properties): array 80 | { 81 | $handled = []; 82 | foreach ($properties as $propertyName) { 83 | $propertyMetadata = AnnotationCollector::getClassPropertyAnnotation($targetClassName, $propertyName); 84 | if (! $propertyMetadata) { 85 | continue; 86 | } 87 | foreach ($propertyMetadata as $annotationName => $annotation) { 88 | if ($callbacks = PropertyHandlerManager::get($annotationName)) { 89 | foreach ($callbacks as $callback) { 90 | $callback($this, $currentClassName, $targetClassName, $propertyName, $annotation); 91 | } 92 | $handled[] = $propertyName; 93 | } 94 | } 95 | } 96 | 97 | return $handled; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Aop/PropertyHandlerVisitor.php: -------------------------------------------------------------------------------- 1 | visitorMetadata->hasExtends === null) { 37 | if ($node->extends) { 38 | $this->visitorMetadata->hasExtends = true; 39 | } else { 40 | $this->visitorMetadata->hasExtends = false; 41 | } 42 | } 43 | } 44 | if ($node instanceof Node\Stmt\ClassMethod) { 45 | if ($node->name->toString() === '__construct') { 46 | $this->visitorMetadata->hasConstructor = true; 47 | $this->visitorMetadata->constructorNode = $node; 48 | } 49 | } 50 | return null; 51 | } 52 | 53 | public function leaveNode(Node $node) 54 | { 55 | if (! $this->visitorMetadata->hasConstructor && $node instanceof Node\Stmt\Class_ && ! $node->isAnonymous()) { 56 | $constructor = $this->buildConstructor(); 57 | if ($this->visitorMetadata->hasExtends) { 58 | $constructor->stmts[] = $this->buildCallParentConstructorStatement(); 59 | } 60 | $constructor->stmts[] = $this->buildMethodCallStatement(); 61 | $node->stmts = array_merge([$this->buildProxyTraitUseStatement()], [$constructor], $node->stmts); 62 | $this->visitorMetadata->hasConstructor = true; 63 | } else { 64 | if ($node instanceof Node\Stmt\ClassMethod && $node->name->toString() === '__construct') { 65 | $node->stmts = array_merge([$this->buildMethodCallStatement()], $node->stmts); 66 | } 67 | if ($node instanceof Node\Stmt\Class_ && ! $node->isAnonymous()) { 68 | $node->stmts = array_merge([$this->buildProxyTraitUseStatement()], $node->stmts); 69 | } 70 | } 71 | return null; 72 | } 73 | 74 | protected function buildConstructor(): Node\Stmt\ClassMethod 75 | { 76 | if ($this->visitorMetadata->constructorNode instanceof Node\Stmt\ClassMethod) { 77 | // Returns the parsed constructor class method node. 78 | $constructor = $this->visitorMetadata->constructorNode; 79 | } else { 80 | // Create a new constructor class method node. 81 | $constructor = new Node\Stmt\ClassMethod('__construct'); 82 | $reflection = ReflectionManager::reflectClass($this->visitorMetadata->className); 83 | try { 84 | $parameters = $reflection->getMethod('__construct')->getParameters(); 85 | foreach ($parameters as $parameter) { 86 | $constructor->params[] = PhpParser::getInstance()->getNodeFromReflectionParameter($parameter); 87 | } 88 | } catch (ReflectionException) { 89 | // Cannot found __construct method in parent class or traits, do nothing. 90 | } 91 | } 92 | return $constructor; 93 | } 94 | 95 | protected function buildCallParentConstructorStatement(): Node\Stmt 96 | { 97 | $hasConstructor = new Node\Expr\FuncCall(new Name('method_exists'), [ 98 | new Node\Arg(new Node\Expr\ClassConstFetch(new Name('parent'), 'class')), 99 | new Node\Arg(new Node\Scalar\String_('__construct')), 100 | ]); 101 | return new Node\Stmt\If_($hasConstructor, [ 102 | 'stmts' => [ 103 | new Node\Stmt\Expression(new Node\Expr\StaticCall(new Name('parent'), '__construct', [ 104 | new Node\Arg(new Node\Expr\FuncCall(new Name('func_get_args')), false, true), 105 | ])), 106 | ], 107 | ]); 108 | } 109 | 110 | protected function buildMethodCallStatement(): Node\Stmt\Expression 111 | { 112 | return new Node\Stmt\Expression(new Node\Expr\MethodCall(new Node\Expr\Variable('this'), '__handlePropertyHandler', [ 113 | new Node\Arg(new Node\Scalar\MagicConst\Class_()), 114 | ])); 115 | } 116 | 117 | /** 118 | * Build `use PropertyHandlerTrait;` statement. 119 | */ 120 | protected function buildProxyTraitUseStatement(): TraitUse 121 | { 122 | $traits = []; 123 | foreach ($this->proxyTraits as $proxyTrait) { 124 | // Should not check the trait whether exist to avoid class autoload. 125 | if (! is_string($proxyTrait)) { 126 | continue; 127 | } 128 | // Add backslash prefix if the proxy trait does not start with backslash. 129 | $proxyTrait[0] !== '\\' && $proxyTrait = '\\' . $proxyTrait; 130 | $traits[] = new Name($proxyTrait); 131 | } 132 | return new TraitUse($traits); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Aop/ProxyCallVisitor.php: -------------------------------------------------------------------------------- 1 | stmts as $class) { 70 | if ($class instanceof Node\Stmt\ClassLike) { 71 | $this->visitorMetadata->classLike = get_class($class); 72 | } 73 | } 74 | } 75 | 76 | return null; 77 | } 78 | 79 | public function enterNode(Node $node) 80 | { 81 | switch ($node) { 82 | case $node instanceof ClassMethod: 83 | $this->shouldRewrite = $this->shouldRewrite($node); 84 | break; 85 | } 86 | 87 | return null; 88 | } 89 | 90 | public function leaveNode(Node $node) 91 | { 92 | switch ($node) { 93 | case $node instanceof ClassMethod: 94 | if (! $this->shouldRewrite($node)) { 95 | return $node; 96 | } 97 | // Rewrite the method to proxy call method. 98 | return $this->rewriteMethod($node); 99 | case $node instanceof Trait_: 100 | // If the node is trait and php version >= 7.3, it can `use ProxyTrait` like class. 101 | case $node instanceof Enum_: 102 | // If the node is enum and php version >= 8.1, it can `use ProxyTrait` like class. 103 | case $node instanceof Class_ && ! $node->isAnonymous(): 104 | // Add use proxy traits. 105 | $stmts = $node->stmts; 106 | if ($stmt = $this->buildProxyCallTraitUseStatement()) { 107 | array_unshift($stmts, $stmt); 108 | } 109 | $node->stmts = $stmts; 110 | unset($stmts); 111 | return $node; 112 | case $node instanceof MagicConstFunction: 113 | // Rewrite __FUNCTION__ to $__function__ variable. 114 | if ($this->shouldRewrite) { 115 | return new Variable('__function__'); 116 | } 117 | break; 118 | case $node instanceof MagicConstMethod: 119 | // Rewrite __METHOD__ to $__method__ variable. 120 | if ($this->shouldRewrite) { 121 | return new Variable('__method__'); 122 | } 123 | break; 124 | case $node instanceof MagicConstDir: 125 | // Rewrite __DIR__ as the real directory path 126 | if ($file = Composer::getLoader()->findFile($this->visitorMetadata->className)) { 127 | return new String_(dirname(realpath($file))); 128 | } 129 | break; 130 | case $node instanceof MagicConstFile: 131 | // Rewrite __FILE__ to the real file path 132 | if ($file = Composer::getLoader()->findFile($this->visitorMetadata->className)) { 133 | return new String_(realpath($file)); 134 | } 135 | break; 136 | } 137 | return null; 138 | } 139 | 140 | /** 141 | * Build `use ProxyTrait;`. 142 | */ 143 | private function buildProxyCallTraitUseStatement(): ?TraitUse 144 | { 145 | $traits = []; 146 | foreach ($this->proxyTraits as $proxyTrait) { 147 | if (! is_string($proxyTrait) || ! trait_exists($proxyTrait)) { 148 | continue; 149 | } 150 | // Add backslash prefix if the proxy trait does not start with backslash. 151 | $proxyTrait[0] !== '\\' && $proxyTrait = '\\' . $proxyTrait; 152 | $traits[] = new Name($proxyTrait); 153 | } 154 | 155 | if (empty($traits)) { 156 | return null; 157 | } 158 | return new TraitUse($traits); 159 | } 160 | 161 | /** 162 | * Rewrite a normal class method to a proxy call method, 163 | * include normal class method and static method. 164 | */ 165 | private function rewriteMethod(ClassMethod $node): ClassMethod 166 | { 167 | // Build the static proxy call method base on the original method. 168 | $shouldReturn = true; 169 | $returnType = $node->getReturnType(); 170 | if ($returnType instanceof Identifier && $returnType->name === 'void') { 171 | $shouldReturn = false; 172 | } 173 | $staticCall = new StaticCall(new Name('self'), '__proxyCall', [ 174 | // __CLASS__ 175 | new Arg($this->getMagicConst()), 176 | // __FUNCTION__ 177 | new Arg(new MagicConstFunction()), 178 | // ['order' => ['param1', 'param2'], 'keys' => compact('param1', 'param2'), 'variadic' => 'param2'] 179 | new Arg($this->getArguments($node->getParams())), 180 | // A closure that wrapped original method code. 181 | new Arg(new Closure([ 182 | 'params' => $this->filterModifier($node->getParams()), 183 | 'uses' => [ 184 | new Variable('__function__'), 185 | new Variable('__method__'), 186 | ], 187 | 'stmts' => $node->stmts, 188 | ])), 189 | ]); 190 | $stmts = $this->unshiftMagicMethods([]); 191 | if ($shouldReturn) { 192 | $stmts[] = new Return_($staticCall); 193 | } else { 194 | $stmts[] = new Expression($staticCall); 195 | } 196 | $node->stmts = $stmts; 197 | return $node; 198 | } 199 | 200 | /** 201 | * @param Node\Param[] $params 202 | * @return Node\Param[] 203 | */ 204 | private function filterModifier(array $params): array 205 | { 206 | return array_map(function (Node\Param $param) { 207 | $tempParam = clone $param; 208 | $tempParam->flags &= ~Class_::VISIBILITY_MODIFIER_MASK & ~Class_::MODIFIER_READONLY; 209 | return $tempParam; 210 | }, $params); 211 | } 212 | 213 | /** 214 | * @param Node\Param[] $params 215 | */ 216 | private function getArguments(array $params): Array_ 217 | { 218 | if (empty($params)) { 219 | return new Array_([ 220 | new ArrayItem( 221 | key: new String_('keys'), 222 | value: new Array_([], ['kind' => Array_::KIND_SHORT]), 223 | ), 224 | ], ['kind' => Array_::KIND_SHORT]); 225 | } 226 | // ['param1', 'param2', ...] 227 | $methodParamsList = new Array_( 228 | array_map(fn (Node\Param $param) => new ArrayItem(new String_($param->var->name)), $params), 229 | ['kind' => Array_::KIND_SHORT] 230 | ); 231 | return new Array_([ 232 | new ArrayItem( 233 | key: new String_('order'), 234 | value: $methodParamsList, 235 | ), 236 | new ArrayItem( 237 | key: new String_('keys'), 238 | value: new FuncCall(new Name('compact'), [new Arg($methodParamsList)]) 239 | ), 240 | new ArrayItem( 241 | key: new String_('variadic'), 242 | value: $this->getVariadicParamName($params), 243 | )], ['kind' => Array_::KIND_SHORT]); 244 | } 245 | 246 | /** 247 | * @param Node\Param[] $params 248 | */ 249 | private function getVariadicParamName(array $params): String_ 250 | { 251 | foreach ($params as $param) { 252 | if ($param->variadic) { 253 | return new String_($param->var->name); 254 | } 255 | } 256 | return new String_(''); 257 | } 258 | 259 | private function unshiftMagicMethods(array $stmts = []): array 260 | { 261 | $magicConstFunction = new Expression(new Assign(new Variable('__function__'), new MagicConstFunction())); 262 | $magicConstMethod = new Expression(new Assign(new Variable('__method__'), new MagicConstMethod())); 263 | array_unshift($stmts, $magicConstFunction, $magicConstMethod); 264 | return $stmts; 265 | } 266 | 267 | private function getMagicConst(): Node\Scalar\MagicConst 268 | { 269 | return match ($this->visitorMetadata->classLike) { 270 | Trait_::class => new MagicConstTrait(), 271 | default => new MagicConstClass(), 272 | }; 273 | } 274 | 275 | private function shouldRewrite(ClassMethod $node): bool 276 | { 277 | if ($this->visitorMetadata->classLike == Node\Stmt\Interface_::class || $node->isAbstract()) { 278 | return false; 279 | } 280 | 281 | $rewriteCollection = Aspect::parse($this->visitorMetadata->className); 282 | 283 | return $rewriteCollection->shouldRewrite($node->name->toString()); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/Aop/ProxyManager.php: -------------------------------------------------------------------------------- 1 | filesystem = new Filesystem(); 37 | $this->proxies = $this->generateProxyFiles($this->initProxiesByReflectionClassMap( 38 | $this->classMap 39 | )); 40 | } 41 | 42 | public function getProxies(): array 43 | { 44 | return $this->proxies; 45 | } 46 | 47 | public function getProxyDir(): string 48 | { 49 | return $this->proxyDir; 50 | } 51 | 52 | public function getAspectClasses(): array 53 | { 54 | $aspectClasses = []; 55 | $classesAspects = AspectCollector::get('classes', []); 56 | foreach ($classesAspects as $aspect => $rules) { 57 | foreach ($rules as $rule) { 58 | if (isset($this->proxies[$rule])) { 59 | $aspectClasses[$aspect][$rule] = $this->proxies[$rule]; 60 | } 61 | } 62 | } 63 | return $aspectClasses; 64 | } 65 | 66 | protected function generateProxyFiles(array $proxies = []): array 67 | { 68 | $proxyFiles = []; 69 | if (! $proxies) { 70 | return $proxyFiles; 71 | } 72 | if (! file_exists($this->getProxyDir())) { 73 | mkdir($this->getProxyDir(), 0755, true); 74 | } 75 | // WARNING: Ast class SHOULD NOT use static instance, because it will read the code from file, then would be caused coroutine switch. 76 | $ast = new Ast(); 77 | foreach ($proxies as $className => $aspects) { 78 | $proxyFiles[$className] = $this->putProxyFile($ast, $className); 79 | } 80 | return $proxyFiles; 81 | } 82 | 83 | protected function putProxyFile(Ast $ast, $className) 84 | { 85 | $proxyFilePath = $this->getProxyFilePath($className); 86 | $modified = true; 87 | if (file_exists($proxyFilePath)) { 88 | $modified = $this->isModified($className, $proxyFilePath); 89 | } 90 | 91 | if ($modified) { 92 | $code = $ast->proxy($className); 93 | file_put_contents($proxyFilePath, $code); 94 | } 95 | 96 | return $proxyFilePath; 97 | } 98 | 99 | protected function isModified(string $className, ?string $proxyFilePath = null): bool 100 | { 101 | $proxyFilePath = $proxyFilePath ?? $this->getProxyFilePath($className); 102 | $time = $this->filesystem->lastModified($proxyFilePath); 103 | $origin = $this->classMap[$className]; 104 | if ($time >= $this->filesystem->lastModified($origin)) { 105 | return false; 106 | } 107 | 108 | return true; 109 | } 110 | 111 | protected function getProxyFilePath($className) 112 | { 113 | return $this->getProxyDir() . str_replace('\\', '_', $className) . '.proxy.php'; 114 | } 115 | 116 | protected function isMatch(string $rule, string $target): bool 117 | { 118 | if (str_contains($rule, '::')) { 119 | [$rule] = explode('::', $rule); 120 | } 121 | if (! str_contains($rule, '*') && $rule === $target) { 122 | return true; 123 | } 124 | $preg = str_replace(['*', '\\'], ['.*', '\\\\'], $rule); 125 | $pattern = "/^{$preg}$/"; 126 | 127 | if (preg_match($pattern, $target)) { 128 | return true; 129 | } 130 | 131 | return false; 132 | } 133 | 134 | protected function initProxiesByReflectionClassMap(array $reflectionClassMap = []): array 135 | { 136 | // According to the data of AspectCollector to parse all the classes that need proxy. 137 | $proxies = []; 138 | if (! $reflectionClassMap) { 139 | return $proxies; 140 | } 141 | $classesAspects = AspectCollector::get('classes', []); 142 | foreach ($classesAspects as $aspect => $rules) { 143 | foreach ($rules as $rule) { 144 | foreach ($reflectionClassMap as $class => $path) { 145 | if (! $this->isMatch($rule, $class)) { 146 | continue; 147 | } 148 | $proxies[$class][] = $aspect; 149 | } 150 | } 151 | } 152 | 153 | foreach ($reflectionClassMap as $className => $path) { 154 | // Aggregate the class annotations 155 | $classAnnotations = $this->retrieveAnnotations($className . '._c'); 156 | // Aggregate all methods annotations 157 | $methodAnnotations = $this->retrieveAnnotations($className . '._m'); 158 | // Aggregate all properties annotations 159 | $propertyAnnotations = $this->retrieveAnnotations($className . '._p'); 160 | $annotations = array_unique(array_merge($classAnnotations, $methodAnnotations, $propertyAnnotations)); 161 | if ($annotations) { 162 | $annotationsAspects = AspectCollector::get('annotations', []); 163 | foreach ($annotationsAspects as $aspect => $rules) { 164 | foreach ($rules as $rule) { 165 | foreach ($annotations as $annotation) { 166 | if ($this->isMatch($rule, $annotation)) { 167 | $proxies[$className][] = $aspect; 168 | } 169 | } 170 | } 171 | } 172 | } 173 | } 174 | return $proxies; 175 | } 176 | 177 | protected function retrieveAnnotations(string $annotationCollectorKey): array 178 | { 179 | $defined = []; 180 | $annotations = AnnotationCollector::get($annotationCollectorKey, []); 181 | 182 | foreach ($annotations as $name => $annotation) { 183 | if (is_object($annotation)) { 184 | $defined[] = $name; 185 | } else { 186 | $defined = array_merge($defined, array_keys($annotation)); 187 | } 188 | } 189 | return $defined; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Aop/ProxyTrait.php: -------------------------------------------------------------------------------- 1 | [], 44 | 'order' => [], 45 | ]; 46 | $reflectParameters = ReflectionManager::reflectMethod($className, $method)->getParameters(); 47 | $leftArgCount = count($args); 48 | foreach ($reflectParameters as $reflectionParameter) { 49 | $arg = $reflectionParameter->isVariadic() ? $args : array_shift($args); 50 | if (! isset($arg) && $leftArgCount <= 0) { 51 | $arg = $reflectionParameter->getDefaultValue(); 52 | } 53 | --$leftArgCount; 54 | $map['keys'][$reflectionParameter->getName()] = $arg; 55 | $map['order'][] = $reflectionParameter->getName(); 56 | } 57 | return $map; 58 | } 59 | 60 | protected static function handleAround(ProceedingJoinPoint $proceedingJoinPoint) 61 | { 62 | $className = $proceedingJoinPoint->className; 63 | $methodName = $proceedingJoinPoint->methodName; 64 | if (! AspectManager::has($className, $methodName)) { 65 | AspectManager::set($className, $methodName, []); 66 | $aspects = array_unique(array_merge(static::getClassesAspects($className, $methodName), static::getAnnotationAspects($className, $methodName))); 67 | $queue = new SplPriorityQueue(); 68 | foreach ($aspects as $aspect) { 69 | $queue->insert($aspect, AspectCollector::getPriority($aspect)); 70 | } 71 | while ($queue->valid()) { 72 | AspectManager::insert($className, $methodName, $queue->current()); 73 | $queue->next(); 74 | } 75 | 76 | unset($aspects, $queue); 77 | } 78 | 79 | if (empty(AspectManager::get($className, $methodName))) { 80 | return $proceedingJoinPoint->processOriginalMethod(); 81 | } 82 | 83 | return static::makePipeline()->via('process') 84 | ->through(AspectManager::get($className, $methodName)) 85 | ->send($proceedingJoinPoint) 86 | ->then(function (ProceedingJoinPoint $proceedingJoinPoint) { 87 | return $proceedingJoinPoint->processOriginalMethod(); 88 | }); 89 | } 90 | 91 | protected static function makePipeline(): Pipeline 92 | { 93 | $container = ApplicationContext::getContainer(); 94 | if (method_exists($container, 'make')) { 95 | $pipeline = $container->make(Pipeline::class); 96 | } else { 97 | $pipeline = new Pipeline($container); 98 | } 99 | return $pipeline; 100 | } 101 | 102 | protected static function getClassesAspects(string $className, string $method): array 103 | { 104 | $aspects = AspectCollector::get('classes', []); 105 | $matchedAspect = []; 106 | foreach ($aspects as $aspect => $rules) { 107 | foreach ($rules as $rule) { 108 | if (Aspect::isMatch($className, $method, $rule)) { 109 | $matchedAspect[] = $aspect; 110 | break; 111 | } 112 | } 113 | } 114 | // The matched aspects maybe have duplicate aspect, should unique it when use it. 115 | return $matchedAspect; 116 | } 117 | 118 | protected static function getAnnotationAspects(string $className, string $method): array 119 | { 120 | $matchedAspect = []; 121 | 122 | $classAnnotations = AnnotationCollector::get($className . '._c', []); 123 | $methodAnnotations = AnnotationCollector::get($className . '._m.' . $method, []); 124 | $annotations = array_unique(array_merge(array_keys($classAnnotations), array_keys($methodAnnotations))); 125 | if (! $annotations) { 126 | return $matchedAspect; 127 | } 128 | 129 | $aspects = AspectCollector::get('annotations', []); 130 | foreach ($aspects as $aspect => $rules) { 131 | foreach ($rules as $rule) { 132 | foreach ($annotations as $annotation) { 133 | if (str_contains($rule, '*')) { 134 | $preg = str_replace(['*', '\\'], ['.*', '\\\\'], $rule); 135 | $pattern = "/^{$preg}$/"; 136 | if (! preg_match($pattern, $annotation)) { 137 | continue; 138 | } 139 | } elseif ($rule !== $annotation) { 140 | continue; 141 | } 142 | $matchedAspect[] = $aspect; 143 | } 144 | } 145 | } 146 | // The matched aspects maybe have duplicate aspect, should unique it when use it. 147 | return $matchedAspect; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Aop/RegisterInjectPropertyHandler.php: -------------------------------------------------------------------------------- 1 | has($annotation->value)) { 41 | $reflectionProperty->setValue($object, $container->get($annotation->value)); 42 | } elseif ($annotation->required) { 43 | throw new NotFoundException("No entry or class found for '{$annotation->value}'"); 44 | } 45 | } catch (Throwable $throwable) { 46 | if ($annotation->required) { 47 | throw $throwable; 48 | } 49 | } 50 | } 51 | }); 52 | 53 | static::$registered = true; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Aop/RewriteCollection.php: -------------------------------------------------------------------------------- 1 | methods[] = $method; 53 | } else { 54 | $preg = str_replace(['*', '\\'], ['.*', '\\\\'], $method); 55 | $this->pattern[] = "/^{$preg}$/"; 56 | } 57 | } 58 | 59 | return $this; 60 | } 61 | 62 | public function shouldRewrite(string $method): bool 63 | { 64 | if ($this->level === self::CLASS_LEVEL) { 65 | if (in_array($method, $this->shouldNotRewriteMethods)) { 66 | return false; 67 | } 68 | return true; 69 | } 70 | 71 | if (in_array($method, $this->methods)) { 72 | return true; 73 | } 74 | 75 | foreach ($this->pattern as $pattern) { 76 | if (preg_match($pattern, $method)) { 77 | return true; 78 | } 79 | } 80 | 81 | return false; 82 | } 83 | 84 | public function setLevel(int $level): self 85 | { 86 | $this->level = $level; 87 | return $this; 88 | } 89 | 90 | public function getLevel(): int 91 | { 92 | return $this->level; 93 | } 94 | 95 | public function getMethods(): array 96 | { 97 | return $this->methods; 98 | } 99 | 100 | public function getClass(): string 101 | { 102 | return $this->class; 103 | } 104 | 105 | /** 106 | * Return the methods that should not rewrite. 107 | */ 108 | public function getShouldNotRewriteMethods(): array 109 | { 110 | return $this->shouldNotRewriteMethods; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Aop/VisitorMetadata.php: -------------------------------------------------------------------------------- 1 | addClassMap($config->getClassMap()); 53 | 54 | $scanner = new Scanner($config, $handler); 55 | $composerLoader->addClassMap( 56 | $scanner->scan($composerLoader->getClassMap(), $proxyFileDirPath) 57 | ); 58 | 59 | // Initialize Lazy Loader. This will prepend LazyLoader to the top of autoload queue. 60 | LazyLoader::bootstrap($configDir); 61 | } 62 | 63 | /** 64 | * @see DotenvManager::load() 65 | * @deprecated use DotenvManager instead 66 | */ 67 | protected static function loadDotenv(): void 68 | { 69 | $repository = RepositoryBuilder::createWithNoAdapters() 70 | ->addAdapter(Adapter\PutenvAdapter::class) 71 | ->immutable() 72 | ->make(); 73 | 74 | Dotenv::create($repository, [BASE_PATH])->load(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ClosureDefinitionCollector.php: -------------------------------------------------------------------------------- 1 | getParameters(); 30 | 31 | $definitions = $this->getDefinitionsFromParameters($parameters); 32 | static::set($key, $definitions); 33 | return $definitions; 34 | } 35 | 36 | public function getReturnType(Closure $closure): ReflectionType 37 | { 38 | $key = spl_object_hash($closure) . '@return'; 39 | if (static::has($key)) { 40 | return static::get($key); 41 | } 42 | $returnType = (new ReflectionFunction($closure))->getReturnType(); 43 | $type = $this->createType('', $returnType, $returnType ? $returnType->allowsNull() : true); 44 | static::set($key, $type); 45 | return $type; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ClosureDefinitionCollectorInterface.php: -------------------------------------------------------------------------------- 1 | [ 41 | MethodDefinitionCollectorInterface::class => MethodDefinitionCollector::class, 42 | ClosureDefinitionCollectorInterface::class => ClosureDefinitionCollector::class, 43 | ], 44 | 'aspects' => [ 45 | InjectAspect::class, 46 | ], 47 | 'annotations' => [ 48 | 'scan' => [ 49 | 'paths' => [ 50 | __DIR__, 51 | ], 52 | 'collectors' => [ 53 | AnnotationCollector::class, 54 | AspectCollector::class, 55 | ], 56 | ], 57 | 'ignore_annotations' => [ 58 | 'mixin', 59 | ], 60 | ], 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Container.php: -------------------------------------------------------------------------------- 1 | definitionResolver = new ResolverDispatcher($this); 43 | // Auto-register the container. 44 | $this->resolvedEntries = [ 45 | self::class => $this, 46 | PsrContainerInterface::class => $this, 47 | HyperfContainerInterface::class => $this, 48 | ]; 49 | } 50 | 51 | /** 52 | * @internal 53 | */ 54 | public function make(string $name, array $parameters = []) 55 | { 56 | $definition = $this->getDefinition($name); 57 | 58 | if (! $definition) { 59 | throw new NotFoundException("No entry or class found for '{$name}'"); 60 | } 61 | 62 | return $this->resolveDefinition($definition, $parameters); 63 | } 64 | 65 | /** 66 | * @internal 67 | * @param mixed $entry 68 | */ 69 | public function set(string $name, $entry): void 70 | { 71 | $this->resolvedEntries[$name] = $entry; 72 | } 73 | 74 | /** 75 | * @internal 76 | */ 77 | public function unbind(string $name): void 78 | { 79 | if ($this->has($name)) { 80 | unset($this->resolvedEntries[$name]); 81 | } 82 | } 83 | 84 | /** 85 | * @internal 86 | * @param mixed $definition 87 | */ 88 | public function define(string $name, $definition): void 89 | { 90 | $this->setDefinition($name, $definition); 91 | } 92 | 93 | /** 94 | * @internal 95 | * @param mixed $id 96 | */ 97 | public function get($id) 98 | { 99 | // If the entry is already resolved we return it 100 | if (isset($this->resolvedEntries[$id]) || array_key_exists($id, $this->resolvedEntries)) { 101 | return $this->resolvedEntries[$id]; 102 | } 103 | return $this->resolvedEntries[$id] = $this->make($id); 104 | } 105 | 106 | /** 107 | * @internal 108 | * @param mixed $id 109 | */ 110 | public function has($id): bool 111 | { 112 | if (! is_string($id)) { 113 | throw new InvalidArgumentException(sprintf('The name parameter must be of type string, %s given', is_object($id) ? get_class($id) : gettype($id))); 114 | } 115 | 116 | if (array_key_exists($id, $this->resolvedEntries)) { 117 | return true; 118 | } 119 | 120 | $definition = $this->getDefinition($id); 121 | if ($definition === null) { 122 | return false; 123 | } 124 | 125 | if ($definition instanceof ObjectDefinition) { 126 | return $definition->isInstantiable(); 127 | } 128 | 129 | return true; 130 | } 131 | 132 | /** 133 | * @param array|callable|string $definition 134 | */ 135 | protected function setDefinition(string $name, $definition): void 136 | { 137 | // Clear existing entry if it exists 138 | if (array_key_exists($name, $this->resolvedEntries)) { 139 | unset($this->resolvedEntries[$name]); 140 | } 141 | $this->fetchedDefinitions = []; // Completely clear this local cache 142 | 143 | $this->definitionSource->addDefinition($name, $definition); 144 | } 145 | 146 | protected function getDefinition(string $name): ?DefinitionInterface 147 | { 148 | // Local cache that avoids fetching the same definition twice 149 | if (! array_key_exists($name, $this->fetchedDefinitions)) { 150 | $this->fetchedDefinitions[$name] = $this->definitionSource->getDefinition($name); 151 | } 152 | 153 | return $this->fetchedDefinitions[$name]; 154 | } 155 | 156 | /** 157 | * Resolves a definition. 158 | */ 159 | protected function resolveDefinition(DefinitionInterface $definition, array $parameters = []) 160 | { 161 | return $this->definitionResolver->resolve($definition, $parameters); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Definition/DefinitionInterface.php: -------------------------------------------------------------------------------- 1 | source = $this->normalizeSource($source); 32 | } 33 | 34 | /** 35 | * Returns the DI definition for the entry name. 36 | */ 37 | public function getDefinition(string $name): ?DefinitionInterface 38 | { 39 | return $this->source[$name] ??= $this->autowire($name); 40 | } 41 | 42 | /** 43 | * @return array definitions indexed by their name 44 | */ 45 | public function getDefinitions(): array 46 | { 47 | return $this->source; 48 | } 49 | 50 | /** 51 | * @param array|callable|string $definition 52 | */ 53 | public function addDefinition(string $name, $definition): static 54 | { 55 | $this->source[$name] = $this->normalizeDefinition($name, $definition); 56 | return $this; 57 | } 58 | 59 | public function clearDefinitions(): void 60 | { 61 | $this->source = []; 62 | } 63 | 64 | /** 65 | * Read the type-hinting from the parameters of the function. 66 | */ 67 | protected function getParametersDefinition(ReflectionFunctionAbstract $constructor): array 68 | { 69 | $parameters = []; 70 | 71 | foreach ($constructor->getParameters() as $index => $parameter) { 72 | // Skip optional parameters. 73 | if ($parameter->isOptional()) { 74 | continue; 75 | } 76 | 77 | $parameterType = $parameter->getType(); 78 | if ($parameterType instanceof ReflectionNamedType && ! $parameterType->isBuiltin()) { 79 | $parameters[$index] = new Reference($parameterType->getName()); 80 | } 81 | } 82 | 83 | return $parameters; 84 | } 85 | 86 | /** 87 | * Normalize the user definition source to a standard definition source. 88 | */ 89 | protected function normalizeSource(array $source): array 90 | { 91 | $definitions = []; 92 | foreach ($source as $identifier => $definition) { 93 | $normalizedDefinition = $this->normalizeDefinition($identifier, $definition); 94 | if (! is_null($normalizedDefinition)) { 95 | $definitions[$identifier] = $normalizedDefinition; 96 | } 97 | } 98 | return $definitions; 99 | } 100 | 101 | /** 102 | * @param array|callable|string $definition 103 | */ 104 | protected function normalizeDefinition(string $identifier, $definition): ?DefinitionInterface 105 | { 106 | if ($definition instanceof PriorityDefinition) { 107 | $definition = $definition->getDefinition(); 108 | } 109 | 110 | if (is_string($definition) && class_exists($definition)) { 111 | if (method_exists($definition, '__invoke')) { 112 | return new FactoryDefinition($identifier, $definition, []); 113 | } 114 | return $this->autowire($identifier, new ObjectDefinition($identifier, $definition)); 115 | } 116 | 117 | if (is_callable($definition)) { 118 | return new FactoryDefinition($identifier, $definition, []); 119 | } 120 | 121 | return null; 122 | } 123 | 124 | protected function autowire(string $name, ?ObjectDefinition $definition = null): ?ObjectDefinition 125 | { 126 | $className = $definition ? $definition->getClassName() : $name; 127 | if (! class_exists($className) && ! interface_exists($className)) { 128 | return $definition; 129 | } 130 | 131 | $definition = $definition ?: new ObjectDefinition($name); 132 | 133 | /** 134 | * Constructor. 135 | */ 136 | $class = ReflectionManager::reflectClass($className); 137 | $constructor = $class->getConstructor(); 138 | if ($constructor && $constructor->isPublic()) { 139 | $constructorInjection = new MethodInjection('__construct', $this->getParametersDefinition($constructor)); 140 | $definition->completeConstructorInjection($constructorInjection); 141 | } 142 | 143 | return $definition; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Definition/DefinitionSourceFactory.php: -------------------------------------------------------------------------------- 1 | name; 34 | } 35 | 36 | public function setName(string $name): self 37 | { 38 | $this->name = $name; 39 | return $this; 40 | } 41 | 42 | public function getFactory(): callable|string 43 | { 44 | return $this->factory; 45 | } 46 | 47 | public function getParameters(): array 48 | { 49 | return $this->parameters; 50 | } 51 | 52 | /** 53 | * Determine if the definition need to transfer to a proxy class. 54 | */ 55 | public function isNeedProxy(): bool 56 | { 57 | return $this->needProxy; 58 | } 59 | 60 | public function setNeedProxy($needProxy): self 61 | { 62 | $this->needProxy = $needProxy; 63 | return $this; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Definition/MethodInjection.php: -------------------------------------------------------------------------------- 1 | methodName); 24 | } 25 | 26 | public function getName(): string 27 | { 28 | return ''; 29 | } 30 | 31 | public function setName(string $name) 32 | { 33 | // The name does not matter for method injections, so do nothing. 34 | } 35 | 36 | public function getParameters(): array 37 | { 38 | return $this->parameters; 39 | } 40 | 41 | public function merge(self $definition) 42 | { 43 | // In case of conflicts, the current definition prevails. 44 | $this->parameters = $this->parameters + $definition->parameters; 45 | } 46 | 47 | /** 48 | * Reset the target should be resolved. 49 | * If it is the FactoryDefinition, then the target means $factory property, 50 | * If it is the ObjectDefinition, then the target means $className property. 51 | */ 52 | public function setTarget(string $value) 53 | { 54 | $this->methodName = $value; 55 | } 56 | 57 | /** 58 | * Determine if the definition need to transfer to a proxy class. 59 | */ 60 | public function isNeedProxy(): bool 61 | { 62 | // Method injection does not has proxy. 63 | return false; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Definition/ObjectDefinition.php: -------------------------------------------------------------------------------- 1 | setClassName($className ?? $name); 30 | } 31 | 32 | public function __toString(): string 33 | { 34 | return sprintf('Object[%s]', $this->getClassName()); 35 | } 36 | 37 | public function getName(): string 38 | { 39 | return $this->name; 40 | } 41 | 42 | public function setName(string $name): self 43 | { 44 | $this->name = $name; 45 | return $this; 46 | } 47 | 48 | public function setClassName(?string $className = null): void 49 | { 50 | $this->className = $className; 51 | 52 | $this->updateStatusCache(); 53 | } 54 | 55 | public function getClassName(): string 56 | { 57 | return $this->className ?? $this->name; 58 | } 59 | 60 | public function isClassExists(): bool 61 | { 62 | return $this->classExists; 63 | } 64 | 65 | public function isInstantiable(): bool 66 | { 67 | return $this->instantiable; 68 | } 69 | 70 | /** 71 | * @return null|MethodInjection 72 | */ 73 | public function getConstructorInjection() 74 | { 75 | return $this->constructorInjection; 76 | } 77 | 78 | public function setConstructorInjection(MethodInjection $injection): self 79 | { 80 | $this->constructorInjection = $injection; 81 | return $this; 82 | } 83 | 84 | public function completeConstructorInjection(MethodInjection $injection): void 85 | { 86 | if ($this->constructorInjection !== null) { 87 | // Merge 88 | $this->constructorInjection->merge($injection); 89 | } else { 90 | // Set 91 | $this->constructorInjection = $injection; 92 | } 93 | } 94 | 95 | private function updateStatusCache(): void 96 | { 97 | $className = $this->getClassName(); 98 | 99 | $this->classExists = class_exists($className) || interface_exists($className); 100 | 101 | if (! $this->classExists) { 102 | $this->instantiable = false; 103 | return; 104 | } 105 | 106 | $this->instantiable = ReflectionManager::reflectClass($className)->isInstantiable(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Definition/PriorityDefinition.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | protected array $dependencies = []; 21 | 22 | public function __construct(string $class, int $priority = 0) 23 | { 24 | $this->dependencies[$class] = $priority; 25 | } 26 | 27 | public function getDependencies(): array 28 | { 29 | return $this->dependencies; 30 | } 31 | 32 | public function merge(PriorityDefinition $factory): static 33 | { 34 | foreach ($factory->getDependencies() as $class => $priority) { 35 | if (isset($this->dependencies[$class]) && $priority <= $this->dependencies[$class]) { 36 | continue; 37 | } 38 | 39 | $this->dependencies[$class] = $priority; 40 | } 41 | 42 | return $this; 43 | } 44 | 45 | public function getDefinition(): string 46 | { 47 | $dependencies = array_flip($this->dependencies); 48 | ksort($dependencies); 49 | return array_pop($dependencies); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Definition/PropertyDefinition.php: -------------------------------------------------------------------------------- 1 | propertyName; 42 | } 43 | 44 | /** 45 | * Set the name of the entry in the container. 46 | */ 47 | public function setName(string $name) 48 | { 49 | $this->propertyName = $name; 50 | } 51 | 52 | public function getValue() 53 | { 54 | return $this->value; 55 | } 56 | 57 | public function getClassName(): ?string 58 | { 59 | return $this->className; 60 | } 61 | 62 | /** 63 | * Determine if the definition need to transfer to a proxy class. 64 | */ 65 | public function isNeedProxy(): bool 66 | { 67 | return false; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Definition/PropertyHandlerManager.php: -------------------------------------------------------------------------------- 1 | targetEntryName); 39 | } 40 | 41 | /** 42 | * Returns the name of the entry in the container. 43 | */ 44 | public function getName(): string 45 | { 46 | return $this->name; 47 | } 48 | 49 | /** 50 | * Set the name of the entry in the container. 51 | */ 52 | public function setName(string $name) 53 | { 54 | $this->name = $name; 55 | } 56 | 57 | public function getTargetEntryName(): string 58 | { 59 | return $this->targetEntryName; 60 | } 61 | 62 | public function resolve(ContainerInterface $container) 63 | { 64 | return $container->get($this->getTargetEntryName()); 65 | } 66 | 67 | public function isResolvable(ContainerInterface $container): bool 68 | { 69 | return $container->has($this->getTargetEntryName()); 70 | } 71 | 72 | /** 73 | * Determine if the definition need to transfer to a proxy class. 74 | */ 75 | public function isNeedProxy(): bool 76 | { 77 | return $this->needProxy; 78 | } 79 | 80 | public function setNeedProxy($needProxy): self 81 | { 82 | $this->needProxy = $needProxy; 83 | return $this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Definition/SelfResolvingDefinitionInterface.php: -------------------------------------------------------------------------------- 1 | sealed) { 26 | return; 27 | } 28 | 29 | if (in_array($name, $this->list)) { 30 | $this->sealed = true; 31 | } 32 | 33 | $this->updateListAndMessage($name); 34 | } 35 | 36 | private function updateListAndMessage(string $name) 37 | { 38 | array_unshift($this->list, $name); 39 | $listAsString = implode(' -> ', $this->list); 40 | $this->message = "depth limit reached due to the following dependencies: {$listAsString}"; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Exception/ConflictAnnotationException.php: -------------------------------------------------------------------------------- 1 | factory = new BuilderFactory(); 48 | $this->builder = $this->factory; 49 | } 50 | 51 | abstract public function addClassRelationship(): AbstractLazyProxyBuilder; 52 | 53 | public function addClassBoilerplate(string $proxyClassName, string $originalClassName): AbstractLazyProxyBuilder 54 | { 55 | $namespace = join('\\', array_slice(explode('\\', $proxyClassName), 0, -1)); 56 | $this->namespace = $namespace; 57 | $this->proxyClassName = $proxyClassName; 58 | $this->originalClassName = $originalClassName; 59 | $this->builder = $this->factory->class(class_basename($proxyClassName)) 60 | ->addStmt(new ClassConst([new Const_('PROXY_TARGET', new String_($originalClassName))])) 61 | ->addStmt($this->factory->useTrait('\Hyperf\Di\LazyLoader\LazyProxyTrait')) 62 | ->setDocComment("/** 63 | * Be careful: This is a lazy proxy, not the real {$originalClassName} from container. 64 | * 65 | * {@inheritdoc} 66 | */"); 67 | return $this; 68 | } 69 | 70 | public function addNodes(array $nodes): AbstractLazyProxyBuilder 71 | { 72 | foreach ($nodes as $stmt) { 73 | $this->builder = $this->builder->addStmt($stmt); 74 | } 75 | return $this; 76 | } 77 | 78 | public function getNode(): Node 79 | { 80 | if ($this->namespace) { 81 | return $this->factory 82 | ->namespace($this->namespace) 83 | ->addStmt($this->builder) 84 | ->getNode(); 85 | } 86 | return $this->builder->getNode(); 87 | } 88 | 89 | public function getNamespace(): string 90 | { 91 | return $this->namespace; 92 | } 93 | 94 | public function getProxyClassName(): string 95 | { 96 | return $this->proxyClassName; 97 | } 98 | 99 | public function getOriginalClassName(): string 100 | { 101 | return $this->originalClassName; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/LazyLoader/ClassLazyProxyBuilder.php: -------------------------------------------------------------------------------- 1 | originalClassName, '\\')) { 20 | $originalClassName = '\\' . $this->originalClassName; 21 | } else { 22 | $originalClassName = $this->originalClassName; 23 | } 24 | $this->builder = $this->builder->extend($originalClassName); 25 | return $this; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/LazyLoader/FallbackLazyProxyBuilder.php: -------------------------------------------------------------------------------- 1 | originalClassName, '\\')) { 20 | $originalClassName = '\\' . $this->originalClassName; 21 | } else { 22 | $originalClassName = $this->originalClassName; 23 | } 24 | $this->builder = $this->builder->implement($originalClassName); 25 | return $this; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/LazyLoader/LazyLoader.php: -------------------------------------------------------------------------------- 1 | register(); 43 | } 44 | 45 | /** 46 | * Get or create the singleton lazy loader instance. 47 | */ 48 | public static function bootstrap(string $configDir): LazyLoader 49 | { 50 | if (static::$instance) { 51 | return static::$instance; 52 | } 53 | $path = $configDir . self::CONFIG_FILE_NAME; 54 | 55 | $config = []; 56 | if (file_exists($path)) { 57 | $config = include $path; 58 | } 59 | return static::$instance = new static($config); 60 | } 61 | 62 | /** 63 | * Load a class proxy if it is registered. 64 | * 65 | * @return null|bool 66 | */ 67 | public function load(string $proxy) 68 | { 69 | if (array_key_exists($proxy, $this->config) || str_starts_with($proxy, 'HyperfLazy\\')) { 70 | $this->loadProxy($proxy); 71 | return true; 72 | } 73 | return null; 74 | } 75 | 76 | /** 77 | * Register the loader on the auto-loader stack. 78 | */ 79 | protected function register(): void 80 | { 81 | if (! $this->registered) { 82 | $this->prependToLoaderStack(); 83 | $this->registered = true; 84 | } 85 | } 86 | 87 | /** 88 | * Load a real-time facade for the given proxy. 89 | */ 90 | protected function loadProxy(string $proxy) 91 | { 92 | require_once $this->ensureProxyExists($proxy); 93 | } 94 | 95 | /** 96 | * Ensure that the given proxy has an existing real-time facade class. 97 | */ 98 | protected function ensureProxyExists(string $proxy): string 99 | { 100 | $dir = BASE_PATH . '/runtime/container/proxy/'; 101 | if (! file_exists($dir)) { 102 | mkdir($dir, 0755, true); 103 | } 104 | 105 | $code = $this->generatorLazyProxy( 106 | $proxy, 107 | $this->config[$proxy] ?? Str::after($proxy, 'HyperfLazy\\') 108 | ); 109 | 110 | $path = str_replace('\\', '_', $dir . $proxy . '_' . crc32($code) . '.php'); 111 | $key = md5($path); 112 | // If the proxy file does not exist, then try to acquire the coroutine lock. 113 | if (! file_exists($path) && CoLocker::lock($key)) { 114 | $targetPath = $path . '.' . uniqid(); 115 | file_put_contents($targetPath, $code); 116 | rename($targetPath, $path); 117 | CoLocker::unlock($key); 118 | } 119 | return $path; 120 | } 121 | 122 | /** 123 | * Format the lazy proxy with the proper namespace and class. 124 | */ 125 | protected function generatorLazyProxy(string $proxy, string $target): string 126 | { 127 | $targetReflection = new ReflectionClass($target); 128 | if ($this->isUnsupportedReflectionType($targetReflection)) { 129 | $builder = new FallbackLazyProxyBuilder(); 130 | return $this->buildNewCode($builder, $proxy, $targetReflection); 131 | } 132 | if ($targetReflection->isInterface()) { 133 | $builder = new InterfaceLazyProxyBuilder(); 134 | return $this->buildNewCode($builder, $proxy, $targetReflection); 135 | } 136 | $builder = new ClassLazyProxyBuilder(); 137 | return $this->buildNewCode($builder, $proxy, $targetReflection); 138 | } 139 | 140 | /** 141 | * Prepend the load method to the auto-loader stack. 142 | */ 143 | protected function prependToLoaderStack(): void 144 | { 145 | /** @var callable(string): void */ 146 | $load = [$this, 'load']; 147 | spl_autoload_register($load, true, true); 148 | } 149 | 150 | /** 151 | * These conditions are really hard to proxy via inheritance. 152 | * Luckily these conditions are very rarely met. 153 | * 154 | * TODO: implement some of them. 155 | * 156 | * @param ReflectionClass $targetReflection [description] 157 | * @return bool [description] 158 | */ 159 | private function isUnsupportedReflectionType(ReflectionClass $targetReflection): bool 160 | { 161 | return $targetReflection->isFinal(); 162 | } 163 | 164 | private function buildNewCode(AbstractLazyProxyBuilder $builder, string $proxy, ReflectionClass $reflectionClass): string 165 | { 166 | $target = $reflectionClass->getName(); 167 | $nodes = PhpParser::getInstance()->getNodesFromReflectionClass($reflectionClass); 168 | $builder->addClassBoilerplate($proxy, $target); 169 | $builder->addClassRelationship(); 170 | $traverser = new NodeTraverser(); 171 | $methods = PhpParser::getInstance()->getAllMethodsFromStmts($nodes); 172 | $visitor = new PublicMethodVisitor($methods, $builder->getOriginalClassName()); 173 | $traverser->addVisitor(new NameResolver()); 174 | $traverser->addVisitor($visitor); 175 | $traverser->traverse($nodes); 176 | $builder->addNodes($visitor->nodes); 177 | $prettyPrinter = new Standard(); 178 | return $prettyPrinter->prettyPrintFile([$builder->getNode()]); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/LazyLoader/LazyProxyTrait.php: -------------------------------------------------------------------------------- 1 | {$var}); 24 | } 25 | } 26 | 27 | public function __call($method, $arguments) 28 | { 29 | $obj = $this->getInstance(); 30 | return call_user_func([$obj, $method], ...$arguments); 31 | } 32 | 33 | public function __get($name) 34 | { 35 | return $this->getInstance()->{$name}; 36 | } 37 | 38 | public function __set($name, $value) 39 | { 40 | $this->getInstance()->{$name} = $value; 41 | } 42 | 43 | public function __isset($name) 44 | { 45 | return isset($this->getInstance()->{$name}); 46 | } 47 | 48 | public function __unset($name) 49 | { 50 | unset($this->getInstance()->{$name}); 51 | } 52 | 53 | public function __wakeup() 54 | { 55 | $vars = get_object_vars($this); 56 | foreach (array_keys($vars) as $var) { 57 | unset($this->{$var}); 58 | } 59 | } 60 | 61 | /** 62 | * Return The Proxy Target. 63 | * @return mixed 64 | */ 65 | public function getInstance() 66 | { 67 | return ApplicationContext::getContainer()->get(self::PROXY_TARGET); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/LazyLoader/PublicMethodVisitor.php: -------------------------------------------------------------------------------- 1 | originalClassName = $originalClassName; 48 | } 49 | 50 | public function enterNode(Node $node) 51 | { 52 | if ($node instanceof Interface_ || $node instanceof Class_) { 53 | $node->stmts = $this->stmts; 54 | } 55 | if ($node instanceof ClassMethod) { 56 | $methodCall = new MethodCall( 57 | new Variable('this'), 58 | '__call', 59 | [ 60 | new Node\Arg(new MagicConstFunction()), 61 | new Node\Arg(new FuncCall(new Name('func_get_args'))), 62 | ] 63 | ); 64 | $shouldReturn = true; 65 | if ($node->getReturnType() && method_exists($node->getReturnType(), 'toString')) { 66 | if ($node->getReturnType()->toString() === 'self') { 67 | $node->returnType = new Name($this->originalClassName); 68 | } 69 | if ($node->getReturnType()->toString() === 'void') { 70 | $shouldReturn = false; 71 | $methodCall = new Expression($methodCall); 72 | } 73 | } 74 | $shouldReturn && $methodCall = new Return_($methodCall); 75 | $node->stmts = [ 76 | $methodCall, 77 | ]; 78 | $node->flags &= ~Class_::MODIFIER_ABSTRACT; 79 | } 80 | return null; 81 | } 82 | 83 | public function leaveNode(Node $node) 84 | { 85 | if ($node instanceof ClassMethod && $node->isPublic() && ! $node->isMagic()) { 86 | $this->nodes[] = $node; 87 | } 88 | return null; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/MetadataCacheCollector.php: -------------------------------------------------------------------------------- 1 | collectors = array_unique(array_merge( 24 | $this->collectors, 25 | [$collector] 26 | )); 27 | } 28 | 29 | public function clear() 30 | { 31 | $this->collectors = []; 32 | } 33 | 34 | public function serialize(): string 35 | { 36 | $metadata = []; 37 | foreach ($this->collectors as $collector) { 38 | if (is_string($collector) && method_exists($collector, 'serialize')) { 39 | $metadata[$collector] = $collector::serialize(); 40 | } 41 | } 42 | 43 | return json_encode($metadata); 44 | } 45 | 46 | public function unserialize($serialized): void 47 | { 48 | $metadatas = json_decode($serialized, true) ?? []; 49 | $collectors = []; 50 | foreach ($metadatas as $collector => $metadata) { 51 | if (method_exists($collector, 'deserialize')) { 52 | $collector::deserialize($metadata); 53 | $collectors[] = $collector; 54 | } 55 | } 56 | 57 | $this->collectors = $collectors; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/MetadataCollector.php: -------------------------------------------------------------------------------- 1 | getParameters(); 31 | $definitions = []; 32 | foreach ($parameters as $parameter) { 33 | $type = $parameter->getType()->getName(); 34 | switch ($type) { 35 | case 'int': 36 | case 'float': 37 | case 'string': 38 | case 'array': 39 | case 'bool': 40 | $definition = [ 41 | 'type' => $type, 42 | 'name' => $parameter->getName(), 43 | 'ref' => '', 44 | 'allowsNull' => $parameter->allowsNull(), 45 | ]; 46 | if ($parameter->isDefaultValueAvailable()) { 47 | $definition['defaultValue'] = $parameter->getDefaultValue(); 48 | } 49 | $definitions[] = $definition; 50 | break; 51 | default: 52 | // Object 53 | $definitions[] = [ 54 | 'type' => 'object', 55 | 'name' => $parameter->getName(), 56 | 'ref' => $type ?? null, 57 | 'allowsNull' => $parameter->allowsNull(), 58 | ]; 59 | break; 60 | } 61 | } 62 | static::set($key, $definitions); 63 | return $definitions; 64 | } 65 | 66 | public function getParameters(string $class, string $method): array 67 | { 68 | $key = $class . '::' . $method . '@params'; 69 | if (static::has($key)) { 70 | return static::get($key); 71 | } 72 | $parameters = ReflectionManager::reflectClass($class)->getMethod($method)->getParameters(); 73 | 74 | $definitions = $this->getDefinitionsFromParameters($parameters); 75 | static::set($key, $definitions); 76 | return $definitions; 77 | } 78 | 79 | public function getReturnType(string $class, string $method): ReflectionType 80 | { 81 | $key = $class . '::' . $method . '@return'; 82 | if (static::has($key)) { 83 | return static::get($key); 84 | } 85 | $returnType = ReflectionManager::reflectClass($class)->getMethod($method)->getReturnType(); 86 | $type = $this->createType('', $returnType, $returnType ? $returnType->allowsNull() : true); 87 | static::set($key, $type); 88 | return $type; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/MethodDefinitionCollectorInterface.php: -------------------------------------------------------------------------------- 1 | getMethod($method); 49 | } 50 | return static::$container['method'][$key]; 51 | } 52 | 53 | public static function reflectProperty(string $className, string $property): ReflectionProperty 54 | { 55 | $key = $className . '::' . $property; 56 | if (! isset(static::$container['property'][$key])) { 57 | if (! class_exists($className)) { 58 | throw new InvalidArgumentException("Class {$className} not exist"); 59 | } 60 | static::$container['property'][$key] = static::reflectClass($className)->getProperty($property); 61 | } 62 | return static::$container['property'][$key]; 63 | } 64 | 65 | public static function reflectPropertyNames(string $className) 66 | { 67 | $key = $className; 68 | if (! isset(static::$container['property_names'][$key])) { 69 | if (! class_exists($className) && ! interface_exists($className) && ! trait_exists($className)) { 70 | throw new InvalidArgumentException("Class {$className} not exist"); 71 | } 72 | static::$container['property_names'][$key] = value(function () use ($className) { 73 | $properties = static::reflectClass($className)->getProperties(); 74 | $result = []; 75 | foreach ($properties as $property) { 76 | $result[] = $property->getName(); 77 | } 78 | return $result; 79 | }); 80 | } 81 | return static::$container['property_names'][$key]; 82 | } 83 | 84 | public static function clear(?string $key = null): void 85 | { 86 | if ($key === null) { 87 | static::$container = []; 88 | } 89 | } 90 | 91 | public static function getPropertyDefaultValue(ReflectionProperty $property) 92 | { 93 | return method_exists($property, 'getDefaultValue') 94 | ? $property->getDefaultValue() 95 | : $property->getDeclaringClass()->getDefaultProperties()[$property->getName()] ?? null; 96 | } 97 | 98 | public static function getAllClasses(array $paths): array 99 | { 100 | $finder = new Finder(); 101 | $finder->files()->in($paths)->name('*.php'); 102 | 103 | return static::getAllClassesByFinder($finder); 104 | } 105 | 106 | public static function getAllClassesByFinder(Finder $finder): array 107 | { 108 | $parser = new Ast(); 109 | 110 | $reflectionClasses = []; 111 | foreach ($finder as $file) { 112 | try { 113 | $stmts = $parser->parse($file->getContents()); 114 | if (! $className = $parser->parseClassByStmts($stmts)) { 115 | continue; 116 | } 117 | $reflectionClasses[$className] = static::reflectClass($className); 118 | } catch (Throwable $e) { 119 | echo sprintf( 120 | "\033[31m%s\033[0m", 121 | '[ERROR] DI Reflection Manager collecting class reflections failed. ' . PHP_EOL 122 | . "File: {$file->getRealPath()}." . PHP_EOL 123 | . 'Exception: ' . $e->getMessage() 124 | ) . PHP_EOL; 125 | } 126 | } 127 | return $reflectionClasses; 128 | } 129 | 130 | public static function getContainer(): array 131 | { 132 | return self::$container; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/ReflectionType.php: -------------------------------------------------------------------------------- 1 | name; 24 | } 25 | 26 | public function allowsNull(): bool 27 | { 28 | return $this->allowsNull; 29 | } 30 | 31 | public function getMeta(string $key) 32 | { 33 | return $this->metadata[$key] ?? null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Resolver/DepthGuard.php: -------------------------------------------------------------------------------- 1 | depthLimit = $depthLimit; 44 | } 45 | 46 | public function increment() 47 | { 48 | Context::override('di.depth', function ($depth) { 49 | $depth = $depth ?? 0; 50 | if (++$depth > $this->depthLimit) { 51 | throw new CircularDependencyException(); 52 | } 53 | return $depth; 54 | }); 55 | } 56 | 57 | public function decrement() 58 | { 59 | Context::override('di.depth', function ($depth) { 60 | return --$depth; 61 | }); 62 | } 63 | 64 | public function call(string $name, callable $callable) 65 | { 66 | try { 67 | $this->increment(); 68 | return $callable(); 69 | } catch (CircularDependencyException $exception) { 70 | $exception->addDefinitionName($name); 71 | throw $exception; 72 | } finally { 73 | $this->decrement(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Resolver/FactoryResolver.php: -------------------------------------------------------------------------------- 1 | getFactory(); 40 | if (! method_exists($callable, '__invoke')) { 41 | throw new NotCallableException(); 42 | } 43 | if (is_string($callable)) { 44 | $callable = $this->container->get($callable); 45 | } 46 | return $callable($this->container, $parameters); 47 | } catch (NotCallableException $e) { 48 | // Custom error message to help debugging 49 | if (is_string($callable) && class_exists($callable) && method_exists($callable, '__invoke')) { 50 | throw new InvalidDefinitionException(sprintf('Entry "%s" cannot be resolved: factory %s. Invokable classes cannot be automatically resolved if autowiring is disabled on the container, you need to enable autowiring or define the entry manually.', $definition->getName(), $e->getMessage())); 51 | } 52 | 53 | throw new InvalidDefinitionException(sprintf('Entry "%s" cannot be resolved: factory %s', $definition->getName(), $e->getMessage())); 54 | } 55 | } 56 | 57 | /** 58 | * Check if a definition can be resolved. 59 | * 60 | * @param DefinitionInterface $definition object that defines how the value should be obtained 61 | * @param array $parameters optional parameters to use to build the entry 62 | */ 63 | public function isResolvable(DefinitionInterface $definition, array $parameters = []): bool 64 | { 65 | return true; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Resolver/ObjectResolver.php: -------------------------------------------------------------------------------- 1 | parameterResolver = new ParameterResolver($this->definitionResolver); 33 | } 34 | 35 | /** 36 | * Resolve a definition to a value. 37 | * 38 | * @param DefinitionInterface $definition object that defines how the value should be obtained 39 | * @param array $parameters optional parameters to use to build the entry 40 | * @return mixed value obtained from the definition 41 | * @throws InvalidDefinitionException 42 | * @throws DependencyException 43 | */ 44 | public function resolve(DefinitionInterface $definition, array $parameters = []) 45 | { 46 | if (! $definition instanceof ObjectDefinition) { 47 | throw InvalidDefinitionException::create( 48 | $definition, 49 | sprintf('Entry "%s" cannot be resolved: the class is not instanceof ObjectDefinition', $definition->getName()) 50 | ); 51 | } 52 | return $this->createInstance($definition, $parameters); 53 | } 54 | 55 | /** 56 | * Check if a definition can be resolved. 57 | * 58 | * @param ObjectDefinition $definition object that defines how the value should be obtained 59 | * @param array $parameters optional parameters to use to build the entry 60 | */ 61 | public function isResolvable(DefinitionInterface $definition, array $parameters = []): bool 62 | { 63 | return $definition->isInstantiable(); 64 | } 65 | 66 | private function createInstance(ObjectDefinition $definition, array $parameters) 67 | { 68 | // Check that the class is instantiable 69 | if (! $definition->isInstantiable()) { 70 | // Check that the class exists 71 | if (! $definition->isClassExists()) { 72 | throw InvalidDefinitionException::create($definition, sprintf('Entry "%s" cannot be resolved: the class doesn\'t exist', $definition->getName())); 73 | } 74 | 75 | throw InvalidDefinitionException::create($definition, sprintf('Entry "%s" cannot be resolved: the class is not instantiable', $definition->getName())); 76 | } 77 | 78 | $classReflection = null; 79 | try { 80 | $className = $definition->getClassName(); 81 | $classReflection = ReflectionManager::reflectClass($className); 82 | $constructorInjection = $definition->getConstructorInjection(); 83 | 84 | $args = $this->parameterResolver->resolveParameters($constructorInjection, $classReflection->getConstructor(), $parameters); 85 | $object = new $className(...$args); 86 | } catch (NotFoundExceptionInterface $e) { 87 | throw new DependencyException(sprintf('Error while injecting dependencies into %s: %s', $classReflection ? $classReflection->getName() : '', $e->getMessage()), 0, $e); 88 | } catch (InvalidDefinitionException $e) { 89 | throw InvalidDefinitionException::create($definition, sprintf('Entry "%s" cannot be resolved: %s', $definition->getName(), $e->getMessage())); 90 | } 91 | return $object; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Resolver/ParameterResolver.php: -------------------------------------------------------------------------------- 1 | getParameters() : []; 39 | foreach ($method->getParameters() as $index => $parameter) { 40 | if (array_key_exists($parameter->getName(), $parameters)) { 41 | $value = &$parameters[$parameter->getName()]; 42 | } elseif (array_key_exists($index, $parameters)) { 43 | $value = &$parameters[$index]; 44 | } elseif (array_key_exists($index, $definitionParameters)) { 45 | $value = &$definitionParameters[$index]; 46 | } else { 47 | if ($parameter->isDefaultValueAvailable() || $parameter->isOptional()) { 48 | $args[] = $this->getParameterDefaultValue($parameter, $method); 49 | continue; 50 | } 51 | throw new InvalidDefinitionException(sprintf( 52 | 'Parameter $%s of %s has no value defined or guessable', 53 | $parameter->getName(), 54 | $this->getFunctionName($method) 55 | )); 56 | } 57 | 58 | // Nested definitions 59 | if ($value instanceof DefinitionInterface) { 60 | // If the container cannot produce the entry, we can use the default parameter value 61 | if ($parameter->isOptional() && ! $this->definitionResolver->isResolvable($value)) { 62 | $value = $this->getParameterDefaultValue($parameter, $method); 63 | } else { 64 | $value = $this->definitionResolver->resolve($value); 65 | } 66 | } 67 | 68 | $args[] = &$value; 69 | } 70 | 71 | return $args; 72 | } 73 | 74 | private function getParameterDefaultValue(ReflectionParameter $parameter, ReflectionMethod $function) 75 | { 76 | try { 77 | return $parameter->getDefaultValue(); 78 | } catch (ReflectionException) { 79 | throw new InvalidDefinitionException(sprintf( 80 | 'The parameter "%s" of %s has no type defined or guessable. It has a default value, ' 81 | . 'but the default value can\'t be read through Reflection because it is a PHP internal class.', 82 | $parameter->getName(), 83 | $this->getFunctionName($function) 84 | )); 85 | } 86 | } 87 | 88 | private function getFunctionName(ReflectionMethod $method): string 89 | { 90 | return $method->getName() . '()'; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Resolver/ResolverDispatcher.php: -------------------------------------------------------------------------------- 1 | resolve($this->container); 45 | } 46 | 47 | $guard = DepthGuard::getInstance(); 48 | 49 | return $guard->call( 50 | $definition->getName(), 51 | fn () => $this->getDefinitionResolver($definition)->resolve($definition, $parameters) 52 | ); 53 | } 54 | 55 | /** 56 | * Check if a definition can be resolved. 57 | * 58 | * @param DefinitionInterface $definition object that defines how the value should be obtained 59 | * @param array $parameters optional parameters to use to build the entry 60 | */ 61 | public function isResolvable(DefinitionInterface $definition, array $parameters = []): bool 62 | { 63 | if ($definition instanceof SelfResolvingDefinitionInterface) { 64 | return $definition->isResolvable($this->container); 65 | } 66 | 67 | $guard = DepthGuard::getInstance(); 68 | 69 | return $guard->call( 70 | $definition->getName(), 71 | fn () => $this->getDefinitionResolver($definition)->isResolvable($definition, $parameters) 72 | ); 73 | } 74 | 75 | /** 76 | * Returns a resolver capable of handling the given definition. 77 | * 78 | * @throws RuntimeException no definition resolver was found for this type of definition 79 | */ 80 | private function getDefinitionResolver(DefinitionInterface $definition): ResolverInterface 81 | { 82 | return match (true) { 83 | $definition instanceof ObjectDefinition => $this->objectResolver ??= new ObjectResolver($this->container, $this), 84 | $definition instanceof FactoryDefinition => $this->factoryResolver ??= new FactoryResolver($this->container, $this), 85 | default => throw new RuntimeException('No definition resolver was configured for definition of type ' . get_class($definition)), 86 | }; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Resolver/ResolverInterface.php: -------------------------------------------------------------------------------- 1 | bin = $bin; 36 | $this->stub = $stub; 37 | } 38 | 39 | public function scan(): Scanned 40 | { 41 | if (env(static::SCAN_PROC_WORKER)) { 42 | return new Scanned(false); 43 | } 44 | 45 | $proc = proc_open( 46 | [$this->bin, $this->stub], 47 | [0 => STDIN, 1 => ['pipe', 'w'], 2 => ['redirect', 1]], 48 | $pipes, 49 | null, 50 | [static::SCAN_PROC_WORKER => '(true)'] 51 | ); 52 | 53 | $output = ''; 54 | do { 55 | $output .= fread($pipes[1], 8192); 56 | } while (! feof($pipes[1])); 57 | 58 | if (proc_close($proc) !== 0) { 59 | echo $output; 60 | exit(-1); 61 | } 62 | 63 | return new Scanned(true); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ScanHandler/ScanHandlerInterface.php: -------------------------------------------------------------------------------- 1 | scanned; 24 | } 25 | } 26 | --------------------------------------------------------------------------------