zenstruck / console-extra

A modular set of features to reduce configuration boilerplate for your Symfony commands.
MIT License
78 stars 3 forks source link

Idea: console-extra-bundle, to provide a make:invokable-command #31

Open tacman opened 2 years ago

tacman commented 2 years ago

What a cool library!

Sometimes instead of loading fixtures, I want a simple command that let's me create/update an object. For example:

bin/console make:book --title "Rust" --isbn "29294-42" --author "..."

Obviously, I can create that now using Symfony's make:command, but I like how tight the code is using console-extra.

Perhaps Symfony 7 will incorporate this structure into make:command, but in the meantime, it'd be nice to have something that facilitated create it.

kbond commented 2 years ago

Can you describe more what you're looking for from this library?

tacman commented 2 years ago

From this library (console-extra), I don't have any specific things I'm looking for, though the V2 list #29 looks good.

From the bundle, I'm just thinking of something that acts like make:entity, but generates an invokable controller.

So bin/console make:invokable-controller create:book --description "Creates or updates a book in the database" --arg book --arg isbn --arg author would generate

#[AsCommand('create:book', 'Creates or updates a book in the database')]
final class CreateBookCommand extends InvokableServiceCommand
{
    use ConfigureWithAttributes;

    public function __invoke(
        IO $io,

        #[Argument]
        string $author,

        #[Argument]
        string $isbn,

        #[Argument]
        string $author,

    ): void {

        $io->success('Finished');
    }
}

Maybe instead of --arg, it could be --string, --array, etc.

Just brainstorming.

tacman commented 2 years ago

Of course, I'd want to use the model factories from zenstruck/foundry.

kbond commented 2 years ago

Oh ok, you refer to a maker for an invokable controller (make:invokable-controller) but you actually mean an invokable command maker (make:invokable-command).

Indeed, I think this is a good idea. I have an open issue related to this: #3.

To start, maybe make:invokable-command --arg book --arg isbn --arg author --option foo would generate stringproperties for the --arg's and boolproperties for the --options as that's probably the most common types. We could then possibly add --arg-array, --option-array, etc...

Trying to think how best to wire such a maker. This library doesn't include a bundle so it seems a bit much to create a bundle just for wiring the maker in dev. What about a contrib-recipe that wires the maker service?

tacman commented 2 years ago

What about a contrib-recipe that wires the maker service?

I'm not sure how that would work. contrib-recipe is one of the few parts of the Symfony ecosystem that I dislike. But I'm sure there's a solution there.

Alternatively, there could be a make-invokable-bundle, that would provide makers for both controllers and commands.

One maker that's been missing (IMHO) is a make:dto. Given your experience with make:factory, it seems like there's some overlap. IF you were to create something like that, then perhaps you could have a zenstruck-maker-bundle for all of these.

OR, if Foundry is not specific to Model Factories, you could move bundle to there, since it's already got the maker and such.

Again, just brainstorming.

tacman commented 2 years ago

I took a stab at this in my own maker-bundle. Feedback welcome.

symfony new --webapp make-invokable-command-demo && cd make-invokable-command-demo 
composer req survos/maker-bundle --dev
bin/console survos:make:command app:test "Just a silly test"  --arg name  --arg code --oint-arg size --obool-arg force

Generates

<?php

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Zenstruck\Console\Attribute\Argument;
use Zenstruck\Console\Attribute\Option;
use Zenstruck\Console\ConfigureWithAttributes;
use Zenstruck\Console\IO;
use Zenstruck\Console\InvokableServiceCommand;
use Zenstruck\Console\RunsCommands;
use Zenstruck\Console\RunsProcesses;

#[AsCommand('app:test', 'Just a silly test')]
final class AppTestCommand extends InvokableServiceCommand
{
use ConfigureWithAttributes, RunsCommands, RunsProcesses;

public function __invoke(
IO $io,

// custom injections
// UserRepository $repo,

// expand the arguments and options
#[Argument(description: 'string')]
string $name,
#[Argument(description: 'string')]
string $code,
#[Argument(description: '?int')]
?int $size,
#[Argument(description: '?bool')]
?bool $force,

#[Option(name: 'role', shortcut: 'r')]
array $roles,

): void {

   $io->note(sprintf("string %s %s", 'name', $name) ?? 'null');
   $io->note(sprintf("string %s %s", 'code', $code) ?? 'null');
   $io->note(sprintf("?int %s %s", 'size', $size) ?? 'null');
   $io->note(sprintf("?bool %s %s", 'force', $force) ?? 'null');

// $this->runCommand('another:command');
// $this->runProcess('/some/script');

$io->success('app:test success.');
}

}

And works as expected:

 bin/console app:test --help
Description:
  Just a silly test

Usage:
  app:test [options] [--] <name> <code> [<size> [<force>]]

Arguments:
  name                  string
  code                  string
  size                  ?int
  force                 ?bool

Options:
  -r, --role=ROLE        (multiple values allowed)
  -h, --help            Display help for the given command. When no command is given display help for the list command
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi|--no-ansi  Force (or disable --no-ansi) ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -e, --env=ENV         The Environment name. [default: "dev"]
      --no-debug        Switch off debug mode.
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
tac@pop-os:~/survos/play/make-invokable-command-demo$ 
tac@pop-os:~/survos/play/make-invokable-command-demo$ bin/console app:test "Bob Smith" smith_robert 15 

 ! [NOTE] string name Bob Smith                                                                                         

 ! [NOTE] string code smith_robert                                                                                      

 ! [NOTE] ?int size 15                                                                                                  

 ! [NOTE] ?bool force                                                                                                   

 [OK] app:test success.                                                                                                 

Still a work in progress. Creating the options from the command line isn't implemented yet, and still some polish to do.

kbond commented 2 years ago

Cool! I like this.

What do you think about the following syntax for arguments (special type prefix - defaults to string)?

bin/console make:invokable-command app:test name code ?int:size ?bool:force --description="Just a silly test"

For args:

We could do something similar for options.

Additional, making it interactive like make:entity would be very useful I think!

tacman commented 2 years ago

I like that format, I'll play around with it.

I wonder if ?bool should be handled differently, since it's really a flag to the command. That is, generate the equivalent of:

->addOption('force', InputOption::VALUE_NONE|InputOption::VALUE_NEGATABLE)

On Fri, Sep 30, 2022 at 10:54 AM Kevin Bond @.***> wrote:

Cool! I like this.

What do you think about the following syntax for arguments (special type prefix - defaults to string)?

bin/console make:invokable-command app:test name code ?int:size ?bool:force --description="Just a silly test"

For args:

  • name: string $name
  • ?name: ?string $name
  • ?int:name: ?int $name
  • name[]: array $name

We could do something similar for options.

Additional, making it interactive like make:entity would be very useful I think!

— Reply to this email directly, view it on GitHub https://github.com/zenstruck/console-extra/issues/31#issuecomment-1263680773, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEXIQIAS4IP6NLERFU4EWTWA35LFANCNFSM57FNRKAA . You are receiving this because you authored the thread.Message ID: @.***>

kbond commented 2 years ago

I wonder if ?bool should be handled differently, since it's really a flag

Good point!

kbond commented 2 years ago

I do want to deprecate/remove docblock/@command syntax, but this might be helpful to determine the syntax for a maker.

tacman commented 2 years ago

I've played around some more, this is now working, though the internals are kinda ugly.

bin/console survos:make:command app:greet \
  string:message:"What do you want to say?" \
  array:names:"Who do you want to greet (separate multiple names with a space)?" \
  int:iterations-i?:"How many times should the message be printed?" \
  array:colors?:"Which colors do you like?" \
  bool:yell?

bin/console app:greet --help
Usage:
  app:greet [options] [--] <message> <names>...

Arguments:
  message                      What do you want to say?
  names                        Who do you want to greet (separate multiple names with a space)?

Options:
  -i, --iterations=ITERATIONS  How many times should the message be printed?
      --colors=COLORS          Which colors do you like? (multiple values allowed)
      --yell                   (bool)

The syntax rules

type // If a ? preceeds the type, then it's an optional argument
:
name // if the name ends in ?, then it's an option.  If the name has a hyphenated suffix, use that is the shortcut.
:
description

I played around with a few ways to do default, I think the best way is to parse an = in the name, e.g.

  int:iterations-i?=7:"How many times should the message be printed?" \
tacman commented 2 years ago

Additional, making it interactive like make:entity would be very useful I think!

This is how I incorporate make:entity into a script with multiple fields:

cat <<'EOF' | sed "s/:/\n/g"  |  bin/console make:entity Country
name:string:55:no
alpha2:string:2:no
alpha3:string:3:yes
EOF

I don't love it, I prefer the approach we've been working on here, but perhaps the better solution is to implement the interactive approach like make:entity and use this pipe approach.

kbond commented 2 years ago

I think, at the end of the day, an ugly syntax/internals is ok because the expectation would be to use the maker interactively.

tacman commented 2 years ago

Yeah, my use case is a bit funky -- I want to have a single bash script that can create a complete application (and related tutorial), without ever opening an IDE.

So the workflow I'm playing with now, for creating a demo application of a bundle I'm working on, is

The primary reason I want to do this from symfony new to symfony run entirely from the command line is because often I can't figure out how to use a bundle from within my application. Often demos don't work with the latest version of Symfony or PHP, so before I even get started I'm running rector and composer recipes:update and the like, tweaking security.yaml and doctrine_migrations.yaml, etc.

Anyway, not particularly relevant, but I quite like the elegance of your invokable controllers (and your twig components, but that's a topic for another place).

kbond commented 2 years ago

Ok, yeah, we've been playing around with the idea of scaffolds that would be provided by either the maker-bundle or flex itself (as a special type of recipe). Sounds similar to what you're describing.