Fixed: basePath self→static binding, namespace detection, event wiring,
SQLite cache, file cache driver. All Mod Boot classes converted to
$listens pattern for lifecycle event discovery.
Working endpoints:
- /v1/explorer/info — live chain height, difficulty, aliases
- /v1/explorer/stats — formatted chain statistics
- /v1/names/directory — alias directory grouped by type
- /v1/names/available/{name} — name availability check
- /v1/names/lookup/{name} — name details
Co-Authored-By: Charon <charon@lethean.io>
181 lines
5 KiB
PHP
181 lines
5 KiB
PHP
<?php
|
|
|
|
/*
|
|
* Core PHP Framework
|
|
*
|
|
* Licensed under the European Union Public Licence (EUPL) v1.2.
|
|
* See LICENSE file for details.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Core\Actions;
|
|
|
|
use Core\ModuleScanner;
|
|
use RecursiveDirectoryIterator;
|
|
use RecursiveIteratorIterator;
|
|
use ReflectionClass;
|
|
|
|
/**
|
|
* Scans directories for Action classes with the #[Scheduled] attribute.
|
|
*
|
|
* Unlike ModuleScanner (which scans Boot.php files), this scanner finds
|
|
* any PHP class with the #[Scheduled] attribute in the given directories.
|
|
*
|
|
* It uses PHP's native reflection to read attributes — no file parsing.
|
|
*
|
|
* @see Scheduled The attribute this scanner discovers
|
|
* @see ModuleScanner Similar pattern for Boot.php discovery
|
|
*/
|
|
class ScheduledActionScanner
|
|
{
|
|
/**
|
|
* Scan directories for classes with #[Scheduled] attribute.
|
|
*
|
|
* @param array<string> $paths Directories to scan recursively
|
|
* @return array<class-string, Scheduled> Map of class name to attribute instance
|
|
*/
|
|
public function scan(array $paths): array
|
|
{
|
|
$results = [];
|
|
|
|
foreach ($paths as $path) {
|
|
if (! is_dir($path)) {
|
|
continue;
|
|
}
|
|
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS)
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
if ($file->getExtension() !== 'php') {
|
|
continue;
|
|
}
|
|
|
|
// Skip test directories — test files extend base classes
|
|
// that aren't available without dev dependencies.
|
|
// Convention: module test dirs use capital "Tests/" (e.g. app/Mod/Lem/Tests/).
|
|
if (preg_match('#[/\\\\]Tests[/\\\\]#', $file->getPathname())
|
|
|| str_ends_with($file->getBasename(), 'Test.php')) {
|
|
continue;
|
|
}
|
|
|
|
$contents = file_get_contents($file->getPathname());
|
|
|
|
if ($contents === false || ! str_contains($contents, '#[Scheduled')) {
|
|
continue;
|
|
}
|
|
|
|
$class = $this->classFromFile($file->getPathname(), $contents);
|
|
|
|
if ($class === null) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
if (! class_exists($class)) {
|
|
continue;
|
|
}
|
|
} catch (\Throwable) {
|
|
// Class may reference unavailable dependencies (e.g. dev-only)
|
|
continue;
|
|
}
|
|
|
|
$attribute = $this->extractScheduled($class);
|
|
|
|
if ($attribute !== null) {
|
|
$results[$class] = $attribute;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Extract the #[Scheduled] attribute from a class.
|
|
*/
|
|
private function extractScheduled(string $class): ?Scheduled
|
|
{
|
|
try {
|
|
$ref = new ReflectionClass($class);
|
|
$attrs = $ref->getAttributes(Scheduled::class);
|
|
|
|
if (empty($attrs)) {
|
|
return null;
|
|
}
|
|
|
|
return $attrs[0]->newInstance();
|
|
} catch (\ReflectionException) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Derive fully qualified class name from a PHP file.
|
|
*
|
|
* Reads the file's namespace declaration and class name.
|
|
*/
|
|
private function classFromFile(string $file, ?string $contents = null): ?string
|
|
{
|
|
$contents ??= file_get_contents($file);
|
|
|
|
if ($contents === false) {
|
|
return null;
|
|
}
|
|
|
|
$tokens = token_get_all($contents);
|
|
$namespace = null;
|
|
$class = null;
|
|
|
|
$count = count($tokens);
|
|
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$token = $tokens[$i];
|
|
|
|
if (! is_array($token)) {
|
|
continue;
|
|
}
|
|
|
|
if ($token[0] === T_NAMESPACE) {
|
|
$namespaceParts = [];
|
|
|
|
for ($j = $i + 1; $j < $count; $j++) {
|
|
$t = $tokens[$j];
|
|
|
|
if (is_array($t) && in_array($t[0], [T_NAME_QUALIFIED, T_STRING, T_NS_SEPARATOR], true)) {
|
|
$namespaceParts[] = $t[1];
|
|
} elseif ($t === ';' || $t === '{') {
|
|
break;
|
|
}
|
|
}
|
|
|
|
$namespace = implode('', $namespaceParts) ?: null;
|
|
}
|
|
|
|
if ($token[0] === T_CLASS) {
|
|
for ($j = $i + 1; $j < $count; $j++) {
|
|
$t = $tokens[$j];
|
|
|
|
if (is_array($t) && $t[0] === T_WHITESPACE) {
|
|
continue;
|
|
}
|
|
if (is_array($t) && $t[0] === T_STRING) {
|
|
$class = $t[1];
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($class === null) {
|
|
return null;
|
|
}
|
|
|
|
return $namespace !== null ? "{$namespace}\\{$class}" : $class;
|
|
}
|
|
}
|