As developers, we often need to work with the command line — executing shell commands, parsing output, or automating system tasks. While many libraries exist for CLI interaction, they can sometimes feel bloated or overly complex when all you need is something lightweight and expressive.
That’s where PhpFluentConsole comes in — a simple yet flexible library designed to make command-line interaction in PHP fluent, readable, and easy to extend.
Why This Library?
PhpFluentConsole doesn’t try to replace popular CLI frameworks — it complements them. It’s great for situations where you just need to run system commands, collect their output, and maybe scan for patterns or return codes — all without diving deep into docs or verbose config.
If you've ever wanted to wrap system commands in a clean API or build your own custom tooling on top of shell commands, this is for you.
Key Features
Fluent Interface
Commands are built using a fluid, chainable syntax:
$cli = new ConsoleRunner(); $cli->setCommand('echo') ->addKey('Hello, World!');
Return Code Handling
Want to know if a command ran successfully?
if (!$cli->run()) { echo "Command failed with code: " . $cli->getReturnCode(); }
Output Matching with Regex
Looking for specific info in the output? Just match it with a pattern:
if ($cli->run()) { $matches = $cli->getMatches('/Error/'); print_r($matches); }
Simple Output Access
Grab all output lines as an array:
if ($cli->run()) { print_r($cli->getOutput()); }
Installation
composer require mikhailovlab/php-fluent-console
API Overview
Set the command to run
public function setCommand(string $cmd): self
Add a command argument
public function addKey(string $key): self
Sets the encoding for output. For example, '866' to display Cyrillic in Windows.
public function encoding(?string $encoding = null): self
Sets a flag that the encoding should be converted back. This method is useful for returning the output in the original encoding for working with cli.
public function decoding(): self
Returns the current command.
public function getCommand(): string
Executes the command, returns true if return code is 0
public function run(): bool
Returns the command output as an array
public function getOutput(): array
Returns the process exit code
public function getReturnCode(): int
Checks output for a regex-based error
public function hasError(string $pattern): bool
Returns output lines that match a regular expression.
public function getMatches(string|array $patterns): array
Examples
Example 1: List Docker Containers
$cli = new ConsoleRunner(); $cli->setCommand('docker') ->addKey('ps'); if ($cli->run()) { print_r($cli->getOutput()); }
Example 2: We receive a list of containers with electronic signatures
$cli = new ConsoleRunner() ->setCommand('csptest') ->addKey('-keyset') ->addKey('-enum_cont') ->addKey('-verifycontext') ->addKey('-fqcn'); if ($cli->run()) { print_r($cli->getMatches('#\\\\.*#')); } $pattern = '/\[ErrorCode:\s*(0x[0-9A-Fa-f]+)\]/'; print_r('Error code: ' . $cli->getMatches($pattern)[0]);
Example 3: Extending with __call Magic
Instead of calling addKey('-flag') every time, we can extend the base class and use method names that map to CLI flags:
class customRunner extends ConsoleRunner { private $methods = [ 'keyset', 'enum_cont', 'verifycontext', 'fqcn' ]; public function __call(string $name, array $arguments): self { if (in_array($name, $this->methods)) { $this->addKey('-' . $name); if (!empty($arguments)) { foreach ($arguments as $arg) { $this->addKey((string) $arg); } } return $this; } throw new \BadMethodCallException("Method $name is not supported"); } }
Now you can call CLI flags as methods:
try{ $cli = new customRunner() ->setCommand('csptest') ->keyset() ->enum_cont() ->verifycontext() ->fqcn(); if ($cli->run()) { print_r($cli->getMatches('#\\\\.*#')); } $pattern = '/\[ErrorCode:\s*(0x[0-9A-Fa-f]+)\]/'; print_r('Error code: ' . $cli->getMatches($pattern)[0]); }catch (Exception $e){ print_r($e->getMessage()); }
You can even move command logic into your own semantic methods:
try{ $containers = new customRunner() ->getContainers(); print_r($containers); }catch (Exception $e){ print_r($e->getMessage()); }
Conclusion
PhpFluentConsole is a handy, fluent tool for working with shell commands in PHP. Whether you’re parsing logs, automating dev tools, or building a higher-level CLI wrapper, this library helps keep your code clean and maintainable.
👉 Check it out on GitHub: https://github.com/MikhailovLab/php-fluent-console
Feel free to drop a star, create an issue, or suggest improvements!
Top comments (11)
Taking the
docker ps
example, this could be written with Symfony Console as;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.
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.
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.
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.
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.
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
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.
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/
This is an implicit comparison. You do add later that it doesn't. So it is ambiguous at best.
I looked at the CryptoPro library and you don't need this library to make it work.
What if someone changes the command from csptest? Then all the methods you hardcoded can be be wrong.
That is why I suggested a builder pattern, to keep the command and the arguments together.
You don't even check the values of the methods, you just add the array one by one as a key.
Not every command argument uses this signature.
You don't even use this library as the base class of CryptoPro. You use it as a hard coded dependency.
There is no benefit using this library over actual design patterns that help you keep the coupling loose. I suggest you learn more about them and about SOLID to understand the problems you are causing yourself in the future.
Thank you again, David, for the detailed review and your time — I really appreciate the depth of your feedback.
The CryptoProBuilder library was intentionally designed to be pragmatic and flexible. In my examples, I show that you can inherit from the base class and build your own abstraction layer — for example, to create wrappers for specific commands. In the CryptoPro case, I chose not to use inheritance directly, but to inject a separate command class instead. This decision was made because of the complexity of CryptoPro: some parameters are reused across many commands, and this approach made it easier to manage them explicitly.
As for the
__call()
method:You're right — it does not validate argument values. But that's a conscious trade-off. CryptoPro commands often expect very diverse argument types (e.g. ['file1', 'file2'], strings, booleans, or even nothing). There’s no consistent schema to validate against, and even the official documentation lacks uniformity.
Trying to validate arguments strictly inside the library would either:
Add a lot of unnecessary complexity, or
Create a false sense of safety that would break when real-world commands diverge.
That's why arguments are passed as-is — much like how they would be written directly in a shell.
To make the interface more convenient, I’ve included pre-registered method names based on common command-line flags. But developers are not limited to them. The library supports full customization via registerMethods() and addKey() — which makes it extensible for any future changes or project-specific needs.
Here’s an example of how developers can go beyond predefined syntax and build custom queries:
Regarding SOLID:
I understand your concern, but I respectfully disagree that this is a critical violation. The architecture is split by concerns:
PhpFluentConsole handles CLI construction, encoding fixes, and fluent execution.
CryptoProBuilder focuses on wrapping cryptographic commands and simplifying interaction with CryptoPro utilities.
They're coupled by design — because one depends on the other to execute — but their responsibilities remain separate. This is a common, effective pattern in domain-specific libraries, especially when solving tightly scoped engineering problems.
Lastly, I want to emphasize again:
This library isn’t built to compete with large CLI frameworks or process managers. It exists for developers and teams who need fast solutions in high-friction environments like CryptoPro — where time is often wasted debugging encodings, deciphering inconsistent documentation, and reconstructing CLI commands.
If someone prefers a cleaner architecture (e.g. builders, interfaces, parser injection), that’s absolutely valid and can be implemented on top of the current base.
In the spirit of open source, I’d genuinely love to see your ideas as contributions. If you have time or interest, feel free to suggest a PR or start a design discussion — I’m always open to making it better.
Sometimes, real flexibility comes from reducing abstraction — not adding it.
You inject nothing, you hard coded PhpFluentConsole. This means the CLI command will always be executed by the
exec
function. PHP offers other CLI executing functions that have their own tasks. But it is impossible to use them because of the hard coding.When people fill in a form, you don't validate the input because it can be an array, or a string, or a boolean or a file? This is just a bad excuse
The only reason you need those methods is because it only caters to a single command, csptest.
CLI construction and encoding fixes are two different concerns, so you just proved the class violates the single responsibility principle.
This is just a bunch of words that are explaining nothing.
Welcome to programming. This is no excuse to ignore design patterns.
You are ignoring my remarks here, I'm not going to invest more time to get ignored in a PR.
The classes are inflexible because no design patterns or SOLID principles are followed.
This remark proves the lack of willingness to learn.
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?
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.