* Added vertical rendering to com.noccy.pdo.shell * Added missing Log classes for com.noccy.apiclient
		
			
				
	
	
		
			370 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			370 lines
		
	
	
		
			10 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' ])]
 | 
						|
    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)
 | 
						|
    {
 | 
						|
        
 | 
						|
    }
 | 
						|
} |