mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-08 09:25:35 +08:00
Refactor package stage handling and update class structures for improved flexibility
This commit is contained in:
191
src/StaticPHP/Registry/ArtifactLoader.php
Normal file
191
src/StaticPHP/Registry/ArtifactLoader.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Registry;
|
||||
|
||||
use StaticPHP\Artifact\Artifact;
|
||||
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
|
||||
use StaticPHP\Attribute\Artifact\AfterSourceExtract;
|
||||
use StaticPHP\Attribute\Artifact\BinaryExtract;
|
||||
use StaticPHP\Attribute\Artifact\CustomBinary;
|
||||
use StaticPHP\Attribute\Artifact\CustomSource;
|
||||
use StaticPHP\Attribute\Artifact\SourceExtract;
|
||||
use StaticPHP\Config\ArtifactConfig;
|
||||
use StaticPHP\Exception\ValidationException;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
class ArtifactLoader
|
||||
{
|
||||
/** @var null|array<string, Artifact> Artifact instances */
|
||||
private static ?array $artifacts = null;
|
||||
|
||||
public static function initArtifactInstances(): void
|
||||
{
|
||||
if (self::$artifacts !== null) {
|
||||
return;
|
||||
}
|
||||
foreach (ArtifactConfig::getAll() as $name => $item) {
|
||||
$artifact = new Artifact($name, $item);
|
||||
self::$artifacts[$name] = $artifact;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getArtifactInstance(string $artifact_name): ?Artifact
|
||||
{
|
||||
self::initArtifactInstances();
|
||||
return self::$artifacts[$artifact_name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load artifact definitions from PSR-4 directory.
|
||||
*
|
||||
* @param string $dir Directory path
|
||||
* @param string $base_namespace Base namespace for dir's PSR-4 mapping
|
||||
* @param bool $auto_require Whether to auto-require PHP files (for external plugins not in autoload)
|
||||
*/
|
||||
public static function loadFromPsr4Dir(string $dir, string $base_namespace, bool $auto_require = false): void
|
||||
{
|
||||
self::initArtifactInstances();
|
||||
$classes = FileSystem::getClassesPsr4($dir, $base_namespace, auto_require: $auto_require);
|
||||
foreach ($classes as $class) {
|
||||
self::loadFromClass($class);
|
||||
}
|
||||
}
|
||||
|
||||
public static function loadFromClass(string $class): void
|
||||
{
|
||||
$ref = new \ReflectionClass($class);
|
||||
|
||||
$class_instance = $ref->newInstance();
|
||||
|
||||
foreach ($ref->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
self::processCustomSourceAttribute($ref, $method, $class_instance);
|
||||
self::processCustomBinaryAttribute($ref, $method, $class_instance);
|
||||
self::processSourceExtractAttribute($ref, $method, $class_instance);
|
||||
self::processBinaryExtractAttribute($ref, $method, $class_instance);
|
||||
self::processAfterSourceExtractAttribute($ref, $method, $class_instance);
|
||||
self::processAfterBinaryExtractAttribute($ref, $method, $class_instance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process #[CustomSource] attribute.
|
||||
*/
|
||||
private static function processCustomSourceAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
|
||||
{
|
||||
$attributes = $method->getAttributes(CustomSource::class);
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var CustomSource $instance */
|
||||
$instance = $attribute->newInstance();
|
||||
$artifact_name = $instance->artifact_name;
|
||||
if (isset(self::$artifacts[$artifact_name])) {
|
||||
self::$artifacts[$artifact_name]->setCustomSourceCallback([$class_instance, $method->getName()]);
|
||||
} else {
|
||||
throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomSource] on '{$ref->getName()}::{$method->getName()}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process #[CustomBinary] attribute.
|
||||
*/
|
||||
private static function processCustomBinaryAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
|
||||
{
|
||||
$attributes = $method->getAttributes(CustomBinary::class);
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var CustomBinary $instance */
|
||||
$instance = $attribute->newInstance();
|
||||
$artifact_name = $instance->artifact_name;
|
||||
if (isset(self::$artifacts[$artifact_name])) {
|
||||
foreach ($instance->support_os as $os) {
|
||||
self::$artifacts[$artifact_name]->setCustomBinaryCallback($os, [$class_instance, $method->getName()]);
|
||||
}
|
||||
} else {
|
||||
throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomBinary] on '{$ref->getName()}::{$method->getName()}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process #[SourceExtract] attribute.
|
||||
* This attribute allows completely taking over the source extraction process.
|
||||
*/
|
||||
private static function processSourceExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
|
||||
{
|
||||
$attributes = $method->getAttributes(SourceExtract::class);
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var SourceExtract $instance */
|
||||
$instance = $attribute->newInstance();
|
||||
$artifact_name = $instance->artifact_name;
|
||||
if (isset(self::$artifacts[$artifact_name])) {
|
||||
self::$artifacts[$artifact_name]->setSourceExtractCallback([$class_instance, $method->getName()]);
|
||||
} else {
|
||||
throw new ValidationException("Artifact '{$artifact_name}' not found for #[SourceExtract] on '{$ref->getName()}::{$method->getName()}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process #[BinaryExtract] attribute.
|
||||
* This attribute allows completely taking over the binary extraction process.
|
||||
*/
|
||||
private static function processBinaryExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
|
||||
{
|
||||
$attributes = $method->getAttributes(BinaryExtract::class);
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var BinaryExtract $instance */
|
||||
$instance = $attribute->newInstance();
|
||||
$artifact_name = $instance->artifact_name;
|
||||
if (isset(self::$artifacts[$artifact_name])) {
|
||||
self::$artifacts[$artifact_name]->setBinaryExtractCallback(
|
||||
[$class_instance, $method->getName()],
|
||||
$instance->platforms
|
||||
);
|
||||
} else {
|
||||
throw new ValidationException("Artifact '{$artifact_name}' not found for #[BinaryExtract] on '{$ref->getName()}::{$method->getName()}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process #[AfterSourceExtract] attribute.
|
||||
* This attribute registers a hook that runs after source extraction completes.
|
||||
*/
|
||||
private static function processAfterSourceExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
|
||||
{
|
||||
$attributes = $method->getAttributes(AfterSourceExtract::class);
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var AfterSourceExtract $instance */
|
||||
$instance = $attribute->newInstance();
|
||||
$artifact_name = $instance->artifact_name;
|
||||
if (isset(self::$artifacts[$artifact_name])) {
|
||||
self::$artifacts[$artifact_name]->addAfterSourceExtractCallback([$class_instance, $method->getName()]);
|
||||
} else {
|
||||
throw new ValidationException("Artifact '{$artifact_name}' not found for #[AfterSourceExtract] on '{$ref->getName()}::{$method->getName()}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process #[AfterBinaryExtract] attribute.
|
||||
* This attribute registers a hook that runs after binary extraction completes.
|
||||
*/
|
||||
private static function processAfterBinaryExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void
|
||||
{
|
||||
$attributes = $method->getAttributes(AfterBinaryExtract::class);
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var AfterBinaryExtract $instance */
|
||||
$instance = $attribute->newInstance();
|
||||
$artifact_name = $instance->artifact_name;
|
||||
if (isset(self::$artifacts[$artifact_name])) {
|
||||
self::$artifacts[$artifact_name]->addAfterBinaryExtractCallback(
|
||||
[$class_instance, $method->getName()],
|
||||
$instance->platforms
|
||||
);
|
||||
} else {
|
||||
throw new ValidationException("Artifact '{$artifact_name}' not found for #[AfterBinaryExtract] on '{$ref->getName()}::{$method->getName()}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
src/StaticPHP/Registry/DoctorLoader.php
Normal file
123
src/StaticPHP/Registry/DoctorLoader.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Registry;
|
||||
|
||||
use StaticPHP\Attribute\Doctor\CheckItem;
|
||||
use StaticPHP\Attribute\Doctor\FixItem;
|
||||
use StaticPHP\Attribute\Doctor\OptionalCheck;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
class DoctorLoader
|
||||
{
|
||||
/**
|
||||
* @var array<int, array{0: CheckItem, 1: callable}> $doctor_items Loaded doctor check item instances
|
||||
*/
|
||||
private static array $doctor_items = [];
|
||||
|
||||
/**
|
||||
* @var array<string, callable> $fix_items loaded doctor fix item instances
|
||||
*/
|
||||
private static array $fix_items = [];
|
||||
|
||||
/**
|
||||
* Load doctor check items from PSR-4 directory.
|
||||
*
|
||||
* @param string $dir Directory path
|
||||
* @param string $base_namespace Base namespace for dir's PSR-4 mapping
|
||||
* @param bool $auto_require Whether to auto-require PHP files (for external plugins not in autoload)
|
||||
*/
|
||||
public static function loadFromPsr4Dir(string $dir, string $base_namespace, bool $auto_require = false): void
|
||||
{
|
||||
$classes = FileSystem::getClassesPsr4($dir, $base_namespace, auto_require: $auto_require);
|
||||
foreach ($classes as $class) {
|
||||
self::loadFromClass($class, false);
|
||||
}
|
||||
|
||||
// sort check items by level
|
||||
usort(self::$doctor_items, function ($a, $b) {
|
||||
return $a[0]->level > $b[0]->level ? -1 : ($a[0]->level == $b[0]->level ? 0 : 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load doctor check items from a class.
|
||||
*
|
||||
* @param string $class Class name to load doctor check items from
|
||||
* @param bool $sort Whether to re-sort Doctor items (default: true)
|
||||
*/
|
||||
public static function loadFromClass(string $class, bool $sort = true): void
|
||||
{
|
||||
// passthough to all the functions if #[OptionalCheck] is set on class level
|
||||
$optional_passthrough = null;
|
||||
$reflection = new \ReflectionClass($class);
|
||||
$class_instance = $reflection->newInstance();
|
||||
// parse #[OptionalCheck]
|
||||
$optional = $reflection->getAttributes(OptionalCheck::class)[0] ?? null;
|
||||
if ($optional !== null) {
|
||||
/** @var OptionalCheck $instance */
|
||||
$instance = $optional->newInstance();
|
||||
if (is_callable($instance->check)) {
|
||||
$optional_passthrough = $instance->check;
|
||||
}
|
||||
}
|
||||
|
||||
$doctor_items = [];
|
||||
$fix_item_map = [];
|
||||
|
||||
// finx check items and fix items from methods in class
|
||||
foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
// passthrough for this method if #[OptionalCheck] is set on method level
|
||||
$optional = $optional_passthrough ?? null;
|
||||
foreach ($method->getAttributes(OptionalCheck::class) as $method_attr) {
|
||||
$optional_check = $method_attr->newInstance();
|
||||
if (is_callable($optional_check->check)) {
|
||||
$optional = $optional_check->check;
|
||||
}
|
||||
}
|
||||
|
||||
// parse #[CheckItem]
|
||||
foreach ($method->getAttributes(CheckItem::class) as $attr) {
|
||||
/** @var CheckItem $instance */
|
||||
$instance = $attr->newInstance();
|
||||
$instance->callback = [$class_instance, $method->getName()];
|
||||
// put CheckItem instance and optional check callback (or null) to $doctor_items
|
||||
$doctor_items[] = [$instance, $optional];
|
||||
}
|
||||
|
||||
// parse #[FixItem]
|
||||
$fix_item = $method->getAttributes(FixItem::class)[0] ?? null;
|
||||
if ($fix_item !== null) {
|
||||
$instance = $fix_item->newInstance();
|
||||
$fix_item_map[$instance->name] = [$class_instance, $method->getName()];
|
||||
}
|
||||
}
|
||||
|
||||
// add to array
|
||||
self::$doctor_items = array_merge(self::$doctor_items, $doctor_items);
|
||||
self::$fix_items = array_merge(self::$fix_items, $fix_item_map);
|
||||
|
||||
if ($sort) {
|
||||
// sort check items by level
|
||||
usort(self::$doctor_items, function ($a, $b) {
|
||||
return $a[0]->level > $b[0]->level ? -1 : ($a[0]->level == $b[0]->level ? 0 : 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns loaded doctor check items.
|
||||
*
|
||||
* @return array<int, array{0: CheckItem, 1: callable}>
|
||||
*/
|
||||
public static function getDoctorItems(): array
|
||||
{
|
||||
return self::$doctor_items;
|
||||
}
|
||||
|
||||
public static function getFixItem(string $name): ?callable
|
||||
{
|
||||
return self::$fix_items[$name] ?? null;
|
||||
}
|
||||
}
|
||||
379
src/StaticPHP/Registry/PackageLoader.php
Normal file
379
src/StaticPHP/Registry/PackageLoader.php
Normal file
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Registry;
|
||||
|
||||
use StaticPHP\Attribute\Package\AfterStage;
|
||||
use StaticPHP\Attribute\Package\BeforeStage;
|
||||
use StaticPHP\Attribute\Package\BuildFor;
|
||||
use StaticPHP\Attribute\Package\CustomPhpConfigureArg;
|
||||
use StaticPHP\Attribute\Package\Extension;
|
||||
use StaticPHP\Attribute\Package\Info;
|
||||
use StaticPHP\Attribute\Package\InitPackage;
|
||||
use StaticPHP\Attribute\Package\Library;
|
||||
use StaticPHP\Attribute\Package\PatchBeforeBuild;
|
||||
use StaticPHP\Attribute\Package\ResolveBuild;
|
||||
use StaticPHP\Attribute\Package\Stage;
|
||||
use StaticPHP\Attribute\Package\Target;
|
||||
use StaticPHP\Attribute\Package\Validate;
|
||||
use StaticPHP\Config\PackageConfig;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\RegistryException;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Package\LibraryPackage;
|
||||
use StaticPHP\Package\Package;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Package\PhpExtensionPackage;
|
||||
use StaticPHP\Package\TargetPackage;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
class PackageLoader
|
||||
{
|
||||
/** @var array<string, Package> */
|
||||
private static ?array $packages = null;
|
||||
|
||||
private static array $before_stages = [];
|
||||
|
||||
private static array $after_stages = [];
|
||||
|
||||
/** @var array<string, true> Track loaded classes to prevent duplicates */
|
||||
private static array $loaded_classes = [];
|
||||
|
||||
public static function initPackageInstances(): void
|
||||
{
|
||||
if (self::$packages !== null) {
|
||||
return;
|
||||
}
|
||||
// init packages instance from config
|
||||
foreach (PackageConfig::getAll() as $name => $item) {
|
||||
$pkg = match ($item['type']) {
|
||||
'target', 'virtual-target' => new TargetPackage($name, $item['type']),
|
||||
'library' => new LibraryPackage($name, $item['type']),
|
||||
'php-extension' => new PhpExtensionPackage($name, $item['type']),
|
||||
default => null,
|
||||
};
|
||||
if ($pkg !== null) {
|
||||
self::$packages[$name] = $pkg;
|
||||
} else {
|
||||
throw new RegistryException("Package [{$name}] has unknown type [{$item['type']}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load package definitions from PSR-4 directory.
|
||||
*
|
||||
* @param string $dir Directory path
|
||||
* @param string $base_namespace Base namespace for dir's PSR-4 mapping
|
||||
* @param bool $auto_require Whether to auto-require PHP files (for external plugins not in autoload)
|
||||
*/
|
||||
public static function loadFromPsr4Dir(string $dir, string $base_namespace, bool $auto_require = false): void
|
||||
{
|
||||
self::initPackageInstances();
|
||||
$classes = FileSystem::getClassesPsr4($dir, $base_namespace, auto_require: $auto_require);
|
||||
foreach ($classes as $class) {
|
||||
self::loadFromClass($class);
|
||||
}
|
||||
}
|
||||
|
||||
public static function hasPackage(string $name): bool
|
||||
{
|
||||
return isset(self::$packages[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Package instance by its name.
|
||||
*
|
||||
* @param string $name The name of the package
|
||||
* @return Package Returns the Package instance if found, otherwise null
|
||||
*/
|
||||
public static function getPackage(string $name): Package
|
||||
{
|
||||
if (!isset(self::$packages[$name])) {
|
||||
throw new WrongUsageException("Package [{$name}] not found.");
|
||||
}
|
||||
return self::$packages[$name];
|
||||
}
|
||||
|
||||
public static function getTargetPackage(string $name): TargetPackage
|
||||
{
|
||||
$pkg = self::getPackage($name);
|
||||
if ($pkg instanceof TargetPackage) {
|
||||
return $pkg;
|
||||
}
|
||||
throw new WrongUsageException("Package [{$name}] is not a TargetPackage.");
|
||||
}
|
||||
|
||||
public static function getLibraryPackage(string $name): LibraryPackage
|
||||
{
|
||||
$pkg = self::getPackage($name);
|
||||
if ($pkg instanceof LibraryPackage) {
|
||||
return $pkg;
|
||||
}
|
||||
throw new WrongUsageException("Package [{$name}] is not a LibraryPackage.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all loaded Package instances.
|
||||
*/
|
||||
public static function getPackages(array|string|null $type_filter = null): iterable
|
||||
{
|
||||
foreach (self::$packages as $name => $package) {
|
||||
if ($type_filter === null) {
|
||||
yield $name => $package;
|
||||
} elseif ($package->getType() === $type_filter) {
|
||||
yield $name => $package;
|
||||
} elseif (is_array($type_filter) && in_array($package->getType(), $type_filter, true)) {
|
||||
yield $name => $package;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init package instance from defined classes and attributes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function loadFromClass(mixed $class): void
|
||||
{
|
||||
$refClass = new \ReflectionClass($class);
|
||||
$class_name = $refClass->getName();
|
||||
|
||||
// Skip if already loaded to prevent duplicate registrations
|
||||
if (isset(self::$loaded_classes[$class_name])) {
|
||||
return;
|
||||
}
|
||||
self::$loaded_classes[$class_name] = true;
|
||||
|
||||
$attributes = $refClass->getAttributes();
|
||||
foreach ($attributes as $attribute) {
|
||||
$pkg = null;
|
||||
|
||||
$attribute_instance = $attribute->newInstance();
|
||||
if ($attribute_instance instanceof Target === false &&
|
||||
$attribute_instance instanceof Library === false &&
|
||||
$attribute_instance instanceof Extension === false) {
|
||||
// not a package attribute
|
||||
continue;
|
||||
}
|
||||
$package_type = PackageConfig::get($attribute_instance->name, 'type');
|
||||
if ($package_type === null) {
|
||||
throw new RegistryException("Package [{$attribute_instance->name}] not defined in config, please check your config files.");
|
||||
}
|
||||
|
||||
// if class has parent class and matches the attribute instance, use custom class
|
||||
if ($refClass->getParentClass() !== false) {
|
||||
if (is_a($class_name, Package::class, true)) {
|
||||
self::$packages[$attribute_instance->name] = new $class_name($attribute_instance->name, $package_type);
|
||||
$instance_class = self::$packages[$attribute_instance->name];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($instance_class)) {
|
||||
$instance_class = $refClass->newInstance();
|
||||
}
|
||||
|
||||
$pkg = self::$packages[$attribute_instance->name];
|
||||
|
||||
// validate package type matches
|
||||
$pkg_type_attr = match ($attribute->getName()) {
|
||||
Target::class => ['target', 'virtual-target'],
|
||||
Library::class => ['library'],
|
||||
Extension::class => ['php-extension'],
|
||||
default => null,
|
||||
};
|
||||
if (!in_array($package_type, $pkg_type_attr, true)) {
|
||||
throw new RegistryException("Package [{$attribute_instance->name}] type mismatch: config type is [{$package_type}], but attribute type is [" . implode('|', $pkg_type_attr) . '].');
|
||||
}
|
||||
if ($pkg !== null && !PackageConfig::isPackageExists($pkg->getName())) {
|
||||
throw new RegistryException("Package [{$pkg->getName()}] config not found for class {$class}");
|
||||
}
|
||||
|
||||
// init method attributes
|
||||
$methods = $refClass->getMethods(\ReflectionMethod::IS_PUBLIC);
|
||||
foreach ($methods as $method) {
|
||||
$method_attributes = $method->getAttributes();
|
||||
foreach ($method_attributes as $method_attribute) {
|
||||
$method_instance = $method_attribute->newInstance();
|
||||
match ($method_attribute->getName()) {
|
||||
// #[BuildFor(PHP_OS_FAMILY)]
|
||||
BuildFor::class => self::addBuildFunction($pkg, $method_instance, [$instance_class, $method->getName()]),
|
||||
// #[CustomPhpConfigureArg(PHP_OS_FAMILY)]
|
||||
CustomPhpConfigureArg::class => self::bindCustomPhpConfigureArg($pkg, $method_attribute->newInstance(), [$instance_class, $method->getName()]),
|
||||
// #[Stage('stage_name')]
|
||||
Stage::class => self::addStage($method, $pkg, $instance_class, $method_instance),
|
||||
// #[InitPackage] (run now with package context)
|
||||
InitPackage::class => ApplicationContext::invoke([$instance_class, $method->getName()], [
|
||||
Package::class => $pkg,
|
||||
$pkg::class => $pkg,
|
||||
]),
|
||||
// #[InitBuild]
|
||||
ResolveBuild::class => $pkg instanceof TargetPackage ? $pkg->setResolveBuildCallback([$instance_class, $method->getName()]) : null,
|
||||
// #[Info]
|
||||
Info::class => $pkg->setInfoCallback([$instance_class, $method->getName()]),
|
||||
// #[Validate]
|
||||
Validate::class => $pkg->setValidateCallback([$instance_class, $method->getName()]),
|
||||
// #[PatchBeforeBuild]
|
||||
PatchBeforeBuild::class => $pkg->setPatchBeforeBuildCallback([$instance_class, $method->getName()]),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
// register package
|
||||
self::$packages[$pkg->getName()] = $pkg;
|
||||
}
|
||||
|
||||
if (!isset($instance_class)) {
|
||||
$instance_class = $refClass->newInstance();
|
||||
}
|
||||
|
||||
// parse non-package available attributes
|
||||
foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
$method_attributes = $method->getAttributes();
|
||||
foreach ($method_attributes as $method_attribute) {
|
||||
$method_instance = $method_attribute->newInstance();
|
||||
match ($method_attribute->getName()) {
|
||||
// #[BeforeStage('package_name', 'stage')] and #[AfterStage('package_name', 'stage')]
|
||||
BeforeStage::class => self::addBeforeStage($method, $pkg ?? null, $instance_class, $method_instance),
|
||||
AfterStage::class => self::addAfterStage($method, $pkg ?? null, $instance_class, $method_instance),
|
||||
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable
|
||||
{
|
||||
// match condition
|
||||
$installer = ApplicationContext::get(PackageInstaller::class);
|
||||
$stages = self::$before_stages[$package_name][$stage] ?? [];
|
||||
foreach ($stages as [$callback, $only_when_package_resolved]) {
|
||||
if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) {
|
||||
continue;
|
||||
}
|
||||
yield $callback;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getAfterStageCallbacks(string $package_name, string $stage): array
|
||||
{
|
||||
// match condition
|
||||
$installer = ApplicationContext::get(PackageInstaller::class);
|
||||
$stages = self::$after_stages[$package_name][$stage] ?? [];
|
||||
$result = [];
|
||||
foreach ($stages as [$callback, $only_when_package_resolved]) {
|
||||
if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) {
|
||||
continue;
|
||||
}
|
||||
$result[] = $callback;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register default stages for all PhpExtensionPackage instances.
|
||||
* Should be called after all registries have been loaded.
|
||||
*/
|
||||
public static function registerAllDefaultStages(): void
|
||||
{
|
||||
foreach (self::$packages as $pkg) {
|
||||
if ($pkg instanceof PhpExtensionPackage) {
|
||||
$pkg->registerDefaultStages();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check loaded stage events for consistency.
|
||||
*/
|
||||
public static function checkLoadedStageEvents(): void
|
||||
{
|
||||
foreach (['BeforeStage' => self::$before_stages, 'AfterStage' => self::$after_stages] as $event_name => $ev_all) {
|
||||
foreach ($ev_all as $package_name => $stages) {
|
||||
// check package exists
|
||||
if (!self::hasPackage($package_name)) {
|
||||
throw new RegistryException(
|
||||
"{$event_name} event registered for unknown package [{$package_name}]."
|
||||
);
|
||||
}
|
||||
$pkg = self::getPackage($package_name);
|
||||
foreach ($stages as $stage_name => $before_events) {
|
||||
foreach ($before_events as [$event_callable, $only_when_package_resolved]) {
|
||||
// check only_when_package_resolved package exists
|
||||
if ($only_when_package_resolved !== null && !self::hasPackage($only_when_package_resolved)) {
|
||||
throw new RegistryException("{$event_name} event in package [{$package_name}] for stage [{$stage_name}] has unknown only_when_package_resolved package [{$only_when_package_resolved}].");
|
||||
}
|
||||
// check callable is valid
|
||||
if (!is_callable($event_callable)) {
|
||||
throw new RegistryException(
|
||||
"{$event_name} event in package [{$package_name}] for stage [{$stage_name}] has invalid callable.",
|
||||
);
|
||||
}
|
||||
}
|
||||
// check stage exists
|
||||
if (!$pkg->hasStage($stage_name)) {
|
||||
throw new RegistryException("Package stage [{$stage_name}] is not registered in package [{$package_name}].");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a custom PHP configure argument callback to a php-extension package.
|
||||
*/
|
||||
private static function bindCustomPhpConfigureArg(Package $pkg, object $attr, callable $fn): void
|
||||
{
|
||||
if (!$pkg instanceof PhpExtensionPackage) {
|
||||
throw new RegistryException("Class [{$pkg->getName()}] must implement PhpExtensionPackage for CustomPhpConfigureArg attribute.");
|
||||
}
|
||||
$pkg->addCustomPhpConfigureArgCallback($attr->os, $fn);
|
||||
}
|
||||
|
||||
private static function addBuildFunction(Package $pkg, object $attr, callable $fn): void
|
||||
{
|
||||
$pkg->addBuildFunction($attr->os, $fn);
|
||||
}
|
||||
|
||||
private static function addStage(\ReflectionMethod $method, Package $pkg, object $instance_class, object $method_instance): void
|
||||
{
|
||||
$name = $method_instance->function;
|
||||
if ($name === null) {
|
||||
$name = $method->getName();
|
||||
}
|
||||
$pkg->addStage($name, [$instance_class, $method->getName()]);
|
||||
}
|
||||
|
||||
private static function addBeforeStage(\ReflectionMethod $method, ?Package $pkg, mixed $instance_class, object $method_instance): void
|
||||
{
|
||||
/** @var BeforeStage $method_instance */
|
||||
$stage = $method_instance->stage;
|
||||
$stage = match (true) {
|
||||
is_string($stage) => $stage,
|
||||
is_array($stage) && count($stage) === 2 => $stage[1],
|
||||
default => throw new RegistryException('Invalid stage definition in BeforeStage attribute.'),
|
||||
};
|
||||
if ($method_instance->package_name === '' && $pkg === null) {
|
||||
throw new RegistryException('Package name must not be empty when no package context is available for BeforeStage attribute.');
|
||||
}
|
||||
$package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name;
|
||||
self::$before_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved];
|
||||
}
|
||||
|
||||
private static function addAfterStage(\ReflectionMethod $method, ?Package $pkg, mixed $instance_class, object $method_instance): void
|
||||
{
|
||||
$stage = $method_instance->stage;
|
||||
$stage = match (true) {
|
||||
is_string($stage) => $stage,
|
||||
is_array($stage) && count($stage) === 2 => $stage[1],
|
||||
default => throw new RegistryException('Invalid stage definition in AfterStage attribute.'),
|
||||
};
|
||||
if ($method_instance->package_name === '' && $pkg === null) {
|
||||
throw new RegistryException('Package name must not be empty when no package context is available for AfterStage attribute.');
|
||||
}
|
||||
$package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name;
|
||||
self::$after_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved];
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Registry;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactLoader;
|
||||
use StaticPHP\Config\ArtifactConfig;
|
||||
use StaticPHP\Config\PackageConfig;
|
||||
use StaticPHP\ConsoleApplication;
|
||||
use StaticPHP\Doctor\DoctorLoader;
|
||||
use StaticPHP\Exception\EnvironmentException;
|
||||
use StaticPHP\Package\PackageLoader;
|
||||
use StaticPHP\Exception\RegistryException;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
@@ -30,19 +27,19 @@ class Registry
|
||||
{
|
||||
$yaml = file_get_contents($registry_file);
|
||||
if ($yaml === false) {
|
||||
throw new EnvironmentException("Failed to read registry file: {$registry_file}");
|
||||
throw new RegistryException("Failed to read registry file: {$registry_file}");
|
||||
}
|
||||
$data = match (pathinfo($registry_file, PATHINFO_EXTENSION)) {
|
||||
'json' => json_decode($yaml, true),
|
||||
'yaml', 'yml' => Yaml::parse($yaml),
|
||||
default => throw new EnvironmentException("Unsupported registry file format: {$registry_file}"),
|
||||
default => throw new RegistryException("Unsupported registry file format: {$registry_file}"),
|
||||
};
|
||||
if (!is_array($data)) {
|
||||
throw new EnvironmentException("Invalid registry format in file: {$registry_file}");
|
||||
throw new RegistryException("Invalid registry format in file: {$registry_file}");
|
||||
}
|
||||
$registry_name = $data['name'] ?? null;
|
||||
if (!is_string($registry_name) || empty($registry_name)) {
|
||||
throw new EnvironmentException("Registry 'name' is missing or invalid in file: {$registry_file}");
|
||||
throw new RegistryException("Registry 'name' is missing or invalid in file: {$registry_file}");
|
||||
}
|
||||
|
||||
// Prevent loading the same registry twice
|
||||
@@ -190,6 +187,16 @@ class Registry
|
||||
}
|
||||
}
|
||||
|
||||
public static function checkLoadedRegistries(): void
|
||||
{
|
||||
// Register default stages for all PhpExtensionPackage instances
|
||||
// This must be done after all registries are loaded to ensure custom stages take precedence
|
||||
PackageLoader::registerAllDefaultStages();
|
||||
|
||||
// check BeforeStage, AfterStage is valid
|
||||
PackageLoader::checkLoadedStageEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of loaded registry names.
|
||||
*
|
||||
@@ -252,7 +259,7 @@ class Registry
|
||||
}
|
||||
|
||||
// Class not found and no file path provided
|
||||
throw new EnvironmentException(
|
||||
throw new RegistryException(
|
||||
"Class '{$class}' not found. For external registries, either:\n" .
|
||||
" 1. Add an 'autoload' entry pointing to your composer autoload file\n" .
|
||||
" 2. Use 'psr-4' instead of 'classes' for auto-discovery\n" .
|
||||
@@ -272,7 +279,7 @@ class Registry
|
||||
$path = $relative_path_base . DIRECTORY_SEPARATOR . $path;
|
||||
}
|
||||
if (!file_exists($path)) {
|
||||
throw new EnvironmentException("Path does not exist: {$path}");
|
||||
throw new RegistryException("Path does not exist: {$path}");
|
||||
}
|
||||
return FileSystem::convertPath($path);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user