Files
kiri-container/Scanner.php
T
2025-12-31 01:10:27 +08:00

525 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace Kiri\Di;
use Kiri\Di\Inject\Container;
use Kiri\Abstracts\Component;
use Kiri\Di\Inject\Skip;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionMethod;
use DirectoryIterator;
use Throwable;
class Scanner extends Component
{
#[Container(ContainerInterface::class)]
public ContainerInterface $container;
/**
* @var array 已处理的文件路径列表
*/
public array $files = [];
/**
* @var array 文件修改时间缓存
*/
private array $fileMtimes = [];
/**
* @var bool|null OPcache可用性缓存
*/
private ?bool $hasOpcache = null;
/**
* @var array 配置选项
*/
private array $config = [
'skip_patterns' => ['/vendor/', '/tests/', '/cache/', '/node_modules/'],
'skip_directories' => [],
'extensions' => ['php'],
'max_depth' => 20,
'follow_links' => false,
'cache_enabled' => true,
'cache_ttl' => 3600,
'debug' => false,
];
/**
* 设置扫描器配置
*/
public function setConfig(array $config): void
{
$this->config = array_merge($this->config, $config);
}
/**
* 主扫描方法,支持缓存
*/
public function scan(string $path, ?string $cacheFile = null): void
{
if ($this->config['cache_enabled']) {
$cacheFile = $cacheFile ?? $this->getDefaultCacheFile();
if ($this->loadFromCache($cacheFile, $path)) {
$this->processCachedFiles();
return;
}
}
$this->scanDirectory($path);
if ($this->config['cache_enabled']) {
$this->saveToCache($cacheFile);
}
}
/**
* 扫描目录(替换原有的load_directory
*/
private function scanDirectory(string $path, int $depth = 0): void
{
if ($depth > $this->config['max_depth']) {
return;
}
try {
$dir = new DirectoryIterator($path);
} catch (Throwable $e) {
$this->logError($e, ['path' => $path, 'action' => 'open_directory']);
return;
}
foreach ($dir as $item) {
if ($this->shouldSkipItem($item)) {
continue;
}
$realPath = $item->getRealPath();
if ($item->isDir()) {
if ($this->shouldSkipDirectory($realPath)) {
continue;
}
// 如果是符号链接且不跟随链接,则跳过
if ($item->isLink() && !$this->config['follow_links']) {
continue;
}
$this->scanDirectory($realPath, $depth + 1);
} elseif ($item->isFile()) {
$this->processFile($realPath);
}
}
}
/**
* 判断是否应该跳过该目录项
*/
private function shouldSkipItem(DirectoryIterator $item): bool
{
return $item->isDot() || str_starts_with($item->getFilename(), '.');
}
/**
* 判断是否应该跳过目录
*/
private function shouldSkipDirectory(string $path): bool
{
$path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
// 检查配置文件中的跳过目录
$skipDirs = array_merge(
$this->config['skip_directories'],
config('site.scanner.skip', [])
);
if (in_array($path, $skipDirs)) {
return true;
}
// 检查跳过模式
foreach ($this->config['skip_patterns'] as $pattern) {
if (strpos($path, $pattern) !== false) {
return true;
}
}
return false;
}
/**
* 处理单个文件
*/
private function processFile(string $path): void
{
// 检查文件扩展名
if (!in_array(pathinfo($path, PATHINFO_EXTENSION), $this->config['extensions'])) {
return;
}
// 检查是否已处理
if (in_array($path, $this->files)) {
return;
}
// 检查文件是否需要重新处理(基于修改时间)
if (!$this->shouldProcessFile($path)) {
return;
}
$this->files[] = $path;
try {
$this->loadAndParseFile($path);
} catch (Throwable $e) {
$this->logError($e, [
'file' => $path,
'action' => 'process_file',
]);
// 如果是语法错误,可以选择抛出或记录
if ($e instanceof \ParseError) {
throw $e;
}
}
}
/**
* 判断文件是否需要处理
*/
private function shouldProcessFile(string $path): bool
{
if (!file_exists($path)) {
return false;
}
$mtime = filemtime($path);
// 如果没有缓存过修改时间,或者文件已更新,则需要处理
if (!isset($this->fileMtimes[$path]) || $this->fileMtimes[$path] < $mtime) {
$this->fileMtimes[$path] = $mtime;
return true;
}
return false;
}
/**
* 加载并解析文件
*/
private function loadAndParseFile(string $path): void
{
// OPcache优化
$this->optimizeWithOpcache($path);
// 加载文件
require_once $path;
// 尝试从文件内容提取类名(更准确)
$class = $this->extractClassNameFromFile($path);
if ($class && class_exists($class)) {
$this->analyzeClass($class);
}
}
/**
* 使用OPcache优化文件
*/
private function optimizeWithOpcache(string $path): void
{
if ($this->hasOpcache === null) {
$this->hasOpcache = function_exists('opcache_invalidate')
&& function_exists('opcache_compile_file');
}
if ($this->hasOpcache) {
try {
opcache_invalidate($path, true);
opcache_compile_file($path);
} catch (Throwable $e) {
// 静默处理OPcache错误,不影响主要功能
if ($this->config['debug']) {
$this->logError($e, [
'file' => $path,
'action' => 'opcache_optimize',
]);
}
}
}
}
/**
* 从文件内容提取完整的类名(命名空间+类名)
*/
private function extractClassNameFromFile(string $path): ?string
{
$content = @file_get_contents($path);
if (!$content) {
return null;
}
$tokens = token_get_all($content);
$namespace = '';
$class = '';
$classToken = false;
$collecting = false;
foreach ($tokens as $token) {
if (is_array($token)) {
if ($token[0] === T_NAMESPACE) {
$namespace = '';
$collecting = true;
} elseif ($collecting && $token[0] === T_STRING) {
$namespace .= $token[1];
} elseif ($collecting && $token[0] === T_NS_SEPARATOR) {
$namespace .= '\\';
} elseif ($collecting && $token[0] === T_WHITESPACE) {
// 忽略空白
} else {
$collecting = false;
}
if ($token[0] === T_CLASS || $token[0] === T_INTERFACE || $token[0] === T_TRAIT) {
$classToken = true;
} elseif ($classToken && $token[0] === T_STRING) {
$class = $token[1];
break;
}
} else {
if ($token === ';' || $token === '{') {
$collecting = false;
}
}
}
if (!$class) {
return null;
}
return $namespace ? $namespace . '\\' . $class : $class;
}
/**
* 分析类及其方法
*/
private function analyzeClass(string $class): void
{
try {
$reflect = $this->container->getReflectionClass($class);
if (!$reflect->isInstantiable() ||
$reflect->isTrait() ||
$reflect->isEnum() ||
$reflect->isInterface()) {
return;
}
// 检查跳过注解
if ($this->shouldSkipClass($reflect)) {
return;
}
// 分析类的方法
$this->analyzeClassMethods($reflect, $class);
} catch (Throwable $e) {
$this->logError($e, [
'class' => $class,
'action' => 'analyze_class',
]);
}
}
/**
* 判断是否应该跳过该类
*/
private function shouldSkipClass(ReflectionClass $reflect): bool
{
$attributes = array_map(
fn($attr) => $attr->getName(),
$reflect->getAttributes()
);
return in_array(Skip::class, $attributes, true) ||
in_array(\Attribute::class, $attributes, true);
}
/**
* 分析类的方法
*/
private function analyzeClassMethods(ReflectionClass $reflect, string $class): void
{
var_dump($reflect->getName());
foreach ($reflect->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
// 跳过静态方法和继承的方法
if ($method->isStatic() || $method->getDeclaringClass()->getName() !== $class) {
continue;
}
$this->processMethodAttributes($method, $class);
}
}
/**
* 处理方法上的注解
*/
private function processMethodAttributes(ReflectionMethod $method, string $class): void
{
foreach ($method->getAttributes() as $attribute) {
$attributeName = $attribute->getName();
if (!class_exists($attributeName)) {
continue;
}
try {
$instance = $attribute->newInstance();
if (method_exists($instance, 'dispatch')) {
$instance->dispatch($class, $method->getName());
}
} catch (Throwable $e) {
$this->logError($e, [
'class' => $class,
'method' => $method->getName(),
'attribute' => $attributeName,
'action' => 'process_attribute',
]);
}
}
}
/**
* 缓存相关方法
*/
private function getDefaultCacheFile(): string
{
$cacheDir = sys_get_temp_dir() . '/kiri-scanner-cache/';
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
$projectHash = md5(__DIR__);
return $cacheDir . 'scanner_' . $projectHash . '.json';
}
/**
* 从缓存加载
*/
private function loadFromCache(string $cacheFile, string $path): bool
{
if (!file_exists($cacheFile)) {
return false;
}
// 检查缓存是否过期
if (filemtime($cacheFile) + $this->config['cache_ttl'] < time()) {
return false;
}
// 检查目录是否有更新
if ($this->hasDirectoryChanged($path, $cacheFile)) {
return false;
}
$data = json_decode(file_get_contents($cacheFile), true);
if (!is_array($data) || !isset($data['files'], $data['mtimes'])) {
return false;
}
$this->files = $data['files'];
$this->fileMtimes = $data['mtimes'];
return true;
}
/**
* 检查目录是否有更新
*/
private function hasDirectoryChanged(string $path, string $cacheFile): bool
{
$cacheMtime = filemtime($cacheFile);
// 简单的实现:检查目录本身和一级子目录
$dirIterator = new DirectoryIterator($path);
foreach ($dirIterator as $item) {
if ($item->isDot()) {
continue;
}
$itemPath = $item->getRealPath();
if (filemtime($itemPath) > $cacheMtime) {
return true;
}
}
return false;
}
/**
* 保存到缓存
*/
private function saveToCache(string $cacheFile): void
{
$data = [
'files' => $this->files,
'mtimes' => $this->fileMtimes,
'timestamp' => time(),
'version' => '1.0',
];
file_put_contents($cacheFile, json_encode($data, JSON_PRETTY_PRINT));
// 清理旧缓存文件
$this->cleanupOldCacheFiles(dirname($cacheFile));
}
/**
* 清理旧的缓存文件
*/
private function cleanupOldCacheFiles(string $cacheDir): void
{
$files = glob($cacheDir . '/scanner_*.json');
$now = time();
foreach ($files as $file) {
if (filemtime($file) + (7 * 24 * 3600) < $now) { // 清理7天前的缓存
unlink($file);
}
}
}
/**
* 处理缓存的文件(批量处理,避免重复加载)
*/
private function processCachedFiles(): void
{
foreach ($this->files as $file) {
if ($this->shouldProcessFile($file)) {
$this->loadAndParseFile($file);
}
}
}
/**
* 错误记录方法
*/
private function logError(Throwable $e, array $context = []): void
{
json_log($e, $context);
}
/**
* 重置扫描器状态
*/
public function reset(): void
{
$this->files = [];
$this->fileMtimes = [];
$this->hasOpcache = null;
}
}