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>
This commit is contained in:
宣言就是Siam 2024-05-13 20:19:06 +08:00 committed by GitHub
parent 583ef5b908
commit e2fb772745
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 351 additions and 0 deletions

View File

@ -1,5 +1,9 @@
# v3.1.22 - TBD
## Added
- [#6734](https://github.com/hyperf/hyperf/pull/6734) Auto complete options for as command and closure command.
# v3.1.21 - 2024-05-09
## Added

View File

@ -13,6 +13,7 @@ declare(strict_types=1);
namespace Hyperf\Command;
use Hyperf\Command\Concerns\InteractsWithIO;
use Hyperf\Stringable\Str;
use Psr\Container\ContainerInterface;
use function Hyperf\Support\class_uses_recursive;
@ -31,6 +32,24 @@ final class AsCommand extends Command
$this->parameterParser = $container->get(ParameterParser::class);
parent::__construct();
$options = $this->parameterParser->parseMethodOptions($class, $method);
$definition = $this->getDefinition();
foreach ($options as $option) {
$name = $option->getName();
$snakeName = Str::snake($option->getName(), '-');
if (
$definition->hasOption($name)
|| $definition->hasArgument($name)
|| $definition->hasOption($snakeName)
|| $definition->hasArgument($snakeName)
) {
continue;
}
$definition->addOption($option);
}
}
public function handle()

View File

@ -15,6 +15,7 @@ namespace Hyperf\Command;
use Closure;
use Hyperf\Crontab\Crontab;
use Hyperf\Crontab\Schedule;
use Hyperf\Stringable\Str;
use Psr\Container\ContainerInterface;
use function Hyperf\Tappable\tap;
@ -32,6 +33,24 @@ final class ClosureCommand extends Command
$this->parameterParser = $container->get(ParameterParser::class);
parent::__construct();
$options = $this->parameterParser->parseClosureOptions($closure);
$definition = $this->getDefinition();
foreach ($options as $option) {
$name = $option->getName();
$snakeName = Str::snake($option->getName(), '-');
if (
$definition->hasOption($name)
|| $definition->hasArgument($name)
|| $definition->hasOption($snakeName)
|| $definition->hasArgument($snakeName)
) {
continue;
}
$definition->addOption($option);
}
}
public function handle()

View File

@ -19,6 +19,7 @@ use Hyperf\Di\MethodDefinitionCollectorInterface;
use Hyperf\Stringable\Str;
use InvalidArgumentException;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Input\InputOption;
class ParameterParser
{
@ -62,6 +63,58 @@ class ParameterParser
return $this->getInjections($definitions, "{$class}::{$method}", $arguments);
}
/**
* @return InputOption[]
*/
public function parseClosureOptions(Closure $closure): array
{
if (! $this->closureDefinitionCollector) {
return [];
}
$definitions = $this->closureDefinitionCollector->getParameters($closure);
$options = [];
foreach ($definitions as $definition) {
$type = $definition->getName();
if (! in_array($type, ['int', 'float', 'string', 'bool'])) {
continue;
}
$name = $definition->getMeta('name');
$mode = $definition->allowsNull() ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED;
$default = $definition->getMeta('defaultValue');
$options[] = new InputOption($name, null, $mode, '', $default, []);
}
return $options;
}
/**
* @return InputOption[]
*/
public function parseMethodOptions(string $class, string $method): array
{
if (! $this->methodDefinitionCollector) {
return [];
}
$definitions = $this->methodDefinitionCollector->getParameters($class, $method);
$options = [];
foreach ($definitions as $definition) {
$type = $definition->getName();
if (! in_array($type, ['int', 'float', 'string', 'bool'])) {
continue;
}
$name = $definition->getMeta('name');
$mode = $definition->allowsNull() ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED;
$default = $definition->getMeta('defaultValue');
$options[] = new InputOption($name, null, $mode, '', $default, []);
}
return $options;
}
private function getInjections(array $definitions, string $callableName, array $arguments): array
{
$injections = [];

View File

@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Command;
use Hyperf\Command\AsCommand;
use Hyperf\Command\ClosureCommand;
use Hyperf\Command\Console;
use Hyperf\Command\Listener\RegisterCommandListener;
use Hyperf\Command\ParameterParser;
use Hyperf\Config\Config;
use Hyperf\Context\ApplicationContext;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\ContainerInterface;
use Hyperf\Contract\NormalizerInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Di\Annotation\AnnotationReader;
use Hyperf\Di\Annotation\ScanConfig;
use Hyperf\Di\Annotation\Scanner;
use Hyperf\Di\ClosureDefinitionCollector;
use Hyperf\Di\ClosureDefinitionCollectorInterface;
use Hyperf\Di\MethodDefinitionCollector;
use Hyperf\Di\MethodDefinitionCollectorInterface;
use Hyperf\Di\ReflectionManager;
use Hyperf\Di\ScanHandler\NullScanHandler;
use Hyperf\Serializer\SerializerFactory;
use Hyperf\Serializer\SymfonyNormalizer;
use HyperfTest\Command\Command\Annotation\TestAsCommand;
use Mockery;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
* @method void registerAnnotationCommands()
* @method void registerClosureCommands()
* @property string $signature
*/
#[CoversNothing]
class AsCommandAndClosureCommandTest extends TestCase
{
/**
* @var AsCommand[]
*/
protected array $containerSet = [];
protected ContainerInterface $container;
protected function setUp(): void
{
parent::setUp();
if (! empty($this->containerSet)) {
return;
}
$scanner = new Scanner(new ScanConfig(false, '/'), new NullScanHandler());
$reader = new AnnotationReader();
$scanner->collect($reader, ReflectionManager::reflectClass(TestAsCommand::class));
$this->container = $container = $this->getContainer();
}
protected function tearDown(): void
{
Mockery::close();
$this->containerSet = [];
}
public function testRegisterAsCommand()
{
$container = $this->container;
(fn () => $this->registerAnnotationCommands())->call(
new RegisterCommandListener($container, $container->get(ConfigInterface::class), $container->get(StdoutLoggerInterface::class))
);
$commands = array_values($this->containerSet);
$this->assertCount(3, $commands);
$runCommand = $commands[0];
$runCommandDefinition = $runCommand->getDefinition();
$this->assertEquals($this->getSignature($runCommand), 'command:as-command:run');
$this->assertEquals(count($runCommandDefinition->getOptions()), 1);
$this->assertEquals(count($runCommandDefinition->getArguments()), 0);
$this->assertNotNull($runCommandDefinition->getOption('disable-event-dispatcher'));
$runWithDefinedOptionsCommand = $commands[1];
$runWithDefinedOptionsCommandDefinition = $runWithDefinedOptionsCommand->getDefinition();
$this->assertEquals($this->getSignature($runWithDefinedOptionsCommand), 'command:as-command:runWithDefinedOptions {--name=}');
$this->assertEquals(count($runWithDefinedOptionsCommandDefinition->getOptions()), 2);
$this->assertEquals(count($runWithDefinedOptionsCommandDefinition->getArguments()), 0);
$this->assertNotNull($runCommandDefinition->getOption('disable-event-dispatcher'));
$this->assertNotNull($runWithDefinedOptionsCommandDefinition->getOption('name'));
$runWithoutOptionsCommand = $commands[2];
$runWithoutOptionsCommandDefinition = $runWithoutOptionsCommand->getDefinition();
$this->assertEquals($this->getSignature($runWithoutOptionsCommand), 'command:as-command:runWithoutOptions');
$this->assertEquals(count($runWithoutOptionsCommandDefinition->getOptions()), 4);
$this->assertEquals(count($runWithoutOptionsCommandDefinition->getArguments()), 0);
$this->assertNotNull($runCommandDefinition->getOption('disable-event-dispatcher'));
$this->assertNotNull($runWithoutOptionsCommandDefinition->getOption('name'));
$this->assertNotNull($runWithoutOptionsCommandDefinition->getOption('age'));
$this->assertNotNull($runWithoutOptionsCommandDefinition->getOption('testBool'));
}
public function testRegisterClosureCommand()
{
$runCommand = Console::command('command:closure:run', function () {
return 'closure';
});
$runCommandDefinition = $runCommand->getDefinition();
$this->assertEquals($this->getSignature($runCommand), 'command:closure:run');
$this->assertEquals(count($runCommandDefinition->getOptions()), 1);
$this->assertEquals(count($runCommandDefinition->getArguments()), 0);
$this->assertNotNull($runCommandDefinition->getOption('disable-event-dispatcher'));
$runWithDefinedOptionsCommand = Console::command('command:closure:withDefineOptions {--name=}', function (string $name) {
return 'with define options';
});
$runWithDefinedOptionsCommandDefinition = $runWithDefinedOptionsCommand->getDefinition();
$this->assertEquals($this->getSignature($runWithDefinedOptionsCommand), 'command:closure:withDefineOptions {--name=}');
$this->assertEquals(count($runWithDefinedOptionsCommandDefinition->getOptions()), 2);
$this->assertEquals(count($runWithDefinedOptionsCommandDefinition->getArguments()), 0);
$this->assertNotNull($runCommandDefinition->getOption('disable-event-dispatcher'));
$this->assertNotNull($runWithDefinedOptionsCommandDefinition->getOption('name'));
$runWithoutOptionsCommand = Console::command('command:closure:withoutDefineOptions', function (string $name, int $age = 9, bool $testBool = false) {
return 'with define options';
});
$runWithoutOptionsCommandDefinition = $runWithoutOptionsCommand->getDefinition();
$this->assertEquals($this->getSignature($runWithoutOptionsCommand), 'command:closure:withoutDefineOptions');
$this->assertEquals(count($runWithoutOptionsCommandDefinition->getOptions()), 4);
$this->assertEquals(count($runWithoutOptionsCommandDefinition->getArguments()), 0);
$this->assertNotNull($runCommandDefinition->getOption('disable-event-dispatcher'));
$this->assertNotNull($runWithoutOptionsCommandDefinition->getOption('name'));
$this->assertNotNull($runWithoutOptionsCommandDefinition->getOption('age'));
$this->assertNotNull($runWithoutOptionsCommandDefinition->getOption('testBool'));
}
public function testParameterParser()
{
$container = $this->container;
$parameterParser = $container->get(ParameterParser::class);
$class = TestAsCommand::class;
$method = 'runWithoutOptions';
$arguments = [
'name' => 'Hyperf',
'test-bool' => '123', // snake case
];
$result = $parameterParser->parseMethodParameters($class, $method, $arguments);
$this->assertEquals([
'Hyperf',
9,
true,
], $result);
}
protected function getSignature(AsCommand|ClosureCommand $asCommand): string
{
return (fn () => $this->signature)->call($asCommand);
}
/**
* @return ContainerInterface
*/
protected function getContainer()
{
$container = Mockery::mock(ContainerInterface::class);
ApplicationContext::setContainer($container);
$container->shouldReceive('has')->with(ConfigInterface::class)->andReturnTrue();
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturnUsing(function () {
return new Config([
]);
});
$container->shouldReceive('get')->with(StdoutLoggerInterface::class)->andReturnUsing(function () {
$logger = Mockery::mock(StdoutLoggerInterface::class);
$logger->shouldReceive('debug')->andReturn(null);
$logger->shouldReceive('log')->andReturn(null);
return $logger;
});
$container->shouldReceive('get')->with(NormalizerInterface::class)->andReturn(new SymfonyNormalizer((new SerializerFactory())->__invoke()));
$container->shouldReceive('has')->with(NormalizerInterface::class)->andReturn(true);
$container->shouldReceive('get')->with(MethodDefinitionCollectorInterface::class)->andReturn(new MethodDefinitionCollector());
$container->shouldReceive('has')->with(MethodDefinitionCollectorInterface::class)->andReturn(true);
$container->shouldReceive('has')->with(ClosureDefinitionCollectorInterface::class)->andReturn(true);
$container->shouldReceive('get')->with(ClosureDefinitionCollectorInterface::class)->andReturn(new ClosureDefinitionCollector());
$container->shouldReceive('get')->with(ParameterParser::class)->andReturn(new ParameterParser($container));
$container->shouldReceive('get')->with(TestAsCommand::class)->andReturn(new TestAsCommand());
$container->shouldReceive('set')->withAnyArgs()->andReturnUsing(function ($key, $value) {
$this->containerSet[$key] = $value;
});
$container->shouldReceive('get')->with(ClosureCommand::class)->andReturn(ClosureCommand::class);
// closure command
$container->shouldReceive('make')->with(ClosureCommand::class, Mockery::any())
->andReturnUsing(function ($class, $arguments) {
return new ClosureCommand($this->container, $arguments['signature'], $arguments['closure']);
});
return $container;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Command\Command\Annotation;
use Hyperf\Command\Annotation\AsCommand;
class TestAsCommand
{
#[AsCommand('command:as-command:run')]
public function run()
{
return 'run';
}
#[AsCommand('command:as-command:runWithDefinedOptions {--name=}')]
public function runWithDefinedOptions(string $name)
{
return 'runWithDefinedOptions';
}
#[AsCommand('command:as-command:runWithoutOptions')]
public function runWithoutOptions(string $name, int $age = 9, bool $testBool = false)
{
return 'runWithoutOptions';
}
}