mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-03-19 13:24:51 +08:00
Add DumpStagesCommand to dump package stages and their locations
This commit is contained in:
parent
bb257cffd6
commit
e9279940d7
157
src/StaticPHP/Command/Dev/DumpStagesCommand.php
Normal file
157
src/StaticPHP/Command/Dev/DumpStagesCommand.php
Normal file
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command\Dev;
|
||||
|
||||
use StaticPHP\Command\BaseCommand;
|
||||
use StaticPHP\Registry\PackageLoader;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
#[AsCommand('dev:dump-stages', 'Dump all package stages with their file locations for quick indexing')]
|
||||
class DumpStagesCommand extends BaseCommand
|
||||
{
|
||||
protected bool $no_motd = true;
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->addArgument('packages', InputArgument::OPTIONAL, 'Comma-separated list of packages to dump, e.g. "openssl,zlib,curl". Dumps all packages if omitted.');
|
||||
$this->addArgument('output', InputArgument::OPTIONAL, 'Output file path', ROOT_DIR . '/dump-stages.json');
|
||||
$this->addOption('relative', 'r', InputOption::VALUE_NONE, 'Output file paths relative to ROOT_DIR');
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$outputFile = $this->getArgument('output');
|
||||
$useRelative = (bool) $this->getOption('relative');
|
||||
|
||||
$filterPackages = null;
|
||||
if ($packagesArg = $this->getArgument('packages')) {
|
||||
$filterPackages = array_flip(parse_comma_list($packagesArg));
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach (PackageLoader::getPackages() as $name => $pkg) {
|
||||
if ($filterPackages !== null && !isset($filterPackages[$name])) {
|
||||
continue;
|
||||
}
|
||||
$entry = [
|
||||
'type' => $pkg->getType(),
|
||||
'stages' => [],
|
||||
'before_stages' => [],
|
||||
'after_stages' => [],
|
||||
];
|
||||
|
||||
// Resolve main stages
|
||||
foreach ($pkg->getStages() as $stageName => $callable) {
|
||||
$location = $this->resolveCallableLocation($callable);
|
||||
if ($location !== null && $useRelative) {
|
||||
$location['file'] = $this->toRelativePath($location['file']);
|
||||
}
|
||||
$entry['stages'][$stageName] = $location;
|
||||
}
|
||||
|
||||
$result[$name] = $entry;
|
||||
}
|
||||
|
||||
// Resolve before/after stage external callbacks
|
||||
foreach (PackageLoader::getAllBeforeStages() as $pkgName => $stages) {
|
||||
if ($filterPackages !== null && !isset($filterPackages[$pkgName])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($stages as $stageName => $callbacks) {
|
||||
foreach ($callbacks as [$callable, $onlyWhen]) {
|
||||
$location = $this->resolveCallableLocation($callable);
|
||||
if ($location !== null && $useRelative) {
|
||||
$location['file'] = $this->toRelativePath($location['file']);
|
||||
}
|
||||
$entry_data = $location ?? [];
|
||||
if ($onlyWhen !== null) {
|
||||
$entry_data['only_when_package_resolved'] = $onlyWhen;
|
||||
}
|
||||
$result[$pkgName]['before_stages'][$stageName][] = $entry_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (PackageLoader::getAllAfterStages() as $pkgName => $stages) {
|
||||
if ($filterPackages !== null && !isset($filterPackages[$pkgName])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($stages as $stageName => $callbacks) {
|
||||
foreach ($callbacks as [$callable, $onlyWhen]) {
|
||||
$location = $this->resolveCallableLocation($callable);
|
||||
if ($location !== null && $useRelative) {
|
||||
$location['file'] = $this->toRelativePath($location['file']);
|
||||
}
|
||||
$entry_data = $location ?? [];
|
||||
if ($onlyWhen !== null) {
|
||||
$entry_data['only_when_package_resolved'] = $onlyWhen;
|
||||
}
|
||||
$result[$pkgName]['after_stages'][$stageName][] = $entry_data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
file_put_contents($outputFile, $json . PHP_EOL);
|
||||
|
||||
$this->output->writeln('<info>Dumped stages for ' . count($result) . " package(s) to: {$outputFile}</info>");
|
||||
return static::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the file, start line, class and method name of a callable using reflection.
|
||||
*
|
||||
* @return null|array{file: string, line: false|int, class: string, method: string}
|
||||
*/
|
||||
private function resolveCallableLocation(mixed $callable): ?array
|
||||
{
|
||||
try {
|
||||
if (is_array($callable) && count($callable) === 2) {
|
||||
$ref = new \ReflectionMethod($callable[0], $callable[1]);
|
||||
return [
|
||||
'class' => $ref->getDeclaringClass()->getName(),
|
||||
'method' => $ref->getName(),
|
||||
'file' => (string) $ref->getFileName(),
|
||||
'line' => $ref->getStartLine(),
|
||||
];
|
||||
}
|
||||
if ($callable instanceof \Closure) {
|
||||
$ref = new \ReflectionFunction($callable);
|
||||
$scopeClass = $ref->getClosureScopeClass();
|
||||
return [
|
||||
'class' => $scopeClass !== null ? $scopeClass->getName() : '{closure}',
|
||||
'method' => '{closure}',
|
||||
'file' => (string) $ref->getFileName(),
|
||||
'line' => $ref->getStartLine(),
|
||||
];
|
||||
}
|
||||
if (is_string($callable) && str_contains($callable, '::')) {
|
||||
[$class, $method] = explode('::', $callable, 2);
|
||||
$ref = new \ReflectionMethod($class, $method);
|
||||
return [
|
||||
'class' => $ref->getDeclaringClass()->getName(),
|
||||
'method' => $ref->getName(),
|
||||
'file' => (string) $ref->getFileName(),
|
||||
'line' => $ref->getStartLine(),
|
||||
];
|
||||
}
|
||||
} catch (\ReflectionException) {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function toRelativePath(string $absolutePath): string
|
||||
{
|
||||
$root = rtrim(ROOT_DIR, '/') . '/';
|
||||
if (str_starts_with($absolutePath, $root)) {
|
||||
return substr($absolutePath, strlen($root));
|
||||
}
|
||||
return $absolutePath;
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ namespace StaticPHP;
|
||||
|
||||
use StaticPHP\Command\BuildLibsCommand;
|
||||
use StaticPHP\Command\BuildTargetCommand;
|
||||
use StaticPHP\Command\Dev\DumpStagesCommand;
|
||||
use StaticPHP\Command\Dev\EnvCommand;
|
||||
use StaticPHP\Command\Dev\IsInstalledCommand;
|
||||
use StaticPHP\Command\Dev\LintConfigCommand;
|
||||
@ -67,6 +68,7 @@ class ConsoleApplication extends Application
|
||||
new EnvCommand(),
|
||||
new LintConfigCommand(),
|
||||
new PackLibCommand(),
|
||||
new DumpStagesCommand(),
|
||||
]);
|
||||
|
||||
// add additional commands from registries
|
||||
|
||||
@ -128,6 +128,16 @@ abstract class Package
|
||||
$this->stages[$name] = $stage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all defined stages for this package.
|
||||
*
|
||||
* @return array<string, callable>
|
||||
*/
|
||||
public function getStages(): array
|
||||
{
|
||||
return $this->stages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the package has a specific stage defined.
|
||||
*
|
||||
|
||||
@ -240,6 +240,26 @@ class PackageLoader
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered before-stage callbacks (raw).
|
||||
*
|
||||
* @return array<string, array<string, list<array{0: callable, 1: ?string}>>>
|
||||
*/
|
||||
public static function getAllBeforeStages(): array
|
||||
{
|
||||
return self::$before_stages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered after-stage callbacks (raw).
|
||||
*
|
||||
* @return array<string, array<string, list<array{0: callable, 1: ?string}>>>
|
||||
*/
|
||||
public static function getAllAfterStages(): array
|
||||
{
|
||||
return self::$after_stages;
|
||||
}
|
||||
|
||||
public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable
|
||||
{
|
||||
// match condition
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user