Skip to content

Commit abe64fa

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

7 files changed

Lines changed: 166 additions & 23 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::getRequiredAttributeArguments(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: 4 additions & 10 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,22 +81,16 @@ 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();
10094
$commandName = $arguments['name'] ?? $arguments[0] ?? '';
10195
$aliases = $arguments['aliases'] ?? $arguments[2] ?? [];
10296
$commandNames = [$commandName, ...((array) $aliases)];

src/Reflection/ClassReflection.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
24+
/**
25+
* Centralizes small reflection lookups used by DevTools runtime metadata.
26+
*/
27+
final class ClassReflection
28+
{
29+
/**
30+
* @param class-string $className
31+
* @param class-string $parentClass
32+
*/
33+
public static function isInstantiableSubclassOf(string $className, string $parentClass): bool
34+
{
35+
$reflection = new ReflectionClass($className);
36+
37+
return $reflection->isInstantiable() && $reflection->isSubclassOf($parentClass);
38+
}
39+
40+
/**
41+
* @param class-string $className
42+
* @param class-string $attributeClass
43+
*
44+
* @return array<string|int, mixed>|null
45+
*/
46+
public static function getAttributeArguments(string $className, string $attributeClass): ?array
47+
{
48+
$reflection = new ReflectionClass($className);
49+
$attribute = $reflection->getAttributes($attributeClass)[0] ?? null;
50+
51+
return null === $attribute ? null : $attribute->getArguments();
52+
}
53+
54+
/**
55+
* @param class-string $className
56+
* @param class-string $attributeClass
57+
*
58+
* @return array<string|int, mixed>
59+
*/
60+
public static function getRequiredAttributeArguments(string $className, string $attributeClass): array
61+
{
62+
$reflection = new ReflectionClass($className);
63+
64+
return $reflection->getAttributes($attributeClass)[0]
65+
->getArguments();
66+
}
67+
}

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: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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+
], ClassReflection::getAttributeArguments(SelfUpdateCommand::class, AsCommand::class));
62+
}
63+
64+
/**
65+
* @return void
66+
*/
67+
#[Test]
68+
public function getAttributeArgumentsWillReturnNullWhenAttributeDoesNotExist(): void
69+
{
70+
self::assertNull(ClassReflection::getAttributeArguments(self::class, AsCommand::class));
71+
}
72+
73+
/**
74+
* @return void
75+
*/
76+
#[Test]
77+
public function getRequiredAttributeArgumentsWillReturnArgumentsForMatchingAttribute(): void
78+
{
79+
self::assertSame([
80+
'name' => 'dev-tools:self-update',
81+
'description' => 'Updates the installed fast-forward/dev-tools package.',
82+
'aliases' => ['self-update', 'selfupdate'],
83+
], ClassReflection::getRequiredAttributeArguments(SelfUpdateCommand::class, AsCommand::class));
84+
}
85+
}

0 commit comments

Comments
 (0)