DEV Community

Fluent CLI in PHP: Creating Console Commands with __call and No Boilerplate

Dmitrii Mikhailov on June 30, 2025

As developers, we often need to work with the command line — executing shell commands, parsing output, or automating system tasks. While many libra...
Collapse
 
xwero profile image
david duymelinck

Taking the docker ps example, this could be written with Symfony Console as;

$application = new Application('My App', '1.0.0');
$default = new ClosureCommand(
    'default',             
    fn (InputInterface $in, OutputInterface $out): int =>  
    {
        $process = new Process(['docker', 'ps'])->run();

        $out->writeln($process->getOutput());
        return Command::SUCCESS;
    }
);

$application->add($default)
       ->setDefaultCommand($default->getName(), true)
       ->run();
Enter fullscreen mode Exit fullscreen mode

It is more code because it is more decoupled, but is not a whole lot more.

At first I saw less code, which made me perk up. But after noticing how the runner and the command are inseparable, I lost interest.

While I think it is a good idea. I'm going to stick with a "big" CLI library.

Collapse
 
dmitr_mikhailov profile image
Dmitrii Mikhailov

In the article I said that this is not a replacement for large libraries, but a lightweight alternative to create other libraries on its basis, without complex documentation. Each tool has its own purpose. For example, on top of this library I developed a wrapper for working with a crypto provider that signs documents in a Russian company worth more than billion of dollars.

Collapse
 
xwero profile image
david duymelinck

I understand that it is not a replacement, and I was off track because it had CLI in the name.

On second look I think a more fair comparison is with the Process component.

Thread Thread
 
dmitr_mikhailov profile image
Dmitrii Mikhailov

I thought about it. But it has other goals. My library was developed as a basis for assembling other libraries, so the command constructor and the executor were separated - it is assumed that the processing will be done in another library, which will parse the output. Also in this library I added encodings for the cli, because the windows console incorrectly outputs some characters due to the old encoding 866, which can also lead to problems with parsing the output through regular expressions.

Thread Thread
 
xwero profile image
david duymelinck • Edited

The main problem is that you are comparing your library with libraries that have different goals.
You compare your library with application command runners like the Console component. But the goal of that component is to be a front for custom console commands. Your library is not capable to call commands that are not available in the OS.

I checked the code and the workhorse of your library is the php exec function.
So that is why I think the Process component is closer to your library, because that is also a wrapper for a php function, proc_open.
And both functions have the same goal, executing a CLI command.

I agree that the Process component differs from your library, because the focus of the component is to make it easier to work with the commands.
From the post I get that readability is a big factor for your library; using fluent methods, using __call to make the keys limited and easier to add.
But do you really need a library for that?

If I would need a command call that can be manipulated by the code, I would use a builder pattern.

abstract class CommandBuilder  implements Stringable
{
     public function __construct(protected string $command, protected array $arguments = [])
     {}

     public function addArgument(string $argument)
     {
           $this->arguments[] = $argument;

           return $this;
     }

     public function __toString()
     {
          return count($this->arguments) > 0 ? $this->command .  '  '  .  join(' ', $this->arguments) :  $this->command;
      }
}

// usage

class Csptest extends CommandBuilder
{
    public static function factory()
    {
         return new static('csptest');
    }

    public function keySet()
    {
        $this->arguments[] = '-keyset';

        return $this;
    }
   // all other arguments here
}

$myCsptest = Csptest::factory()->keySet();
Enter fullscreen mode Exit fullscreen mode

This is much cleaner than using the __call method to make the keys easier and safer to add.
And now you can use $myCsptest with any function or library that executes a CLI command.

The same applies for the windows output problem. The function or library that executes the command should not be responsible for the parsing of the output.
The library is a bunch of single responsibility principle violations. So if you want to continue with the library, I suggest you fix those violations.
It could be something like

class CommandManager
{
     // all three types are interfaces
     public function __constructor(
         private CommandBuilder $command,
         private CommandExecutor $ececutor,
         private CommandOutputParser $parser,
      )
     {}

     public function addArgument(mixed $argument)
     {
          $this->command->addArgument($argument);

          return $this
      }

     public function run()
     {
          $output =  $this->executor->run();

          $this->parser->commandOutput($output);

          return $this;
      }

      public function getOutput()
      {
          return $this->parser->getOutput();
      }
}
Enter fullscreen mode Exit fullscreen mode

This class doesn't do much anymore, but that is a good thing. Now there are a lot more options available for other developers to fine tune the command lifecycle.
This is a very basic version of a CLI framework, not your library as it is now.

Thread Thread
 
dmitr_mikhailov profile image
Dmitrii Mikhailov • Edited

First of all, I did not compare my library with others, and initially stated that its task is not to compete with other libraries. I did not quite understand your thesis about the limitations of my library. It works via exec, that's right, it can be used as a query constructor via a fluent interface, or it can be extended by another class that will add an abstraction layer - the second approach is needed to create your own library. If you need simple work with the command line - you use a fluent interface. If you create your own library - you inherit from the base class and use __call to dynamically call methods, because you will immediately intercept errors if you specified the key incorrectly. That's it.
You can check my article about CryptoProBuilder to better understand the meaning of the library. Sorry, the article is in Russian, but you can use the built-in chrome translator.
habr.com/ru/articles/924478/

Collapse
 
dotallio profile image
Dotallio

This is smart - the __call magic plus the fluent chaining seriously cuts down the CLI boilerplate I always dread. Have you tried this for custom scripts outside the usual devops tasks, like automating DB routines or deploy flows?

Collapse
 
dmitr_mikhailov profile image
Dmitrii Mikhailov

This library was developed as a basis for other libraries. On its basis I developed another library CryptoProBuilding - this library allows working with electronic signatures, encrypting data with asymmetric keys, creating hashes for signatures, etc. This is already a serious level, since in Russia it is necessary to use FSB-certified software to work with an electronic signature. These standards are much stricter than international ones.