'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("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 { $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) { $this->output->writeln("{$var}: ".var_export($value,true).""); } return; } if ($value !== null) { if (!array_key_exists($varname, $this->options)) { $this->output->writeln("No such option {$varname}"); return; } $this->options[$varname] = $args; } else { $this->output->writeln(var_export($this->options[$varname]??null,true)); } } private function doSelectCommand(array $args) { $name = array_shift($args); $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; } $pdo = $this->db->getPDO(); try { $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(); } }