Skip to content

Commit a918fb2

Browse files
xuanyanwowhzhhuangdijia
authored
Auto complete options for as command and closure command. (#6734)
Co-authored-by: hzh <hzh@addcn.com> Co-authored-by: Deeka Wong <8337659+huangdijia@users.noreply.github.com>
1 parent 8a3435d commit a918fb2

5 files changed

Lines changed: 347 additions & 0 deletions

File tree

src/AsCommand.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
namespace Hyperf\Command;
1414

1515
use Hyperf\Command\Concerns\InteractsWithIO;
16+
use Hyperf\Stringable\Str;
1617
use Psr\Container\ContainerInterface;
1718

1819
use function Hyperf\Support\class_uses_recursive;
@@ -31,6 +32,24 @@ public function __construct(
3132
$this->parameterParser = $container->get(ParameterParser::class);
3233

3334
parent::__construct();
35+
36+
$options = $this->parameterParser->parseMethodOptions($class, $method);
37+
$definition = $this->getDefinition();
38+
foreach ($options as $option) {
39+
$name = $option->getName();
40+
$snakeName = Str::snake($option->getName(), '-');
41+
42+
if (
43+
$definition->hasOption($name)
44+
|| $definition->hasArgument($name)
45+
|| $definition->hasOption($snakeName)
46+
|| $definition->hasArgument($snakeName)
47+
) {
48+
continue;
49+
}
50+
51+
$definition->addOption($option);
52+
}
3453
}
3554

3655
public function handle()

src/ClosureCommand.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Closure;
1616
use Hyperf\Crontab\Crontab;
1717
use Hyperf\Crontab\Schedule;
18+
use Hyperf\Stringable\Str;
1819
use Psr\Container\ContainerInterface;
1920

2021
use function Hyperf\Tappable\tap;
@@ -32,6 +33,24 @@ public function __construct(
3233
$this->parameterParser = $container->get(ParameterParser::class);
3334

3435
parent::__construct();
36+
37+
$options = $this->parameterParser->parseClosureOptions($closure);
38+
$definition = $this->getDefinition();
39+
foreach ($options as $option) {
40+
$name = $option->getName();
41+
$snakeName = Str::snake($option->getName(), '-');
42+
43+
if (
44+
$definition->hasOption($name)
45+
|| $definition->hasArgument($name)
46+
|| $definition->hasOption($snakeName)
47+
|| $definition->hasArgument($snakeName)
48+
) {
49+
continue;
50+
}
51+
52+
$definition->addOption($option);
53+
}
3554
}
3655

3756
public function handle()

src/ParameterParser.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Hyperf\Stringable\Str;
2020
use InvalidArgumentException;
2121
use Psr\Container\ContainerInterface;
22+
use Symfony\Component\Console\Input\InputOption;
2223

2324
class ParameterParser
2425
{
@@ -62,6 +63,58 @@ public function parseMethodParameters(string $class, string $method, array $argu
6263
return $this->getInjections($definitions, "{$class}::{$method}", $arguments);
6364
}
6465

66+
/**
67+
* @return InputOption[]
68+
*/
69+
public function parseClosureOptions(Closure $closure): array
70+
{
71+
if (! $this->closureDefinitionCollector) {
72+
return [];
73+
}
74+
75+
$definitions = $this->closureDefinitionCollector->getParameters($closure);
76+
$options = [];
77+
78+
foreach ($definitions as $definition) {
79+
$type = $definition->getName();
80+
if (! in_array($type, ['int', 'float', 'string', 'bool'])) {
81+
continue;
82+
}
83+
$name = $definition->getMeta('name');
84+
$mode = $definition->allowsNull() ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED;
85+
$default = $definition->getMeta('defaultValue');
86+
$options[] = new InputOption($name, null, $mode, '', $default, []);
87+
}
88+
89+
return $options;
90+
}
91+
92+
/**
93+
* @return InputOption[]
94+
*/
95+
public function parseMethodOptions(string $class, string $method): array
96+
{
97+
if (! $this->methodDefinitionCollector) {
98+
return [];
99+
}
100+
101+
$definitions = $this->methodDefinitionCollector->getParameters($class, $method);
102+
$options = [];
103+
104+
foreach ($definitions as $definition) {
105+
$type = $definition->getName();
106+
if (! in_array($type, ['int', 'float', 'string', 'bool'])) {
107+
continue;
108+
}
109+
$name = $definition->getMeta('name');
110+
$mode = $definition->allowsNull() ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED;
111+
$default = $definition->getMeta('defaultValue');
112+
$options[] = new InputOption($name, null, $mode, '', $default, []);
113+
}
114+
115+
return $options;
116+
}
117+
65118
private function getInjections(array $definitions, string $callableName, array $arguments): array
66119
{
67120
$injections = [];
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* This file is part of Hyperf.
6+
*
7+
* @link https://www.hyperf.io
8+
* @document https://hyperf.wiki
9+
* @contact group@hyperf.io
10+
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
11+
*/
12+
13+
namespace HyperfTest\Command;
14+
15+
use Hyperf\Command\AsCommand;
16+
use Hyperf\Command\ClosureCommand;
17+
use Hyperf\Command\Console;
18+
use Hyperf\Command\Listener\RegisterCommandListener;
19+
use Hyperf\Command\ParameterParser;
20+
use Hyperf\Config\Config;
21+
use Hyperf\Context\ApplicationContext;
22+
use Hyperf\Contract\ConfigInterface;
23+
use Hyperf\Contract\ContainerInterface;
24+
use Hyperf\Contract\NormalizerInterface;
25+
use Hyperf\Contract\StdoutLoggerInterface;
26+
use Hyperf\Di\Annotation\AnnotationReader;
27+
use Hyperf\Di\Annotation\ScanConfig;
28+
use Hyperf\Di\Annotation\Scanner;
29+
use Hyperf\Di\ClosureDefinitionCollector;
30+
use Hyperf\Di\ClosureDefinitionCollectorInterface;
31+
use Hyperf\Di\MethodDefinitionCollector;
32+
use Hyperf\Di\MethodDefinitionCollectorInterface;
33+
use Hyperf\Di\ReflectionManager;
34+
use Hyperf\Di\ScanHandler\NullScanHandler;
35+
use Hyperf\Serializer\SerializerFactory;
36+
use Hyperf\Serializer\SymfonyNormalizer;
37+
use HyperfTest\Command\Command\Annotation\TestAsCommand;
38+
use Mockery;
39+
use PHPUnit\Framework\Attributes\CoversNothing;
40+
use PHPUnit\Framework\TestCase;
41+
42+
/**
43+
* @internal
44+
* @coversNothing
45+
* @method void registerAnnotationCommands()
46+
* @method void registerClosureCommands()
47+
* @property string $signature
48+
*/
49+
#[CoversNothing]
50+
class AsCommandAndClosureCommandTest extends TestCase
51+
{
52+
/**
53+
* @var AsCommand[]
54+
*/
55+
protected array $containerSet = [];
56+
57+
protected ContainerInterface $container;
58+
59+
protected function setUp(): void
60+
{
61+
parent::setUp();
62+
63+
if (! empty($this->containerSet)) {
64+
return;
65+
}
66+
67+
$scanner = new Scanner(new ScanConfig(false, '/'), new NullScanHandler());
68+
$reader = new AnnotationReader();
69+
$scanner->collect($reader, ReflectionManager::reflectClass(TestAsCommand::class));
70+
71+
$this->container = $container = $this->getContainer();
72+
}
73+
74+
protected function tearDown(): void
75+
{
76+
Mockery::close();
77+
$this->containerSet = [];
78+
}
79+
80+
public function testRegisterAsCommand()
81+
{
82+
$container = $this->container;
83+
(fn () => $this->registerAnnotationCommands())->call(
84+
new RegisterCommandListener($container, $container->get(ConfigInterface::class), $container->get(StdoutLoggerInterface::class))
85+
);
86+
87+
$commands = array_values($this->containerSet);
88+
$this->assertCount(3, $commands);
89+
90+
$runCommand = $commands[0];
91+
$runCommandDefinition = $runCommand->getDefinition();
92+
$this->assertEquals($this->getSignature($runCommand), 'command:as-command:run');
93+
$this->assertEquals(count($runCommandDefinition->getOptions()), 1);
94+
$this->assertEquals(count($runCommandDefinition->getArguments()), 0);
95+
$this->assertNotNull($runCommandDefinition->getOption('disable-event-dispatcher'));
96+
97+
$runWithDefinedOptionsCommand = $commands[1];
98+
$runWithDefinedOptionsCommandDefinition = $runWithDefinedOptionsCommand->getDefinition();
99+
$this->assertEquals($this->getSignature($runWithDefinedOptionsCommand), 'command:as-command:runWithDefinedOptions {--name=}');
100+
$this->assertEquals(count($runWithDefinedOptionsCommandDefinition->getOptions()), 2);
101+
$this->assertEquals(count($runWithDefinedOptionsCommandDefinition->getArguments()), 0);
102+
$this->assertNotNull($runCommandDefinition->getOption('disable-event-dispatcher'));
103+
$this->assertNotNull($runWithDefinedOptionsCommandDefinition->getOption('name'));
104+
105+
$runWithoutOptionsCommand = $commands[2];
106+
$runWithoutOptionsCommandDefinition = $runWithoutOptionsCommand->getDefinition();
107+
$this->assertEquals($this->getSignature($runWithoutOptionsCommand), 'command:as-command:runWithoutOptions');
108+
$this->assertEquals(count($runWithoutOptionsCommandDefinition->getOptions()), 4);
109+
$this->assertEquals(count($runWithoutOptionsCommandDefinition->getArguments()), 0);
110+
$this->assertNotNull($runCommandDefinition->getOption('disable-event-dispatcher'));
111+
$this->assertNotNull($runWithoutOptionsCommandDefinition->getOption('name'));
112+
$this->assertNotNull($runWithoutOptionsCommandDefinition->getOption('age'));
113+
$this->assertNotNull($runWithoutOptionsCommandDefinition->getOption('testBool'));
114+
}
115+
116+
public function testRegisterClosureCommand()
117+
{
118+
$runCommand = Console::command('command:closure:run', function () {
119+
return 'closure';
120+
});
121+
$runCommandDefinition = $runCommand->getDefinition();
122+
$this->assertEquals($this->getSignature($runCommand), 'command:closure:run');
123+
$this->assertEquals(count($runCommandDefinition->getOptions()), 1);
124+
$this->assertEquals(count($runCommandDefinition->getArguments()), 0);
125+
$this->assertNotNull($runCommandDefinition->getOption('disable-event-dispatcher'));
126+
127+
$runWithDefinedOptionsCommand = Console::command('command:closure:withDefineOptions {--name=}', function (string $name) {
128+
return 'with define options';
129+
});
130+
$runWithDefinedOptionsCommandDefinition = $runWithDefinedOptionsCommand->getDefinition();
131+
$this->assertEquals($this->getSignature($runWithDefinedOptionsCommand), 'command:closure:withDefineOptions {--name=}');
132+
$this->assertEquals(count($runWithDefinedOptionsCommandDefinition->getOptions()), 2);
133+
$this->assertEquals(count($runWithDefinedOptionsCommandDefinition->getArguments()), 0);
134+
$this->assertNotNull($runCommandDefinition->getOption('disable-event-dispatcher'));
135+
$this->assertNotNull($runWithDefinedOptionsCommandDefinition->getOption('name'));
136+
137+
$runWithoutOptionsCommand = Console::command('command:closure:withoutDefineOptions', function (string $name, int $age = 9, bool $testBool = false) {
138+
return 'with define options';
139+
});
140+
$runWithoutOptionsCommandDefinition = $runWithoutOptionsCommand->getDefinition();
141+
$this->assertEquals($this->getSignature($runWithoutOptionsCommand), 'command:closure:withoutDefineOptions');
142+
$this->assertEquals(count($runWithoutOptionsCommandDefinition->getOptions()), 4);
143+
$this->assertEquals(count($runWithoutOptionsCommandDefinition->getArguments()), 0);
144+
$this->assertNotNull($runCommandDefinition->getOption('disable-event-dispatcher'));
145+
$this->assertNotNull($runWithoutOptionsCommandDefinition->getOption('name'));
146+
$this->assertNotNull($runWithoutOptionsCommandDefinition->getOption('age'));
147+
$this->assertNotNull($runWithoutOptionsCommandDefinition->getOption('testBool'));
148+
}
149+
150+
public function testParameterParser()
151+
{
152+
$container = $this->container;
153+
$parameterParser = $container->get(ParameterParser::class);
154+
155+
$class = TestAsCommand::class;
156+
$method = 'runWithoutOptions';
157+
$arguments = [
158+
'name' => 'Hyperf',
159+
'test-bool' => '123', // snake case
160+
];
161+
162+
$result = $parameterParser->parseMethodParameters($class, $method, $arguments);
163+
$this->assertEquals([
164+
'Hyperf',
165+
9,
166+
true,
167+
], $result);
168+
}
169+
170+
protected function getSignature(AsCommand|ClosureCommand $asCommand): string
171+
{
172+
return (fn () => $this->signature)->call($asCommand);
173+
}
174+
175+
/**
176+
* @return ContainerInterface
177+
*/
178+
protected function getContainer()
179+
{
180+
$container = Mockery::mock(ContainerInterface::class);
181+
ApplicationContext::setContainer($container);
182+
183+
$container->shouldReceive('has')->with(ConfigInterface::class)->andReturnTrue();
184+
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturnUsing(function () {
185+
return new Config([
186+
]);
187+
});
188+
189+
$container->shouldReceive('get')->with(StdoutLoggerInterface::class)->andReturnUsing(function () {
190+
$logger = Mockery::mock(StdoutLoggerInterface::class);
191+
$logger->shouldReceive('debug')->andReturn(null);
192+
$logger->shouldReceive('log')->andReturn(null);
193+
return $logger;
194+
});
195+
196+
$container->shouldReceive('get')->with(NormalizerInterface::class)->andReturn(new SymfonyNormalizer((new SerializerFactory())->__invoke()));
197+
$container->shouldReceive('has')->with(NormalizerInterface::class)->andReturn(true);
198+
$container->shouldReceive('get')->with(MethodDefinitionCollectorInterface::class)->andReturn(new MethodDefinitionCollector());
199+
$container->shouldReceive('has')->with(MethodDefinitionCollectorInterface::class)->andReturn(true);
200+
$container->shouldReceive('has')->with(ClosureDefinitionCollectorInterface::class)->andReturn(true);
201+
$container->shouldReceive('get')->with(ClosureDefinitionCollectorInterface::class)->andReturn(new ClosureDefinitionCollector());
202+
203+
$container->shouldReceive('get')->with(ParameterParser::class)->andReturn(new ParameterParser($container));
204+
$container->shouldReceive('get')->with(TestAsCommand::class)->andReturn(new TestAsCommand());
205+
206+
$container->shouldReceive('set')->withAnyArgs()->andReturnUsing(function ($key, $value) {
207+
$this->containerSet[$key] = $value;
208+
});
209+
210+
$container->shouldReceive('get')->with(ClosureCommand::class)->andReturn(ClosureCommand::class);
211+
212+
// closure command
213+
$container->shouldReceive('make')->with(ClosureCommand::class, Mockery::any())
214+
->andReturnUsing(function ($class, $arguments) {
215+
return new ClosureCommand($this->container, $arguments['signature'], $arguments['closure']);
216+
});
217+
218+
return $container;
219+
}
220+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* This file is part of Hyperf.
6+
*
7+
* @link https://www.hyperf.io
8+
* @document https://hyperf.wiki
9+
* @contact group@hyperf.io
10+
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
11+
*/
12+
13+
namespace HyperfTest\Command\Command\Annotation;
14+
15+
use Hyperf\Command\Annotation\AsCommand;
16+
17+
class TestAsCommand
18+
{
19+
#[AsCommand('command:as-command:run')]
20+
public function run()
21+
{
22+
return 'run';
23+
}
24+
25+
#[AsCommand('command:as-command:runWithDefinedOptions {--name=}')]
26+
public function runWithDefinedOptions(string $name)
27+
{
28+
return 'runWithDefinedOptions';
29+
}
30+
31+
#[AsCommand('command:as-command:runWithoutOptions')]
32+
public function runWithoutOptions(string $name, int $age = 9, bool $testBool = false)
33+
{
34+
return 'runWithoutOptions';
35+
}
36+
}

0 commit comments

Comments
 (0)