10 Commits

Author SHA1 Message Date
as2252258 689fd09358 Add scanner manifest replay control 2026-07-10 17:03:41 +08:00
as2252258 2753a42d82 eee 2026-07-07 17:00:04 +08:00
as2252258 8d340a8a45 eee 2026-07-07 16:00:06 +08:00
as2252258 e1a23090d6 eee 2026-07-07 15:09:38 +08:00
as2252258 b08fdb1801 eee 2026-07-03 14:25:26 +08:00
as2252258 a0b975003a eee 2026-06-28 20:20:20 +08:00
as2252258 81250722ea eee 2026-06-24 20:45:36 +08:00
as2252258 b7712d3d9d eee 2026-06-24 20:21:29 +08:00
as2252258 c38fb6ac4c eee 2026-06-24 20:11:11 +08:00
as2252258 7827b8d5b1 eee 2026-06-12 23:57:19 +08:00
6 changed files with 639 additions and 133 deletions
+68 -6
View File
@@ -6,34 +6,75 @@ namespace Kiri\Di;
class ChangeSet class ChangeSet
{ {
/**
* @var array
*/
private array $changedFiles = []; private array $changedFiles = [];
/**
* @var array
*/
private array $removedFiles = []; private array $removedFiles = [];
/**
* @var array
*/
private array $changedClasses = []; private array $changedClasses = [];
/**
* @var array
*/
private array $removedClasses = []; private array $removedClasses = [];
/**
* @param string $file
* @return void
*/
public function addChangedFile(string $file): void public function addChangedFile(string $file): void
{ {
$this->changedFiles[$file] = true; $this->changedFiles[$file] = true;
} }
/**
* @param string $file
* @return void
*/
public function addRemovedFile(string $file): void public function addRemovedFile(string $file): void
{ {
$this->removedFiles[$file] = true; $this->removedFiles[$file] = true;
} }
/**
* @param string $class
* @return void
*/
public function addChangedClass(string $class): void public function addChangedClass(string $class): void
{ {
$this->changedClasses[$class] = true; $this->changedClasses[$class] = true;
} }
/**
* @param string $class
* @return void
*/
public function addRemovedClass(string $class): void public function addRemovedClass(string $class): void
{ {
$this->removedClasses[$class] = true; $this->removedClasses[$class] = true;
} }
/**
* @param ChangeSet $changeSet
* @return $this
*/
public function merge(ChangeSet $changeSet): self public function merge(ChangeSet $changeSet): self
{ {
foreach ($changeSet->getChangedFiles() as $file) { foreach ($changeSet->getChangedFiles() as $file) {
@@ -55,39 +96,60 @@ class ChangeSet
return $this; return $this;
} }
/**
* @return array
*/
public function getChangedFiles(): array public function getChangedFiles(): array
{ {
return array_keys($this->changedFiles); return array_keys($this->changedFiles);
} }
/**
* @return array
*/
public function getRemovedFiles(): array public function getRemovedFiles(): array
{ {
return array_keys($this->removedFiles); return array_keys($this->removedFiles);
} }
/**
* @return array
*/
public function getChangedClasses(): array public function getChangedClasses(): array
{ {
return array_keys($this->changedClasses); return array_keys($this->changedClasses);
} }
/**
* @return array
*/
public function getRemovedClasses(): array public function getRemovedClasses(): array
{ {
return array_keys($this->removedClasses); return array_keys($this->removedClasses);
} }
/**
* @return bool
*/
public function hasChanges(): bool public function hasChanges(): bool
{ {
return $this->changedFiles !== [] return $this->changedFiles !== [] || $this->removedFiles !== [] || $this->changedClasses !== [] || $this->removedClasses !== [];
|| $this->removedFiles !== []
|| $this->changedClasses !== []
|| $this->removedClasses !== [];
} }
/**
* @return array
*/
public function toArray(): array public function toArray(): array
{ {
return [ return [
'changed_files' => $this->getChangedFiles(), 'changed_files' => $this->getChangedFiles(),
'removed_files' => $this->getRemovedFiles(), 'removed_files' => $this->getRemovedFiles(),
'changed_classes' => $this->getChangedClasses(), 'changed_classes' => $this->getChangedClasses(),
'removed_classes' => $this->getRemovedClasses(), 'removed_classes' => $this->getRemovedClasses(),
]; ];
+85 -2
View File
@@ -207,6 +207,75 @@ class Container implements ContainerInterface
} }
/**
* 清理所有非基础设施类的单例引用,释放 Swoole 常驻进程中的内存
* 保留框架核心类(Kiri、Database、Psr、Symfony)的单例
* 应在请求/任务完成后调用,防止单例映射无限增长
* @return void
*/
public function clearNonInfrastructure(): void
{
$keepPrefixes = [
ContainerInterface::class,
'Kiri\\', 'Database\\', 'Psr\\', 'Symfony\\',
'Swoole\\', 'MongoDB\\',
];
foreach (array_keys($this->_singletons) as $className) {
$shouldKeep = false;
foreach ($keepPrefixes as $prefix) {
if (str_starts_with($className, $prefix)) {
$shouldKeep = true;
break;
}
}
if (!$shouldKeep) {
unset($this->_singletons[$className]);
}
}
foreach (array_keys($this->_reflection) as $className) {
$shouldKeep = false;
foreach ($keepPrefixes as $prefix) {
if (str_starts_with($className, $prefix)) {
$shouldKeep = true;
break;
}
}
if (!$shouldKeep) {
unset($this->_reflection[$className]);
}
}
foreach (array_keys($this->_parameters) as $className) {
$shouldKeep = false;
foreach ($keepPrefixes as $prefix) {
if (str_starts_with($className, $prefix)) {
$shouldKeep = true;
break;
}
}
if (!$shouldKeep) {
unset($this->_parameters[$className]);
}
}
}
/**
* 获取当前单例缓存统计信息,用于内存监控
* @return array{singletons: int, reflections: int, params: int}
*/
public function getMemoryStats(): array
{
return [
'singletons' => count($this->_singletons),
'reflections' => count($this->_reflection),
'params' => count($this->_parameters),
];
}
/** /**
* @param string $className * @param string $className
* @param array $construct * @param array $construct
@@ -225,7 +294,14 @@ class Container implements ContainerInterface
$construct = $this->getMethodParams($handler); $construct = $this->getMethodParams($handler);
} }
$newInstance = $reflect->newInstanceArgs($construct); $isController = class_exists(\Kiri\Router\Base\Controller::class) && $reflect->isSubclassOf(\Kiri\Router\Base\Controller::class);
$needsProxy = !$isController && class_exists(\Kiri\Router\Defer\DeferRegistry::class) && \Kiri\Router\Defer\DeferRegistry::hasAny($className);
if ($needsProxy) {
$newInstance = \Kiri\Router\Defer\DeferProxyGenerator::create($className, $construct);
} else {
$newInstance = $reflect->newInstanceArgs($construct);
}
return $this->runInit($reflect, static::configure($newInstance, $config)); return $this->runInit($reflect, static::configure($newInstance, $config));
} }
@@ -252,7 +328,14 @@ class Container implements ContainerInterface
if (empty($construct) && ($handler = $reflect->getConstructor()) !== null) { if (empty($construct) && ($handler = $reflect->getConstructor()) !== null) {
$construct = $this->getMethodParams($handler); $construct = $this->getMethodParams($handler);
} }
$newInstance = $reflect->newInstanceArgs($construct); $isController = class_exists(\Kiri\Router\Base\Controller::class) && $reflect->isSubclassOf(\Kiri\Router\Base\Controller::class);
$needsProxy = !$isController && class_exists(\Kiri\Router\Defer\DeferRegistry::class) && \Kiri\Router\Defer\DeferRegistry::hasAny($reflect->getName());
if ($needsProxy) {
$newInstance = \Kiri\Router\Defer\DeferProxyGenerator::create($reflect->getName(), $construct);
} else {
$newInstance = $reflect->newInstanceArgs($construct);
}
return $this->runInit($reflect, static::configure($newInstance, $config)); return $this->runInit($reflect, static::configure($newInstance, $config));
} }
+8 -3
View File
@@ -6,13 +6,16 @@ namespace Kiri\Di;
class HotReloadState class HotReloadState
{ {
private const MAX_AGE_SECONDS = 30; private const int MAX_AGE_SECONDS = 30;
public function store(array $changedFiles): void public function store(array $changedFiles): void
{ {
$normalizedFiles = array_map([$this, 'normalizePath'], array_filter($changedFiles));
$normalizedFiles = array_values(array_unique($normalizedFiles));
$payload = [ $payload = [
'timestamp' => time(), 'timestamp' => time(),
'changed_files' => array_values(array_unique(array_map([$this, 'normalizePath'], array_filter($changedFiles)))), 'changed_files' => $normalizedFiles,
]; ];
$directory = dirname($this->getFilePath()); $directory = dirname($this->getFilePath());
@@ -46,7 +49,9 @@ class HotReloadState
} }
$files = is_array($data['changed_files'] ?? null) ? $data['changed_files'] : []; $files = is_array($data['changed_files'] ?? null) ? $data['changed_files'] : [];
return array_values(array_unique(array_map([$this, 'normalizePath'], $files))); $files = array_map([$this, 'normalizePath'], $files);
$files = array_values(array_unique($files));
return $files;
} }
private function getFilePath(): string private function getFilePath(): string
+44 -1
View File
@@ -8,6 +8,13 @@ class ScanManifest
{ {
private array $entries = []; private array $entries = [];
/**
* @param string $path
* @param int $mtime
* @param array $classes
* @return void
*/
public function set(string $path, int $mtime, array $classes): void public function set(string $path, int $mtime, array $classes): void
{ {
$this->entries[$path] = [ $this->entries[$path] = [
@@ -16,21 +23,41 @@ class ScanManifest
]; ];
} }
/**
* @param string $path
* @return bool
*/
public function has(string $path): bool public function has(string $path): bool
{ {
return isset($this->entries[$path]); return isset($this->entries[$path]);
} }
/**
* @param string $path
* @return int|null
*/
public function getMtime(string $path): ?int public function getMtime(string $path): ?int
{ {
return $this->entries[$path]['mtime'] ?? null; return $this->entries[$path]['mtime'] ?? null;
} }
/**
* @param string $path
* @return array
*/
public function getClasses(string $path): array public function getClasses(string $path): array
{ {
return $this->entries[$path]['classes'] ?? []; return $this->entries[$path]['classes'] ?? [];
} }
/**
* @param string $path
* @return array
*/
public function remove(string $path): array public function remove(string $path): array
{ {
$classes = $this->getClasses($path); $classes = $this->getClasses($path);
@@ -38,20 +65,36 @@ class ScanManifest
return $classes; return $classes;
} }
/**
* @return array
*/
public function all(): array public function all(): array
{ {
return $this->entries; return $this->entries;
} }
/**
* @param string|null $prefix
* @return array
*/
public function paths(?string $prefix = null): array public function paths(?string $prefix = null): array
{ {
if ($prefix === null) { if ($prefix === null) {
return array_keys($this->entries); return array_keys($this->entries);
} }
return array_values(array_filter(array_keys($this->entries), fn(string $path) => str_starts_with($path, $prefix))); $keys = array_keys($this->entries);
$filtered = array_filter($keys, fn(string $path) => str_starts_with($path, $prefix));
return array_values($filtered);
} }
/**
* @param array $entries
* @return void
*/
public function fromArray(array $entries): void public function fromArray(array $entries): void
{ {
$this->entries = []; $this->entries = [];
+433 -120
View File
@@ -8,6 +8,7 @@ use DirectoryIterator;
use Kiri\Abstracts\Component; use Kiri\Abstracts\Component;
use Kiri\Di\Inject\Container; use Kiri\Di\Inject\Container;
use Kiri\Di\Inject\Skip; use Kiri\Di\Inject\Skip;
use Kiri\Di\Interface\InjectMethodInterface;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use ReflectionClass; use ReflectionClass;
use ReflectionMethod; use ReflectionMethod;
@@ -40,10 +41,13 @@ class Scanner extends Component
'follow_links' => false, 'follow_links' => false,
'cache_enabled' => false, 'cache_enabled' => false,
'cache_ttl' => 3600, 'cache_ttl' => 3600,
'opcache_compile' => false,
'require_files_without_classes' => false,
'debug' => false, 'debug' => false,
'class_name_strategy' => 'auto',
]; ];
private static ?string $currentFile = null;
public function __construct() public function __construct()
{ {
$this->basePath = $this->normalizePath($_SERVER['PWD'] ?? APP_PATH ?? getcwd()); $this->basePath = $this->normalizePath($_SERVER['PWD'] ?? APP_PATH ?? getcwd());
@@ -79,15 +83,13 @@ class Scanner extends Component
} }
$this->syncLegacyState(); $this->syncLegacyState();
if ($this->config['cache_enabled'] && $cacheFile !== null) {
if ($this->config['cache_enabled']) {
$this->saveToCache($cacheFile); $this->saveToCache($cacheFile);
} }
return $this->changeSet; return $this->changeSet;
} }
public function scanFiles(array $files, ?string $scopePath = null, ?string $cacheFile = null): ChangeSet public function scanFiles(array $files, ?string $scopePath = null, ?string $cacheFile = null, bool $force = false, bool $replayManifest = true): ChangeSet
{ {
$this->changeSet = new ChangeSet(); $this->changeSet = new ChangeSet();
$this->visitedFiles = []; $this->visitedFiles = [];
@@ -106,21 +108,85 @@ class Scanner extends Component
foreach (array_values(array_unique($files)) as $file) { foreach (array_values(array_unique($files)) as $file) {
$path = $this->normalizePath($file); $path = $this->normalizePath($file);
if (file_exists($path)) { if (file_exists($path)) {
$this->processFile($path); $this->processFile($path, $force);
continue; continue;
} }
$this->markRemovedFile($path); $this->markRemovedFile($path);
} }
if ($cacheLoaded && $scopePath !== null) { if ($replayManifest && $cacheLoaded && $scopePath !== null) {
$this->replayManifest($scopePath, $this->changeSet->getChangedFiles()); $this->replayManifest($scopePath, $this->changeSet->getChangedFiles());
} }
$this->syncLegacyState(); $this->syncLegacyState();
if ($this->config['cache_enabled'] && $scopePath !== null && $cacheFile !== null) {
$this->saveToCache($cacheFile);
}
return $this->changeSet; return $this->changeSet;
} }
public function expandDependentFiles(array $files, ?string $scopePath = null): array
{
$expanded = [];
foreach (array_values(array_unique($files)) as $file) {
$path = $this->normalizePath($file);
$expanded[$path] = true;
}
if ($scopePath === null || $expanded === []) {
return array_keys($expanded);
}
$scopePath = $this->normalizePath($scopePath);
$scopeFiles = $this->collectFiles($scopePath);
$symbols = [];
foreach (array_keys($expanded) as $file) {
if (!file_exists($file)) {
continue;
}
foreach ($this->parseDeclaredSymbols($file) as $class => $kind) {
$symbols[$class] = $kind;
}
}
if ($symbols === []) {
return array_keys($expanded);
}
$processedSymbols = [];
do {
$newSymbols = array_diff_key($symbols, $processedSymbols);
if ($newSymbols === []) {
break;
}
$processedSymbols += $newSymbols;
$changed = false;
foreach ($scopeFiles as $file) {
$file = $this->normalizePath($file);
if (isset($expanded[$file]) || !file_exists($file)) {
continue;
}
$dependencies = $this->parseFileDependencies($file);
if (array_intersect_key($dependencies, $symbols) === []) {
continue;
}
$expanded[$file] = true;
foreach ($this->parseDeclaredSymbols($file) as $class => $kind) {
if (!isset($symbols[$class])) {
$symbols[$class] = $kind;
$changed = true;
}
}
}
} while ($changed);
return array_keys($expanded);
}
private function scanDirectory(string $path, int $depth = 0): void private function scanDirectory(string $path, int $depth = 0): void
{ {
if ($depth > $this->config['max_depth']) { if ($depth > $this->config['max_depth']) {
@@ -185,7 +251,7 @@ class Scanner extends Component
return false; return false;
} }
private function processFile(string $path): void private function processFile(string $path, bool $force = false): void
{ {
$path = $this->normalizePath($path); $path = $this->normalizePath($path);
if (!in_array(pathinfo($path, PATHINFO_EXTENSION), $this->config['extensions'], true)) { if (!in_array(pathinfo($path, PATHINFO_EXTENSION), $this->config['extensions'], true)) {
@@ -193,7 +259,7 @@ class Scanner extends Component
} }
$this->visitedFiles[$path] = true; $this->visitedFiles[$path] = true;
if (!$this->shouldProcessFile($path)) { if (!$force && !$this->shouldProcessFile($path)) {
return; return;
} }
@@ -239,118 +305,45 @@ class Scanner extends Component
private function loadAndParseFile(string $path): array private function loadAndParseFile(string $path): array
{ {
$this->optimizeWithOpcache($path); $this->optimizeWithOpcache($path);
require_once $path;
$classes = $this->getClassNamesForFile($path); self::$currentFile = $path;
$classes = array_keys($this->parseDeclaredSymbols($path));
$loadFile = $classes === [] ? (bool)$this->config['require_files_without_classes'] : false;
foreach ($classes as $class) { foreach ($classes as $class) {
if (class_exists($class)) { if (!$this->isDeclaredSymbol($class)) {
$this->analyzeClass($class); $loadFile = true;
break;
} }
} }
return $classes; try {
} if ($loadFile) {
$before = $this->getDeclaredSymbols();
private function getClassNamesForFile(string $path): array require_once $path;
{ if ($classes === []) {
$strategy = $this->config['class_name_strategy']; $after = $this->getDeclaredSymbols();
$classes = []; $classes = array_values(array_diff($after, $before));
if (in_array($strategy, ['auto', 'extract', 'both'], true)) {
$class = $this->extractClassNameFromFile($path);
if ($class !== null) {
$classes[] = $class;
}
}
if ($classes === [] || in_array($strategy, ['rename', 'both'], true)) {
$classes[] = $this->renamePathToClassName($path);
}
return array_values(array_unique(array_filter($classes)));
}
private function canExtractClassName(): bool
{
return function_exists('token_get_all');
}
private function extractClassNameFromFile(string $path): ?string
{
if (!$this->canExtractClassName()) {
return null;
}
$content = @file_get_contents($path);
if ($content === false || $content === '') {
return null;
}
$tokens = @token_get_all($content);
if (!$tokens) {
return null;
}
$namespace = '';
$class = '';
$collectingNamespace = false;
$collectingClass = false;
$previousToken = null;
foreach ($tokens as $token) {
if (is_array($token)) {
$text = $token[1];
if ($token[0] === T_NAMESPACE) {
$namespace = '';
$collectingNamespace = true;
} elseif ($collectingNamespace && in_array($token[0], [T_STRING, T_NAME_QUALIFIED, T_NS_SEPARATOR], true)) {
$namespace .= $text;
} elseif ($collectingNamespace && $token[0] === T_WHITESPACE) {
} elseif (in_array($token[0], [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], true) && $previousToken !== T_DOUBLE_COLON && $previousToken !== T_NEW) {
$collectingClass = true;
} elseif ($collectingClass && $token[0] === T_STRING) {
$class = $text;
break;
} elseif ($collectingNamespace) {
$collectingNamespace = false;
} }
}
if ($token[0] !== T_WHITESPACE) { foreach ($classes as $class) {
$previousToken = $token[0]; if (class_exists($class)) {
$this->analyzeClass($class);
} }
continue;
} }
if ($collectingNamespace && ($token === ';' || $token === '{')) { return $classes;
$collectingNamespace = false; } finally {
} self::$currentFile = null;
if ($token === '{') {
$collectingClass = false;
}
$previousToken = $token;
} }
if ($class === '') {
return null;
}
return $namespace !== '' ? $namespace . '\\' . $class : $class;
} }
private function renamePathToClassName(string $path): string
{
$relativePath = str_replace($this->basePath, '', $this->normalizePath($path));
$relativePath = str_replace('.php', '', $relativePath);
$parts = explode('/', trim($relativePath, '/\\'));
$parts = array_values(array_filter($parts, fn(string $part) => $part !== ''));
$parts = array_map('ucfirst', $parts);
return implode('\\', $parts);
}
private function optimizeWithOpcache(string $path): void private function optimizeWithOpcache(string $path): void
{ {
if (!$this->config['opcache_compile']) {
return;
}
if ($this->hasOpcache === null) { if ($this->hasOpcache === null) {
$this->hasOpcache = function_exists('opcache_invalidate') && function_exists('opcache_compile_file'); $this->hasOpcache = function_exists('opcache_invalidate') && function_exists('opcache_compile_file');
} }
@@ -372,6 +365,307 @@ class Scanner extends Component
} }
} }
private function parseDeclaredSymbols(string $path): array
{
$tokens = token_get_all(file_get_contents($path) ?: '');
$namespace = '';
$classes = [];
$count = count($tokens);
for ($i = 0; $i < $count; $i++) {
$token = $tokens[$i];
if (!is_array($token)) {
continue;
}
if ($token[0] === T_NAMESPACE) {
$namespace = $this->parseNamespace($tokens, $i + 1);
continue;
}
if (!$this->isClassLikeToken($token[0])) {
continue;
}
if ($token[0] === T_CLASS && ($this->isAnonymousClass($tokens, $i) || $this->isClassConstant($tokens, $i))) {
continue;
}
$name = $this->parseClassLikeName($tokens, $i + 1);
if ($name === null) {
continue;
}
$classes[ltrim($namespace . '\\' . $name, '\\')] = $this->getClassLikeKind($tokens, $i);
}
return $classes;
}
private function parseNamespace(array $tokens, int $offset): string
{
$namespace = '';
$count = count($tokens);
for ($i = $offset; $i < $count; $i++) {
$token = $tokens[$i];
if ($token === ';' || $token === '{') {
break;
}
if (is_array($token) && in_array($token[0], [T_STRING, T_NAME_QUALIFIED, T_NS_SEPARATOR], true)) {
$namespace .= $token[1];
}
}
return trim($namespace, '\\');
}
private function parseClassLikeName(array $tokens, int $offset): ?string
{
$count = count($tokens);
for ($i = $offset; $i < $count; $i++) {
$token = $tokens[$i];
if (!is_array($token)) {
continue;
}
if ($token[0] === T_STRING) {
return $token[1];
}
}
return null;
}
private function isClassLikeToken(int $token): bool
{
return $token === T_CLASS
|| $token === T_INTERFACE
|| $token === T_TRAIT
|| (defined('T_ENUM') && $token === T_ENUM);
}
private function isAnonymousClass(array $tokens, int $offset): bool
{
for ($i = $offset - 1; $i >= 0; $i--) {
$token = $tokens[$i];
if (is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
continue;
}
return is_array($token) && $token[0] === T_NEW;
}
return false;
}
private function isClassConstant(array $tokens, int $offset): bool
{
for ($i = $offset - 1; $i >= 0; $i--) {
$token = $tokens[$i];
if (is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
continue;
}
return is_array($token) && $token[0] === T_DOUBLE_COLON;
}
return false;
}
private function parseFileDependencies(string $path): array
{
$tokens = token_get_all(file_get_contents($path) ?: '');
$namespace = '';
$imports = [];
$dependencies = [];
$count = count($tokens);
$braceDepth = 0;
for ($i = 0; $i < $count; $i++) {
$token = $tokens[$i];
if ($token === '{') {
$braceDepth++;
continue;
}
if ($token === '}') {
$braceDepth = max(0, $braceDepth - 1);
continue;
}
if (!is_array($token)) {
continue;
}
if ($token[0] === T_NAMESPACE) {
$namespace = $this->parseNamespace($tokens, $i + 1);
continue;
}
if ($token[0] === T_USE) {
if ($braceDepth === 0) {
foreach ($this->parseUseImports($tokens, $i + 1, $namespace) as $alias => $class) {
$imports[$alias] = $class;
}
continue;
}
foreach ($this->parseNameList($tokens, $i + 1, [';', '{']) as $name) {
$dependencies[$this->resolveClassName($name, $namespace, $imports)] = true;
}
continue;
}
if ($token[0] === T_EXTENDS || $token[0] === T_IMPLEMENTS) {
foreach ($this->parseNameList($tokens, $i + 1, ['{']) as $name) {
$dependencies[$this->resolveClassName($name, $namespace, $imports)] = true;
}
}
}
return $dependencies;
}
private function parseUseImports(array $tokens, int $offset, string $namespace): array
{
$imports = [];
$count = count($tokens);
for ($i = $offset; $i < $count; $i++) {
$token = $tokens[$i];
if ($token === ';') {
break;
}
if (is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
continue;
}
if (is_array($token) && in_array($token[0], [T_FUNCTION, T_CONST], true)) {
return [];
}
$name = $this->collectQualifiedName($tokens, $i);
if ($name === null) {
continue;
}
$alias = null;
$j = $i + 1;
while ($j < $count) {
$next = $tokens[$j];
if ($next === ',' || $next === ';') {
break;
}
if (is_array($next) && $next[0] === T_AS) {
$aliasOffset = $j + 1;
$alias = $this->collectQualifiedName($tokens, $aliasOffset);
break;
}
$j++;
}
$class = trim($name, '\\');
$short = $alias ?: basename(str_replace('\\', '/', $class));
$imports[strtolower($short)] = $class;
$i = $j;
}
return $imports;
}
private function parseNameList(array $tokens, int $offset, array $stoppers): array
{
$names = [];
$count = count($tokens);
for ($i = $offset; $i < $count; $i++) {
$token = $tokens[$i];
if (is_string($token) && in_array($token, $stoppers, true)) {
break;
}
$name = $this->collectQualifiedName($tokens, $i);
if ($name !== null) {
$names[] = $name;
}
}
return $names;
}
private function collectQualifiedName(array $tokens, int &$offset): ?string
{
$count = count($tokens);
$name = '';
$started = false;
for ($i = $offset; $i < $count; $i++) {
$token = $tokens[$i];
if (!$started && is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
continue;
}
if (is_array($token) && in_array($token[0], [T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED, T_NS_SEPARATOR], true)) {
$name .= $token[1];
$started = true;
$offset = $i;
continue;
}
break;
}
return $name === '' ? null : $name;
}
private function resolveClassName(string $name, string $namespace, array $imports): string
{
$name = trim($name, '\\');
if ($name === '') {
return $name;
}
$parts = explode('\\', $name);
$first = strtolower($parts[0]);
if (isset($imports[$first])) {
array_shift($parts);
return $imports[$first] . ($parts === [] ? '' : '\\' . implode('\\', $parts));
}
return $namespace === '' ? $name : $namespace . '\\' . $name;
}
private function getClassLikeKind(array $tokens, int $offset): string
{
$token = $tokens[$offset];
if (!is_array($token)) {
return 'class';
}
if ($token[0] === T_TRAIT) {
return 'trait';
}
if ($token[0] === T_INTERFACE) {
return 'interface';
}
if (defined('T_ENUM') && $token[0] === T_ENUM) {
return 'enum';
}
for ($i = $offset - 1; $i >= 0; $i--) {
$previous = $tokens[$i];
if (is_array($previous) && in_array($previous[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
continue;
}
return is_array($previous) && $previous[0] === T_ABSTRACT ? 'abstract_class' : 'class';
}
return 'class';
}
private function getDeclaredSymbols(): array
{
return array_merge(get_declared_classes(), get_declared_interfaces(), get_declared_traits());
}
private function isDeclaredSymbol(string $class): bool
{
return class_exists($class, false)
|| interface_exists($class, false)
|| trait_exists($class, false)
|| (function_exists('enum_exists') && enum_exists($class, false));
}
private function analyzeClass(string $class): void private function analyzeClass(string $class): void
{ {
try { try {
@@ -403,8 +697,9 @@ class Scanner extends Component
private function analyzeClassMethods(ReflectionClass $reflect, string $class): void private function analyzeClassMethods(ReflectionClass $reflect, string $class): void
{ {
foreach ($reflect->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { foreach ($reflect->getMethods() as $method) {
if ($method->isStatic() || $method->getDeclaringClass()->getName() !== $class) { $declaringClass = $method->getDeclaringClass();
if ($method->isStatic() || ($declaringClass->getName() !== $class && !$declaringClass->isTrait())) {
continue; continue;
} }
@@ -422,7 +717,7 @@ class Scanner extends Component
try { try {
$instance = $attribute->newInstance(); $instance = $attribute->newInstance();
if (method_exists($instance, 'dispatch')) { if ($instance instanceof InjectMethodInterface) {
$instance->dispatch($class, $method->getName()); $instance->dispatch($class, $method->getName());
} }
} catch (Throwable $e) { } catch (Throwable $e) {
@@ -561,16 +856,26 @@ class Scanner extends Component
return array_map(fn($attr) => $attr->getName(), $reflect->getAttributes()); return array_map(fn($attr) => $attr->getName(), $reflect->getAttributes());
} }
public function getStats(): array public function getStats(): array
{ {
return [ return [
'total_files' => count($this->files), 'total_files' => count($this->files),
'cached_mtimes' => count($this->fileMtimes), 'cached_mtimes' => count($this->fileMtimes),
'base_path' => $this->basePath, 'base_path' => $this->basePath,
'config' => $this->config, 'config' => $this->config,
'manifest_entries' => count($this->manifest->all()), 'manifest_entries' => count($this->manifest->all()),
]; ];
} }
/**
* 返回 Master 扫描产生的完整清单数据,供 Worker 轻量重建注解路由
* @return array{string: array{mtime: int, classes: string[]}}
*/
public function getManifestClasses(): array
{
return $this->manifest->all();
}
public function reset(): void public function reset(): void
{ {
@@ -582,6 +887,11 @@ class Scanner extends Component
$this->visitedFiles = []; $this->visitedFiles = [];
} }
public static function getCurrentFile(): ?string
{
return self::$currentFile;
}
private function detectRemovedFiles(string $path): void private function detectRemovedFiles(string $path): void
{ {
$prefix = rtrim($path, '/\\') . '/'; $prefix = rtrim($path, '/\\') . '/';
@@ -684,6 +994,9 @@ class Scanner extends Component
if (method_exists($this->container, 'forgetClass')) { if (method_exists($this->container, 'forgetClass')) {
$this->container->forgetClass($class); $this->container->forgetClass($class);
} }
if (class_exists(\Kiri\Router\Defer\DeferRegistry::class)) {
\Kiri\Router\Defer\DeferRegistry::removeClass($class);
}
} }
} }
} }
+1 -1
View File
@@ -9,7 +9,7 @@
], ],
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": ">=8.4", "php": ">=8.5",
"psr/container": "^2.0" "psr/container": "^2.0"
}, },
"autoload": { "autoload": {