Add ConditionalOn attribute

This commit is contained in:
crazywhalecc
2026-05-07 14:40:14 +08:00
parent 8e46cda227
commit d3b7c9106f
2 changed files with 56 additions and 7 deletions

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Attribute\Package;
/**
* Makes a #[BeforeStage] or #[AfterStage] hook conditional on DI container bindings.
*
* The hook is only invoked when ALL specified classes are currently bound in the
* DI container. Multiple #[ConditionalOn] attributes on the same method use AND
* semantics — every condition must hold for the hook to run.
*
* Example:
*
* #[ConditionalOn(PgoContext::class)]
* #[BeforeStage('php', 'build')]
* public function injectPgoFlags(PgoContext $ctx): void { ... }
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
readonly class ConditionalOn
{
/**
* @param class-string $class the class that must be present in the DI container for this hook to run
*/
public function __construct(public string $class) {}
}

View File

@@ -7,6 +7,7 @@ namespace StaticPHP\Registry;
use StaticPHP\Attribute\Package\AfterStage;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\ConditionalOn;
use StaticPHP\Attribute\Package\CustomPhpConfigureArg;
use StaticPHP\Attribute\Package\Extension;
use StaticPHP\Attribute\Package\Info;
@@ -50,14 +51,14 @@ class PackageLoader
/**
* Source metadata for #[BeforeStage] hooks, keyed by target package name → stage name.
*
* @var array<string, array<string, list<array{class: string, method: string, only_when: ?string}>>>
* @var array<string, array<string, list<array{class: string, method: string, only_when: ?string, conditionals: list<class-string>}>>>
*/
private static array $before_stage_meta = [];
/**
* Source metadata for #[AfterStage] hooks, keyed by target package name → stage name.
*
* @var array<string, array<string, list<array{class: string, method: string, only_when: ?string}>>>
* @var array<string, array<string, list<array{class: string, method: string, only_when: ?string, conditionals: list<class-string>}>>>
*/
private static array $after_stage_meta = [];
@@ -369,10 +370,15 @@ class PackageLoader
// match condition
$installer = ApplicationContext::get(PackageInstaller::class);
$stages = self::$before_stages[$package_name][$stage] ?? [];
foreach ($stages as [$callback, $only_when_package_resolved]) {
foreach ($stages as [$callback, $only_when_package_resolved, $conditionals]) {
if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) {
continue;
}
foreach ($conditionals as $class) {
if (!ApplicationContext::has($class)) {
continue 2;
}
}
yield $callback;
}
}
@@ -383,10 +389,15 @@ class PackageLoader
$installer = ApplicationContext::get(PackageInstaller::class);
$stages = self::$after_stages[$package_name][$stage] ?? [];
$result = [];
foreach ($stages as [$callback, $only_when_package_resolved]) {
foreach ($stages as [$callback, $only_when_package_resolved, $conditionals]) {
if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) {
continue;
}
foreach ($conditionals as $class) {
if (!ApplicationContext::has($class)) {
continue 2;
}
}
$result[] = $callback;
}
return $result;
@@ -422,7 +433,7 @@ class PackageLoader
}
$pkg = self::getPackage($package_name);
foreach ($stages as $stage_name => $before_events) {
foreach ($before_events as [$event_callable, $only_when_package_resolved]) {
foreach ($before_events as [$event_callable, $only_when_package_resolved, $conditionals]) {
// 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}].");
@@ -488,12 +499,17 @@ class PackageLoader
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];
$conditionals = array_map(
fn (\ReflectionAttribute $a) => $a->newInstance()->class,
[...$method->getDeclaringClass()->getAttributes(ConditionalOn::class), ...$method->getAttributes(ConditionalOn::class)],
);
self::$before_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved, $conditionals];
$registering_class = get_class($instance_class);
self::$before_stage_meta[$package_name][$stage][] = [
'class' => $registering_class,
'method' => $method->getName(),
'only_when' => $method_instance->only_when_package_resolved,
'conditionals' => $conditionals,
];
self::$class_before_stage_meta[$registering_class][$package_name][$stage][] = [
'method' => $method->getName(),
@@ -513,12 +529,17 @@ class PackageLoader
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];
$conditionals = array_map(
fn (\ReflectionAttribute $a) => $a->newInstance()->class,
[...$method->getDeclaringClass()->getAttributes(ConditionalOn::class), ...$method->getAttributes(ConditionalOn::class)],
);
self::$after_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved, $conditionals];
$registering_class = get_class($instance_class);
self::$after_stage_meta[$package_name][$stage][] = [
'class' => $registering_class,
'method' => $method->getName(),
'only_when' => $method_instance->only_when_package_resolved,
'conditionals' => $conditionals,
];
self::$class_after_stage_meta[$registering_class][$package_name][$stage][] = [
'method' => $method->getName(),
@@ -541,6 +562,7 @@ class PackageLoader
ResolveBuild::class => 'ResolveBuild',
Info::class => 'Info',
Validate::class => 'Validate',
ConditionalOn::class => 'ConditionalOn',
default => null,
};
}