Christopher Vagnetoft
f4257b39e4
* Pipe improvements; better filter code, pipeline etc. * Moved commands in PDO plugin to dedicated namespace
371 lines
11 KiB
PHP
371 lines
11 KiB
PHP
<?php
|
|
|
|
namespace SparkPlug\Com\Noccy\Pdo\Shell\Shell;
|
|
|
|
use Attribute;
|
|
use SparkPlug\Com\Noccy\Pdo\PdoResource;
|
|
use Spark\Commands\Command;
|
|
use Spark\SparkApplication;
|
|
use Symfony\Component\Console\Helper\Table;
|
|
use Symfony\Component\Console\Helper\TableSeparator;
|
|
use Symfony\Component\Console\Input\InputArgument;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
|
|
class PdoShell {
|
|
|
|
private OutputInterface $output;
|
|
|
|
private string $resource = "db";
|
|
|
|
private ?PdoResource $db = null;
|
|
|
|
private array $vars = [];
|
|
|
|
private bool $running = false;
|
|
|
|
private array $options = [];
|
|
|
|
private ?array $lastQuery = null;
|
|
|
|
#[EnumSetting('output', [ 'table', 'vertical', 'dump' ])]
|
|
#[EnumSetting('table.style', [ 'box', 'compact', 'borderless' ])]
|
|
private array $defaultOptions = [
|
|
'output' => 'table',
|
|
'table.maxwidth' => 40,
|
|
'table.style' => 'box',
|
|
];
|
|
|
|
public function __construct(OutputInterface $output)
|
|
{
|
|
$this->output = $output;
|
|
$this->options = $this->defaultOptions;
|
|
}
|
|
|
|
private function promptForCommand()
|
|
{
|
|
$prompt = sprintf("PDO:[%s%s]> ", $this->resource, $this->db?"":"?");
|
|
$input = readline($prompt);
|
|
|
|
return $input;
|
|
}
|
|
|
|
private function parseCommand(string $input): array
|
|
{
|
|
$parsed = str_getcsv($input, ' ', '"');
|
|
$parsed = array_map([$this,"expand"], $parsed);
|
|
$command = array_shift($parsed);
|
|
return [
|
|
strtolower($command),
|
|
$parsed
|
|
];
|
|
}
|
|
|
|
private function expand(string $string): string
|
|
{
|
|
return $string;
|
|
}
|
|
|
|
public function run()
|
|
{
|
|
if ($this->running == true) return;
|
|
|
|
$this->running = true;
|
|
while ($this->running) {
|
|
|
|
$input = $this->promptForCommand();
|
|
if (!trim($input)) {
|
|
continue;
|
|
}
|
|
readline_add_history($input);
|
|
if (str_starts_with($input, ".")) {
|
|
[$cmd,$args] = $this->parseCommand($input);
|
|
$this->handleCommand($cmd, $args);
|
|
} else {
|
|
$this->doQuery($input, []);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function runCommands(array $commands)
|
|
{
|
|
foreach ($commands as $input) {
|
|
if (str_starts_with($input, ".")) {
|
|
[$cmd,$args] = $this->parseCommand($input);
|
|
$this->handleCommand($cmd, $args);
|
|
} else {
|
|
$this->doQuery($input, []);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function handleCommand(string $command, array $args)
|
|
{
|
|
|
|
switch ($command) {
|
|
case '.save':
|
|
$this->doSaveCommand($args);
|
|
break;
|
|
case '.show':
|
|
$this->doShowCommand($args);
|
|
break;
|
|
case '.select':
|
|
$this->doSelectCommand($args);
|
|
break;
|
|
case '.set':
|
|
$this->doSetCommand($args);
|
|
break;
|
|
case '.var':
|
|
$this->doVarCommand($args);
|
|
break;
|
|
case '.query':
|
|
$this->doQueryCommand($args);
|
|
break;
|
|
case '.help':
|
|
$this->doHelpCommand($args);
|
|
break;
|
|
|
|
case '.quit':
|
|
case '.exit':
|
|
$this->running = false;
|
|
break;;
|
|
default:
|
|
$this->output->writeln("<error>Bad command. Try .help</>");
|
|
}
|
|
}
|
|
|
|
private function doHelpCommand(array $args)
|
|
{
|
|
$cmds = [
|
|
'.help' => "Show this help",
|
|
'.save FILE' => "Save the last query and result to a .json file",
|
|
'.select RES' => "Select the database resource to query",
|
|
'.show [FILE]' => "Show the last query, or one saved to file",
|
|
'.set [KEY [VALUE]]' => "Set a configuration value",
|
|
'.var [NAME [VALUE]]' => "Set a variable, or show variable value",
|
|
'.exit|.quit' => "Exit the shell",
|
|
'SQL' => "Run SQL against the database",
|
|
'.query SQL [PARAM..]' => "Escape and run a query using ? as placeholder",
|
|
];
|
|
foreach ($cmds as $cmd=>$info) {
|
|
$this->output->writeln(" <options=bold>{$cmd}</> - <info>{$info}</>");
|
|
}
|
|
}
|
|
|
|
private function doSaveCommand(array $args)
|
|
{
|
|
if (empty($this->lastQuery)) {
|
|
$this->output->writeln("<error>No last query to save!</>");
|
|
return;
|
|
}
|
|
$filename = array_shift($args);
|
|
if (empty($filename)) {
|
|
for($n=0;$n<999;$n++) {
|
|
$filename = sprintf("query.%03d.json", $n);
|
|
if (!file_exists($filename)) break;
|
|
}
|
|
}
|
|
file_put_contents($filename, json_encode($this->lastQuery, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
|
|
$this->output->writeln("<info>Wrote {$filename}</>");
|
|
}
|
|
|
|
private function doShowCommand(array $args)
|
|
{
|
|
$file = array_shift($args);
|
|
if ($file) {
|
|
if (!file_exists($file)) {
|
|
$this->output->writeln("<error>File not found: {$file}</>");
|
|
return;
|
|
}
|
|
$json = json_decode(file_get_contents($file), true);
|
|
$query = $json['query']??'?';
|
|
$res = $json['result']??[];
|
|
} else {
|
|
if (!$this->lastQuery) {
|
|
return;
|
|
}
|
|
$query = $this->lastQuery['query'];
|
|
$res = $this->lastQuery['result'];
|
|
}
|
|
|
|
$this->output->writeln("<comment>{$query}</>");
|
|
|
|
switch ($this->options['output']) {
|
|
case 'table':
|
|
$this->dumpQueryTable($res);
|
|
break;
|
|
case 'vertical':
|
|
$this->dumpQueryVertical($res);
|
|
break;
|
|
default:
|
|
print_r($res);
|
|
}
|
|
|
|
}
|
|
|
|
private function doSetCommand(array $args)
|
|
{
|
|
$varname = array_shift($args);
|
|
$value = array_shift($args);
|
|
|
|
if (empty($varname)) {
|
|
foreach ($this->options as $var=>$value) {
|
|
$this->output->writeln("<info>{$var}</>: <comment>".var_export($value,true)."</>");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ($value !== null) {
|
|
if (!array_key_exists($varname, $this->options)) {
|
|
$this->output->writeln("<error>No such option {$varname}</>");
|
|
return;
|
|
}
|
|
$this->options[$varname] = $value;
|
|
} else {
|
|
$this->output->writeln(var_export($this->options[$varname]??null,true));
|
|
}
|
|
}
|
|
|
|
private function doSelectCommand(array $args)
|
|
{
|
|
$name = array_shift($args);
|
|
|
|
if (!$name) {
|
|
$app = SparkApplication::$instance;
|
|
$resources = $app->getResourceManager();
|
|
|
|
$named = $resources->getAllNamedResources();
|
|
foreach ($named as $name=>$resource) {
|
|
if (!($resource instanceof PdoResource)) {
|
|
continue;
|
|
}
|
|
$this->output->writeln(
|
|
sprintf(" <comment>%s</><info>%s</>", ($name==$this->resource?"*":" "), $name)
|
|
);
|
|
}
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
$res = get_resource($name);
|
|
if ($res instanceof PdoResource) {
|
|
$this->db = $res;
|
|
$this->resource = $name;
|
|
$this->output->writeln("<fg=green>** Selected {$name}</>");
|
|
} else {
|
|
$this->output->writeln("<error>Invalid resource {$name}</>");
|
|
}
|
|
}
|
|
|
|
private function doVarCommand(array $args)
|
|
{
|
|
$varname = array_shift($args);
|
|
|
|
if (empty($varname)) {
|
|
foreach ($this->vars as $var=>$value) {
|
|
$this->output->writeln("<info>{$var}</>=<comment>".var_export($value,true)."</>");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (count($args) > 1) {
|
|
$this->vars[$varname] = $args;
|
|
} elseif (count($args) > 0) {
|
|
$this->vars[$varname] = array_shift($args);
|
|
} else {
|
|
$this->output->writeln(var_export($this->vars[$varname]??null,true));
|
|
}
|
|
}
|
|
|
|
private function doQueryCommand(array $args)
|
|
{
|
|
$query = array_shift($args);
|
|
$this->doQuery($query, $args);
|
|
}
|
|
|
|
private function doQuery(string $query, array $params=[])
|
|
{
|
|
if (!$query) {
|
|
return;
|
|
}
|
|
if (!$this->db) {
|
|
$this->output->writeln("<error>No database resource selected</>");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$pdo = $this->db->getPDO();
|
|
$stmt = $pdo->prepare($query);
|
|
$stmt->execute($params);
|
|
$res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
|
} catch (\PDOException $e) {
|
|
$this->output->writeln("<error>{$e->getMessage()}</>");
|
|
return;
|
|
}
|
|
|
|
$this->lastQuery = [
|
|
'timestamp' => microtime(true),
|
|
'resource' => $this->resource,
|
|
'query' => $query,
|
|
'params' => $params,
|
|
'result' => $res
|
|
];
|
|
|
|
switch ($this->options['output']) {
|
|
case 'table':
|
|
$this->dumpQueryTable($res);
|
|
break;
|
|
case 'vertical':
|
|
$this->dumpQueryVertical($res);
|
|
break;
|
|
default:
|
|
print_r($res);
|
|
}
|
|
}
|
|
|
|
private function dumpQueryTable(array $res)
|
|
{
|
|
if (count($res) == 0) return;
|
|
$table = new Table($this->output);
|
|
$table->setHeaders(array_keys(reset($res)));
|
|
$table->setStyle($this->options['table.style']);
|
|
$max = $this->options['table.maxwidth'];
|
|
|
|
foreach ($res as $row) {
|
|
$table->addRow(array_map(function ($v) use ($max) {
|
|
return strlen($v)>$max ? substr($v, 0, $max)."..." : $v;
|
|
}, $row));
|
|
}
|
|
$table->render();
|
|
}
|
|
|
|
private function dumpQueryVertical(array $res)
|
|
{
|
|
if (count($res) == 0) return;
|
|
$names = array_keys(reset($res));
|
|
$nameWidth = max(array_map("strlen", $names)) + 4;
|
|
|
|
foreach ($res as $row) {
|
|
foreach ($row as $k=>$v) {
|
|
$this->output->writeln(
|
|
sprintf("<info>%{$nameWidth}s</>: %s",$k, $v)
|
|
);
|
|
}
|
|
if ($row != end($res)) {
|
|
$this->output->writeln(str_repeat("-", $nameWidth));
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
#[Attribute(Attribute::TARGET_PROPERTY)]
|
|
class EnumSetting
|
|
{
|
|
public function __construct(string $property, array $valid)
|
|
{
|
|
|
|
}
|
|
} |