mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-03-17 20:34:51 +08:00
Introduce AttributeMapper for managing extensions and doctor attributes
This commit is contained in:
parent
e28580de00
commit
722bb31815
@ -46,20 +46,14 @@ abstract class BuilderBase
|
||||
/**
|
||||
* Convert libraries to class
|
||||
*
|
||||
* @param array<string> $sorted_libraries Libraries to build (if not empty, must sort first)
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
* @throws WrongUsageException
|
||||
* @param array<string> $sorted_libraries Libraries to build (if not empty, must sort first)
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract public function proveLibs(array $sorted_libraries);
|
||||
|
||||
/**
|
||||
* Set-Up libraries
|
||||
*
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
* @throws WrongUsageException
|
||||
*/
|
||||
public function setupLibs(): void
|
||||
{
|
||||
@ -139,9 +133,6 @@ abstract class BuilderBase
|
||||
|
||||
/**
|
||||
* Check if there is a cpp extensions or libraries.
|
||||
*
|
||||
* @throws FileSystemException
|
||||
* @throws WrongUsageException
|
||||
*/
|
||||
public function hasCpp(): bool
|
||||
{
|
||||
@ -174,15 +165,10 @@ abstract class BuilderBase
|
||||
/**
|
||||
* Verify the list of "ext" extensions for validity and declare an Extension object to check the dependencies of the extensions.
|
||||
*
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
* @throws \ReflectionException
|
||||
* @throws \Throwable|WrongUsageException
|
||||
* @internal
|
||||
*/
|
||||
public function proveExts(array $static_extensions, array $shared_extensions = [], bool $skip_check_deps = false, bool $skip_extract = false): void
|
||||
{
|
||||
CustomExt::loadCustomExt();
|
||||
// judge ext
|
||||
foreach ($static_extensions as $ext) {
|
||||
// if extension does not support static build, throw exception
|
||||
@ -213,7 +199,7 @@ abstract class BuilderBase
|
||||
}
|
||||
|
||||
foreach ([...$static_extensions, ...$shared_extensions] as $extension) {
|
||||
$class = CustomExt::getExtClass($extension);
|
||||
$class = AttributeMapper::getExtensionClassByName($extension) ?? Extension::class;
|
||||
/** @var Extension $ext */
|
||||
$ext = new $class($extension, $this);
|
||||
if (in_array($extension, $static_extensions)) {
|
||||
@ -247,11 +233,6 @@ abstract class BuilderBase
|
||||
*/
|
||||
abstract public function testPHP(int $build_target = BUILD_TARGET_NONE);
|
||||
|
||||
/**
|
||||
* @throws WrongUsageException
|
||||
* @throws RuntimeException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public function buildSharedExts(): void
|
||||
{
|
||||
$lines = file(BUILD_BIN_PATH . '/php-config');
|
||||
@ -284,9 +265,6 @@ abstract class BuilderBase
|
||||
/**
|
||||
* Generate extension enable arguments for configure.
|
||||
* e.g. --enable-mbstring
|
||||
*
|
||||
* @throws FileSystemException
|
||||
* @throws WrongUsageException
|
||||
*/
|
||||
public function makeStaticExtensionArgs(): string
|
||||
{
|
||||
@ -321,9 +299,6 @@ abstract class BuilderBase
|
||||
|
||||
/**
|
||||
* Get PHP Version ID from php-src/main/php_version.h
|
||||
*
|
||||
* @throws RuntimeException
|
||||
* @throws WrongUsageException
|
||||
*/
|
||||
public function getPHPVersionID(): int
|
||||
{
|
||||
|
||||
@ -39,9 +39,6 @@ class imap extends LinuxLibraryBase
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function build(): void
|
||||
{
|
||||
if ($this->builder->getLib('openssl')) {
|
||||
|
||||
@ -69,9 +69,6 @@ abstract class BaseCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WrongUsageException
|
||||
*/
|
||||
abstract public function handle(): int;
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
@ -79,22 +76,17 @@ abstract class BaseCommand extends Command
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
|
||||
global $ob_logger;
|
||||
if ($input->getOption('debug') || $output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) {
|
||||
$ob_logger = new ConsoleLogger(LogLevel::DEBUG, decorated: !$input->getOption('no-ansi'));
|
||||
define('DEBUG_MODE', true);
|
||||
} else {
|
||||
$ob_logger = new ConsoleLogger(decorated: !$input->getOption('no-ansi'));
|
||||
}
|
||||
// init log
|
||||
$this->initLogFiles();
|
||||
|
||||
// windows fallback
|
||||
Prompt::fallbackWhen(PHP_OS_FAMILY === 'Windows');
|
||||
ConfirmPrompt::fallbackUsing(function (ConfirmPrompt $prompt) use ($input, $output) {
|
||||
$helper = new QuestionHelper();
|
||||
$case = $prompt->default ? ' [Y/n] ' : ' [y/N] ';
|
||||
$question = new ConfirmationQuestion($prompt->label . $case, $prompt->default);
|
||||
return $helper->ask($input, $output, $question);
|
||||
});
|
||||
// init logger
|
||||
$this->initConsoleLogger();
|
||||
|
||||
// load attribute maps
|
||||
AttributeMapper::init();
|
||||
|
||||
// init windows fallback
|
||||
$this->initWindowsPromptFallback($input, $output);
|
||||
|
||||
// init GlobalEnv
|
||||
if (!$this instanceof BuildCommand) {
|
||||
@ -178,4 +170,58 @@ abstract class BaseCommand extends Command
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize spc log files.
|
||||
*/
|
||||
private function initLogFiles(): void
|
||||
{
|
||||
$log_dir = SPC_LOGS_DIR;
|
||||
if (!file_exists($log_dir)) {
|
||||
mkdir($log_dir, 0755, true);
|
||||
} elseif (!$this->getOption('preserve-log')) {
|
||||
// Clean up old log files
|
||||
$files = glob($log_dir . '/*.log');
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize console logger.
|
||||
*/
|
||||
private function initConsoleLogger(): void
|
||||
{
|
||||
global $ob_logger;
|
||||
if ($this->input->getOption('debug') || $this->output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) {
|
||||
$ob_logger = new ConsoleLogger(LogLevel::DEBUG, decorated: !$this->input->getOption('no-ansi'));
|
||||
define('DEBUG_MODE', true);
|
||||
} else {
|
||||
$ob_logger = new ConsoleLogger(decorated: !$this->input->getOption('no-ansi'));
|
||||
}
|
||||
$log_file_fd = fopen(SPC_OUTPUT_LOG, 'a');
|
||||
$ob_logger->addLogCallback(function ($level, $output) use ($log_file_fd) {
|
||||
if ($log_file_fd) {
|
||||
fwrite($log_file_fd, strip_ansi_colors($output) . "\n");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Windows prompt fallback for laravel-prompts.
|
||||
*/
|
||||
private function initWindowsPromptFallback(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
Prompt::fallbackWhen(PHP_OS_FAMILY === 'Windows');
|
||||
ConfirmPrompt::fallbackUsing(function (ConfirmPrompt $prompt) use ($input, $output) {
|
||||
$helper = new QuestionHelper();
|
||||
$case = $prompt->default ? ' [Y/n] ' : ' [y/N] ';
|
||||
$question = new ConfirmationQuestion($prompt->label . $case, $prompt->default);
|
||||
return $helper->ask($input, $output, $question);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,10 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace SPC\command;
|
||||
|
||||
use SPC\doctor\CheckListHandler;
|
||||
use SPC\doctor\CheckResult;
|
||||
use SPC\exception\RuntimeException;
|
||||
use SPC\doctor\DoctorHandler;
|
||||
use SPC\util\AttributeMapper;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
|
||||
@ -16,72 +18,78 @@ class DoctorCommand extends BaseCommand
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->addOption('auto-fix', null, null, 'Automatically fix failed items (if possible)');
|
||||
$this->addOption('auto-fix', null, InputOption::VALUE_OPTIONAL, 'Automatically fix failed items (if possible)', false);
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
try {
|
||||
$checker = new CheckListHandler();
|
||||
// skipped items
|
||||
$skip_items = array_filter(explode(',', getenv('SPC_SKIP_DOCTOR_CHECK_ITEMS') ?: ''));
|
||||
$fix_policy = match ($this->input->getOption('auto-fix')) {
|
||||
'never' => FIX_POLICY_DIE,
|
||||
true, null => FIX_POLICY_AUTOFIX,
|
||||
default => FIX_POLICY_PROMPT,
|
||||
};
|
||||
$fix_map = AttributeMapper::getDoctorFixMap();
|
||||
|
||||
$fix_policy = $this->input->getOption('auto-fix') ? FIX_POLICY_AUTOFIX : FIX_POLICY_PROMPT;
|
||||
foreach ($checker->runChecks() as $check) {
|
||||
if ($check->limit_os !== null && $check->limit_os !== PHP_OS_FAMILY) {
|
||||
continue;
|
||||
foreach (DoctorHandler::getValidCheckList() as $check) {
|
||||
// output
|
||||
$this->output->write("Checking <comment>{$check->item_name}</comment> ... ");
|
||||
|
||||
// null => skipped
|
||||
if (($result = call_user_func($check->callback)) === null) {
|
||||
$this->output->writeln('skipped');
|
||||
continue;
|
||||
}
|
||||
// invalid return value => skipped
|
||||
if (!$result instanceof CheckResult) {
|
||||
$this->output->writeln('<error>Skipped due to invalid return value</error>');
|
||||
continue;
|
||||
}
|
||||
// true => OK
|
||||
if ($result->isOK()) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->output->writeln($result->getMessage() ?? (string) ConsoleColor::green('✓'));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Failed => output error message
|
||||
$this->output->writeln('<error>' . $result->getMessage() . '</error>');
|
||||
// If the result is not fixable, fail immediately
|
||||
if ($result->getFixItem() === '') {
|
||||
$this->output->writeln('This check item can not be fixed !');
|
||||
return static::FAILURE;
|
||||
}
|
||||
if (!isset($fix_map[$result->getFixItem()])) {
|
||||
$this->output->writeln("<error>Internal error: Unknown fix item: {$result->getFixItem()}</error>");
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
// prompt for fix
|
||||
if ($fix_policy === FIX_POLICY_PROMPT) {
|
||||
if (!confirm('Do you want to fix it?')) {
|
||||
$this->output->writeln('<comment>You canceled fix.</comment>');
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
$this->output->write('Checking <comment>' . $check->item_name . '</comment> ... ');
|
||||
|
||||
// check if this item is skipped
|
||||
if (in_array($check->item_name, $skip_items) || ($result = call_user_func($check->callback)) === null) {
|
||||
$this->output->writeln('skipped');
|
||||
} elseif ($result instanceof CheckResult) {
|
||||
if ($result->isOK()) {
|
||||
$this->output->writeln($result->getMessage() ?? 'ok');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Failed
|
||||
$this->output->writeln('<error>' . $result->getMessage() . '</error>');
|
||||
switch ($fix_policy) {
|
||||
case FIX_POLICY_DIE:
|
||||
throw new RuntimeException('Some check items can not be fixed !');
|
||||
case FIX_POLICY_PROMPT:
|
||||
if ($result->getFixItem() !== '') {
|
||||
$question = confirm('Do you want to fix it?');
|
||||
if ($question) {
|
||||
$checker->emitFix($this->output, $result);
|
||||
} else {
|
||||
throw new RuntimeException('You cancelled fix');
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException('Some check items can not be fixed !');
|
||||
}
|
||||
break;
|
||||
case FIX_POLICY_AUTOFIX:
|
||||
if ($result->getFixItem() !== '') {
|
||||
$this->output->writeln('Automatically fixing ' . $result->getFixItem() . ' ...');
|
||||
$checker->emitFix($this->output, $result);
|
||||
} else {
|
||||
throw new RuntimeException('Some check items can not be fixed !');
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (DoctorHandler::emitFix($this->output, $result)) {
|
||||
$this->output->writeln('<info>Fix applied successfully!</info>');
|
||||
} else {
|
||||
$this->output->writeln('<error>Failed to apply fix!</error>');
|
||||
return static::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
$this->output->writeln('<info>Doctor check complete !</info>');
|
||||
} catch (\Throwable $e) {
|
||||
$this->output->writeln('<error>' . $e->getMessage() . '</error>');
|
||||
|
||||
if (extension_loaded('pcntl')) {
|
||||
pcntl_signal(SIGINT, SIG_IGN);
|
||||
// auto fix
|
||||
if ($fix_policy === FIX_POLICY_AUTOFIX) {
|
||||
$this->output->writeln('Automatically fixing ' . $result->getFixItem() . ' ...');
|
||||
if (DoctorHandler::emitFix($this->output, $result)) {
|
||||
$this->output->writeln('<info>Fix applied successfully!</info>');
|
||||
} else {
|
||||
$this->output->writeln('<error>Failed to apply fix!</error>');
|
||||
return static::FAILURE;
|
||||
}
|
||||
}
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
$this->output->writeln('<info>Doctor check complete !</info>');
|
||||
return static::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace SPC\doctor;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
class AsFixItem
|
||||
{
|
||||
public function __construct(public string $name) {}
|
||||
|
||||
@ -1,111 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\doctor;
|
||||
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
use SPC\store\FileSystem;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class CheckListHandler
|
||||
{
|
||||
/** @var AsCheckItem[] */
|
||||
private array $check_list = [];
|
||||
|
||||
private array $fix_map = [];
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
/**
|
||||
* @return array<AsCheckItem>
|
||||
* @throws \ReflectionException
|
||||
* @throws RuntimeException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public function runChecks(bool $include_manual = false): array
|
||||
{
|
||||
return $this->loadCheckList($include_manual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function emitFix(OutputInterface $output, CheckResult $result): void
|
||||
{
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
sapi_windows_set_ctrl_handler(function () use ($output) {
|
||||
$output->writeln('<error>You cancelled fix</error>');
|
||||
});
|
||||
} elseif (extension_loaded('pcntl')) {
|
||||
pcntl_signal(SIGINT, function () use ($output) {
|
||||
$output->writeln('<error>You cancelled fix</error>');
|
||||
});
|
||||
}
|
||||
|
||||
$fix_result = call_user_func($this->fix_map[$result->getFixItem()], ...$result->getFixParams());
|
||||
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
sapi_windows_set_ctrl_handler(null);
|
||||
} elseif (extension_loaded('pcntl')) {
|
||||
pcntl_signal(SIGINT, SIG_IGN);
|
||||
}
|
||||
|
||||
if ($fix_result) {
|
||||
$output->writeln('<info>Fix done</info>');
|
||||
} else {
|
||||
$output->writeln('<error>Fix failed</error>');
|
||||
throw new RuntimeException('Some check item are not fixed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Doctor check item list
|
||||
*
|
||||
* @return array<AsCheckItem>
|
||||
* @throws \ReflectionException
|
||||
* @throws RuntimeException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
private function loadCheckList(bool $include_manual = false): array
|
||||
{
|
||||
foreach (FileSystem::getClassesPsr4(__DIR__ . '/item', 'SPC\doctor\item') as $class) {
|
||||
$ref = new \ReflectionClass($class);
|
||||
$optional = $ref->getAttributes(OptionalCheck::class)[0] ?? null;
|
||||
if ($optional !== null) {
|
||||
/** @var OptionalCheck $instance */
|
||||
$instance = $optional->newInstance();
|
||||
if (is_callable($instance->check) && !call_user_func($instance->check)) {
|
||||
continue; // skip this class if optional check is false
|
||||
}
|
||||
}
|
||||
foreach ($ref->getMethods() as $method) {
|
||||
foreach ($method->getAttributes() as $a) {
|
||||
if (is_a($a->getName(), AsCheckItem::class, true)) {
|
||||
/** @var AsCheckItem $instance */
|
||||
$instance = $a->newInstance();
|
||||
if (!$include_manual && $instance->manual) {
|
||||
continue;
|
||||
}
|
||||
$instance->callback = [new $class(), $method->getName()];
|
||||
$this->check_list[] = $instance;
|
||||
} elseif (is_a($a->getName(), AsFixItem::class, true)) {
|
||||
/** @var AsFixItem $instance */
|
||||
$instance = $a->newInstance();
|
||||
// Redundant fix item
|
||||
if (isset($this->fix_map[$instance->name])) {
|
||||
throw new RuntimeException('Redundant doctor fix item: ' . $instance->name);
|
||||
}
|
||||
$this->fix_map[$instance->name] = [new $class(), $method->getName()];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort check list by level
|
||||
usort($this->check_list, fn (AsCheckItem $a, AsCheckItem $b) => $a->level > $b->level ? -1 : ($a->level == $b->level ? 0 : 1));
|
||||
|
||||
return $this->check_list;
|
||||
}
|
||||
}
|
||||
63
src/SPC/doctor/DoctorHandler.php
Normal file
63
src/SPC/doctor/DoctorHandler.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\doctor;
|
||||
|
||||
use SPC\exception\SPCException;
|
||||
use SPC\util\AttributeMapper;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
final class DoctorHandler
|
||||
{
|
||||
/**
|
||||
* Returns a list of valid check items.
|
||||
*
|
||||
* @return array<AsCheckItem>
|
||||
*/
|
||||
public static function getValidCheckList(): iterable
|
||||
{
|
||||
foreach (AttributeMapper::getDoctorCheckMap() as [$item, $optional]) {
|
||||
/* @var AsCheckItem $item */
|
||||
// optional check
|
||||
if ($optional !== null && !call_user_func($optional)) {
|
||||
continue; // skip this when the optional check is false
|
||||
}
|
||||
// limit_os check
|
||||
if ($item->limit_os !== null && $item->limit_os !== PHP_OS_FAMILY) {
|
||||
continue;
|
||||
}
|
||||
// skipped items by env
|
||||
$skip_items = array_filter(explode(',', getenv('SPC_SKIP_DOCTOR_CHECK_ITEMS') ?: ''));
|
||||
if (in_array($item->item_name, $skip_items)) {
|
||||
continue; // skip this item
|
||||
}
|
||||
yield $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the fix for a given CheckResult.
|
||||
*
|
||||
* @param OutputInterface $output the output interface to write messages to
|
||||
* @param CheckResult $result the result of the check that needs fixing
|
||||
* @return bool returns true if the fix was successful, false otherwise
|
||||
*/
|
||||
public static function emitFix(OutputInterface $output, CheckResult $result): bool
|
||||
{
|
||||
keyboard_interrupt_register(function () use ($output) {
|
||||
$output->writeln('<error>You cancelled fix</error>');
|
||||
});
|
||||
try {
|
||||
$fix_result = call_user_func(AttributeMapper::getDoctorFixMap()[$result->getFixItem()], ...$result->getFixParams());
|
||||
} catch (SPCException $e) {
|
||||
$output->writeln('<error>Fix failed: ' . $e->getMessage() . '</error>');
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln('<error>Fix failed with an unexpected error: ' . $e->getMessage() . '</error>');
|
||||
return false;
|
||||
}
|
||||
keyboard_interrupt_unregister();
|
||||
return $fix_result;
|
||||
}
|
||||
}
|
||||
133
src/SPC/util/AttributeMapper.php
Normal file
133
src/SPC/util/AttributeMapper.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\util;
|
||||
|
||||
use SPC\doctor\AsCheckItem;
|
||||
use SPC\doctor\AsFixItem;
|
||||
use SPC\doctor\OptionalCheck;
|
||||
use SPC\store\FileSystem;
|
||||
|
||||
/**
|
||||
* AttributeMapper is responsible for mapping extension names to their respective classes
|
||||
* using PHP attributes.
|
||||
*
|
||||
* This class scans the extension classes for the CustomExt attribute and builds a mapping
|
||||
* of extension names to their class names, which can be used to retrieve the class by name.
|
||||
* @internal it is intended for internal use within the SPC builder framework
|
||||
*/
|
||||
class AttributeMapper
|
||||
{
|
||||
/** @param array<string, string> $extensions The mapping of extension names to their classes */
|
||||
private static array $ext_attr_map = [];
|
||||
|
||||
/** @var array<string, array<string, array|callable>> $doctor_map The mapping of doctor modules */
|
||||
private static array $doctor_map = [
|
||||
'check' => [],
|
||||
'fix' => [],
|
||||
];
|
||||
|
||||
public static function init(): void
|
||||
{
|
||||
// Load CustomExt attributes from extension classes
|
||||
self::loadExtensionAttributes();
|
||||
|
||||
// Load doctor check items
|
||||
self::loadDoctorAttributes();
|
||||
|
||||
// TODO: 3.0, refactor library loader and vendor loader here
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class name of an extension by its attributed name.
|
||||
*
|
||||
* @param string $name The name of the extension (attributed name)
|
||||
* @return null|string Returns the class name of the extension if it exists, otherwise null
|
||||
*/
|
||||
public static function getExtensionClassByName(string $name): ?string
|
||||
{
|
||||
return self::$ext_attr_map[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function getDoctorCheckMap(): array
|
||||
{
|
||||
return self::$doctor_map['check'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function getDoctorFixMap(): array
|
||||
{
|
||||
return self::$doctor_map['fix'];
|
||||
}
|
||||
|
||||
private static function loadExtensionAttributes(): void
|
||||
{
|
||||
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/builder/extension', 'SPC\builder\extension');
|
||||
foreach ($classes as $class) {
|
||||
$reflection = new \ReflectionClass($class);
|
||||
foreach ($reflection->getAttributes(CustomExt::class) as $attribute) {
|
||||
/** @var CustomExt $instance */
|
||||
$instance = $attribute->newInstance();
|
||||
self::$ext_attr_map[$instance->ext_name] = $class;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function loadDoctorAttributes(): void
|
||||
{
|
||||
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/doctor/item', 'SPC\doctor\item');
|
||||
foreach ($classes as $class) {
|
||||
$optional_passthrough = null;
|
||||
$ref = new \ReflectionClass($class);
|
||||
// #[OptionalCheck]
|
||||
$optional = $ref->getAttributes(OptionalCheck::class)[0] ?? null;
|
||||
if ($optional !== null) {
|
||||
/** @var OptionalCheck $instance */
|
||||
$instance = $optional->newInstance();
|
||||
if (is_callable($instance->check)) {
|
||||
$optional_passthrough = $instance->check;
|
||||
}
|
||||
}
|
||||
$check_items = [];
|
||||
$fix_items = [];
|
||||
// load check items and fix items
|
||||
foreach ($ref->getMethods() as $method) {
|
||||
$optional_passthrough_single = $optional_passthrough ?? null;
|
||||
// #[OptionalCheck]
|
||||
foreach ($method->getAttributes(OptionalCheck::class) as $method_attr) {
|
||||
$optional_check = $method_attr->newInstance();
|
||||
if (is_callable($optional_check->check)) {
|
||||
$optional_passthrough_single = $optional_check->check;
|
||||
}
|
||||
}
|
||||
// #[AsCheckItem]
|
||||
foreach ($method->getAttributes(AsCheckItem::class) as $method_attr) {
|
||||
// [{AsCheckItem object}, {OptionalCheck callable or null}]
|
||||
$obj = $method_attr->newInstance();
|
||||
$obj->callback = [new $class(), $method->getName()];
|
||||
$check_items[] = [$obj, $optional_passthrough_single];
|
||||
}
|
||||
// #[AsFixItem]
|
||||
$fix_item = $method->getAttributes(AsFixItem::class)[0] ?? null;
|
||||
if ($fix_item !== null) {
|
||||
// [{AsFixItem object}, {OptionalCheck callable or null}]
|
||||
$obj = $fix_item->newInstance();
|
||||
$fix_items[$obj->name] = [new $class(), $method->getName()];
|
||||
}
|
||||
}
|
||||
|
||||
// add to doctor map
|
||||
self::$doctor_map['check'] = array_merge(self::$doctor_map['check'], $check_items);
|
||||
self::$doctor_map['fix'] = array_merge(self::$doctor_map['fix'], $fix_items);
|
||||
}
|
||||
|
||||
// sort check items by level
|
||||
usort(self::$doctor_map['check'], fn (array $a, array $b) => $a[0]->level > $b[0]->level ? -1 : ($a[0]->level == $b[0]->level ? 0 : 1));
|
||||
}
|
||||
}
|
||||
@ -5,8 +5,6 @@ declare(strict_types=1);
|
||||
namespace SPC\util;
|
||||
|
||||
use SPC\builder\Extension;
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\store\FileSystem;
|
||||
|
||||
/**
|
||||
* Custom extension attribute and manager
|
||||
@ -17,43 +15,10 @@ use SPC\store\FileSystem;
|
||||
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS)]
|
||||
class CustomExt
|
||||
{
|
||||
private static array $custom_ext_class = [];
|
||||
|
||||
/**
|
||||
* Constructor for custom extension attribute
|
||||
*
|
||||
* @param string $ext_name The extension name
|
||||
*/
|
||||
public function __construct(protected string $ext_name) {}
|
||||
|
||||
/**
|
||||
* Load all custom extension classes
|
||||
*
|
||||
* This method scans the extension directory and registers all classes
|
||||
* that have the CustomExt attribute.
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function loadCustomExt(): void
|
||||
{
|
||||
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/builder/extension', 'SPC\builder\extension');
|
||||
foreach ($classes as $class) {
|
||||
$reflection = new \ReflectionClass($class);
|
||||
foreach ($reflection->getAttributes(CustomExt::class) as $attribute) {
|
||||
self::$custom_ext_class[$attribute->getArguments()[0]] = $class;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class name for a custom extension
|
||||
*
|
||||
* @param string $ext_name The extension name
|
||||
* @return string The class name for the extension
|
||||
*/
|
||||
public static function getExtClass(string $ext_name): string
|
||||
{
|
||||
return self::$custom_ext_class[$ext_name] ?? Extension::class;
|
||||
}
|
||||
public function __construct(public string $ext_name) {}
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace SPC\Tests\builder;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SPC\builder\BuilderBase;
|
||||
use SPC\builder\BuilderProvider;
|
||||
@ -14,7 +13,7 @@ use SPC\exception\RuntimeException;
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\store\FileSystem;
|
||||
use SPC\store\LockFile;
|
||||
use SPC\util\CustomExt;
|
||||
use SPC\util\AttributeMapper;
|
||||
use SPC\util\DependencyUtil;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
@ -36,9 +35,8 @@ class BuilderTest extends TestCase
|
||||
$this->builder = BuilderProvider::makeBuilderByInput(new ArgvInput());
|
||||
[$extensions, $libs] = DependencyUtil::getExtsAndLibs(['mbregex']);
|
||||
$this->builder->proveLibs($libs);
|
||||
CustomExt::loadCustomExt();
|
||||
foreach ($extensions as $extension) {
|
||||
$class = CustomExt::getExtClass($extension);
|
||||
$class = AttributeMapper::getExtensionClassByName($extension) ?? Extension::class;
|
||||
$ext = new $class($extension, $this->builder);
|
||||
$this->builder->addExt($ext);
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ namespace SPC\Tests\builder;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SPC\builder\BuilderProvider;
|
||||
use SPC\builder\Extension;
|
||||
use SPC\util\CustomExt;
|
||||
use SPC\util\AttributeMapper;
|
||||
use SPC\util\DependencyUtil;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
@ -23,9 +23,8 @@ class ExtensionTest extends TestCase
|
||||
$builder = BuilderProvider::makeBuilderByInput(new ArgvInput());
|
||||
[$extensions, $libs] = DependencyUtil::getExtsAndLibs(['mbregex']);
|
||||
$builder->proveLibs($libs);
|
||||
CustomExt::loadCustomExt();
|
||||
foreach ($extensions as $extension) {
|
||||
$class = CustomExt::getExtClass($extension);
|
||||
$class = AttributeMapper::getExtensionClassByName($extension) ?? Extension::class;
|
||||
$ext = new $class($extension, $builder);
|
||||
$builder->addExt($ext);
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace SPC\Tests\doctor;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SPC\doctor\CheckListHandler;
|
||||
use SPC\doctor\DoctorHandler;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -14,9 +14,9 @@ final class CheckListHandlerTest extends TestCase
|
||||
{
|
||||
public function testRunChecksReturnsListOfCheck(): void
|
||||
{
|
||||
$list = new CheckListHandler();
|
||||
$list = new DoctorHandler();
|
||||
|
||||
$id = $list->runChecks();
|
||||
$id = $list->getValidCheckList();
|
||||
foreach ($id as $item) {
|
||||
$this->assertInstanceOf('SPC\doctor\AsCheckItem', $item);
|
||||
}
|
||||
|
||||
@ -5,8 +5,6 @@ declare(strict_types=1);
|
||||
namespace SPC\Tests\store;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
use SPC\store\FileSystem;
|
||||
|
||||
/**
|
||||
|
||||
@ -4,3 +4,5 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../src/globals/internal-env.php';
|
||||
require_once __DIR__ . '/mock/SPC_store.php';
|
||||
|
||||
\SPC\util\AttributeMapper::init();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user