Skip to content

Commit 6508518

Browse files
committed
Add DevTools self-update command
1 parent 1240169 commit 6508518

7 files changed

Lines changed: 177 additions & 26 deletions

File tree

src/Console/Command/SelfUpdateCommand.php

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
namespace FastForward\DevTools\Console\Command;
2121

2222
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
23+
use FastForward\DevTools\Reflection\ClassReflection;
2324
use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface;
2425
use Psr\Log\LoggerInterface;
25-
use ReflectionClass;
2626
use Symfony\Component\Console\Attribute\AsCommand;
2727
use Symfony\Component\Console\Command\Command;
2828
use Symfony\Component\Console\Input\InputInterface;
@@ -65,21 +65,12 @@ public static function getCommandNames(): array
6565
return $commandNames;
6666
}
6767

68-
$reflection = new ReflectionClass(self::class);
69-
$attribute = $reflection->getAttributes(AsCommand::class)[0] ?? null;
70-
71-
if (null === $attribute) {
72-
return $commandNames = [];
73-
}
74-
75-
$arguments = $attribute->getArguments();
76-
$commandName = $arguments['name'] ?? $arguments[0] ?? '';
77-
$aliases = $arguments['aliases'] ?? $arguments[2] ?? [];
78-
$commandNames = [$commandName, ...((array) $aliases)];
68+
$arguments = ClassReflection::getAttributeArguments(self::class, AsCommand::class);
69+
$commandNames = [$arguments['name'], ...$arguments['aliases']];
7970

8071
return $commandNames = array_values(array_filter(
8172
$commandNames,
82-
static fn(mixed $commandName): bool => \is_string($commandName) && '' !== $commandName,
73+
static fn(string $commandName): bool => '' !== $commandName,
8374
));
8475
}
8576

src/Console/CommandLoader/DevToolsCommandLoader.php

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
namespace FastForward\DevTools\Console\CommandLoader;
2121

2222
use FastForward\DevTools\Filesystem\FinderFactoryInterface;
23+
use FastForward\DevTools\Reflection\ClassReflection;
2324
use Psr\Container\ContainerInterface;
24-
use ReflectionClass;
2525
use RuntimeException;
2626
use Symfony\Component\Console\Attribute\AsCommand;
2727
use Symfony\Component\Console\Command\Command;
@@ -81,25 +81,17 @@ private function getCommandMap(FinderFactoryInterface $finderFactory): array
8181

8282
foreach ($commandsDirectory as $file) {
8383
$class = $namespace . $file->getBasename('.php');
84-
$reflection = new ReflectionClass($class);
85-
if (! $reflection->isInstantiable()) {
84+
if (! ClassReflection::isInstantiableSubclassOf($class, Command::class)) {
8685
continue;
8786
}
8887

89-
if (! $reflection->isSubclassOf(Command::class)) {
90-
continue;
91-
}
92-
93-
$attribute = $reflection->getAttributes(AsCommand::class)[0] ?? null;
88+
$arguments = ClassReflection::getAttributeArguments($class, AsCommand::class);
9489

95-
if (null === $attribute) {
90+
if (null === $arguments) {
9691
continue;
9792
}
9893

99-
$arguments = $attribute->getArguments();
100-
$commandName = $arguments['name'] ?? $arguments[0] ?? '';
101-
$aliases = $arguments['aliases'] ?? $arguments[2] ?? [];
102-
$commandNames = [$commandName, ...((array) $aliases)];
94+
$commandNames = [$arguments['name'], ...((array) $arguments['aliases'])];
10395

10496
foreach ($commandNames as $commandName) {
10597
if (! \is_string($commandName)) {

src/Reflection/ClassReflection.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Fast Forward Development Tools for PHP projects.
7+
*
8+
* This file is part of fast-forward/dev-tools project.
9+
*
10+
* @author Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
11+
* @license https://opensource.org/licenses/MIT MIT License
12+
*
13+
* @see https://github.com/php-fast-forward/
14+
* @see https://github.com/php-fast-forward/dev-tools
15+
* @see https://github.com/php-fast-forward/dev-tools/issues
16+
* @see https://php-fast-forward.github.io/dev-tools/
17+
* @see https://datatracker.ietf.org/doc/html/rfc2119
18+
*/
19+
20+
namespace FastForward\DevTools\Reflection;
21+
22+
use ReflectionClass;
23+
use ReflectionMethod;
24+
25+
/**
26+
* Centralizes small reflection lookups used by DevTools runtime metadata.
27+
*/
28+
final class ClassReflection
29+
{
30+
/**
31+
* @param class-string $className
32+
* @param class-string $parentClass
33+
*/
34+
public static function isInstantiableSubclassOf(string $className, string $parentClass): bool
35+
{
36+
$reflection = new ReflectionClass($className);
37+
38+
return $reflection->isInstantiable() && $reflection->isSubclassOf($parentClass);
39+
}
40+
41+
/**
42+
* @param class-string $className
43+
* @param class-string $attributeClass
44+
*
45+
* @return array<string, mixed>|null
46+
*/
47+
public static function getAttributeArguments(string $className, string $attributeClass): ?array
48+
{
49+
$reflection = new ReflectionClass($className);
50+
$attribute = $reflection->getAttributes($attributeClass)[0] ?? null;
51+
52+
if (null === $attribute) {
53+
return null;
54+
}
55+
56+
$arguments = $attribute->getArguments();
57+
$constructor = new ReflectionMethod($attributeClass, '__construct');
58+
$normalizedArguments = [];
59+
60+
foreach ($constructor->getParameters() as $parameter) {
61+
$normalizedArguments[$parameter->getName()] = $arguments[$parameter->getName()]
62+
?? $arguments[$parameter->getPosition()]
63+
?? ($parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null);
64+
}
65+
66+
return $normalizedArguments;
67+
}
68+
}

tests/Console/Command/SelfUpdateCommandTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Prophecy\Argument;
2323
use FastForward\DevTools\Console\Command\SelfUpdateCommand;
2424
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
25+
use FastForward\DevTools\Reflection\ClassReflection;
2526
use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface;
2627
use PHPUnit\Framework\Attributes\CoversClass;
2728
use PHPUnit\Framework\Attributes\Test;
@@ -35,6 +36,7 @@
3536
use Symfony\Component\Console\Output\OutputInterface;
3637

3738
#[CoversClass(SelfUpdateCommand::class)]
39+
#[UsesClass(ClassReflection::class)]
3840
#[UsesTrait(LogsCommandResults::class)]
3941
final class SelfUpdateCommandTest extends TestCase
4042
{

tests/Console/CommandLoader/DevToolsCommandLoaderTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use FastForward\DevTools\Console\Command\TestsCommand;
2626
use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader;
2727
use FastForward\DevTools\Filesystem\FinderFactoryInterface;
28+
use FastForward\DevTools\Reflection\ClassReflection;
2829
use RuntimeException;
2930
use PHPUnit\Framework\Attributes\CoversClass;
3031
use PHPUnit\Framework\Attributes\Test;
@@ -39,6 +40,7 @@
3940
use Symfony\Component\Finder\SplFileInfo;
4041

4142
#[CoversClass(DevToolsCommandLoader::class)]
43+
#[UsesClass(ClassReflection::class)]
4244
#[UsesClass(AgentsCommand::class)]
4345
#[UsesClass(SyncCommand::class)]
4446
final class DevToolsCommandLoaderTest extends TestCase

tests/Console/DevToolsTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use FastForward\DevTools\Process\ProcessBuilder;
3434
use FastForward\DevTools\Process\ProcessQueue;
3535
use FastForward\DevTools\Process\XdebugDisablingProcessEnvironmentConfigurator;
36+
use FastForward\DevTools\Reflection\ClassReflection;
3637
use FastForward\DevTools\SelfUpdate\ComposerSelfUpdateRunner;
3738
use FastForward\DevTools\SelfUpdate\ComposerVersionChecker;
3839
use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface;
@@ -66,6 +67,7 @@
6667
#[UsesClass(DevToolsServiceProvider::class)]
6768
#[UsesClass(WorkingProjectPathResolver::class)]
6869
#[UsesClass(SelfUpdateCommand::class)]
70+
#[UsesClass(ClassReflection::class)]
6971
#[UsesClass(LogLevelOutputFormatter::class)]
7072
#[UsesClass(GithubActionOutput::class)]
7173
#[UsesClass(ColorPreservingProcessEnvironmentConfigurator::class)]
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Fast Forward Development Tools for PHP projects.
7+
*
8+
* This file is part of fast-forward/dev-tools project.
9+
*
10+
* @author Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
11+
* @license https://opensource.org/licenses/MIT MIT License
12+
*
13+
* @see https://github.com/php-fast-forward/
14+
* @see https://github.com/php-fast-forward/dev-tools
15+
* @see https://github.com/php-fast-forward/dev-tools/issues
16+
* @see https://php-fast-forward.github.io/dev-tools/
17+
* @see https://datatracker.ietf.org/doc/html/rfc2119
18+
*/
19+
20+
namespace FastForward\DevTools\Tests\Reflection;
21+
22+
use FastForward\DevTools\Console\Command\SelfUpdateCommand;
23+
use FastForward\DevTools\Reflection\ClassReflection;
24+
use PHPUnit\Framework\Attributes\CoversClass;
25+
use PHPUnit\Framework\Attributes\Test;
26+
use PHPUnit\Framework\TestCase;
27+
use Symfony\Component\Console\Attribute\AsCommand;
28+
use Symfony\Component\Console\Command\Command;
29+
30+
#[CoversClass(ClassReflection::class)]
31+
final class ClassReflectionTest extends TestCase
32+
{
33+
/**
34+
* @return void
35+
*/
36+
#[Test]
37+
public function isInstantiableSubclassOfWillReturnTrueForMatchingClass(): void
38+
{
39+
self::assertTrue(ClassReflection::isInstantiableSubclassOf(SelfUpdateCommand::class, Command::class));
40+
}
41+
42+
/**
43+
* @return void
44+
*/
45+
#[Test]
46+
public function isInstantiableSubclassOfWillReturnFalseForNonMatchingClass(): void
47+
{
48+
self::assertFalse(ClassReflection::isInstantiableSubclassOf(self::class, Command::class));
49+
}
50+
51+
/**
52+
* @return void
53+
*/
54+
#[Test]
55+
public function getAttributeArgumentsWillReturnArgumentsForMatchingAttribute(): void
56+
{
57+
self::assertSame([
58+
'name' => 'dev-tools:self-update',
59+
'description' => 'Updates the installed fast-forward/dev-tools package.',
60+
'aliases' => ['self-update', 'selfupdate'],
61+
'hidden' => false,
62+
'help' => null,
63+
'usages' => [],
64+
], ClassReflection::getAttributeArguments(SelfUpdateCommand::class, AsCommand::class));
65+
}
66+
67+
/**
68+
* @return void
69+
*/
70+
#[Test]
71+
public function getAttributeArgumentsWillReturnNullWhenAttributeDoesNotExist(): void
72+
{
73+
self::assertNull(ClassReflection::getAttributeArguments(self::class, AsCommand::class));
74+
}
75+
76+
/**
77+
* @return void
78+
*/
79+
#[Test]
80+
public function getAttributeArgumentsWillNormalizePositionalArguments(): void
81+
{
82+
self::assertSame([
83+
'name' => 'fixture',
84+
'description' => 'Fixture command.',
85+
'aliases' => ['alias'],
86+
'hidden' => false,
87+
'help' => null,
88+
'usages' => [],
89+
], ClassReflection::getAttributeArguments(FixtureCommandWithPositionalAttribute::class, AsCommand::class));
90+
}
91+
}
92+
93+
#[AsCommand('fixture', 'Fixture command.', ['alias'])]
94+
final class FixtureCommandWithPositionalAttribute extends Command {}

0 commit comments

Comments
 (0)