alchemy/binary-driver
PHP toolkit for building reusable, testable binary drivers on top of symfony/process. Provides AbstractBinary, binary detection, command generation, logging via PSR-3, and process listeners for debugging and customization across CLI tools.
Installation:
composer require alchemy/binary-driver
Ensure symfony/process and psr/log are also installed (or add them via Composer).
First Use Case:
Create a simple binary driver for a command like ls:
use Alchemy\BinaryDriver\AbstractBinary;
class LsDriver extends AbstractBinary {
public function getName() {
return 'ls';
}
}
Load and Execute:
$driver = Driver::load('ls');
$output = $driver->command(['-a', '-l']);
echo $output;
Key Configuration:
Set timeouts or environment variables via the Configuration class:
$config = new \Alchemy\BinaryDriver\Configuration(['timeout' => 30]);
$driver->setConfiguration($config);
AbstractBinary: Core class for extending binary drivers.Driver::load(): Factory method to instantiate drivers.ProcessBuilderFactory: For creating Symfony Process objects.DebugListener).Driver Creation:
Extend AbstractBinary for custom commands:
class GitDriver extends AbstractBinary {
public function getName() {
return 'git';
}
public function pull() {
return $this->command(['pull']);
}
}
Command Execution:
Use the command() method to run binaries:
$gitDriver = Driver::load('git');
$result = $gitDriver->command(['status']);
Configuration Management: Centralize settings (e.g., timeouts, environment variables):
$config = new \Alchemy\BinaryDriver\Configuration([
'timeout' => 60,
'env' => ['GIT_TERMINAL_PROMPT' => '0']
]);
$driver->setConfiguration($config);
Logging: Inject a PSR-3 logger for observability:
$logger = new \Monolog\Logger('binary_driver');
$driver = Driver::load('git', $logger);
Event Listeners: Attach listeners for real-time output or error handling:
$driver->listen(new \Alchemy\BinaryDriver\Listeners\DebugListener());
$driver->on('error', function ($line) {
error_log('[ERROR] ' . $line);
});
Wrapper for CLI Tools: Encapsulate complex commands (e.g., Docker, FFmpeg) into reusable drivers:
class DockerDriver extends AbstractBinary {
public function build($image) {
return $this->command(['build', '-t', $image, '.']);
}
}
Debugging Workflow: Use listeners to log process output during development:
$driver->listen(new DebugListener());
$driver->on('debug', function ($line) {
echo "[DEBUG] $line\n";
});
Error Handling:
Catch execution failures with ExecutionFailureException:
try {
$driver->command(['invalid-command']);
} catch (\Alchemy\BinaryDriver\Exception\ExecutionFailureException $e) {
echo "Command failed: " . $e->getCommand() . "\n";
echo "Error: " . $e->getErrorOutput() . "\n";
}
Laravel Integration: Bind drivers to the container for dependency injection:
$this->app->bind('git.driver', function () {
return Driver::load('git');
});
Testing: Mock drivers in unit tests:
$mockDriver = Mockery::mock('overload:Alchemy\BinaryDriver\Driver');
$mockDriver->shouldReceive('command')->andReturn('mocked output');
Environment-Specific Config:
Load configurations dynamically (e.g., from .env):
$config = new Configuration([
'timeout' => env('BINARY_DRIVER_TIMEOUT', 30)
]);
ProcessBuilderFactory: Reuse process creation logic:
$factory = new \Alchemy\BinaryDriver\ProcessBuilderFactory('/usr/bin/php');
$process = $factory->create(['-v']);
Binary Detection in PHP-FPM/Nginx:
$_ENV['PATH'] may be empty, causing binary detection to fail.fastcgi_param PATH /your/current/path in Nginx’s fastcgi_params.PHP Version Compatibility:
Throwable changes).symfony/process to v6.x).Listener Event Forwarding:
forwardedEvents() to emit events to the driver.forwardedEvents() in custom listeners:
public function forwardedEvents() {
return ['error', 'out'];
}
Configuration Overrides:
$driver = Driver::load('git');
$driver->setConfiguration($config); // Do this first!
Error Output Handling:
ExecutionFailureException may not capture full error output in all cases.stderr separately:
$driver->listen(new DebugListener());
$driver->on('error', function ($line) {
error_log($line);
});
Enable Verbose Logging:
$logger = new \Monolog\Logger('driver', [
new \Monolog\Handler\StreamHandler('php://stderr', \Monolog\Logger::DEBUG)
]);
$driver = Driver::load('git', $logger);
Inspect Process Objects:
Use ProcessBuilderFactory to debug command construction:
$factory = new \Alchemy\BinaryDriver\ProcessBuilderFactory('/usr/bin/git');
$process = $factory->create(['status']);
echo $process->getCommandLine(); // Debug the exact command
Check Environment Variables:
Ensure $_ENV['PATH'] is set correctly:
echo "PATH: " . printenv('PATH') . "\n";
Custom Listeners:
Extend EventEmitter and implement ListenerInterface:
class CustomListener extends \Evenement\EventEmitter implements \Alchemy\BinaryDriver\ListenerInterface {
public function handle($type, $data) {
// Custom logic
}
public function forwardedEvents() {
return ['custom_event'];
}
}
Driver Decorators: Wrap existing drivers to add behavior:
class LoggingDriverDecorator extends AbstractBinary {
protected $driver;
public function __construct(AbstractBinary $driver) {
$this->driver = $driver;
}
public function command($args) {
$this->logger->info("Executing: " . $this->driver->getName() . " " . implode(' ', $args));
return $this->driver->command($args);
}
}
Configuration Providers: Dynamically load configurations from external sources (e.g., database):
$config = new Configuration([]);
$config->set('timeout', $this->getTimeoutFromDatabase());
Default Timeout:
$config = new Configuration(['timeout' => 30]);
Environment Variables:
env key in Configuration:
$config = new Configuration([
'env' => ['GIT_AUTHOR_NAME' => 'CI']
]);
ArrayAccess vs. Methods:
ArrayAccess and method calls work, but prefer methods for clarity:
// Preferred:
$config->set('timeout', 30);
// Over:
$config['timeout'] = 30;
$gitDriver = Driver::load('git'); // Create once
$gitDriver->command(['status']); //
How can I help you explore Laravel packages today?