Repository files navigation

Quillstack Cli

TestsLatest VersionDownloadsPHP VersionStyleCICodeFactorQuality GateCoverageMaintainabilityReliabilitySecurityLicense

A command line kernel: a command is a class, and what it needs it asks for. Full documentation: https://quillstack.org/cli

Why this exists

No annotations, no definitions, no builder. A class says what it is called, what it does, and what happens when it runs — and the container builds it with whatever it asked for in its constructor.

Console libraries tend to ask you to describe a command twice: once as a class, and once as a definition of its name, arguments and options — in a constructor call, an attribute, or a configuration array. The two drift, and the definition is what runs. Here there is one description, because the class is the definition, and what a command needs it takes in its constructor like anything else in the framework.

Requirements

  • PHP 8.1 or newer
  • A PSR-11 container, to build the commands

Installation

composer require quillstack/cli

Usage

A command

useQuillstack\Cli\CommandInterface;
useQuillstack\Cli\Input;
useQuillstack\Output\OutputInterface;
finalclass GreetCommand implements CommandInterface
{
publicfunction__construct(privatereadonlyGreeting$greeting)
{
}
publicfunctiongetName(): string
{
return'greet';
}
publicfunctiongetDescription(): string
{
return'Says hello to somebody';
}
publicfunctionrun(Input$input, OutputInterface$output): int
{
$output->writeln($this->greeting->to((string) $input->getArgument(0, 'world')));
return0;
}
}

Greeting is built for it, the same way everything else is.

Saying which commands there are

useQuillstack\Cli\CommandProviderInterface;
finalclass CommandProvider implements CommandProviderInterface
{
publicfunctiongetCommands(): array
{
return [GreetCommand::class];
}
}

Running

#!/usr/bin/env php<?phprequire__DIR__ . '/../vendor/autoload.php';
useQuillstack\Cli\Console;
useQuillstack\Cli\CommandProviderInterface;
useQuillstack\DI\Container;
$console = newConsole(newContainer([
CommandProviderInterface::class => CommandProvider::class,
]));
exit($console->run($argv));
$ ./bin/tool greet ada
Hello, ada
$ ./bin/tool
Commands
greet Says hello to somebody
list Lists the commands there are

Typing nothing lists what there is.

What was typed

./bin/tool queue:work emails --sleep=5 --keep-running -v
$input->getCommand(); // 'queue:work'$input->getArgument(0); // 'emails'$input->getArgument(1, 'none'); // 'none'$input->getOption('sleep'); // '5'$input->getOption('keep-running'); // true$input->hasOption('v'); // true

--name=value carries one, --name and -n are the fact that they were written, and -abc is three of them. Only the first = separates, so --dsn=mysql:host=localhost;dbname=shop arrives whole. Options may be written before the arguments, after them, or on both sides.

Failures

Anything a command throws is reported rather than reaching the terminal as a fatal error, and the exit code is 1. Where failures are described — while developing, not on a server — the exception, where it came from, and the trace are shown as well:

newConsole($container, describeFailures: true);

A command nobody knows says so, and says how to find out what there is.

Technical documentation

ClassWhat it is
Consolethe short way in: builds the kernel and runs $argv
ConsoleKernelfinds the command, runs it, turns anything thrown into something readable
Inputwhat was typed, taken apart
CommandInterfacegetName(), getDescription(), run()
CommandProviderInterfacegetCommands(): array — the classes
Commands\ListCommandcomes with the package
Exceptions\CliExceptionwhat everything here extends

ConsoleKernel::add() puts commands on top of whatever the provider lists, which is how quillstack/framework adds the ones that only apply where the application configured something — db:migrate where there are entities, queue:work where there is a queue.

The list command is built by the kernel rather than through the container, because a kernel asked for from a container would be a second one, knowing none of the commands added to the first.

Benchmark

Against symfony/console 7.4.17 and minicli 4.2.1, on PHP 8.4 on an M-series Mac.

The number a user actually feels is how long the whole command takes, so start there. 60 runs of the same greet ada, interleaved, median of five:

Whole processOf which is the library
bare php on an empty file44.22 ms
quillstack/cli46.61 ms2.4 ms
minicli46.92 ms2.7 ms
symfony/console51.92 ms7.7 ms

Read that column on the left first: about 95% of the wait is PHP starting up, and no console library can do anything about it. Choosing between these three moves a command by a few milliseconds against a fixed 44 that you pay regardless.

That said, the part which is the library differs by more than the whole-process figures let you see, because most of those milliseconds are spent reading code off disk once. Measuring bootstrap and dispatch inside a single process, where the loading is amortised away, leaves what it costs to run:

PackageVersionBootstrap + dispatch
quillstack/cli0.6.05.0 µs
minicli4.2.112.5 µs
symfony/console7.4.1778.5 µs

And what has to be loaded to get there, which does not vary by machine:

PackageFiles loadedMemoryOn disk
quillstack/cli30104 KB108 KB
minicli36220 KB352 KB
symfony/console39774 KB952 KB

The reason is not that this is written better. symfony/console is doing considerably more: typed options and arguments with validation, output formatting and styles, progress bars, tables, interactive questions, shell completion, and command discovery. If you want any of those, that 78.5 µs buys them, and this package will not do them for you. What is measured here is the cost of the part all three share — deciding which command was asked for and running it — which is the only part this package has.

Tests

composer test
composer test:coverage

Static analysis

composer stan

The rest of Quillstack

This is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.

License

MIT — see LICENSE.

About

A command line kernel: a command is a class, and what it needs it asks for.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Quillstack Cli

TestsLatest VersionDownloadsPHP VersionStyleCICodeFactorQuality GateCoverageMaintainabilityReliabilitySecurityLicense

A command line kernel: a command is a class, and what it needs it asks for. Full documentation: https://quillstack.org/cli

Why this exists

No annotations, no definitions, no builder. A class says what it is called, what it does, and what happens when it runs — and the container builds it with whatever it asked for in its constructor.

Console libraries tend to ask you to describe a command twice: once as a class, and once as a definition of its name, arguments and options — in a constructor call, an attribute, or a configuration array. The two drift, and the definition is what runs. Here there is one description, because the class is the definition, and what a command needs it takes in its constructor like anything else in the framework.

Requirements

  • PHP 8.1 or newer
  • A PSR-11 container, to build the commands

Installation

composer require quillstack/cli

Usage

A command

useQuillstack\Cli\CommandInterface;
useQuillstack\Cli\Input;
useQuillstack\Output\OutputInterface;
finalclass GreetCommand implements CommandInterface
{
publicfunction__construct(privatereadonlyGreeting$greeting)
{
}
publicfunctiongetName(): string
{
return'greet';
}
publicfunctiongetDescription(): string
{
return'Says hello to somebody';
}
publicfunctionrun(Input$input, OutputInterface$output): int
{
$output->writeln($this->greeting->to((string) $input->getArgument(0, 'world')));
return0;
}
}

Greeting is built for it, the same way everything else is.

Saying which commands there are

useQuillstack\Cli\CommandProviderInterface;
finalclass CommandProvider implements CommandProviderInterface
{
publicfunctiongetCommands(): array
{
return [GreetCommand::class];
}
}

Running

#!/usr/bin/env php<?phprequire__DIR__ . '/../vendor/autoload.php';
useQuillstack\Cli\Console;
useQuillstack\Cli\CommandProviderInterface;
useQuillstack\DI\Container;
$console = newConsole(newContainer([
CommandProviderInterface::class => CommandProvider::class,
]));
exit($console->run($argv));
$ ./bin/tool greet ada
Hello, ada
$ ./bin/tool
Commands
greet Says hello to somebody
list Lists the commands there are

Typing nothing lists what there is.

What was typed

./bin/tool queue:work emails --sleep=5 --keep-running -v
$input->getCommand(); // 'queue:work'$input->getArgument(0); // 'emails'$input->getArgument(1, 'none'); // 'none'$input->getOption('sleep'); // '5'$input->getOption('keep-running'); // true$input->hasOption('v'); // true

--name=value carries one, --name and -n are the fact that they were written, and -abc is three of them. Only the first = separates, so --dsn=mysql:host=localhost;dbname=shop arrives whole. Options may be written before the arguments, after them, or on both sides.

Failures

Anything a command throws is reported rather than reaching the terminal as a fatal error, and the exit code is 1. Where failures are described — while developing, not on a server — the exception, where it came from, and the trace are shown as well:

newConsole($container, describeFailures: true);

A command nobody knows says so, and says how to find out what there is.

Technical documentation

ClassWhat it is
Consolethe short way in: builds the kernel and runs $argv
ConsoleKernelfinds the command, runs it, turns anything thrown into something readable
Inputwhat was typed, taken apart
CommandInterfacegetName(), getDescription(), run()
CommandProviderInterfacegetCommands(): array — the classes
Commands\ListCommandcomes with the package
Exceptions\CliExceptionwhat everything here extends

ConsoleKernel::add() puts commands on top of whatever the provider lists, which is how quillstack/framework adds the ones that only apply where the application configured something — db:migrate where there are entities, queue:work where there is a queue.

The list command is built by the kernel rather than through the container, because a kernel asked for from a container would be a second one, knowing none of the commands added to the first.

Benchmark

Against symfony/console 7.4.17 and minicli 4.2.1, on PHP 8.4 on an M-series Mac.

The number a user actually feels is how long the whole command takes, so start there. 60 runs of the same greet ada, interleaved, median of five:

Whole processOf which is the library
bare php on an empty file44.22 ms
quillstack/cli46.61 ms2.4 ms
minicli46.92 ms2.7 ms
symfony/console51.92 ms7.7 ms

Read that column on the left first: about 95% of the wait is PHP starting up, and no console library can do anything about it. Choosing between these three moves a command by a few milliseconds against a fixed 44 that you pay regardless.

That said, the part which is the library differs by more than the whole-process figures let you see, because most of those milliseconds are spent reading code off disk once. Measuring bootstrap and dispatch inside a single process, where the loading is amortised away, leaves what it costs to run:

PackageVersionBootstrap + dispatch
quillstack/cli0.6.05.0 µs
minicli4.2.112.5 µs
symfony/console7.4.1778.5 µs

And what has to be loaded to get there, which does not vary by machine:

PackageFiles loadedMemoryOn disk
quillstack/cli30104 KB108 KB
minicli36220 KB352 KB
symfony/console39774 KB952 KB

The reason is not that this is written better. symfony/console is doing considerably more: typed options and arguments with validation, output formatting and styles, progress bars, tables, interactive questions, shell completion, and command discovery. If you want any of those, that 78.5 µs buys them, and this package will not do them for you. What is measured here is the cost of the part all three share — deciding which command was asked for and running it — which is the only part this package has.

Tests

composer test
composer test:coverage

Static analysis

composer stan

The rest of Quillstack

This is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.

License

MIT — see LICENSE.

About

A command line kernel: a command is a class, and what it needs it asks for.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Quillstack Cli

TestsLatest VersionDownloadsPHP VersionStyleCICodeFactorQuality GateCoverageMaintainabilityReliabilitySecurityLicense

A command line kernel: a command is a class, and what it needs it asks for. Full documentation: https://quillstack.org/cli

Why this exists

No annotations, no definitions, no builder. A class says what it is called, what it does, and what happens when it runs — and the container builds it with whatever it asked for in its constructor.

Console libraries tend to ask you to describe a command twice: once as a class, and once as a definition of its name, arguments and options — in a constructor call, an attribute, or a configuration array. The two drift, and the definition is what runs. Here there is one description, because the class is the definition, and what a command needs it takes in its constructor like anything else in the framework.

Requirements

  • PHP 8.1 or newer
  • A PSR-11 container, to build the commands

Installation

composer require quillstack/cli

Usage

A command

useQuillstack\Cli\CommandInterface;
useQuillstack\Cli\Input;
useQuillstack\Output\OutputInterface;
finalclass GreetCommand implements CommandInterface
{
publicfunction__construct(privatereadonlyGreeting$greeting)
{
}
publicfunctiongetName(): string
{
return'greet';
}
publicfunctiongetDescription(): string
{
return'Says hello to somebody';
}
publicfunctionrun(Input$input, OutputInterface$output): int
{
$output->writeln($this->greeting->to((string) $input->getArgument(0, 'world')));
return0;
}
}

Greeting is built for it, the same way everything else is.

Saying which commands there are

useQuillstack\Cli\CommandProviderInterface;
finalclass CommandProvider implements CommandProviderInterface
{
publicfunctiongetCommands(): array
{
return [GreetCommand::class];
}
}

Running

#!/usr/bin/env php<?phprequire__DIR__ . '/../vendor/autoload.php';
useQuillstack\Cli\Console;
useQuillstack\Cli\CommandProviderInterface;
useQuillstack\DI\Container;
$console = newConsole(newContainer([
CommandProviderInterface::class => CommandProvider::class,
]));
exit($console->run($argv));
$ ./bin/tool greet ada
Hello, ada
$ ./bin/tool
Commands
greet Says hello to somebody
list Lists the commands there are

Typing nothing lists what there is.

What was typed

./bin/tool queue:work emails --sleep=5 --keep-running -v
$input->getCommand(); // 'queue:work'$input->getArgument(0); // 'emails'$input->getArgument(1, 'none'); // 'none'$input->getOption('sleep'); // '5'$input->getOption('keep-running'); // true$input->hasOption('v'); // true

--name=value carries one, --name and -n are the fact that they were written, and -abc is three of them. Only the first = separates, so --dsn=mysql:host=localhost;dbname=shop arrives whole. Options may be written before the arguments, after them, or on both sides.

Failures

Anything a command throws is reported rather than reaching the terminal as a fatal error, and the exit code is 1. Where failures are described — while developing, not on a server — the exception, where it came from, and the trace are shown as well:

newConsole($container, describeFailures: true);

A command nobody knows says so, and says how to find out what there is.

Technical documentation

ClassWhat it is
Consolethe short way in: builds the kernel and runs $argv
ConsoleKernelfinds the command, runs it, turns anything thrown into something readable
Inputwhat was typed, taken apart
CommandInterfacegetName(), getDescription(), run()
CommandProviderInterfacegetCommands(): array — the classes
Commands\ListCommandcomes with the package
Exceptions\CliExceptionwhat everything here extends

ConsoleKernel::add() puts commands on top of whatever the provider lists, which is how quillstack/framework adds the ones that only apply where the application configured something — db:migrate where there are entities, queue:work where there is a queue.

The list command is built by the kernel rather than through the container, because a kernel asked for from a container would be a second one, knowing none of the commands added to the first.

Benchmark

Against symfony/console 7.4.17 and minicli 4.2.1, on PHP 8.4 on an M-series Mac.

The number a user actually feels is how long the whole command takes, so start there. 60 runs of the same greet ada, interleaved, median of five:

Whole processOf which is the library
bare php on an empty file44.22 ms
quillstack/cli46.61 ms2.4 ms
minicli46.92 ms2.7 ms
symfony/console51.92 ms7.7 ms

Read that column on the left first: about 95% of the wait is PHP starting up, and no console library can do anything about it. Choosing between these three moves a command by a few milliseconds against a fixed 44 that you pay regardless.

That said, the part which is the library differs by more than the whole-process figures let you see, because most of those milliseconds are spent reading code off disk once. Measuring bootstrap and dispatch inside a single process, where the loading is amortised away, leaves what it costs to run:

PackageVersionBootstrap + dispatch
quillstack/cli0.6.05.0 µs
minicli4.2.112.5 µs
symfony/console7.4.1778.5 µs

And what has to be loaded to get there, which does not vary by machine:

PackageFiles loadedMemoryOn disk
quillstack/cli30104 KB108 KB
minicli36220 KB352 KB
symfony/console39774 KB952 KB

The reason is not that this is written better. symfony/console is doing considerably more: typed options and arguments with validation, output formatting and styles, progress bars, tables, interactive questions, shell completion, and command discovery. If you want any of those, that 78.5 µs buys them, and this package will not do them for you. What is measured here is the cost of the part all three share — deciding which command was asked for and running it — which is the only part this package has.

Tests

composer test
composer test:coverage

Static analysis

composer stan

The rest of Quillstack

This is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.

License

MIT — see LICENSE.

About

A command line kernel: a command is a class, and what it needs it asks for.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Quillstack Cli

TestsLatest VersionDownloadsPHP VersionStyleCICodeFactorQuality GateCoverageMaintainabilityReliabilitySecurityLicense

A command line kernel: a command is a class, and what it needs it asks for. Full documentation: https://quillstack.org/cli

Why this exists

No annotations, no definitions, no builder. A class says what it is called, what it does, and what happens when it runs — and the container builds it with whatever it asked for in its constructor.

Console libraries tend to ask you to describe a command twice: once as a class, and once as a definition of its name, arguments and options — in a constructor call, an attribute, or a configuration array. The two drift, and the definition is what runs. Here there is one description, because the class is the definition, and what a command needs it takes in its constructor like anything else in the framework.

Requirements

  • PHP 8.1 or newer
  • A PSR-11 container, to build the commands

Installation

composer require quillstack/cli

Usage

A command

useQuillstack\Cli\CommandInterface;
useQuillstack\Cli\Input;
useQuillstack\Output\OutputInterface;
finalclass GreetCommand implements CommandInterface
{
publicfunction__construct(privatereadonlyGreeting$greeting)
{
}
publicfunctiongetName(): string
{
return'greet';
}
publicfunctiongetDescription(): string
{
return'Says hello to somebody';
}
publicfunctionrun(Input$input, OutputInterface$output): int
{
$output->writeln($this->greeting->to((string) $input->getArgument(0, 'world')));
return0;
}
}

Greeting is built for it, the same way everything else is.

Saying which commands there are

useQuillstack\Cli\CommandProviderInterface;
finalclass CommandProvider implements CommandProviderInterface
{
publicfunctiongetCommands(): array
{
return [GreetCommand::class];
}
}

Running

#!/usr/bin/env php<?phprequire__DIR__ . '/../vendor/autoload.php';
useQuillstack\Cli\Console;
useQuillstack\Cli\CommandProviderInterface;
useQuillstack\DI\Container;
$console = newConsole(newContainer([
CommandProviderInterface::class => CommandProvider::class,
]));
exit($console->run($argv));
$ ./bin/tool greet ada
Hello, ada
$ ./bin/tool
Commands
greet Says hello to somebody
list Lists the commands there are

Typing nothing lists what there is.

What was typed

./bin/tool queue:work emails --sleep=5 --keep-running -v
$input->getCommand(); // 'queue:work'$input->getArgument(0); // 'emails'$input->getArgument(1, 'none'); // 'none'$input->getOption('sleep'); // '5'$input->getOption('keep-running'); // true$input->hasOption('v'); // true

--name=value carries one, --name and -n are the fact that they were written, and -abc is three of them. Only the first = separates, so --dsn=mysql:host=localhost;dbname=shop arrives whole. Options may be written before the arguments, after them, or on both sides.

Failures

Anything a command throws is reported rather than reaching the terminal as a fatal error, and the exit code is 1. Where failures are described — while developing, not on a server — the exception, where it came from, and the trace are shown as well:

newConsole($container, describeFailures: true);

A command nobody knows says so, and says how to find out what there is.

Technical documentation

ClassWhat it is
Consolethe short way in: builds the kernel and runs $argv
ConsoleKernelfinds the command, runs it, turns anything thrown into something readable
Inputwhat was typed, taken apart
CommandInterfacegetName(), getDescription(), run()
CommandProviderInterfacegetCommands(): array — the classes
Commands\ListCommandcomes with the package
Exceptions\CliExceptionwhat everything here extends

ConsoleKernel::add() puts commands on top of whatever the provider lists, which is how quillstack/framework adds the ones that only apply where the application configured something — db:migrate where there are entities, queue:work where there is a queue.

The list command is built by the kernel rather than through the container, because a kernel asked for from a container would be a second one, knowing none of the commands added to the first.

Benchmark

Against symfony/console 7.4.17 and minicli 4.2.1, on PHP 8.4 on an M-series Mac.

The number a user actually feels is how long the whole command takes, so start there. 60 runs of the same greet ada, interleaved, median of five:

Whole processOf which is the library
bare php on an empty file44.22 ms
quillstack/cli46.61 ms2.4 ms
minicli46.92 ms2.7 ms
symfony/console51.92 ms7.7 ms

Read that column on the left first: about 95% of the wait is PHP starting up, and no console library can do anything about it. Choosing between these three moves a command by a few milliseconds against a fixed 44 that you pay regardless.

That said, the part which is the library differs by more than the whole-process figures let you see, because most of those milliseconds are spent reading code off disk once. Measuring bootstrap and dispatch inside a single process, where the loading is amortised away, leaves what it costs to run:

PackageVersionBootstrap + dispatch
quillstack/cli0.6.05.0 µs
minicli4.2.112.5 µs
symfony/console7.4.1778.5 µs

And what has to be loaded to get there, which does not vary by machine:

PackageFiles loadedMemoryOn disk
quillstack/cli30104 KB108 KB
minicli36220 KB352 KB
symfony/console39774 KB952 KB

The reason is not that this is written better. symfony/console is doing considerably more: typed options and arguments with validation, output formatting and styles, progress bars, tables, interactive questions, shell completion, and command discovery. If you want any of those, that 78.5 µs buys them, and this package will not do them for you. What is measured here is the cost of the part all three share — deciding which command was asked for and running it — which is the only part this package has.

Tests

composer test
composer test:coverage

Static analysis

composer stan

The rest of Quillstack

This is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.

License

MIT — see LICENSE.

About

A command line kernel: a command is a class, and what it needs it asks for.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Quillstack Cli

TestsLatest VersionDownloadsPHP VersionStyleCICodeFactorQuality GateCoverageMaintainabilityReliabilitySecurityLicense

A command line kernel: a command is a class, and what it needs it asks for. Full documentation: https://quillstack.org/cli

Why this exists

No annotations, no definitions, no builder. A class says what it is called, what it does, and what happens when it runs — and the container builds it with whatever it asked for in its constructor.

Console libraries tend to ask you to describe a command twice: once as a class, and once as a definition of its name, arguments and options — in a constructor call, an attribute, or a configuration array. The two drift, and the definition is what runs. Here there is one description, because the class is the definition, and what a command needs it takes in its constructor like anything else in the framework.

Requirements

  • PHP 8.1 or newer
  • A PSR-11 container, to build the commands

Installation

composer require quillstack/cli

Usage

A command

useQuillstack\Cli\CommandInterface;
useQuillstack\Cli\Input;
useQuillstack\Output\OutputInterface;
finalclass GreetCommand implements CommandInterface
{
publicfunction__construct(privatereadonlyGreeting$greeting)
{
}
publicfunctiongetName(): string
{
return'greet';
}
publicfunctiongetDescription(): string
{
return'Says hello to somebody';
}
publicfunctionrun(Input$input, OutputInterface$output): int
{
$output->writeln($this->greeting->to((string) $input->getArgument(0, 'world')));
return0;
}
}

Greeting is built for it, the same way everything else is.

Saying which commands there are

useQuillstack\Cli\CommandProviderInterface;
finalclass CommandProvider implements CommandProviderInterface
{
publicfunctiongetCommands(): array
{
return [GreetCommand::class];
}
}

Running

#!/usr/bin/env php<?phprequire__DIR__ . '/../vendor/autoload.php';
useQuillstack\Cli\Console;
useQuillstack\Cli\CommandProviderInterface;
useQuillstack\DI\Container;
$console = newConsole(newContainer([
CommandProviderInterface::class => CommandProvider::class,
]));
exit($console->run($argv));
$ ./bin/tool greet ada
Hello, ada
$ ./bin/tool
Commands
greet Says hello to somebody
list Lists the commands there are

Typing nothing lists what there is.

What was typed

./bin/tool queue:work emails --sleep=5 --keep-running -v
$input->getCommand(); // 'queue:work'$input->getArgument(0); // 'emails'$input->getArgument(1, 'none'); // 'none'$input->getOption('sleep'); // '5'$input->getOption('keep-running'); // true$input->hasOption('v'); // true

--name=value carries one, --name and -n are the fact that they were written, and -abc is three of them. Only the first = separates, so --dsn=mysql:host=localhost;dbname=shop arrives whole. Options may be written before the arguments, after them, or on both sides.

Failures

Anything a command throws is reported rather than reaching the terminal as a fatal error, and the exit code is 1. Where failures are described — while developing, not on a server — the exception, where it came from, and the trace are shown as well:

newConsole($container, describeFailures: true);

A command nobody knows says so, and says how to find out what there is.

Technical documentation

ClassWhat it is
Consolethe short way in: builds the kernel and runs $argv
ConsoleKernelfinds the command, runs it, turns anything thrown into something readable
Inputwhat was typed, taken apart
CommandInterfacegetName(), getDescription(), run()
CommandProviderInterfacegetCommands(): array — the classes
Commands\ListCommandcomes with the package
Exceptions\CliExceptionwhat everything here extends

ConsoleKernel::add() puts commands on top of whatever the provider lists, which is how quillstack/framework adds the ones that only apply where the application configured something — db:migrate where there are entities, queue:work where there is a queue.

The list command is built by the kernel rather than through the container, because a kernel asked for from a container would be a second one, knowing none of the commands added to the first.

Benchmark

Against symfony/console 7.4.17 and minicli 4.2.1, on PHP 8.4 on an M-series Mac.

The number a user actually feels is how long the whole command takes, so start there. 60 runs of the same greet ada, interleaved, median of five:

Whole processOf which is the library
bare php on an empty file44.22 ms
quillstack/cli46.61 ms2.4 ms
minicli46.92 ms2.7 ms
symfony/console51.92 ms7.7 ms

Read that column on the left first: about 95% of the wait is PHP starting up, and no console library can do anything about it. Choosing between these three moves a command by a few milliseconds against a fixed 44 that you pay regardless.

That said, the part which is the library differs by more than the whole-process figures let you see, because most of those milliseconds are spent reading code off disk once. Measuring bootstrap and dispatch inside a single process, where the loading is amortised away, leaves what it costs to run:

PackageVersionBootstrap + dispatch
quillstack/cli0.6.05.0 µs
minicli4.2.112.5 µs
symfony/console7.4.1778.5 µs

And what has to be loaded to get there, which does not vary by machine:

PackageFiles loadedMemoryOn disk
quillstack/cli30104 KB108 KB
minicli36220 KB352 KB
symfony/console39774 KB952 KB

The reason is not that this is written better. symfony/console is doing considerably more: typed options and arguments with validation, output formatting and styles, progress bars, tables, interactive questions, shell completion, and command discovery. If you want any of those, that 78.5 µs buys them, and this package will not do them for you. What is measured here is the cost of the part all three share — deciding which command was asked for and running it — which is the only part this package has.

Tests

composer test
composer test:coverage

Static analysis

composer stan

The rest of Quillstack

This is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.

License

MIT — see LICENSE.

About

A command line kernel: a command is a class, and what it needs it asks for.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Quillstack Cli

TestsLatest VersionDownloadsPHP VersionStyleCICodeFactorQuality GateCoverageMaintainabilityReliabilitySecurityLicense

A command line kernel: a command is a class, and what it needs it asks for. Full documentation: https://quillstack.org/cli

Why this exists

No annotations, no definitions, no builder. A class says what it is called, what it does, and what happens when it runs — and the container builds it with whatever it asked for in its constructor.

Console libraries tend to ask you to describe a command twice: once as a class, and once as a definition of its name, arguments and options — in a constructor call, an attribute, or a configuration array. The two drift, and the definition is what runs. Here there is one description, because the class is the definition, and what a command needs it takes in its constructor like anything else in the framework.

Requirements

  • PHP 8.1 or newer
  • A PSR-11 container, to build the commands

Installation

composer require quillstack/cli

Usage

A command

useQuillstack\Cli\CommandInterface;
useQuillstack\Cli\Input;
useQuillstack\Output\OutputInterface;
finalclass GreetCommand implements CommandInterface
{
publicfunction__construct(privatereadonlyGreeting$greeting)
{
}
publicfunctiongetName(): string
{
return'greet';
}
publicfunctiongetDescription(): string
{
return'Says hello to somebody';
}
publicfunctionrun(Input$input, OutputInterface$output): int
{
$output->writeln($this->greeting->to((string) $input->getArgument(0, 'world')));
return0;
}
}

Greeting is built for it, the same way everything else is.

Saying which commands there are

useQuillstack\Cli\CommandProviderInterface;
finalclass CommandProvider implements CommandProviderInterface
{
publicfunctiongetCommands(): array
{
return [GreetCommand::class];
}
}

Running

#!/usr/bin/env php<?phprequire__DIR__ . '/../vendor/autoload.php';
useQuillstack\Cli\Console;
useQuillstack\Cli\CommandProviderInterface;
useQuillstack\DI\Container;
$console = newConsole(newContainer([
CommandProviderInterface::class => CommandProvider::class,
]));
exit($console->run($argv));
$ ./bin/tool greet ada
Hello, ada
$ ./bin/tool
Commands
greet Says hello to somebody
list Lists the commands there are

Typing nothing lists what there is.

What was typed

./bin/tool queue:work emails --sleep=5 --keep-running -v
$input->getCommand(); // 'queue:work'$input->getArgument(0); // 'emails'$input->getArgument(1, 'none'); // 'none'$input->getOption('sleep'); // '5'$input->getOption('keep-running'); // true$input->hasOption('v'); // true

--name=value carries one, --name and -n are the fact that they were written, and -abc is three of them. Only the first = separates, so --dsn=mysql:host=localhost;dbname=shop arrives whole. Options may be written before the arguments, after them, or on both sides.

Failures

Anything a command throws is reported rather than reaching the terminal as a fatal error, and the exit code is 1. Where failures are described — while developing, not on a server — the exception, where it came from, and the trace are shown as well:

newConsole($container, describeFailures: true);

A command nobody knows says so, and says how to find out what there is.

Technical documentation

ClassWhat it is
Consolethe short way in: builds the kernel and runs $argv
ConsoleKernelfinds the command, runs it, turns anything thrown into something readable
Inputwhat was typed, taken apart
CommandInterfacegetName(), getDescription(), run()
CommandProviderInterfacegetCommands(): array — the classes
Commands\ListCommandcomes with the package
Exceptions\CliExceptionwhat everything here extends

ConsoleKernel::add() puts commands on top of whatever the provider lists, which is how quillstack/framework adds the ones that only apply where the application configured something — db:migrate where there are entities, queue:work where there is a queue.

The list command is built by the kernel rather than through the container, because a kernel asked for from a container would be a second one, knowing none of the commands added to the first.

Benchmark

Against symfony/console 7.4.17 and minicli 4.2.1, on PHP 8.4 on an M-series Mac.

The number a user actually feels is how long the whole command takes, so start there. 60 runs of the same greet ada, interleaved, median of five:

Whole processOf which is the library
bare php on an empty file44.22 ms
quillstack/cli46.61 ms2.4 ms
minicli46.92 ms2.7 ms
symfony/console51.92 ms7.7 ms

Read that column on the left first: about 95% of the wait is PHP starting up, and no console library can do anything about it. Choosing between these three moves a command by a few milliseconds against a fixed 44 that you pay regardless.

That said, the part which is the library differs by more than the whole-process figures let you see, because most of those milliseconds are spent reading code off disk once. Measuring bootstrap and dispatch inside a single process, where the loading is amortised away, leaves what it costs to run:

PackageVersionBootstrap + dispatch
quillstack/cli0.6.05.0 µs
minicli4.2.112.5 µs
symfony/console7.4.1778.5 µs

And what has to be loaded to get there, which does not vary by machine:

PackageFiles loadedMemoryOn disk
quillstack/cli30104 KB108 KB
minicli36220 KB352 KB
symfony/console39774 KB952 KB

The reason is not that this is written better. symfony/console is doing considerably more: typed options and arguments with validation, output formatting and styles, progress bars, tables, interactive questions, shell completion, and command discovery. If you want any of those, that 78.5 µs buys them, and this package will not do them for you. What is measured here is the cost of the part all three share — deciding which command was asked for and running it — which is the only part this package has.

Tests

composer test
composer test:coverage

Static analysis

composer stan

The rest of Quillstack

This is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.

License

MIT — see LICENSE.

About

A command line kernel: a command is a class, and what it needs it asks for.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Quillstack Cli

TestsLatest VersionDownloadsPHP VersionStyleCICodeFactorQuality GateCoverageMaintainabilityReliabilitySecurityLicense

A command line kernel: a command is a class, and what it needs it asks for. Full documentation: https://quillstack.org/cli

Why this exists

No annotations, no definitions, no builder. A class says what it is called, what it does, and what happens when it runs — and the container builds it with whatever it asked for in its constructor.

Console libraries tend to ask you to describe a command twice: once as a class, and once as a definition of its name, arguments and options — in a constructor call, an attribute, or a configuration array. The two drift, and the definition is what runs. Here there is one description, because the class is the definition, and what a command needs it takes in its constructor like anything else in the framework.

Requirements

  • PHP 8.1 or newer
  • A PSR-11 container, to build the commands

Installation

composer require quillstack/cli

Usage

A command

useQuillstack\Cli\CommandInterface;
useQuillstack\Cli\Input;
useQuillstack\Output\OutputInterface;
finalclass GreetCommand implements CommandInterface
{
publicfunction__construct(privatereadonlyGreeting$greeting)
{
}
publicfunctiongetName(): string
{
return'greet';
}
publicfunctiongetDescription(): string
{
return'Says hello to somebody';
}
publicfunctionrun(Input$input, OutputInterface$output): int
{
$output->writeln($this->greeting->to((string) $input->getArgument(0, 'world')));
return0;
}
}

Greeting is built for it, the same way everything else is.

Saying which commands there are

useQuillstack\Cli\CommandProviderInterface;
finalclass CommandProvider implements CommandProviderInterface
{
publicfunctiongetCommands(): array
{
return [GreetCommand::class];
}
}

Running

#!/usr/bin/env php<?phprequire__DIR__ . '/../vendor/autoload.php';
useQuillstack\Cli\Console;
useQuillstack\Cli\CommandProviderInterface;
useQuillstack\DI\Container;
$console = newConsole(newContainer([
CommandProviderInterface::class => CommandProvider::class,
]));
exit($console->run($argv));
$ ./bin/tool greet ada
Hello, ada
$ ./bin/tool
Commands
greet Says hello to somebody
list Lists the commands there are

Typing nothing lists what there is.

What was typed

./bin/tool queue:work emails --sleep=5 --keep-running -v
$input->getCommand(); // 'queue:work'$input->getArgument(0); // 'emails'$input->getArgument(1, 'none'); // 'none'$input->getOption('sleep'); // '5'$input->getOption('keep-running'); // true$input->hasOption('v'); // true

--name=value carries one, --name and -n are the fact that they were written, and -abc is three of them. Only the first = separates, so --dsn=mysql:host=localhost;dbname=shop arrives whole. Options may be written before the arguments, after them, or on both sides.

Failures

Anything a command throws is reported rather than reaching the terminal as a fatal error, and the exit code is 1. Where failures are described — while developing, not on a server — the exception, where it came from, and the trace are shown as well:

newConsole($container, describeFailures: true);

A command nobody knows says so, and says how to find out what there is.

Technical documentation

ClassWhat it is
Consolethe short way in: builds the kernel and runs $argv
ConsoleKernelfinds the command, runs it, turns anything thrown into something readable
Inputwhat was typed, taken apart
CommandInterfacegetName(), getDescription(), run()
CommandProviderInterfacegetCommands(): array — the classes
Commands\ListCommandcomes with the package
Exceptions\CliExceptionwhat everything here extends

ConsoleKernel::add() puts commands on top of whatever the provider lists, which is how quillstack/framework adds the ones that only apply where the application configured something — db:migrate where there are entities, queue:work where there is a queue.

The list command is built by the kernel rather than through the container, because a kernel asked for from a container would be a second one, knowing none of the commands added to the first.

Benchmark

Against symfony/console 7.4.17 and minicli 4.2.1, on PHP 8.4 on an M-series Mac.

The number a user actually feels is how long the whole command takes, so start there. 60 runs of the same greet ada, interleaved, median of five:

Whole processOf which is the library
bare php on an empty file44.22 ms
quillstack/cli46.61 ms2.4 ms
minicli46.92 ms2.7 ms
symfony/console51.92 ms7.7 ms

Read that column on the left first: about 95% of the wait is PHP starting up, and no console library can do anything about it. Choosing between these three moves a command by a few milliseconds against a fixed 44 that you pay regardless.

That said, the part which is the library differs by more than the whole-process figures let you see, because most of those milliseconds are spent reading code off disk once. Measuring bootstrap and dispatch inside a single process, where the loading is amortised away, leaves what it costs to run:

PackageVersionBootstrap + dispatch
quillstack/cli0.6.05.0 µs
minicli4.2.112.5 µs
symfony/console7.4.1778.5 µs

And what has to be loaded to get there, which does not vary by machine:

PackageFiles loadedMemoryOn disk
quillstack/cli30104 KB108 KB
minicli36220 KB352 KB
symfony/console39774 KB952 KB

The reason is not that this is written better. symfony/console is doing considerably more: typed options and arguments with validation, output formatting and styles, progress bars, tables, interactive questions, shell completion, and command discovery. If you want any of those, that 78.5 µs buys them, and this package will not do them for you. What is measured here is the cost of the part all three share — deciding which command was asked for and running it — which is the only part this package has.

Tests

composer test
composer test:coverage

Static analysis

composer stan

The rest of Quillstack

This is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.

License

MIT — see LICENSE.

About

A command line kernel: a command is a class, and what it needs it asks for.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Quillstack Cli

TestsLatest VersionDownloadsPHP VersionStyleCICodeFactorQuality GateCoverageMaintainabilityReliabilitySecurityLicense

A command line kernel: a command is a class, and what it needs it asks for. Full documentation: https://quillstack.org/cli

Why this exists

No annotations, no definitions, no builder. A class says what it is called, what it does, and what happens when it runs — and the container builds it with whatever it asked for in its constructor.

Console libraries tend to ask you to describe a command twice: once as a class, and once as a definition of its name, arguments and options — in a constructor call, an attribute, or a configuration array. The two drift, and the definition is what runs. Here there is one description, because the class is the definition, and what a command needs it takes in its constructor like anything else in the framework.

Requirements

  • PHP 8.1 or newer
  • A PSR-11 container, to build the commands

Installation

composer require quillstack/cli

Usage

A command

useQuillstack\Cli\CommandInterface;
useQuillstack\Cli\Input;
useQuillstack\Output\OutputInterface;
finalclass GreetCommand implements CommandInterface
{
publicfunction__construct(privatereadonlyGreeting$greeting)
{
}
publicfunctiongetName(): string
{
return'greet';
}
publicfunctiongetDescription(): string
{
return'Says hello to somebody';
}
publicfunctionrun(Input$input, OutputInterface$output): int
{
$output->writeln($this->greeting->to((string) $input->getArgument(0, 'world')));
return0;
}
}

Greeting is built for it, the same way everything else is.

Saying which commands there are

useQuillstack\Cli\CommandProviderInterface;
finalclass CommandProvider implements CommandProviderInterface
{
publicfunctiongetCommands(): array
{
return [GreetCommand::class];
}
}

Running

#!/usr/bin/env php<?phprequire__DIR__ . '/../vendor/autoload.php';
useQuillstack\Cli\Console;
useQuillstack\Cli\CommandProviderInterface;
useQuillstack\DI\Container;
$console = newConsole(newContainer([
CommandProviderInterface::class => CommandProvider::class,
]));
exit($console->run($argv));
$ ./bin/tool greet ada
Hello, ada
$ ./bin/tool
Commands
greet Says hello to somebody
list Lists the commands there are

Typing nothing lists what there is.

What was typed

./bin/tool queue:work emails --sleep=5 --keep-running -v
$input->getCommand(); // 'queue:work'$input->getArgument(0); // 'emails'$input->getArgument(1, 'none'); // 'none'$input->getOption('sleep'); // '5'$input->getOption('keep-running'); // true$input->hasOption('v'); // true

--name=value carries one, --name and -n are the fact that they were written, and -abc is three of them. Only the first = separates, so --dsn=mysql:host=localhost;dbname=shop arrives whole. Options may be written before the arguments, after them, or on both sides.

Failures

Anything a command throws is reported rather than reaching the terminal as a fatal error, and the exit code is 1. Where failures are described — while developing, not on a server — the exception, where it came from, and the trace are shown as well:

newConsole($container, describeFailures: true);

A command nobody knows says so, and says how to find out what there is.

Technical documentation

ClassWhat it is
Consolethe short way in: builds the kernel and runs $argv
ConsoleKernelfinds the command, runs it, turns anything thrown into something readable
Inputwhat was typed, taken apart
CommandInterfacegetName(), getDescription(), run()
CommandProviderInterfacegetCommands(): array — the classes
Commands\ListCommandcomes with the package
Exceptions\CliExceptionwhat everything here extends

ConsoleKernel::add() puts commands on top of whatever the provider lists, which is how quillstack/framework adds the ones that only apply where the application configured something — db:migrate where there are entities, queue:work where there is a queue.

The list command is built by the kernel rather than through the container, because a kernel asked for from a container would be a second one, knowing none of the commands added to the first.

Benchmark

Against symfony/console 7.4.17 and minicli 4.2.1, on PHP 8.4 on an M-series Mac.

The number a user actually feels is how long the whole command takes, so start there. 60 runs of the same greet ada, interleaved, median of five:

Whole processOf which is the library
bare php on an empty file44.22 ms
quillstack/cli46.61 ms2.4 ms
minicli46.92 ms2.7 ms
symfony/console51.92 ms7.7 ms

Read that column on the left first: about 95% of the wait is PHP starting up, and no console library can do anything about it. Choosing between these three moves a command by a few milliseconds against a fixed 44 that you pay regardless.

That said, the part which is the library differs by more than the whole-process figures let you see, because most of those milliseconds are spent reading code off disk once. Measuring bootstrap and dispatch inside a single process, where the loading is amortised away, leaves what it costs to run:

PackageVersionBootstrap + dispatch
quillstack/cli0.6.05.0 µs
minicli4.2.112.5 µs
symfony/console7.4.1778.5 µs

And what has to be loaded to get there, which does not vary by machine:

PackageFiles loadedMemoryOn disk
quillstack/cli30104 KB108 KB
minicli36220 KB352 KB
symfony/console39774 KB952 KB

The reason is not that this is written better. symfony/console is doing considerably more: typed options and arguments with validation, output formatting and styles, progress bars, tables, interactive questions, shell completion, and command discovery. If you want any of those, that 78.5 µs buys them, and this package will not do them for you. What is measured here is the cost of the part all three share — deciding which command was asked for and running it — which is the only part this package has.

Tests

composer test
composer test:coverage

Static analysis

composer stan

The rest of Quillstack

This is one component of Quillstack, a PHP framework which is as simple to use as it is strict about what it does.

License

MIT — see LICENSE.

About

A command line kernel: a command is a class, and what it needs it asks for.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages