LazyLoader.php 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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\LazyLoader;
  12. use Hyperf\CodeParser\PhpParser;
  13. use Hyperf\Coroutine\Locker as CoLocker;
  14. use Hyperf\Stringable\Str;
  15. use PhpParser\NodeTraverser;
  16. use PhpParser\NodeVisitor\NameResolver;
  17. use PhpParser\PrettyPrinter\Standard;
  18. use ReflectionClass;
  19. class LazyLoader
  20. {
  21. public const CONFIG_FILE_NAME = 'lazy_loader.php';
  22. /**
  23. * Indicates if a loader has been registered.
  24. */
  25. protected bool $registered = false;
  26. /**
  27. * The singleton instance of the loader.
  28. */
  29. protected static ?LazyLoader $instance = null;
  30. /**
  31. * @param array $config the Configuration object
  32. */
  33. private function __construct(protected array $config)
  34. {
  35. $this->register();
  36. }
  37. /**
  38. * Get or create the singleton lazy loader instance.
  39. */
  40. public static function bootstrap(string $configDir): LazyLoader
  41. {
  42. if (static::$instance) {
  43. return static::$instance;
  44. }
  45. $path = $configDir . self::CONFIG_FILE_NAME;
  46. $config = [];
  47. if (file_exists($path)) {
  48. $config = include $path;
  49. }
  50. return static::$instance = new static($config);
  51. }
  52. /**
  53. * Load a class proxy if it is registered.
  54. *
  55. * @return null|bool
  56. */
  57. public function load(string $proxy)
  58. {
  59. if (array_key_exists($proxy, $this->config) || str_starts_with($proxy, 'HyperfLazy\\')) {
  60. $this->loadProxy($proxy);
  61. return true;
  62. }
  63. return null;
  64. }
  65. /**
  66. * Register the loader on the auto-loader stack.
  67. */
  68. protected function register(): void
  69. {
  70. if (! $this->registered) {
  71. $this->prependToLoaderStack();
  72. $this->registered = true;
  73. }
  74. }
  75. /**
  76. * Load a real-time facade for the given proxy.
  77. */
  78. protected function loadProxy(string $proxy)
  79. {
  80. require_once $this->ensureProxyExists($proxy);
  81. }
  82. /**
  83. * Ensure that the given proxy has an existing real-time facade class.
  84. */
  85. protected function ensureProxyExists(string $proxy): string
  86. {
  87. $dir = BASE_PATH . '/runtime/container/proxy/';
  88. if (! file_exists($dir)) {
  89. mkdir($dir, 0755, true);
  90. }
  91. $code = $this->generatorLazyProxy(
  92. $proxy,
  93. $this->config[$proxy] ?? Str::after($proxy, 'HyperfLazy\\')
  94. );
  95. $path = str_replace('\\', '_', $dir . $proxy . '_' . crc32($code) . '.php');
  96. $key = md5($path);
  97. // If the proxy file does not exist, then try to acquire the coroutine lock.
  98. if (! file_exists($path) && CoLocker::lock($key)) {
  99. $targetPath = $path . '.' . uniqid();
  100. file_put_contents($targetPath, $code);
  101. rename($targetPath, $path);
  102. CoLocker::unlock($key);
  103. }
  104. return $path;
  105. }
  106. /**
  107. * Format the lazy proxy with the proper namespace and class.
  108. */
  109. protected function generatorLazyProxy(string $proxy, string $target): string
  110. {
  111. $targetReflection = new ReflectionClass($target);
  112. if ($this->isUnsupportedReflectionType($targetReflection)) {
  113. $builder = new FallbackLazyProxyBuilder();
  114. return $this->buildNewCode($builder, $proxy, $targetReflection);
  115. }
  116. if ($targetReflection->isInterface()) {
  117. $builder = new InterfaceLazyProxyBuilder();
  118. return $this->buildNewCode($builder, $proxy, $targetReflection);
  119. }
  120. $builder = new ClassLazyProxyBuilder();
  121. return $this->buildNewCode($builder, $proxy, $targetReflection);
  122. }
  123. /**
  124. * Prepend the load method to the auto-loader stack.
  125. */
  126. protected function prependToLoaderStack(): void
  127. {
  128. /** @var callable(string): void */
  129. $load = [$this, 'load'];
  130. spl_autoload_register($load, true, true);
  131. }
  132. /**
  133. * These conditions are really hard to proxy via inheritance.
  134. * Luckily these conditions are very rarely met.
  135. *
  136. * TODO: implement some of them.
  137. *
  138. * @param ReflectionClass $targetReflection [description]
  139. * @return bool [description]
  140. */
  141. private function isUnsupportedReflectionType(ReflectionClass $targetReflection): bool
  142. {
  143. return $targetReflection->isFinal();
  144. }
  145. private function buildNewCode(AbstractLazyProxyBuilder $builder, string $proxy, ReflectionClass $reflectionClass): string
  146. {
  147. $target = $reflectionClass->getName();
  148. $nodes = PhpParser::getInstance()->getNodesFromReflectionClass($reflectionClass);
  149. $builder->addClassBoilerplate($proxy, $target);
  150. $builder->addClassRelationship();
  151. $traverser = new NodeTraverser();
  152. $methods = PhpParser::getInstance()->getAllMethodsFromStmts($nodes);
  153. $visitor = new PublicMethodVisitor($methods, $builder->getOriginalClassName());
  154. $traverser->addVisitor(new NameResolver());
  155. $traverser->addVisitor($visitor);
  156. $traverser->traverse($nodes);
  157. $builder->addNodes($visitor->nodes);
  158. $prettyPrinter = new Standard();
  159. return $prettyPrinter->prettyPrintFile([$builder->getNode()]);
  160. }
  161. }