Scanner.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Hyperf.
  5. *
  6. * @link https://www.hyperf.io
  7. * @document https://hyperf.wiki
  8. * @contact group@hyperf.io
  9. * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
  10. */
  11. namespace Hyperf\Di\Annotation;
  12. use Hyperf\Config\ProviderConfig;
  13. use Hyperf\Di\Aop\ProxyManager;
  14. use Hyperf\Di\Exception\DirectoryNotExistException;
  15. use Hyperf\Di\MetadataCollector;
  16. use Hyperf\Di\ReflectionManager;
  17. use Hyperf\Di\ScanHandler\ScanHandlerInterface;
  18. use Hyperf\Support\Composer;
  19. use Hyperf\Support\Filesystem\Filesystem;
  20. use ReflectionClass;
  21. class Scanner
  22. {
  23. protected Filesystem $filesystem;
  24. protected string $path = BASE_PATH . '/runtime/container/scan.cache';
  25. public function __construct(protected ScanConfig $scanConfig, protected ScanHandlerInterface $handler)
  26. {
  27. $this->filesystem = new Filesystem();
  28. }
  29. public function collect(AnnotationReader $reader, ReflectionClass $reflection): void
  30. {
  31. $className = $reflection->getName();
  32. if ($path = $this->scanConfig->getClassMap()[$className] ?? null) {
  33. if ($reflection->getFileName() !== $path) {
  34. // When the original class is dynamically replaced, the original class should not be collected.
  35. return;
  36. }
  37. }
  38. // Parse class annotations
  39. foreach ($reader->getAttributes($reflection) as $classAnnotation) {
  40. if ($classAnnotation instanceof AnnotationInterface) {
  41. $classAnnotation->collectClass($className);
  42. }
  43. }
  44. // Parse properties annotations
  45. foreach ($reflection->getProperties() as $property) {
  46. foreach ($reader->getAttributes($property) as $propertyAnnotation) {
  47. if ($propertyAnnotation instanceof AnnotationInterface) {
  48. $propertyAnnotation->collectProperty($className, $property->getName());
  49. }
  50. }
  51. }
  52. // Parse methods annotations
  53. foreach ($reflection->getMethods() as $method) {
  54. foreach ($reader->getAttributes($method) as $methodAnnotation) {
  55. if ($methodAnnotation instanceof AnnotationInterface) {
  56. $methodAnnotation->collectMethod($className, $method->getName());
  57. }
  58. }
  59. }
  60. // Parse class constants annotations
  61. foreach ($reflection->getReflectionConstants() as $classConstant) {
  62. foreach ($reader->getAttributes($classConstant) as $constantAnnotation) {
  63. if ($constantAnnotation instanceof AnnotationInterface) {
  64. $constantAnnotation->collectClassConstant($className, $classConstant->getName());
  65. }
  66. }
  67. }
  68. unset($reflection);
  69. }
  70. public function scan(array $classMap = [], string $proxyDir = ''): array
  71. {
  72. $paths = $this->scanConfig->getPaths();
  73. $collectors = $this->scanConfig->getCollectors();
  74. if (! $paths) {
  75. return [];
  76. }
  77. $lastCacheModified = file_exists($this->path) ? $this->filesystem->lastModified($this->path) : 0;
  78. if ($lastCacheModified > 0 && $this->scanConfig->isCacheable()) {
  79. return $this->deserializeCachedScanData($collectors);
  80. }
  81. $scanned = $this->handler->scan();
  82. if ($scanned->isScanned()) {
  83. return $this->deserializeCachedScanData($collectors);
  84. }
  85. $this->deserializeCachedScanData($collectors);
  86. $annotationReader = new AnnotationReader($this->scanConfig->getIgnoreAnnotations());
  87. $paths = $this->normalizeDir($paths);
  88. $classes = ReflectionManager::getAllClasses($paths);
  89. $this->clearRemovedClasses($collectors, $classes);
  90. $reflectionClassMap = [];
  91. foreach ($classes as $className => $reflectionClass) {
  92. $reflectionClassMap[$className] = $reflectionClass->getFileName();
  93. if ($this->filesystem->lastModified($reflectionClass->getFileName()) >= $lastCacheModified) {
  94. /** @var MetadataCollector $collector */
  95. foreach ($collectors as $collector) {
  96. $collector::clear($className);
  97. }
  98. $this->collect($annotationReader, $reflectionClass);
  99. }
  100. }
  101. $this->loadAspects($lastCacheModified);
  102. $data = [];
  103. /** @var MetadataCollector|string $collector */
  104. foreach ($collectors as $collector) {
  105. $data[$collector] = $collector::serialize();
  106. }
  107. // Get the class map of Composer loader
  108. $classMap = array_merge($reflectionClassMap, $classMap);
  109. $proxyManager = new ProxyManager($classMap, $proxyDir);
  110. $proxies = $proxyManager->getProxies();
  111. $aspectClasses = $proxyManager->getAspectClasses();
  112. $this->putCache($this->path, serialize([$data, $proxies, $aspectClasses]));
  113. exit;
  114. }
  115. /**
  116. * Normalizes given directory names by removing directory not exist.
  117. * @throws DirectoryNotExistException
  118. */
  119. public function normalizeDir(array $paths): array
  120. {
  121. $result = [];
  122. foreach ($paths as $path) {
  123. if (is_dir($path)) {
  124. $result[] = $path;
  125. }
  126. }
  127. if ($paths && ! $result) {
  128. throw new DirectoryNotExistException('The scanned directory does not exist');
  129. }
  130. return $result;
  131. }
  132. protected function deserializeCachedScanData(array $collectors): array
  133. {
  134. if (! file_exists($this->path)) {
  135. return [];
  136. }
  137. [$data, $proxies] = unserialize(file_get_contents($this->path));
  138. foreach ($data as $collector => $deserialized) {
  139. /** @var MetadataCollector $collector */
  140. if (in_array($collector, $collectors)) {
  141. $collector::deserialize($deserialized);
  142. }
  143. }
  144. return $proxies;
  145. }
  146. /**
  147. * @param ReflectionClass[] $reflections
  148. */
  149. protected function clearRemovedClasses(array $collectors, array $reflections): void
  150. {
  151. $path = BASE_PATH . '/runtime/container/classes.cache';
  152. $classes = array_keys($reflections);
  153. $data = [];
  154. if ($this->filesystem->exists($path)) {
  155. $data = unserialize($this->filesystem->get($path));
  156. }
  157. $this->putCache($path, serialize($classes));
  158. $removed = array_diff($data, $classes);
  159. foreach ($removed as $class) {
  160. /** @var MetadataCollector $collector */
  161. foreach ($collectors as $collector) {
  162. $collector::clear($class);
  163. }
  164. }
  165. }
  166. protected function putCache(string $path, $data)
  167. {
  168. if (! $this->filesystem->isDirectory($dir = dirname($path))) {
  169. $this->filesystem->makeDirectory($dir, 0755, true);
  170. }
  171. $this->filesystem->put($path, $data);
  172. }
  173. /**
  174. * Load aspects to AspectCollector by configuration files and ConfigProvider.
  175. */
  176. protected function loadAspects(int $lastCacheModified): void
  177. {
  178. $configDir = $this->scanConfig->getConfigDir();
  179. if (! $configDir) {
  180. return;
  181. }
  182. $aspectsPath = $configDir . '/autoload/aspects.php';
  183. $basePath = $configDir . '/config.php';
  184. $aspects = file_exists($aspectsPath) ? include $aspectsPath : [];
  185. $baseConfig = file_exists($basePath) ? include $basePath : [];
  186. $providerConfig = [];
  187. if (class_exists(ProviderConfig::class)) {
  188. $providerConfig = ProviderConfig::load();
  189. }
  190. if (! isset($aspects) || ! is_array($aspects)) {
  191. $aspects = [];
  192. }
  193. if (! isset($baseConfig['aspects']) || ! is_array($baseConfig['aspects'])) {
  194. $baseConfig['aspects'] = [];
  195. }
  196. if (! isset($providerConfig['aspects']) || ! is_array($providerConfig['aspects'])) {
  197. $providerConfig['aspects'] = [];
  198. }
  199. $aspects = array_merge($providerConfig['aspects'], $baseConfig['aspects'], $aspects);
  200. [$removed, $changed] = $this->getChangedAspects($aspects, $lastCacheModified);
  201. // When the aspect removed from config, it should be removed from AspectCollector.
  202. foreach ($removed as $aspect) {
  203. AspectCollector::clear($aspect);
  204. }
  205. foreach ($aspects as $key => $value) {
  206. if (is_numeric($key)) {
  207. $aspect = $value;
  208. $priority = null;
  209. } else {
  210. $aspect = $key;
  211. $priority = (int) $value;
  212. }
  213. if (! in_array($aspect, $changed)) {
  214. continue;
  215. }
  216. [$instanceClasses, $instanceAnnotations, $instancePriority] = AspectLoader::load($aspect);
  217. $classes = $instanceClasses ?: [];
  218. // Annotations
  219. $annotations = $instanceAnnotations ?: [];
  220. // Priority
  221. $priority = $priority ?: ($instancePriority ?? null);
  222. // Save the metadata to AspectCollector
  223. AspectCollector::setAround($aspect, $classes, $annotations, $priority);
  224. }
  225. }
  226. protected function getChangedAspects(array $aspects, int $lastCacheModified): array
  227. {
  228. $path = BASE_PATH . '/runtime/container/aspects.cache';
  229. $classes = [];
  230. foreach ($aspects as $key => $value) {
  231. if (is_numeric($key)) {
  232. $classes[] = $value;
  233. } else {
  234. $classes[] = $key;
  235. }
  236. }
  237. $data = [];
  238. if ($this->filesystem->exists($path)) {
  239. $data = unserialize($this->filesystem->get($path));
  240. }
  241. $this->putCache($path, serialize($classes));
  242. $diff = array_diff($data, $classes);
  243. $changed = array_diff($classes, $data);
  244. $removed = [];
  245. foreach ($diff as $item) {
  246. $annotation = AnnotationCollector::getClassAnnotation($item, Aspect::class);
  247. if (is_null($annotation)) {
  248. $removed[] = $item;
  249. }
  250. }
  251. foreach ($classes as $class) {
  252. $file = Composer::getLoader()->findFile($class);
  253. if ($file === false) {
  254. echo sprintf('Skip class %s, because it does not exist in composer class loader.', $class) . PHP_EOL;
  255. continue;
  256. }
  257. if ($lastCacheModified <= $this->filesystem->lastModified($file)) {
  258. $changed[] = $class;
  259. }
  260. }
  261. return [
  262. array_values(array_unique($removed)),
  263. array_values(array_unique($changed)),
  264. ];
  265. }
  266. }