'table', 'table.maxwidth' => 40, 'table.style' => 'box', 'capture' => false, 'syntax' => 'none', ]; private array $validOptions = [ 'output' => [ 'table', 'vertical', 'dump' ], 'table.maxwidth' => 'int', 'table.style' => [ 'box', 'compact', 'borderless' ], 'capture' => 'bool', 'syntax' => [ 'none', 'mysql', 'sqlite' ] ]; public function __construct(OutputInterface $output) { $this->output = $output; $this->state = new ShellState(); $this->options = $this->defaultOptions; } public function getState(): ShellState { return $this->state; } private function promptForCommand() { $info = $this->resource . ($this->db?"":"?"); if ($this->getOption('capture')) { $info .= '][#' . $this->vars['capture_index']; } $prompt = sprintf("PDO:[%s]> ", $info); $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 getOption(string $name): mixed { return $this->options[$name] ?? null; } 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 '.capture': $this->doSetCommand(['capture', 1]); $this->doVarCommand(['capture_index', 0]); break; case '.quit': case '.exit': $this->running = false; break;; default: $this->output->writeln("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(" {$cmd} - {$info}"); } } private function doSaveCommand(array $args) { if (empty($this->lastQuery)) { $this->output->writeln("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("Wrote {$filename}"); } private function doShowCommand(array $args) { $file = array_shift($args); if ($file) { if (!file_exists($file)) { $this->output->writeln("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("{$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) { if (!array_key_exists($var, $this->validOptions)) { $info = ""; } elseif (is_array($this->validOptions[$var])) { $info = "[".join(",", $this->validOptions[$var])."]"; } else { $info = "<".$this->validOptions[$var].">"; } $this->output->writeln("{$var}: ".var_export($value,true)." ".$info); } return; } if ($value !== null) { if (!array_key_exists($varname, $this->options)) { $this->output->writeln("No such option {$varname}"); return; } if (array_key_exists($varname, $this->validOptions)) { $v = $this->validOptions[$varname]; if (is_array($v)) { if (!in_array($value, $v)) { $this->output->writeln("Bad value for {$varname} Valid values: ".join(", ",$v).""); return; } } elseif (is_string($v)) { switch ($v) { case 'int': if (!ctype_digit($value)) { $this->output->writeln("Bad value for {$varname} Expected integer"); return; } break; case 'bool': $true = in_array($value, [ 1, "true", "yes", "on" ]); $false = in_array($value, [ 0, "false", "no", "off" ]); if (!$true && !$false) { $this->output->writeln("Bad value for {$varname} Expected boolean"); return; } $value = $true; break; } } } $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(" %s%s", ($name==$this->resource?"*":" "), $name) ); } return; } $res = get_resource($name); if ($res instanceof PdoResource) { $this->db = $res; $this->resource = $name; $this->output->writeln("** Selected {$name}"); } else { $this->output->writeln("Invalid resource {$name}"); } } private function doVarCommand(array $args) { $varname = array_shift($args); if (empty($varname)) { foreach ($this->vars as $var=>$value) { $this->output->writeln("{$var}=".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("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("{$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("%{$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) { } }