Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7827b8d5b1 | |||
| 0ff2cd7a06 | |||
| 807b86c4cc | |||
| d23e55391e | |||
| 754042b994 | |||
| 8a4f68e984 | |||
| 3b7e6f992f | |||
| de0a39d207 | |||
| 0110f1947a | |||
| 93fc09342a | |||
| e16ceadaa7 | |||
| 34685ead22 | |||
| c643be8548 | |||
| f9e23c59ef | |||
| f89e8106f5 | |||
| 2f02c5af12 | |||
| 9e954135f6 | |||
| 7de790d65f | |||
| 855da03137 | |||
| 0081a30f22 | |||
| 3f2d3b0f04 | |||
| 5fa5646024 | |||
| 045c9293d5 | |||
| 6ae7f5a721 | |||
| 9775d16db5 | |||
| 03bffb5e5b |
+157
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Kiri\Di;
|
||||
|
||||
class ChangeSet
|
||||
{
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private array $changedFiles = [];
|
||||
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private array $removedFiles = [];
|
||||
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private array $changedClasses = [];
|
||||
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private array $removedClasses = [];
|
||||
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
public function addChangedFile(string $file): void
|
||||
{
|
||||
$this->changedFiles[$file] = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
public function addRemovedFile(string $file): void
|
||||
{
|
||||
$this->removedFiles[$file] = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @return void
|
||||
*/
|
||||
public function addChangedClass(string $class): void
|
||||
{
|
||||
$this->changedClasses[$class] = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @return void
|
||||
*/
|
||||
public function addRemovedClass(string $class): void
|
||||
{
|
||||
$this->removedClasses[$class] = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param ChangeSet $changeSet
|
||||
* @return $this
|
||||
*/
|
||||
public function merge(ChangeSet $changeSet): self
|
||||
{
|
||||
foreach ($changeSet->getChangedFiles() as $file) {
|
||||
$this->addChangedFile($file);
|
||||
}
|
||||
|
||||
foreach ($changeSet->getRemovedFiles() as $file) {
|
||||
$this->addRemovedFile($file);
|
||||
}
|
||||
|
||||
foreach ($changeSet->getChangedClasses() as $class) {
|
||||
$this->addChangedClass($class);
|
||||
}
|
||||
|
||||
foreach ($changeSet->getRemovedClasses() as $class) {
|
||||
$this->addRemovedClass($class);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getChangedFiles(): array
|
||||
{
|
||||
return array_keys($this->changedFiles);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getRemovedFiles(): array
|
||||
{
|
||||
return array_keys($this->removedFiles);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getChangedClasses(): array
|
||||
{
|
||||
return array_keys($this->changedClasses);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getRemovedClasses(): array
|
||||
{
|
||||
return array_keys($this->removedClasses);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChanges(): bool
|
||||
{
|
||||
return $this->changedFiles !== [] || $this->removedFiles !== [] || $this->changedClasses !== [] || $this->removedClasses !== [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'changed_files' => $this->getChangedFiles(),
|
||||
'removed_files' => $this->getRemovedFiles(),
|
||||
'changed_classes' => $this->getChangedClasses(),
|
||||
'removed_classes' => $this->getRemovedClasses(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+79
-6
@@ -14,6 +14,7 @@ use Closure;
|
||||
use Exception;
|
||||
use Kiri\Di\Interface\InjectTargetInterface;
|
||||
use Kiri\Router\Interface\ValidatorInterface;
|
||||
use Kiri\Server\Task\OnTaskFinish;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use ReflectionAttribute;
|
||||
use ReflectionClass;
|
||||
@@ -91,7 +92,8 @@ class Container implements ContainerInterface
|
||||
*/
|
||||
public function get(string $id): object
|
||||
{
|
||||
if (isset($this->_singletons[$id])) return $this->_singletons[$id];
|
||||
if (isset($this->_singletons[$id]))
|
||||
return $this->_singletons[$id];
|
||||
if (isset($this->_interfaces[$id])) {
|
||||
return $this->_singletons[$id] = $this->make($this->_interfaces[$id]);
|
||||
} else {
|
||||
@@ -151,6 +153,60 @@ class Container implements ContainerInterface
|
||||
}
|
||||
|
||||
|
||||
public function forgetReflection(string $className): void
|
||||
{
|
||||
unset($this->_reflection[$className]);
|
||||
}
|
||||
|
||||
|
||||
public function forgetMethodParams(string $className, ?string $method = null): void
|
||||
{
|
||||
if (!isset($this->_parameters[$className])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($method === null) {
|
||||
unset($this->_parameters[$className]);
|
||||
return;
|
||||
}
|
||||
|
||||
unset($this->_parameters[$className][$method]);
|
||||
if ($this->_parameters[$className] === []) {
|
||||
unset($this->_parameters[$className]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function forgetClass(string $className): void
|
||||
{
|
||||
unset($this->_singletons[$className]);
|
||||
$this->forgetReflection($className);
|
||||
$this->forgetMethodParams($className);
|
||||
}
|
||||
|
||||
|
||||
public function forgetNamespace(string $prefix): void
|
||||
{
|
||||
foreach (array_keys($this->_singletons) as $className) {
|
||||
if ($className !== ContainerInterface::class && str_starts_with($className, $prefix)) {
|
||||
unset($this->_singletons[$className]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_keys($this->_reflection) as $className) {
|
||||
if (str_starts_with($className, $prefix)) {
|
||||
unset($this->_reflection[$className]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_keys($this->_parameters) as $className) {
|
||||
if (str_starts_with($className, $prefix)) {
|
||||
unset($this->_parameters[$className]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
* @param array $construct
|
||||
@@ -165,15 +221,24 @@ class Container implements ContainerInterface
|
||||
throw new ReflectionException('Class ' . $className . ' cannot be instantiated');
|
||||
}
|
||||
|
||||
if (($handler = $reflect->getConstructor()) !== null) {
|
||||
if (empty($construct) && ($handler = $reflect->getConstructor()) !== null) {
|
||||
$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\Annotate\DeferRegistry::class) && \Kiri\Router\Annotate\DeferRegistry::hasAny($className);
|
||||
|
||||
if ($needsProxy) {
|
||||
$newInstance = \Kiri\Router\Annotate\DeferProxyGenerator::create($className, $construct);
|
||||
} else {
|
||||
$newInstance = $reflect->newInstanceArgs($construct);
|
||||
}
|
||||
|
||||
return $this->runInit($reflect, static::configure($newInstance, $config));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param ReflectionClass $reflect
|
||||
* @param array $construct
|
||||
@@ -191,10 +256,17 @@ class Container implements ContainerInterface
|
||||
throw new ReflectionException('Class ' . $reflect->getName() . ' cannot be instantiated');
|
||||
}
|
||||
|
||||
if (($handler = $reflect->getConstructor()) !== null) {
|
||||
if (empty($construct) && ($handler = $reflect->getConstructor()) !== null) {
|
||||
$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\Annotate\DeferRegistry::class) && \Kiri\Router\Annotate\DeferRegistry::hasAny($reflect->getName());
|
||||
|
||||
if ($needsProxy) {
|
||||
$newInstance = \Kiri\Router\Annotate\DeferProxyGenerator::create($reflect->getName(), $construct);
|
||||
} else {
|
||||
$newInstance = $reflect->newInstanceArgs($construct);
|
||||
}
|
||||
|
||||
return $this->runInit($reflect, static::configure($newInstance, $config));
|
||||
}
|
||||
@@ -304,7 +376,8 @@ class Container implements ContainerInterface
|
||||
{
|
||||
$className = $parameters->getDeclaringClass()->getName();
|
||||
$methodName = $parameters->getName();
|
||||
if (!isset($this->_parameters[$className])) $this->_parameters[$className] = [];
|
||||
if (!isset($this->_parameters[$className]))
|
||||
$this->_parameters[$className] = [];
|
||||
if (!isset($this->_parameters[$className][$methodName])) {
|
||||
return $this->_parameters[$className][$methodName] = $this->resolveMethodParams($parameters);
|
||||
} else {
|
||||
|
||||
@@ -14,7 +14,7 @@ class CoroutineContext implements ContextInterface
|
||||
*/
|
||||
public static function inCoroutine(): bool
|
||||
{
|
||||
return true;
|
||||
return Coroutine::getCid() > -1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Kiri\Di;
|
||||
|
||||
class HotReloadState
|
||||
{
|
||||
private const int MAX_AGE_SECONDS = 30;
|
||||
|
||||
public function store(array $changedFiles): void
|
||||
{
|
||||
$payload = [
|
||||
'timestamp' => time(),
|
||||
'changed_files' => $changedFiles
|
||||
|> array_filter(...)
|
||||
|> (fn($x) => array_map([$this, 'normalizePath'], $x))
|
||||
|> array_unique(...)
|
||||
|> array_values(...),
|
||||
];
|
||||
|
||||
$directory = dirname($this->getFilePath());
|
||||
if (!is_dir($directory)) {
|
||||
mkdir($directory, 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($this->getFilePath(), json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
public function consume(): array
|
||||
{
|
||||
return $this->peek();
|
||||
}
|
||||
|
||||
public function peek(): array
|
||||
{
|
||||
$file = $this->getFilePath();
|
||||
if (!file_exists($file)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = json_decode((string)file_get_contents($file), true);
|
||||
if (!is_array($data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$timestamp = (int)($data['timestamp'] ?? 0);
|
||||
if ($timestamp < 1 || (time() - $timestamp) > self::MAX_AGE_SECONDS) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = is_array($data['changed_files'] ?? null) ? $data['changed_files'] : [];
|
||||
return array_map([$this, 'normalizePath'], $files)
|
||||
|> array_unique(...)
|
||||
|> array_values(...);
|
||||
}
|
||||
|
||||
private function getFilePath(): string
|
||||
{
|
||||
$basePath = $this->normalizePath($_SERVER['PWD'] ?? APP_PATH ?? getcwd());
|
||||
$runtimePath = defined('APP_PATH')
|
||||
? rtrim(str_replace('\\', '/', APP_PATH), '/') . '/storage/.kiri-hot-reload/'
|
||||
: sys_get_temp_dir() . '/kiri-hot-reload/';
|
||||
|
||||
return $runtimePath . md5($basePath) . '.json';
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
$resolved = realpath($path) ?: $path;
|
||||
return str_replace('\\', '/', $resolved);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Kiri\Di;
|
||||
|
||||
class ScanManifest
|
||||
{
|
||||
private array $entries = [];
|
||||
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param int $mtime
|
||||
* @param array $classes
|
||||
* @return void
|
||||
*/
|
||||
public function set(string $path, int $mtime, array $classes): void
|
||||
{
|
||||
$this->entries[$path] = [
|
||||
'mtime' => $mtime,
|
||||
'classes' => array_values(array_unique($classes)),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $path): bool
|
||||
{
|
||||
return isset($this->entries[$path]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return int|null
|
||||
*/
|
||||
public function getMtime(string $path): ?int
|
||||
{
|
||||
return $this->entries[$path]['mtime'] ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return array
|
||||
*/
|
||||
public function getClasses(string $path): array
|
||||
{
|
||||
return $this->entries[$path]['classes'] ?? [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return array
|
||||
*/
|
||||
public function remove(string $path): array
|
||||
{
|
||||
$classes = $this->getClasses($path);
|
||||
unset($this->entries[$path]);
|
||||
return $classes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->entries;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string|null $prefix
|
||||
* @return array
|
||||
*/
|
||||
public function paths(?string $prefix = null): array
|
||||
{
|
||||
if ($prefix === null) {
|
||||
return array_keys($this->entries);
|
||||
}
|
||||
|
||||
return $this->entries
|
||||
|> array_keys(...)
|
||||
|> (fn($x) => array_filter($x, fn(string $path) => str_starts_with($path, $prefix)))
|
||||
|> array_values(...);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array $entries
|
||||
* @return void
|
||||
*/
|
||||
public function fromArray(array $entries): void
|
||||
{
|
||||
$this->entries = [];
|
||||
foreach ($entries as $path => $entry) {
|
||||
$mtime = (int)($entry['mtime'] ?? 0);
|
||||
$classes = is_array($entry['classes'] ?? null) ? $entry['classes'] : [];
|
||||
$this->set($path, $mtime, $classes);
|
||||
}
|
||||
}
|
||||
}
|
||||
+565
-118
@@ -2,146 +2,593 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace Kiri\Di;
|
||||
|
||||
use Kiri\Di\Inject\Container;
|
||||
use DirectoryIterator;
|
||||
use Kiri\Abstracts\Component;
|
||||
use Kiri\Di\Inject\Container;
|
||||
use Kiri\Di\Inject\Skip;
|
||||
use Kiri\Di\Interface\InjectMethodInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
use Throwable;
|
||||
|
||||
class Scanner extends Component
|
||||
{
|
||||
#[Container(ContainerInterface::class)]
|
||||
public ContainerInterface $container;
|
||||
|
||||
public array $files = [];
|
||||
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
#[Container(ContainerInterface::class)]
|
||||
public ContainerInterface $container;
|
||||
private array $fileMtimes = [];
|
||||
|
||||
private ?bool $hasOpcache = null;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public array $files = [];
|
||||
private string $basePath;
|
||||
|
||||
private ScanManifest $manifest;
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return void
|
||||
* @throws
|
||||
*/
|
||||
public function load_directory(string $path): void
|
||||
{
|
||||
$dir = new \DirectoryIterator($path);
|
||||
$skip = \config('scanner.skip', []);
|
||||
foreach ($dir as $value) {
|
||||
if ($value->isDot() || str_starts_with($value->getFilename(), '.')) {
|
||||
continue;
|
||||
}
|
||||
if ($value->isDir()) {
|
||||
if (in_array($value->getRealPath() . '/', $skip)) {
|
||||
continue;
|
||||
}
|
||||
$this->load_directory($value->getRealPath());
|
||||
} else if ($value->getExtension() == 'php') {
|
||||
if (in_array($value->getRealPath(), $this->files)) {
|
||||
continue;
|
||||
}
|
||||
$this->files[] = $value->getRealPath();
|
||||
$this->load_file($value->getRealPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
private ?ChangeSet $changeSet = null;
|
||||
|
||||
private array $visitedFiles = [];
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
* @return string
|
||||
*/
|
||||
private function rename(string $file): string
|
||||
{
|
||||
$filter = array_filter(explode('/', $file), function ($value) {
|
||||
if (empty($value)) {
|
||||
return false;
|
||||
}
|
||||
return ucfirst($value);
|
||||
});
|
||||
return ucfirst(implode('\\', $filter));
|
||||
}
|
||||
private array $config = [
|
||||
'skip_patterns' => ['/vendor/', '/tests/', '/cache/', '/node_modules/'],
|
||||
'skip_directories' => [],
|
||||
'extensions' => ['php'],
|
||||
'max_depth' => 20,
|
||||
'follow_links' => false,
|
||||
'cache_enabled' => false,
|
||||
'cache_ttl' => 3600,
|
||||
'debug' => false,
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->basePath = $this->normalizePath($_SERVER['PWD'] ?? APP_PATH ?? getcwd());
|
||||
$this->manifest = new ScanManifest();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @return void
|
||||
* @throws
|
||||
*/
|
||||
private function load_file(string $path): void
|
||||
{
|
||||
try {
|
||||
require_once "$path";
|
||||
$path = str_replace($_SERVER['PWD'], '', $path);
|
||||
$path = str_replace('.php', '', $path);
|
||||
$this->parseFile($path);
|
||||
} catch (\Throwable $throwable) {
|
||||
error($throwable);
|
||||
}
|
||||
}
|
||||
public function setConfig(array $config): void
|
||||
{
|
||||
$this->config = array_merge($this->config, $config);
|
||||
}
|
||||
|
||||
public function scan(string $path, ?string $cacheFile = null): ChangeSet
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
$this->changeSet = new ChangeSet();
|
||||
$this->visitedFiles = [];
|
||||
$cacheLoaded = false;
|
||||
|
||||
/**
|
||||
* @param $file
|
||||
* @return void
|
||||
* @throws
|
||||
*/
|
||||
protected function parseFile($file): void
|
||||
{
|
||||
$class = $this->rename($file);
|
||||
if (class_exists($class)) {
|
||||
$reflect = $this->container->getReflectionClass($class);
|
||||
if ($reflect->isInstantiable()) {
|
||||
if ($reflect->isTrait() || $reflect->isEnum() || $reflect->isInterface()) {
|
||||
return;
|
||||
}
|
||||
$attributes = $this->skipNames($reflect);
|
||||
if (in_array(Skip::class, $attributes) || in_array(\Attribute::class, $attributes)) {
|
||||
return;
|
||||
}
|
||||
foreach ($reflect->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
if ($method->isStatic() || $method->getDeclaringClass()->getName() != $class) {
|
||||
continue;
|
||||
}
|
||||
$attributes = $method->getAttributes();
|
||||
foreach ($attributes as $attribute) {
|
||||
if (!class_exists($attribute->getName())) {
|
||||
continue;
|
||||
}
|
||||
$instance = $attribute->newInstance();
|
||||
if (!method_exists($instance, 'dispatch')) {
|
||||
continue;
|
||||
}
|
||||
$instance->dispatch($class, $method->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($this->config['cache_enabled']) {
|
||||
$cacheFile = $cacheFile ?? $this->getDefaultCacheFile();
|
||||
if ($this->loadFromCache($cacheFile, $path)) {
|
||||
$this->replayManifest($path);
|
||||
return $this->changeSet;
|
||||
}
|
||||
$cacheLoaded = $this->manifest->all() !== [];
|
||||
}
|
||||
|
||||
$this->scanDirectory($path);
|
||||
$this->detectRemovedFiles($path);
|
||||
|
||||
/**
|
||||
* @param ReflectionClass $reflect
|
||||
* @return array
|
||||
*/
|
||||
protected function skipNames(ReflectionClass $reflect): array
|
||||
{
|
||||
$attributes = $reflect->getAttributes();
|
||||
$names = [];
|
||||
foreach ($attributes as $attribute) {
|
||||
$names[] = $attribute->getName();
|
||||
}
|
||||
return $names;
|
||||
}
|
||||
if ($cacheLoaded) {
|
||||
$this->replayManifest($path, $this->changeSet->getChangedFiles());
|
||||
}
|
||||
|
||||
$this->syncLegacyState();
|
||||
|
||||
if ($this->config['cache_enabled']) {
|
||||
$this->saveToCache($cacheFile);
|
||||
}
|
||||
|
||||
return $this->changeSet;
|
||||
}
|
||||
|
||||
public function scanFiles(array $files, ?string $scopePath = null, ?string $cacheFile = null): ChangeSet
|
||||
{
|
||||
$this->changeSet = new ChangeSet();
|
||||
$this->visitedFiles = [];
|
||||
$cacheLoaded = false;
|
||||
|
||||
if ($scopePath !== null) {
|
||||
$scopePath = $this->normalizePath($scopePath);
|
||||
}
|
||||
|
||||
if ($this->config['cache_enabled'] && $scopePath !== null) {
|
||||
$cacheFile = $cacheFile ?? $this->getDefaultCacheFile();
|
||||
$this->loadFromCache($cacheFile, $scopePath);
|
||||
$cacheLoaded = $this->manifest->all() !== [];
|
||||
}
|
||||
|
||||
foreach (array_values(array_unique($files)) as $file) {
|
||||
$path = $this->normalizePath($file);
|
||||
if (file_exists($path)) {
|
||||
$this->processFile($path);
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->markRemovedFile($path);
|
||||
}
|
||||
|
||||
if ($cacheLoaded && $scopePath !== null) {
|
||||
$this->replayManifest($scopePath, $this->changeSet->getChangedFiles());
|
||||
}
|
||||
|
||||
$this->syncLegacyState();
|
||||
return $this->changeSet;
|
||||
}
|
||||
|
||||
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 = $this->normalizePath($item->getRealPath() ?: $item->getPathname());
|
||||
if ($item->isDir()) {
|
||||
if ($this->shouldSkipDirectory($realPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($item->isLink() && !$this->config['follow_links']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->scanDirectory($realPath, $depth + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($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($this->normalizePath($path), '/') . '/';
|
||||
$skipDirs = array_merge(
|
||||
$this->config['skip_directories'],
|
||||
config('site.scanner.skip', [])
|
||||
);
|
||||
|
||||
if (in_array($path, $skipDirs, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->config['skip_patterns'] as $pattern) {
|
||||
if (strpos($path, $pattern) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function processFile(string $path): void
|
||||
{
|
||||
$path = $this->normalizePath($path);
|
||||
if (!in_array(pathinfo($path, PATHINFO_EXTENSION), $this->config['extensions'], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->visitedFiles[$path] = true;
|
||||
if (!$this->shouldProcessFile($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$oldClasses = $this->manifest->getClasses($path);
|
||||
$this->invalidateClasses($oldClasses);
|
||||
$newClasses = $this->loadAndParseFile($path);
|
||||
|
||||
$this->manifest->set($path, (int)filemtime($path), $newClasses);
|
||||
$this->changeSet?->addChangedFile($path);
|
||||
|
||||
foreach (array_diff($oldClasses, $newClasses) as $class) {
|
||||
$this->changeSet?->addRemovedClass($class);
|
||||
}
|
||||
|
||||
foreach ($newClasses as $class) {
|
||||
$this->changeSet?->addChangedClass($class);
|
||||
}
|
||||
} 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 = (int)filemtime($path);
|
||||
$cachedMtime = $this->manifest->getMtime($path);
|
||||
|
||||
return $cachedMtime === null || $cachedMtime < $mtime;
|
||||
}
|
||||
|
||||
private function loadAndParseFile(string $path): array
|
||||
{
|
||||
$this->optimizeWithOpcache($path);
|
||||
|
||||
$before = get_declared_classes();
|
||||
require_once $path;
|
||||
$after = get_declared_classes();
|
||||
|
||||
$classes = array_values(array_diff($after, $before));
|
||||
foreach ($classes as $class) {
|
||||
if (class_exists($class)) {
|
||||
$this->analyzeClass($class);
|
||||
}
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
private function optimizeWithOpcache(string $path): void
|
||||
{
|
||||
if ($this->hasOpcache === null) {
|
||||
$this->hasOpcache = function_exists('opcache_invalidate') && function_exists('opcache_compile_file');
|
||||
}
|
||||
|
||||
if (!$this->hasOpcache) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
opcache_invalidate($path, true);
|
||||
opcache_compile_file($path);
|
||||
} catch (Throwable $e) {
|
||||
if ($this->config['debug']) {
|
||||
$this->logError($e, [
|
||||
'file' => $path,
|
||||
'action' => 'opcache_optimize',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
foreach ($reflect->getMethods() 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 ($instance instanceof InjectMethodInterface) {
|
||||
$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 = defined('APP_PATH')
|
||||
? rtrim(str_replace('\\', '/', APP_PATH), '/') . '/storage/.kiri-scanner-cache/'
|
||||
: 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;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents($cacheFile), true);
|
||||
if (!is_array($data) || !isset($data['manifest'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->manifest->fromArray($data['manifest']);
|
||||
$this->syncLegacyState();
|
||||
|
||||
return !$this->hasDirectoryChanged($path);
|
||||
}
|
||||
|
||||
private function hasDirectoryChanged(string $path): bool
|
||||
{
|
||||
$currentFiles = $this->collectFiles($path);
|
||||
$cachedFiles = $this->manifest->paths(rtrim($path, '/\\') . '/');
|
||||
|
||||
sort($currentFiles);
|
||||
sort($cachedFiles);
|
||||
if ($currentFiles !== $cachedFiles) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($currentFiles as $file) {
|
||||
if (!file_exists($file)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->manifest->getMtime($file) !== (int)filemtime($file)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function saveToCache(string $cacheFile): void
|
||||
{
|
||||
$data = [
|
||||
'manifest' => $this->manifest->all(),
|
||||
'timestamp' => time(),
|
||||
'version' => '2.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) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function logError(Throwable $e, array $context = []): void
|
||||
{
|
||||
$fullContext = array_merge($context, [
|
||||
'message' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'time' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
if (function_exists('error')) {
|
||||
error($e, $fullContext);
|
||||
} else {
|
||||
error_log(json_encode($fullContext));
|
||||
}
|
||||
|
||||
if ($this->config['debug'] && php_sapi_name() === 'cli') {
|
||||
echo "Scanner Error: {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}\n";
|
||||
if ($context !== []) {
|
||||
echo 'Context: ' . json_encode($context) . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function load_directory(string $path): void
|
||||
{
|
||||
$this->scan($path);
|
||||
}
|
||||
|
||||
protected function parseFile($file): void
|
||||
{
|
||||
$absolutePath = $this->normalizePath($this->basePath . '/' . ltrim((string)$file, '/')) . '.php';
|
||||
if (file_exists($absolutePath)) {
|
||||
$this->processFile($absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
protected function skipNames(ReflectionClass $reflect): array
|
||||
{
|
||||
return array_map(fn($attr) => $attr->getName(), $reflect->getAttributes());
|
||||
}
|
||||
|
||||
public function getStats(): array
|
||||
{
|
||||
return [
|
||||
'total_files' => count($this->files),
|
||||
'cached_mtimes' => count($this->fileMtimes),
|
||||
'base_path' => $this->basePath,
|
||||
'config' => $this->config,
|
||||
'manifest_entries' => count($this->manifest->all()),
|
||||
];
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->files = [];
|
||||
$this->fileMtimes = [];
|
||||
$this->hasOpcache = null;
|
||||
$this->manifest = new ScanManifest();
|
||||
$this->changeSet = null;
|
||||
$this->visitedFiles = [];
|
||||
}
|
||||
|
||||
private function detectRemovedFiles(string $path): void
|
||||
{
|
||||
$prefix = rtrim($path, '/\\') . '/';
|
||||
foreach ($this->manifest->paths($prefix) as $file) {
|
||||
if (!isset($this->visitedFiles[$file])) {
|
||||
$this->markRemovedFile($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function markRemovedFile(string $path): void
|
||||
{
|
||||
if (!$this->manifest->has($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$classes = $this->manifest->remove($path);
|
||||
$this->invalidateClasses($classes);
|
||||
foreach ($classes as $class) {
|
||||
$this->changeSet?->addRemovedClass($class);
|
||||
}
|
||||
|
||||
$this->changeSet?->addRemovedFile($path);
|
||||
}
|
||||
|
||||
private function syncLegacyState(): void
|
||||
{
|
||||
$this->files = array_keys($this->manifest->all());
|
||||
$this->fileMtimes = [];
|
||||
foreach ($this->manifest->all() as $path => $entry) {
|
||||
$this->fileMtimes[$path] = (int)$entry['mtime'];
|
||||
}
|
||||
}
|
||||
|
||||
private function collectFiles(string $path, int $depth = 0): array
|
||||
{
|
||||
if ($depth > $this->config['max_depth']) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = [];
|
||||
try {
|
||||
$dir = new DirectoryIterator($path);
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($dir as $item) {
|
||||
if ($this->shouldSkipItem($item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$realPath = $this->normalizePath($item->getRealPath() ?: $item->getPathname());
|
||||
if ($item->isDir()) {
|
||||
if ($this->shouldSkipDirectory($realPath)) {
|
||||
continue;
|
||||
}
|
||||
if ($item->isLink() && !$this->config['follow_links']) {
|
||||
continue;
|
||||
}
|
||||
$files = array_merge($files, $this->collectFiles($realPath, $depth + 1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($item->isFile() && in_array(pathinfo($realPath, PATHINFO_EXTENSION), $this->config['extensions'], true)) {
|
||||
$files[] = $realPath;
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string
|
||||
{
|
||||
$resolved = realpath($path) ?: $path;
|
||||
return str_replace('\\', '/', $resolved);
|
||||
}
|
||||
|
||||
private function replayManifest(string $path, array $skipPaths = []): void
|
||||
{
|
||||
$skip = array_fill_keys(array_map([$this, 'normalizePath'], $skipPaths), true);
|
||||
$prefix = rtrim($path, '/\\') . '/';
|
||||
|
||||
foreach ($this->manifest->paths($prefix) as $file) {
|
||||
if (isset($skip[$file])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->manifest->getClasses($file) as $class) {
|
||||
if (class_exists($class)) {
|
||||
$this->analyzeClass($class);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function invalidateClasses(array $classes): void
|
||||
{
|
||||
foreach ($classes as $class) {
|
||||
if (method_exists($this->container, 'forgetClass')) {
|
||||
$this->container->forgetClass($class);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
"php": ">=8.5",
|
||||
"psr/container": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
|
||||
Reference in New Issue
Block a user