it-pack / command-it
A multi purpose command creation and execution library for PHP
Requires
- php: ^8.4
Requires (Dev)
Suggests
- it-pack/command-it-run-it: Future RunIt pipeline adapter for persisting/transporting executions
- psr/clock: Bring-your-own PSR-20 clock adapter (see ClockInterface)
- psr/event-dispatcher: Bring-your-own PSR-14 occurrence adapter (see ExecutionReporterInterface)
- psr/log: Bring-your-own PSR-3 observation adapter (see ExecutionReporterInterface)
- symfony/console: Bring-your-own CLI projection adapter; see Adapter/CLI for the bridge shape
Provides
None
Conflicts
None
Replaces
None
README
A multi purpose command creation and execution library for PHP
Creates commands executable programmatically (e.g., CQRS) or in CLI (via SymfonyConsole, etc. adapters)
CommandIt models command execution itself: a validated intent (the Command) is
dispatched through a bus that runs an explicit lifecycle (received → executing → succeeded|failed), records immutable artifacts (context, measurements,
occurrences) via ports, and never leaks context across Fibers or requests. Core
stays RunIt- and Symfony-free; adapters project it onto specific runtimes. The six
CommandStatus values form a closed lifecycle, but cancelled is reserved —
declared for completeness yet not reachable from the executor until a
CancellationPort is introduced (see docs/architecture/MODEL.md).
Documentation
- Examples — runnable, narrated scripts that walk the two axes (vertical lifecycle, horizontal cross-cutting concerns).
- User Guide — task-oriented, with runnable examples: defining commands, input/coercion, handlers, middleware, context & isolation, reporting, the CLI adapter, custom ports, testing, and benchmarks.
- Architecture model — the domain model.
- Architecture decisions — RunIt integration via ports, lazy handler resolution, and fiber-bound context.
- Milestone plan & confidence tooling (archived).
Installation
composer require it-pack/command-it
CommandIt targets PHP >= 8.4 and has no runtime dependencies. The following adapters are
optional and only needed when you project onto a specific runtime or standard — they are declared
under suggest and are never imported by core:
| Package | When you need it |
|---|---|
symfony/console |
CLI projection (Adapter\CLI) — map definitions to Symfony Console commands |
psr/log |
PSR-3 observation/logger integration |
psr/event-dispatcher |
PSR-14 occurrence integration |
psr/clock |
PSR-20 monotonic clock integration |
The in-box adapters (SystemClock, RandomIdentifierGenerator, CollectingExecutionReporter,
AmbientContextStorage, InMemoryCommandExecutionStorage, SafeSerializer) cover framework-free
programmatic dispatch, so no suggest package is required to run the examples below.
Getting started
The fastest path uses the composition-root factory, which wires the safe-by-default reporter (redaction + publication recursion guard), an ambient-or-fiber-bound context, and in-memory storage for you in one call:
<?php declare(strict_types=1); use ItPack\CommandIt\Bootstrap\CommandItFactory; $commandIt = CommandItFactory::create();
Prefer explicit control? Pass any port to create() to override it, or construct
each piece yourself — the factory is just a convenience over the same wiring:
<?php declare(strict_types=1); use ItPack\CommandIt\Adapter\Common\RandomIdentifierGenerator; use ItPack\CommandIt\Adapter\Common\SystemClock; use ItPack\CommandIt\Adapter\Context\AmbientContextStorage; use ItPack\CommandIt\Adapter\Input\ArrayInputAdapter; use ItPack\CommandIt\Adapter\Memory\InMemoryCommandExecutionStorage; use ItPack\CommandIt\Adapter\Reporting\CollectingExecutionReporter; use ItPack\CommandIt\Adapter\Reporting\SafeReporterFactory; use ItPack\CommandIt\Domain\Argument; use ItPack\CommandIt\Domain\ValueObject\ArgumentName; use ItPack\CommandIt\SDK\CommandBuilder; use ItPack\CommandIt\SDK\CommandDefinitionBuilder; use ItPack\CommandIt\SDK\CommandIt; $commandIt = new CommandIt( new SystemClock(), new RandomIdentifierGenerator(), // Compose the safe-by-default reporter: redaction + publication recursion guard. // A bare reporter is NOT safe for sensitive data (see "Security & isolation"). (new SafeReporterFactory())->wrap(new CollectingExecutionReporter()), new AmbientContextStorage(), new InMemoryCommandExecutionStorage(), ); $definition = (new CommandDefinitionBuilder()) ->name('cache:clear') ->argument(new Argument(ArgumentName::fromString('count'), required: true, type: 'int')) ->build(); $commandIt->register($definition, static fn () => new class () implements \ItPack\CommandIt\Contract\Command\CommandHandlerInterface { public function handles(): string { return 'cache:clear'; } public function handle(\ItPack\CommandIt\Contract\Command\CommandInterface $command): \ItPack\CommandIt\Contract\Command\CommandResultInterface { return \ItPack\CommandIt\Domain\CommandResult::succeed($command->input()->argument('count')); } }); $input = (new ArrayInputAdapter())->parse($definition, ['count' => '5']); $command = (new CommandBuilder())->definition($definition)->input($input)->build(); $result = $commandIt->dispatch($command); // CommandResult with status "succeeded", payload 5 (int)
CommandDefinitionBuilderis immutable;build()returns a validated,readonlydefinition.- Input is validated (required args/options) and coerced by declared
type()(int,float,bool,string). - On a handler failure the original exception is rethrown; the reporter/storage never replace it.
CLI adapter (Symfony Console)
The CLI adapter projects registered definitions onto Symfony Console. symfony/console
is suggest-only — core never imports it; only ItPack\CommandIt\Adapter\CLI does.
<?php declare(strict_types=1); use ItPack\CommandIt\Adapter\CLI\ConsoleCommandRegistry; use Symfony\Component\Console\Application; // $commandIt from the example above, after registering commands. $app = new Application(); $app->setAutoExit(false); foreach ( (new ConsoleCommandRegistry($commandIt->registry(), $commandIt->bus())) ->toConsoleCommands() as $command ) { $app->addCommand($command); } $app->run(); // e.g. `php bin/console cache:clear 5 --force`
ConsoleCommandRegistryenumerates every registeredCommandDefinitionInterfaceinto aConsoleCommandBridge(one SymfonyCommandper definition).- The bridge mirrors arguments/options into a Symfony
InputDefinition;booloptions become flags (--force),int/float/stringvalues are coerced by the shared input-coercion behavior, so CLI input agrees exactly with the Array input adapter. execute()dispatches through theCommandBusInterfaceand writes theCommandResultpayload to the output stream (JSON for arrays/objects). Handler failures are reported on the stream with a non-zero exit code; the original exception is preserved by the core executor.ArgvInputParsercovers the raw$argv→CommandInputpath (validation + coercion) when you build commands without a full SymfonyApplication.
Security & isolation
- Execution context is restored in
finallyby the core executor, so the CLI is safe across repeated invocations in a persistent worker.
Reporting decorators (Adapter/Reporting)
These wrap any ExecutionReporterInterface and stay within the Adapter layer.
SafeReporterFactorycomposes the two decorators below into a single safe-by-default reporter:SafeReporterFactory::wrap($sink)returns aGuardedExecutionReporterwrapping aRedactingExecutionReporter(with aNullExecutionReporterhealth fallback). Always use this for sensitive data — a bare (unwrapped) reporter passed toCommandItapplies neither redaction nor the publication recursion guard.RedactingExecutionReporterapplies aRedactionPolicyInterfaceto every observation/occurrence context before it reaches the delegate. The defaultRedactionPolicyredacts values whose key looks sensitive (password,token,authorization, …), truncates oversized strings, caps array cardinality, and limits nesting depth — so secrets and high-cardinality/oversized data never reach an external sink.GuardedExecutionReporterwraps a publication delegate with a recursion guard and a separate health reporter: if publishing triggers a nested report (a self-observation loop) or throws, the signal is routed to the health reporter instead of re-entering the delegate or bubbling into the business flow. Use it when the reporter publishes through a path that may itself report.
use ItPack\CommandIt\Adapter\Common\RedactionPolicy; use ItPack\CommandIt\Adapter\Reporting\GuardedExecutionReporter; use ItPack\CommandIt\Adapter\Reporting\RedactingExecutionReporter; $reporter = new RedactingExecutionReporter($sink, new RedactionPolicy()); $reporter = new GuardedExecutionReporter($reporter, $healthReporter);
CollectingExecutionReporterkeeps a bounded in-memory buffer. Configure the overflow behavior withOverflowPolicy(DropOldestdefault,DropNewest, orThrow→BufferOverflowException).
Serialization
SafeSerializer(theSerializerInterfacedefault) never produces arbitrary object serialization: resources, closures, and objects withouttoArray()/Stringableare rejected with aSerializationException. Only recursively-primitive data is returned.
See docs/USER_GUIDE.md for the task-oriented guide,
docs/architecture/MODEL.md for the domain model, docs/adr/ for the
load-bearing decisions (RunIt integration, lazy handler resolution, fiber-bound
context), and docs/archive/ROADMAP_1.md for the milestone plan.