Skip to content

Commit 82a6ada

Browse files
authored
Merge pull request #22 from kbond/new-features
Some new features
2 parents f9428ee + 4bdd457 commit 82a6ada

22 files changed

+1004
-116
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,10 @@ jobs:
2323

2424
cs-check:
2525
uses: zenstruck/.github/.github/workflows/php-cs-fixer.yml@main
26+
with:
27+
php: 8
2628

2729
sca:
28-
name: Static Code Analysis
29-
runs-on: ubuntu-latest
30-
steps:
31-
- name: Checkout Code
32-
uses: actions/checkout@v2
33-
34-
- name: Setup PHP
35-
uses: shivammathur/setup-php@v2
36-
with:
37-
php-version: 8.1
38-
coverage: none
39-
40-
- name: Install Dependencies
41-
uses: ramsey/composer-install@v1
42-
43-
- name: Install PHPUnit
44-
run: vendor/bin/simple-phpunit install
45-
46-
- name: Run PHPStan
47-
run: vendor/bin/phpstan --error-format=github
30+
uses: zenstruck/.github/.github/workflows/php-stan.yml@main
31+
with:
32+
php: 8.1

README.md

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@ A modular set of features to reduce configuration boilerplate for your commands:
1313
*/
1414
final class CreateUserCommand extends InvokableServiceCommand
1515
{
16-
use ConfigureWithDocblocks;
16+
use ConfigureWithDocblocks, RunsCommands, RunsProcesses;
1717

1818
public function __invoke(IO $io, UserRepository $repo): void
1919
{
2020
$repo->createUser($io->argument('email'), $io->argument('password'), $io->option('role'));
2121

22+
$this->runCommand('another:command');
23+
$this->runProcess('/some/script');
24+
2225
$io->success('Created user.');
2326
}
2427
}
@@ -85,6 +88,9 @@ class MyCommand extends \Symfony\Component\Console\Command\Command
8588
$role = $io->option('role');
8689

8790
$io->success('created.');
91+
92+
// even if you don't inject IO, it's available as a method:
93+
$this->io(); // IO
8894
}
8995
}
9096
```
@@ -145,6 +151,29 @@ class CreateUserCommand extends Command
145151
}
146152
```
147153

154+
### `ConfigureWithAttributes`
155+
156+
Use this trait to use the `Argument` and `Option` attributes to configure your command's
157+
arguments and options (_PHP 8+ required_):
158+
159+
```php
160+
use Symfony\Component\Console\Command\Command;
161+
use Symfony\Component\Console\Input\InputArgument;
162+
use Zenstruck\Console\Attribute\Argument;
163+
use Zenstruck\Console\Attribute\Option;
164+
use Zenstruck\Console\ConfigureWithAttributes;
165+
166+
#[Argument('arg1', description: 'Argument 1 description', mode: InputArgument::REQUIRED)]
167+
#[Argument('arg2', description: 'Argument 1 description')]
168+
#[Option('option1', description: 'Option 1 description')]
169+
class MyCommand extends Command
170+
{
171+
use ConfigureWithAttributes;
172+
}
173+
```
174+
175+
**NOTE:** This trait is incompatible with [`ConfigureWithDocblocks`](#configurewithdocblocks).
176+
148177
### `ConfigureWithDocblocks`
149178

150179
Use this trait to allow your command to be configured by your command class' docblock.
@@ -194,6 +223,7 @@ class MyCommand extends Command
194223
2. All the configuration can be disabled by using the traditional methods of configuring your command.
195224
3. Command's are still [lazy](https://symfony.com/blog/new-in-symfony-3-4-lazy-commands) using this method of
196225
configuration but there is overhead in parsing the docblocks so be aware of this.
226+
4. This trait is incompatible with [`ConfigureWithAttributes`](#configurewithattributes).
197227

198228
#### `@command` Tag
199229

@@ -215,6 +245,131 @@ class MyCommand extends Command
215245

216246
**TIP**: It is recommended to only do this for very simple commands as it isn't as explicit as splitting the tags out.
217247

248+
### `CommandRunner`
249+
250+
A `CommandRunner` object is available to simplify running commands anywhere (ie controller):
251+
252+
```php
253+
use Zenstruck\Console\CommandRunner;
254+
255+
/** @var \Symfony\Component\Console\Command\Command $command */
256+
257+
CommandRunner::for($command)->run(); // int (the status after running the command)
258+
259+
// pass arguments
260+
CommandRunner::for($command, 'arg --opt')->run(); // int
261+
```
262+
263+
If the application is available, you can use it to run commands:
264+
265+
```php
266+
use Zenstruck\Console\CommandRunner;
267+
268+
/** @var \Symfony\Component\Console\Application $application */
269+
270+
CommandRunner::from($application, 'my:command')->run();
271+
272+
// pass arguments/options
273+
CommandRunner::from($application, 'my:command arg --opt')->run(); // int
274+
```
275+
276+
If your command is interactive, you can pass inputs:
277+
278+
```php
279+
use Zenstruck\Console\CommandRunner;
280+
281+
/** @var \Symfony\Component\Console\Application $application */
282+
283+
CommandRunner::from($application, 'my:command')->run([
284+
'foo', // input 1
285+
'', // input 2 (<enter>)
286+
'y', // input 3
287+
]);
288+
```
289+
290+
By default, output is suppressed, you can optionally capture the output:
291+
292+
```php
293+
use Zenstruck\Console\CommandRunner;
294+
295+
/** @var \Symfony\Component\Console\Application $application */
296+
297+
$output = new \Symfony\Component\Console\Output\BufferedOutput();
298+
299+
CommandRunner::from($application, 'my:command')
300+
->withOutput($output) // any OutputInterface
301+
->run()
302+
;
303+
304+
$output->fetch(); // string (the output)
305+
```
306+
307+
#### `RunsCommands`
308+
309+
You can give your [Invokable Commands](#invokable) the ability to run other commands (defined
310+
in the application) by using the `RunsCommands` trait. These _sub-commands_ will use the same
311+
_output_ as the parent command.
312+
313+
```php
314+
use Symfony\Component\Console\Command;
315+
use Zenstruck\Console\Invokable;
316+
use Zenstruck\Console\RunsCommands;
317+
318+
class MyCommand extends Command
319+
{
320+
use Invokable, RunsCommands;
321+
322+
public function __invoke(): void
323+
{
324+
$this->runCommand('another:command'); // int (sub-command's run status)
325+
326+
// pass arguments/options
327+
$this->runCommand('another:command arg --opt');
328+
329+
// pass inputs for interactive commands
330+
$this->runCommand('another:command', [
331+
'foo', // input 1
332+
'', // input 2 (<enter>)
333+
'y', // input 3
334+
])
335+
}
336+
}
337+
```
338+
339+
### `RunsProcesses`
340+
341+
You can give your [Invokable Commands](#invokable) the ability to run other processes (`symfony/process` required)
342+
by using the `RunsProcesses` trait. Standard output from the process is hidden by default but can be shown by
343+
passing `-v` to the _parent command_. Error output is always shown. If the process fails, a `\RuntimeException`
344+
is thrown.
345+
346+
```php
347+
use Symfony\Component\Console\Command;
348+
use Symfony\Component\Process\Process;
349+
use Zenstruck\Console\Invokable;
350+
use Zenstruck\Console\RunsProcesses;
351+
352+
class MyCommand extends Command
353+
{
354+
use Invokable, RunsProcesses;
355+
356+
public function __invoke(): void
357+
{
358+
$this->runProcess('/some/script');
359+
360+
// construct with array
361+
$this->runProcess(['/some/script', 'arg1', 'arg1']);
362+
363+
// for full control, pass a Process itself
364+
$this->runProcess(
365+
Process::fromShellCommandline('/some/script')
366+
->setTimeout(900)
367+
->setWorkingDirectory('/')
368+
);
369+
}
370+
}
371+
```
372+
218373
### `CommandSummarySubscriber`
219374

220375
Add this event subscriber to your `Application`'s event dispatcher to display a summary after every command is run.

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
"phpstan/phpstan": "^1.4",
2424
"symfony/framework-bundle": "^4.4|^5.0|^6.0",
2525
"symfony/phpunit-bridge": "^5.3",
26+
"symfony/process": "^4.4|^5.0|^6.0",
2627
"symfony/var-dumper": "^4.4|^5.0|^6.0",
27-
"zenstruck/console-test": "^1.2"
28+
"zenstruck/console-test": "^1.3"
2829
},
2930
"suggest": {
3031
"phpdocumentor/reflection-docblock": "To configure your Command's via class docblock."

phpstan-baseline.neon

Lines changed: 12 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,32 @@
11
parameters:
22
ignoreErrors:
33
-
4-
message: "#^Method Zenstruck\\\\Console\\\\Tests\\\\Fixture\\\\Command\\\\DocblockCommand\\:\\:docblock\\(\\) should return Zenstruck\\\\Console\\\\Configuration\\\\DocblockConfiguration\\<static\\(Zenstruck\\\\Console\\\\Tests\\\\Fixture\\\\Command\\\\DocblockCommand\\)\\> but returns Zenstruck\\\\Console\\\\Configuration\\\\DocblockConfiguration\\<Symfony\\\\Component\\\\Console\\\\Command\\\\Command\\>\\.$#"
4+
message: "#^Method Zenstruck\\\\Console\\\\Attribute\\\\Argument\\:\\:__construct\\(\\) has parameter \\$default with no value type specified in iterable type array\\.$#"
55
count: 1
6-
path: tests/Fixture/Command/DocblockCommand.php
6+
path: src/Attribute/Argument.php
77

88
-
9-
message: "#^Parameter \\#1 \\$value of static method Zenstruck\\\\Callback\\:\\:createFor\\(\\) expects \\(callable\\(\\)\\: mixed\\)\\|ReflectionFunction, \\$this\\(Zenstruck\\\\Console\\\\Tests\\\\Fixture\\\\Command\\\\InvokableCommand\\) given\\.$#"
9+
message: "#^Property Zenstruck\\\\Console\\\\Attribute\\\\Argument\\:\\:\\$default type has no value type specified in iterable type array\\.$#"
1010
count: 1
11-
path: tests/Fixture/Command/InvokableCommand.php
11+
path: src/Attribute/Argument.php
1212

1313
-
14-
message: "#^Method Zenstruck\\\\Console\\\\Tests\\\\Fixture\\\\Command\\\\ServiceCommand\\:\\:__invoke\\(\\) has parameter \\$none with no type specified\\.$#"
14+
message: "#^Method Zenstruck\\\\Console\\\\Attribute\\\\Option\\:\\:__construct\\(\\) has parameter \\$default with no value type specified in iterable type array\\.$#"
1515
count: 1
16-
path: tests/Fixture/Command/ServiceCommand.php
16+
path: src/Attribute/Option.php
1717

1818
-
19-
message: "#^Method Zenstruck\\\\Console\\\\Tests\\\\Fixture\\\\CustomIO\\:\\:success\\(\\) has parameter \\$message with no type specified\\.$#"
19+
message: "#^Method Zenstruck\\\\Console\\\\Attribute\\\\Option\\:\\:__construct\\(\\) has parameter \\$shortcut with no value type specified in iterable type array\\.$#"
2020
count: 1
21-
path: tests/Fixture/CustomIO.php
21+
path: src/Attribute/Option.php
2222

2323
-
24-
message: "#^Method Zenstruck\\\\Console\\\\Tests\\\\Fixture\\\\Kernel\\:\\:configureRoutes\\(\\) has parameter \\$routes with no type specified\\.$#"
24+
message: "#^Property Zenstruck\\\\Console\\\\Attribute\\\\Option\\:\\:\\$default type has no value type specified in iterable type array\\.$#"
2525
count: 1
26-
path: tests/Fixture/Kernel.php
26+
path: src/Attribute/Option.php
2727

2828
-
29-
message: "#^Cannot call method addSubscriber\\(\\) on object\\|null\\.$#"
30-
count: 2
31-
path: tests/Integration/EventListener/CommandSummarySubscriberTest.php
32-
33-
-
34-
message: "#^Parameter \\#1 \\$description of method Symfony\\\\Component\\\\Console\\\\Command\\\\Command\\:\\:setDescription\\(\\) expects string, string\\|null given\\.$#"
35-
count: 1
36-
path: tests/Unit/DocblockConfigureTest.php
37-
38-
-
39-
message: "#^Parameter \\#2 \\$haystack of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringContainsString\\(\\) expects string, string\\|null given\\.$#"
40-
count: 1
41-
path: tests/Unit/DocblockConfigureTest.php
42-
43-
-
44-
message: "#^Method class@anonymous/tests/Unit/IOTest\\.php\\:116\\:\\:__invoke\\(\\) has no return type specified\\.$#"
45-
count: 1
46-
path: tests/Unit/IOTest.php
47-
48-
-
49-
message: "#^Method class@anonymous/tests/Unit/IOTest\\.php\\:137\\:\\:__invoke\\(\\) has no return type specified\\.$#"
50-
count: 1
51-
path: tests/Unit/IOTest.php
52-
53-
-
54-
message: "#^Method class@anonymous/tests/Unit/IOTest\\.php\\:90\\:\\:__invoke\\(\\) has no return type specified\\.$#"
55-
count: 1
56-
path: tests/Unit/IOTest.php
57-
58-
-
59-
message: "#^Method class@anonymous/tests/Unit/InvokableTest\\.php\\:110\\:\\:__invoke\\(\\) has no return type specified\\.$#"
60-
count: 1
61-
path: tests/Unit/InvokableTest.php
62-
63-
-
64-
message: "#^Method class@anonymous/tests/Unit/InvokableTest\\.php\\:125\\:\\:__invoke\\(\\) has no return type specified\\.$#"
65-
count: 1
66-
path: tests/Unit/InvokableTest.php
67-
68-
-
69-
message: "#^Method class@anonymous/tests/Unit/InvokableTest\\.php\\:143\\:\\:__invoke\\(\\) has no return type specified\\.$#"
70-
count: 1
71-
path: tests/Unit/InvokableTest.php
72-
73-
-
74-
message: "#^Method class@anonymous/tests/Unit/InvokableTest\\.php\\:143\\:\\:__invoke\\(\\) has parameter \\$none with no type specified\\.$#"
75-
count: 1
76-
path: tests/Unit/InvokableTest.php
77-
78-
-
79-
message: "#^Method class@anonymous/tests/Unit/InvokableTest\\.php\\:176\\:\\:__invoke\\(\\) has no return type specified\\.$#"
80-
count: 1
81-
path: tests/Unit/InvokableTest.php
82-
83-
-
84-
message: "#^Method class@anonymous/tests/Unit/InvokableTest\\.php\\:198\\:\\:__invoke\\(\\) has no return type specified\\.$#"
85-
count: 1
86-
path: tests/Unit/InvokableTest.php
87-
88-
-
89-
message: "#^Method class@anonymous/tests/Unit/InvokableTest\\.php\\:33\\:\\:__invoke\\(\\) has no return type specified\\.$#"
90-
count: 1
91-
path: tests/Unit/InvokableTest.php
92-
93-
-
94-
message: "#^Method class@anonymous/tests/Unit/InvokableTest\\.php\\:33\\:\\:__invoke\\(\\) has parameter \\$none with no type specified\\.$#"
29+
message: "#^Property Zenstruck\\\\Console\\\\Attribute\\\\Option\\:\\:\\$shortcut type has no value type specified in iterable type array\\.$#"
9530
count: 1
96-
path: tests/Unit/InvokableTest.php
31+
path: src/Attribute/Option.php
9732

phpunit.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0"/>
1414
<env name="KERNEL_CLASS" value="Zenstruck\Console\Tests\Fixture\Kernel" />
1515
<env name="SHELL_VERBOSITY" value="-1"/>
16+
<env name="COLUMNS" value="120" />
1617
</php>
1718

1819
<testsuites>

src/Attribute/Argument.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Zenstruck\Console\Attribute;
4+
5+
use Symfony\Component\Console\Input\InputArgument;
6+
7+
/**
8+
* @author Kevin Bond <kevinbond@gmail.com>
9+
*/
10+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
11+
final class Argument
12+
{
13+
/**
14+
* @see InputArgument::__construct()
15+
*/
16+
public function __construct(
17+
private string $name,
18+
private ?int $mode = null,
19+
private string $description = '',
20+
private string|bool|int|float|array|null $default = null,
21+
) {
22+
}
23+
24+
/**
25+
* @internal
26+
*
27+
* @return mixed[]
28+
*/
29+
public function values(): array
30+
{
31+
return [$this->name, $this->mode, $this->description, $this->default];
32+
}
33+
}

0 commit comments

Comments
 (0)