Skip to content

Commit

Permalink
Merge pull request #62 from kbond/upgrade
Browse files Browse the repository at this point in the history
Upgrade
  • Loading branch information
kbond committed Feb 16, 2024
2 parents 1b077c6 + 40fe882 commit 3a4017d
Show file tree
Hide file tree
Showing 25 changed files with 458 additions and 271 deletions.
23 changes: 21 additions & 2 deletions .github/workflows/ci.yml
Expand Up @@ -8,7 +8,26 @@ on:

jobs:
test:
uses: zenstruck/.github/.github/workflows/php-test-symfony.yml@main
name: PHP ${{ matrix.php }}, SF ${{ matrix.symfony }} - ${{ matrix.deps }}
runs-on: ubuntu-latest
strategy:
matrix:
php: [8.1, 8.2, 8.3 ]
deps: [highest]
symfony: [6.4.*, 7.0.*]
include:
- php: 8.1
deps: lowest
symfony: '*'
exclude:
- php: 8.1
symfony: 7.0.*
steps:
- uses: zenstruck/.github/actions/php-test-symfony@main
with:
php: ${{ matrix.php }}
symfony: ${{ matrix.symfony }}
deps: ${{ matrix.deps }}

code-coverage:
uses: zenstruck/.github/.github/workflows/php-coverage-codecov.yml@main
Expand All @@ -27,7 +46,7 @@ jobs:
steps:
- uses: zenstruck/.github@php-cs-fixer
with:
php: 8.0
php: 8.1
key: ${{ secrets.GPG_PRIVATE_KEY }}
token: ${{ secrets.COMPOSER_TOKEN }}

Expand Down
81 changes: 44 additions & 37 deletions README.md
Expand Up @@ -9,7 +9,7 @@ A modular set of features to reduce configuration boilerplate for your Symfony c
#[AsCommand('create:user', 'Creates a user in the database.')]
final class CreateUserCommand extends InvokableServiceCommand
{
use ConfigureWithAttributes, RunsCommands, RunsProcesses;
use RunsCommands, RunsProcesses;

public function __invoke(
IO $io,
Expand Down Expand Up @@ -53,7 +53,7 @@ composer require zenstruck/console-extra

This library is a set of modular features that can be used separately or in combination.

> **Note**
> ![NOTE]
> To reduce command boilerplate even further, it is recommended to create an abstract base command for your
> app that enables all the features you desire. Then have all your app's commands extend this.
Expand All @@ -78,9 +78,9 @@ $io->output(); // get the "wrapped" output

On its own, it isn't very special, but it can be auto-injected into [`Invokable`](#invokable) commands.

### `Invokable`
### `InvokableCommand`

Use this trait to remove the need for extending `Command::execute()` and just inject what your need
Extend this class to remove the need for extending `Command::execute()` and just inject what your need
into your command's `__invoke()` method. The following are parameters that can be auto-injected:

- [`Zenstruck\Console\IO`](#io)
Expand All @@ -94,13 +94,11 @@ into your command's `__invoke()` method. The following are parameters that can b
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Zenstruck\Console\Invokable;
use Zenstruck\Console\InvokableCommand;
use Zenstruck\Console\IO;

class MyCommand extends \Symfony\Component\Console\Command\Command
class MyCommand extends InvokableCommand
{
use Invokable; // enables this feature

// $username/$roles are the argument/option defined below
public function __invoke(IO $io, string $username, array $roles)
{
Expand Down Expand Up @@ -137,8 +135,8 @@ public function __invoke(IO $io): int

### `InvokableServiceCommand`

If using the Symfony Framework, you can take [`Invokable`](#invokable) to the next level by auto-injecting services
into `__invoke()`. This allows your commands to behave like
If using the Symfony Framework, you can take [`InvokableCommand`](#invokablecommand) to the next level by
auto-injecting services into `__invoke()`. This allows your commands to behave like
[Invokable Service Controllers](https://symfony.com/doc/current/controller/service.html#invokable-controllers)
(with `controller.service_arguments`). Instead of a _Request_, you inject [`IO`](#io).

Expand All @@ -164,9 +162,7 @@ class CreateUserCommand extends InvokableServiceCommand

#### Inject with DI Attributes

> **Note**: This feature requires Symfony 6.2+.
In Symfony 6.2+ you can use any
You can use any
[DI attribute](https://symfony.com/doc/current/reference/attributes.html#dependency-injection) on
your `__invoke()` parameters:

Expand All @@ -183,7 +179,7 @@ class SomeCommand extends InvokableServiceCommand
SomeService $service,

#[Autowire('%kernel.environment%')]
string $env,
string $environment,

#[Target('githubApi')]
HttpClientInterface $httpClient,
Expand All @@ -196,45 +192,56 @@ class SomeCommand extends InvokableServiceCommand
}
```

### `ConfigureWithAttributes`
### Configure with Attributes

Use this trait to use the `Argument` and `Option` attributes to configure your command's
arguments and options:
Your commands that extend [`InvokableCommand`](#invokablecommand) or [`InvokableServiceCommand`](#invokableservicecommand)
can configure arguments and options with attributes:

```php
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Zenstruck\Console\Attribute\Argument;
use Zenstruck\Console\Attribute\Option;
use Zenstruck\Console\ConfigureWithAttributes;
use Zenstruck\Console\InvokableCommand;

#[Argument('arg1', description: 'Argument 1 description', mode: InputArgument::REQUIRED)]
#[Argument('arg2', description: 'Argument 1 description')]
#[Argument('arg3', suggestions: ['suggestion1', 'suggestion2'])] // for auto-completion
#[Argument('arg4', suggestions: 'suggestionsForArg4')] // use a method on the command to get suggestions
#[Option('option1', description: 'Option 1 description')]
class MyCommand extends Command
#[Option('option2', suggestions: ['suggestion1', 'suggestion2'])] // for auto-completion
#[Option('option3', suggestions: 'suggestionsForOption3')] // use a method on the command to get suggestions
class MyCommand extends InvokableCommand
{
use ConfigureWithAttributes;
// ...

private function suggestionsForArg4(): array
{
return ['suggestion3', 'suggestion4'];
}

private function suggestionsForOption3(): array
{
return ['suggestion3', 'suggestion4'];
}
}
```

#### Invokable Attributes

If using `ConfigureWithAttributes` and [`Invokable`](#invokable) together, you can add the
`Option`/`Argument` attributes to your `__invoke()` parameters to define and inject arguments/options:
Instead of defining at the class level, you can add the `Option`/`Argument` attributes directly to your
`__invoke()` parameters to define _and_ inject arguments/options:

```php
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Zenstruck\Console\Attribute\Argument;
use Zenstruck\Console\Attribute\Option;
use Zenstruck\Console\ConfigureWithAttributes;
use Zenstruck\Console\Invokable;
use Zenstruck\Console\InvokableCommand;

#[AsCommand('my:command')]
class MyCommand extends Command
class MyCommand extends InvokableCommand
{
use ConfigureWithAttributes, Invokable;

public function __invoke(
#[Argument]
string $username, // defined as a required argument (username)
Expand All @@ -259,7 +266,7 @@ class MyCommand extends Command
}
```

> **Note**
> ![NOTE]
> Option/Argument _modes_ and _defaults_ are detected from the parameter's type-hint/default value
> and cannot be defined on the attribute.
Expand Down Expand Up @@ -324,18 +331,18 @@ $output->fetch(); // string (the output)

#### `RunsCommands`

You can give your [Invokable Commands](#invokable) the ability to run other commands (defined
You can give your [Invokable Commands](#invokablecommand) the ability to run other commands (defined
in the application) by using the `RunsCommands` trait. These _sub-commands_ will use the same
_output_ as the parent command.

```php
use Symfony\Component\Console\Command;
use Zenstruck\Console\Invokable;
use Zenstruck\Console\InvokableCommand;
use Zenstruck\Console\RunsCommands;

class MyCommand extends Command
class MyCommand extends InvokableCommand
{
use Invokable, RunsCommands;
use RunsCommands;

public function __invoke(): void
{
Expand All @@ -356,20 +363,20 @@ class MyCommand extends Command

### `RunsProcesses`

You can give your [Invokable Commands](#invokable) the ability to run other processes (`symfony/process` required)
You can give your [Invokable Commands](#invokablecommand) the ability to run other processes (`symfony/process` required)
by using the `RunsProcesses` trait. Standard output from the process is hidden by default but can be shown by
passing `-v` to the _parent command_. Error output is always shown. If the process fails, a `\RuntimeException`
is thrown.

```php
use Symfony\Component\Console\Command;
use Symfony\Component\Process\Process;
use Zenstruck\Console\Invokable;
use Zenstruck\Console\InvokableCommand;
use Zenstruck\Console\RunsProcesses;

class MyCommand extends Command
class MyCommand extends InvokableCommand
{
use Invokable, RunsProcesses;
use RunsProcesses;

public function __invoke(): void
{
Expand Down Expand Up @@ -403,5 +410,5 @@ services:
autoconfigure: true
```

> **Note**
> ![NOTE]
> This will display a summary after every registered command runs.
16 changes: 9 additions & 7 deletions composer.json
Expand Up @@ -12,21 +12,23 @@
}
],
"require": {
"php": ">=8.0",
"symfony/console": "^5.4|^6.0|^7.0",
"php": ">=8.1",
"symfony/console": "^6.4|^7.0",
"symfony/deprecation-contracts": "^2.2|^3.0",
"symfony/string": "^5.4|^6.0|^7.0",
"zenstruck/callback": "^1.4.2"
},
"require-dev": {
"phpdocumentor/reflection-docblock": "^5.2",
"phpstan/phpstan": "^1.4",
"phpunit/phpunit": "^9.5",
"symfony/framework-bundle": "^5.4|^6.0|^7.0",
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/phpunit-bridge": "^6.2|^7.0",
"symfony/process": "^5.4|^6.0|^7.0",
"symfony/var-dumper": "^5.4|^6.0|^7.0",
"zenstruck/console-test": "^1.3"
"symfony/process": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0",
"zenstruck/console-test": "^1.4"
},
"conflict": {
"symfony/service-contracts": "<3.2"
},
"config": {
"preferred-install": "dist",
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Expand Up @@ -4,7 +4,7 @@
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
bootstrap="vendor/autoload.php"
bootstrap="tests/bootstrap.php"
failOnRisky="true"
failOnWarning="true"
>
Expand Down
34 changes: 28 additions & 6 deletions src/Attribute/Argument.php
Expand Up @@ -11,22 +11,29 @@

namespace Zenstruck\Console\Attribute;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Input\InputArgument;

use function Symfony\Component\String\s;

/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)]
final class Argument
class Argument
{
/**
* @see InputArgument::__construct()
*
* @param string[]|string $suggestions
*/
public function __construct(
public ?string $name = null,
private ?int $mode = null,
private string $description = '',
private string|bool|int|float|array|null $default = null,
private array|string $suggestions = [],
) {
}

Expand All @@ -35,9 +42,9 @@ public function __construct(
*
* @return mixed[]|null
*/
public static function parseParameter(\ReflectionParameter $parameter): ?array
final public static function parseParameter(\ReflectionParameter $parameter, Command $command): ?array
{
if (!$attributes = $parameter->getAttributes(self::class)) {
if (!$attributes = $parameter->getAttributes(self::class, \ReflectionAttribute::IS_INSTANCEOF)) {
return null;
}

Expand All @@ -47,6 +54,11 @@ public static function parseParameter(\ReflectionParameter $parameter): ?array

/** @var self $value */
$value = $attributes[0]->newInstance();

if (!$value->name && $parameter->name !== s($parameter->name)->snake()->replace('_', '-')->toString()) {
trigger_deprecation('zenstruck/console-extra', '1.4', 'Argument names will default to kebab-case in 2.0. Specify the name in #[Argument] explicitly to remove this deprecation.');
}

$value->name ??= $parameter->name;

if ($value->mode) {
Expand All @@ -67,20 +79,30 @@ public static function parseParameter(\ReflectionParameter $parameter): ?array
$value->default = $parameter->getDefaultValue();
}

return $value->values();
return $value->values($command);
}

/**
* @internal
*
* @return mixed[]
*/
public function values(): array
final public function values(Command $command): array
{
if (!$this->name) {
throw new \LogicException(\sprintf('A $name is required when using %s as a command class attribute.', self::class));
}

return [$this->name, $this->mode, $this->description, $this->default];
$suggestions = $this->suggestions;

if (\is_string($suggestions)) {
$suggestions = \Closure::bind(
fn(CompletionInput $i) => $this->{$suggestions}($i),
$command,
$command,
);
}

return [$this->name, $this->mode, $this->description, $this->default, $suggestions];
}
}

0 comments on commit 3a4017d

Please sign in to comment.