Skip to content

Commit 4b6dd63

Browse files
[path] Complete runtime-aware binary fallbacks for remaining commands (#297) (#298)
* [path] Complete runtime-aware binary fallbacks for remaining commands * Update wiki submodule pointer for PR #298 * [path] Fix phpDocumentor template runtime fallbacks * Update wiki submodule pointer for PR #298 --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 725f6f7 commit 4b6dd63

12 files changed

Lines changed: 191 additions & 15 deletions

.github/wiki

Submodule wiki updated from 02bb3c4 to bce2ecf

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Keep global `dev-tools:sync` runs machine-independent by removing only deprecated DevTools-managed Composer GrumPHP default-path metadata while wiring packaged Git hooks to prefer a project-local `grumphp.yml` and otherwise use the active packaged DevTools config path resolved at sync time (#288)
13+
- Complete global runtime fallback support for phpdoc, phpunit, and phpmetrics commands, including packaged phpDocumentor templates used by `docs` and `wiki` (#297)
1314

1415
## [1.24.2] - 2026-04-30
1516

src/Console/Command/DocsCommand.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use FastForward\DevTools\Console\Input\HasJsonOption;
2626
use Twig\Environment;
2727
use FastForward\DevTools\Filesystem\FilesystemInterface;
28+
use FastForward\DevTools\Path\DevToolsPathResolver;
2829
use FastForward\DevTools\Process\ProcessBuilderInterface;
2930
use FastForward\DevTools\Process\ProcessQueueInterface;
3031
use FastForward\DevTools\Path\ManagedWorkspace;
@@ -57,6 +58,11 @@ final class DocsCommand extends Command
5758
use HasJsonOption;
5859
use LogsCommandResults;
5960

61+
/**
62+
* @var string the default phpDocumentor template path relative to the consumer project
63+
*/
64+
private const string DEFAULT_TEMPLATE = 'vendor/fast-forward/phpdoc-bootstrap-template';
65+
6066
/**
6167
* Creates a new DocsCommand instance.
6268
*
@@ -114,7 +120,7 @@ protected function configure(): void
114120
name: 'template',
115121
mode: InputOption::VALUE_OPTIONAL,
116122
description: 'Path to the template directory for the generated HTML documentation.',
117-
default: 'vendor/fast-forward/phpdoc-bootstrap-template',
123+
default: self::DEFAULT_TEMPLATE,
118124
);
119125
}
120126

@@ -136,6 +142,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
136142
$source = $this->filesystem->getAbsolutePath($input->getOption('source'));
137143
$target = $this->filesystem->getAbsolutePath($input->getOption('target'));
138144
$cacheDir = $this->filesystem->getAbsolutePath($input->getOption('cache-dir'));
145+
$template = (string) $input->getOption('template');
146+
147+
if (self::DEFAULT_TEMPLATE === $template) {
148+
$template = DevToolsPathResolver::getPreferredVendorPath(self::DEFAULT_TEMPLATE);
149+
}
139150

140151
$this->logger->info('Generating API documentation...', [
141152
'input' => $input,
@@ -150,7 +161,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
150161
$config = $this->createPhpDocumentorConfig(
151162
source: $source,
152163
target: $target,
153-
template: $input->getOption('template'),
164+
template: $template,
154165
cacheDir: $cacheEnabled ? $cacheDir : sys_get_temp_dir(),
155166
);
156167

@@ -167,7 +178,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
167178
$processBuilder = $processBuilder->withArgument('--no-progress');
168179
}
169180

170-
$phpdoc = $processBuilder->build('vendor/bin/phpdoc');
181+
$phpdoc = $processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('phpdoc')]);
171182

172183
$this->processQueue->add(process: $phpdoc, label: 'Generating API Docs with phpDocumentor');
173184

src/Console/Command/MetricsCommand.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
2323
use FastForward\DevTools\Console\Input\HasJsonOption;
24+
use FastForward\DevTools\Path\DevToolsPathResolver;
2425
use FastForward\DevTools\Process\ProcessBuilderInterface;
2526
use FastForward\DevTools\Process\ProcessQueueInterface;
2627
use FastForward\DevTools\Path\ManagedWorkspace;
@@ -45,9 +46,9 @@ final class MetricsCommand extends Command
4546
use LogsCommandResults;
4647

4748
/**
48-
* @var string the bundled PhpMetrics binary path relative to the consumer root
49+
* @var string the PhpMetrics binary name resolved through the runtime-aware tooling lookup
4950
*/
50-
private const string BINARY = 'vendor/bin/phpmetrics';
51+
private const string BINARY = 'phpmetrics';
5152

5253
/**
5354
* @var int the PHP error reporting mask that suppresses deprecations emitted by PhpMetrics internals
@@ -161,7 +162,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
161162
\PHP_BINARY,
162163
'-derror_reporting=' . self::PHP_ERROR_REPORTING,
163164
'-ddefault_socket_timeout=' . self::PHP_DEFAULT_SOCKET_TIMEOUT,
164-
self::BINARY,
165+
DevToolsPathResolver::getPreferredToolBinaryPath(self::BINARY),
165166
]),
166167
label: 'Generating Metrics with PhpMetrics',
167168
);

src/Console/Command/TestsCommand.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use FastForward\DevTools\Console\Input\HasJsonOption;
2525
use FastForward\DevTools\Composer\Json\ComposerJsonInterface;
2626
use FastForward\DevTools\Filesystem\FilesystemInterface;
27+
use FastForward\DevTools\Path\DevToolsPathResolver;
2728
use FastForward\DevTools\PhpUnit\Bootstrap\BootstrapShimGenerator;
2829
use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoaderInterface;
2930
use FastForward\DevTools\Process\ProcessBuilderInterface;
@@ -209,7 +210,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
209210
$this->processQueue->add(
210211
process: $processBuilder
211212
->withArgument($input->getArgument('path'))
212-
->build('vendor/bin/phpunit'),
213+
->build([DevToolsPathResolver::getPreferredToolBinaryPath('phpunit')]),
213214
label: 'Running PHPUnit Tests',
214215
);
215216

src/Console/Command/WikiCommand.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use FastForward\DevTools\Console\Input\HasJsonOption;
2626
use FastForward\DevTools\Filesystem\FilesystemInterface;
2727
use FastForward\DevTools\Git\GitClientInterface;
28+
use FastForward\DevTools\Path\DevToolsPathResolver;
2829
use FastForward\DevTools\Process\ProcessBuilderInterface;
2930
use FastForward\DevTools\Process\ProcessQueueInterface;
3031
use FastForward\DevTools\Path\ManagedWorkspace;
@@ -54,6 +55,11 @@ final class WikiCommand extends Command
5455
use HasJsonOption;
5556
use LogsCommandResults;
5657

58+
/**
59+
* @var string the default phpDocumentor Markdown template path relative to the consumer project
60+
*/
61+
private const string DEFAULT_TEMPLATE = 'vendor/saggre/phpdocumentor-markdown/themes/markdown';
62+
5763
/**
5864
* Creates a new WikiCommand instance.
5965
*
@@ -138,7 +144,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
138144
$processBuilder = $this->processBuilder
139145
->withArgument('--ansi')
140146
->withArgument('--visibility', 'public,protected')
141-
->withArgument('--template', 'vendor/saggre/phpdocumentor-markdown/themes/markdown')
147+
->withArgument('--template', DevToolsPathResolver::getPreferredVendorPath(self::DEFAULT_TEMPLATE))
142148
->withArgument('--title', $this->composer->getDescription())
143149
->withArgument('--target', $target);
144150

@@ -157,7 +163,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
157163
}
158164

159165
$this->processQueue->add(
160-
process: $processBuilder->build('vendor/bin/phpdoc'),
166+
process: $processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('phpdoc')]),
161167
label: 'Generating Wiki with phpDocumentor',
162168
);
163169

src/Path/DevToolsPathResolver.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,27 @@ public static function getRuntimeToolBinaryPath(string $binary, string $packageP
128128
return Path::join($packagePath, 'vendor', 'bin', $binary);
129129
}
130130

131+
/**
132+
* Returns the active Composer vendor path for the current DevTools installation mode.
133+
*
134+
* Relative vendor paths MAY be passed either with or without a leading
135+
* `vendor/` prefix.
136+
*
137+
* @param string $path the vendor-relative path to resolve
138+
* @param string $packagePath an optional package root path; defaults to the current package root
139+
*/
140+
public static function getRuntimeVendorPath(string $path, string $packagePath = ''): string
141+
{
142+
$packagePath = Path::canonicalize('' === $packagePath ? self::getPackagePath() : $packagePath);
143+
$vendorPath = self::normalizeVendorRelativePath($path);
144+
145+
if (self::isInstalledAsDependency($packagePath)) {
146+
return Path::canonicalize(Path::join($packagePath, '..', '..', $vendorPath));
147+
}
148+
149+
return Path::join($packagePath, 'vendor', $vendorPath);
150+
}
151+
131152
/**
132153
* Returns the preferred tooling binary path for the active project and DevTools runtime.
133154
*
@@ -154,6 +175,33 @@ public static function getPreferredToolBinaryPath(
154175
return self::getRuntimeToolBinaryPath($binary, $packagePath);
155176
}
156177

178+
/**
179+
* Returns the preferred Composer vendor path for the active project and DevTools runtime.
180+
*
181+
* Consumer projects SHOULD take precedence when they provide the requested
182+
* vendor path locally. If the path is absent locally, the method MUST fall
183+
* back to the active DevTools runtime vendor path.
184+
*
185+
* @param string $path the vendor-relative path to resolve
186+
* @param string $projectPath an optional project root path; defaults to the working project root
187+
* @param string $packagePath an optional package root path; defaults to the current package root
188+
*/
189+
public static function getPreferredVendorPath(
190+
string $path,
191+
string $projectPath = '',
192+
string $packagePath = '',
193+
): string {
194+
$projectPath = '' === $projectPath ? WorkingProjectPathResolver::getProjectPath() : $projectPath;
195+
$vendorPath = self::normalizeVendorRelativePath($path);
196+
$projectVendorPath = Path::join($projectPath, 'vendor', $vendorPath);
197+
198+
if (file_exists($projectVendorPath)) {
199+
return $projectVendorPath;
200+
}
201+
202+
return self::getRuntimeVendorPath($vendorPath, $packagePath);
203+
}
204+
157205
/**
158206
* Detects whether the provided path belongs to an installed vendor copy of DevTools.
159207
*
@@ -175,4 +223,20 @@ public static function isRepositoryCheckout(string $packagePath = ''): bool
175223
{
176224
return ! self::isInstalledAsDependency($packagePath);
177225
}
226+
227+
/**
228+
* Normalizes a path relative to the Composer vendor root.
229+
*
230+
* @param string $path the vendor-relative path to normalize
231+
*/
232+
private static function normalizeVendorRelativePath(string $path): string
233+
{
234+
$path = Path::canonicalize($path);
235+
236+
if (str_starts_with($path, 'vendor/')) {
237+
return substr($path, 7);
238+
}
239+
240+
return $path;
241+
}
178242
}

tests/Console/Command/DocsCommandTest.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
2424
use FastForward\DevTools\Console\Command\DocsCommand;
2525
use FastForward\DevTools\Filesystem\FilesystemInterface;
26+
use FastForward\DevTools\Path\DevToolsPathResolver;
2627
use FastForward\DevTools\Process\ProcessBuilderInterface;
2728
use FastForward\DevTools\Process\ProcessQueueInterface;
2829
use FastForward\DevTools\Path\ManagedWorkspace;
30+
use FastForward\DevTools\Path\WorkingProjectPathResolver;
2931
use PHPUnit\Framework\Attributes\CoversClass;
3032
use PHPUnit\Framework\Attributes\Test;
3133
use PHPUnit\Framework\Attributes\UsesClass;
@@ -43,7 +45,9 @@
4345
use Twig\Environment;
4446

4547
#[CoversClass(DocsCommand::class)]
48+
#[UsesClass(DevToolsPathResolver::class)]
4649
#[UsesClass(ManagedWorkspace::class)]
50+
#[UsesClass(WorkingProjectPathResolver::class)]
4751
#[UsesTrait(LogsCommandResults::class)]
4852
final class DocsCommandTest extends TestCase
4953
{
@@ -128,12 +132,19 @@ protected function setUp(): void
128132
]);
129133
$this->composer->getName()
130134
->willReturn('fast-forward/dev-tools');
131-
$this->renderer->render('phpdocumentor.xml', Argument::type('array'))->willReturn('<phpdocumentor />');
135+
$this->renderer->render(
136+
'phpdocumentor.xml',
137+
Argument::that(
138+
static fn(array $context): bool => DevToolsPathResolver::getPreferredVendorPath(
139+
'vendor/fast-forward/phpdoc-bootstrap-template'
140+
) === $context['template']
141+
)
142+
)->willReturn('<phpdocumentor />');
132143
$this->processBuilder->withArgument(Argument::any())->willReturn($this->processBuilder->reveal());
133144
$this->processBuilder->withArgument(Argument::any(), Argument::any())->willReturn(
134145
$this->processBuilder->reveal()
135146
);
136-
$this->processBuilder->build('vendor/bin/phpdoc')
147+
$this->processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('phpdoc')])
137148
->willReturn($this->process->reveal());
138149

139150
$this->command = new DocsCommand(

tests/Console/Command/MetricsCommandTest.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121

2222
use FastForward\DevTools\Console\Command\MetricsCommand;
2323
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
24+
use FastForward\DevTools\Path\DevToolsPathResolver;
2425
use FastForward\DevTools\Process\ProcessBuilderInterface;
2526
use FastForward\DevTools\Process\ProcessQueueInterface;
2627
use FastForward\DevTools\Path\ManagedWorkspace;
28+
use FastForward\DevTools\Path\WorkingProjectPathResolver;
2729
use PHPUnit\Framework\Attributes\CoversClass;
2830
use PHPUnit\Framework\Attributes\Test;
2931
use PHPUnit\Framework\Attributes\UsesClass;
@@ -42,7 +44,9 @@
4244
use function Safe\putenv;
4345

4446
#[CoversClass(MetricsCommand::class)]
47+
#[UsesClass(DevToolsPathResolver::class)]
4548
#[UsesClass(ManagedWorkspace::class)]
49+
#[UsesClass(WorkingProjectPathResolver::class)]
4650
#[UsesTrait(LogsCommandResults::class)]
4751
final class MetricsCommandTest extends TestCase
4852
{
@@ -99,7 +103,8 @@ protected function setUp(): void
99103
$this->processBuilder->build(Argument::that(static fn(array $command): bool => \PHP_BINARY === $command[0]
100104
&& str_starts_with((string) $command[1], '-derror_reporting=')
101105
&& '-ddefault_socket_timeout=1' === $command[2]
102-
&& 'vendor/bin/phpmetrics' === $command[3]))->willReturn($this->process->reveal());
106+
&& DevToolsPathResolver::getPreferredToolBinaryPath('phpmetrics') === $command[3]))
107+
->willReturn($this->process->reveal());
103108
$this->command = new MetricsCommand(
104109
$this->processBuilder->reveal(),
105110
$this->processQueue->reveal(),

tests/Console/Command/TestsCommandTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use FastForward\DevTools\Process\ProcessQueueInterface;
3131
use FastForward\DevTools\Path\ManagedWorkspace;
3232
use FastForward\DevTools\Path\DevToolsPathResolver;
33+
use FastForward\DevTools\Path\WorkingProjectPathResolver;
3334
use PHPUnit\Framework\Attributes\CoversClass;
3435
use PHPUnit\Framework\Attributes\Test;
3536
use PHPUnit\Framework\Attributes\UsesClass;
@@ -54,6 +55,7 @@
5455
#[UsesClass(DevToolsPathResolver::class)]
5556
#[UsesClass(ProcessBuilder::class)]
5657
#[UsesClass(ManagedWorkspace::class)]
58+
#[UsesClass(WorkingProjectPathResolver::class)]
5759
#[UsesTrait(LogsCommandResults::class)]
5860
final class TestsCommandTest extends TestCase
5961
{
@@ -147,6 +149,9 @@ public function executeWillRunPhpUnitProcessWithConfigFile(): void
147149
) && str_contains(
148150
$process->getCommandLine(),
149151
'--bootstrap=' . $generatedBootstrapPath,
152+
) && str_contains(
153+
$process->getCommandLine(),
154+
DevToolsPathResolver::getPreferredToolBinaryPath('phpunit'),
150155
) && str_contains($process->getCommandLine(), '--cache-result') && str_contains(
151156
$process->getCommandLine(),
152157
'--cache-directory=' . getcwd() . '/.dev-tools/cache/phpunit',

0 commit comments

Comments
 (0)