Introduce AttributeMapper for managing extensions and doctor attributes

This commit is contained in:
crazywhalecc
2025-08-06 20:35:52 +08:00
committed by Jerry Ma
parent e28580de00
commit 722bb31815
14 changed files with 338 additions and 265 deletions

View 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));
}
}

View File

@@ -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) {}
}