From 7a08c1c280b806bd5f85fefc45ad3393c7de6d11 Mon Sep 17 00:00:00 2001 From: Inhere Date: Tue, 2 Nov 2021 22:23:47 +0800 Subject: [PATCH] feat: complate the sql to md tool commands --- .../InitApplicationTrait.php | 8 +- app/Concern/StaticPathAliasTrait.php | 91 +++++ app/Console/CliApplication.php | 2 +- ...tsTryReader.php => ContentsAutoReader.php} | 15 +- app/Console/Component/ContentsAutoWriter.php | 19 + app/Console/Controller/ConvertController.php | 25 +- app/Console/Controller/JsonController.php | 3 +- app/Console/Controller/SqlController.php | 50 ++- app/Console/Controller/StringController.php | 105 +++++- app/Console/Controller/UtilController.php | 8 +- app/Helper/AppHelper.php | 41 +-- app/Helper/KiteUtil.php | 8 + app/Http/WebApplication.php | 2 +- app/Kite.php | 3 + app/Lib/Convert/SQLMarkdown.php | 285 --------------- app/Lib/Parser/DBCreateSQL.php | 147 -------- app/Lib/Parser/DBMdTable.php | 156 ++++++++ app/Lib/Parser/DBSchemeSQL.php | 147 ++++++++ app/Lib/Parser/DBTable.php | 336 ++++++++++++++++-- app/Lib/Parser/MkDownTable.php | 20 -- app/Lib/Stream/MapStream.php | 5 +- app/boot.php | 15 +- resource/awesome.md | 9 +- test/unittest/Lib/Parser/DBTableTest.php | 69 ++++ 24 files changed, 994 insertions(+), 575 deletions(-) rename app/{Common/Traits => Concern}/InitApplicationTrait.php (95%) create mode 100644 app/Concern/StaticPathAliasTrait.php rename app/Console/Component/{ContentsTryReader.php => ContentsAutoReader.php} (91%) create mode 100644 app/Console/Component/ContentsAutoWriter.php create mode 100644 app/Helper/KiteUtil.php delete mode 100644 app/Lib/Convert/SQLMarkdown.php delete mode 100644 app/Lib/Parser/DBCreateSQL.php create mode 100644 app/Lib/Parser/DBMdTable.php create mode 100644 app/Lib/Parser/DBSchemeSQL.php delete mode 100644 app/Lib/Parser/MkDownTable.php create mode 100644 test/unittest/Lib/Parser/DBTableTest.php diff --git a/app/Common/Traits/InitApplicationTrait.php b/app/Concern/InitApplicationTrait.php similarity index 95% rename from app/Common/Traits/InitApplicationTrait.php rename to app/Concern/InitApplicationTrait.php index cc73bfd..a0d9992 100644 --- a/app/Common/Traits/InitApplicationTrait.php +++ b/app/Concern/InitApplicationTrait.php @@ -1,10 +1,11 @@ set('txtRender', function () { + return new TextTemplate(); + }); + $box->set('glApi', function () { $config = $this->getArrayParam('gitlab'); diff --git a/app/Concern/StaticPathAliasTrait.php b/app/Concern/StaticPathAliasTrait.php new file mode 100644 index 0000000..efa10ba --- /dev/null +++ b/app/Concern/StaticPathAliasTrait.php @@ -0,0 +1,91 @@ + $realPath) { + // the 1th char must is '@' + if (!$alias || $alias[0] !== '@') { + continue; + } + + self::$aliases[$alias] = self::alias($realPath); + } + } + + /** + * @return array + */ + public static function getAliases(): array + { + return self::$aliases; + } +} diff --git a/app/Console/CliApplication.php b/app/Console/CliApplication.php index 3e33c42..afdb018 100644 --- a/app/Console/CliApplication.php +++ b/app/Console/CliApplication.php @@ -17,7 +17,7 @@ use Inhere\Kite\Console\Listener\BeforeCommandRunListener; use Inhere\Kite\Console\Listener\BeforeRunListener; use Inhere\Kite\Lib\Jump\QuickJump; -use Inhere\Kite\Common\Traits\InitApplicationTrait; +use Inhere\Kite\Concern\InitApplicationTrait; use Inhere\Kite\Console\Listener\NotFoundListener; use Inhere\Kite\Console\Plugin\PluginManager; use Inhere\Kite\Kite; diff --git a/app/Console/Component/ContentsTryReader.php b/app/Console/Component/ContentsAutoReader.php similarity index 91% rename from app/Console/Component/ContentsTryReader.php rename to app/Console/Component/ContentsAutoReader.php index d9db663..852588a 100644 --- a/app/Console/Component/ContentsTryReader.php +++ b/app/Console/Component/ContentsAutoReader.php @@ -10,9 +10,9 @@ use function substr; /** - * class ContentsTryReader + * class ContentsAutoReader */ -class ContentsTryReader extends AbstractObj +class ContentsAutoReader extends AbstractObj { public const TYPE_CLIPBOARD = 'clipboard'; @@ -25,6 +25,17 @@ class ContentsTryReader extends AbstractObj */ protected string $srcType = self::TYPE_STRING; + /** + * @param string $source + * @param array $opts + * + * @return string + */ + public static function readFrom(string $source, array $opts = []): string + { + return (new self())->read($source, $opts); + } + /** * try read contents * diff --git a/app/Console/Component/ContentsAutoWriter.php b/app/Console/Component/ContentsAutoWriter.php new file mode 100644 index 0000000..79a3a97 --- /dev/null +++ b/app/Console/Component/ContentsAutoWriter.php @@ -0,0 +1,19 @@ +getOpt('source'); - $source = AppHelper::tryReadContents($source); + $source = ContentsAutoReader::readFrom($source); if (!$source) { throw new InvalidArgumentException('empty source code for convert'); } - $obj = new SQLMarkdown(); - $sql = $obj->toCreateSQL($source); - + $sql = DBTable::fromMdTable($source)->toCreateSQL(); $output->writeRaw($sql); } @@ -103,20 +100,16 @@ public function md2sqlCommand(FlagsParser $fs, Output $output): void public function sql2mdCommand(FlagsParser $fs, Output $output): void { $source = $fs->getOpt('source'); - $source = AppHelper::tryReadContents($source); + $source = ContentsAutoReader::readFrom($source); if (!$source) { throw new InvalidArgumentException('empty source code for convert'); } - // $obj = new SQLMarkdown(); - // $sql = $obj->toMdTable($source); - - $p = new DBCreateSQL(); - $p->parse($source); - - $sql = $p->genMDTable(); - $output->writeRaw($sql); + $md = DBTable::fromSchemeSQL($source)->toMDTable(); + $output->writeRaw($md); + // $cm = new CliMarkdown(); + // $output->println($cm->parse($md)); } /** diff --git a/app/Console/Controller/JsonController.php b/app/Console/Controller/JsonController.php index 6e7eb44..52c1b36 100644 --- a/app/Console/Controller/JsonController.php +++ b/app/Console/Controller/JsonController.php @@ -13,6 +13,7 @@ use Inhere\Console\Controller; use Inhere\Console\IO\Output; use Inhere\Kite\Console\Component\Clipboard; +use Inhere\Kite\Console\Component\ContentsAutoReader; use Inhere\Kite\Helper\AppHelper; use Inhere\Kite\Kite; use InvalidArgumentException; @@ -97,7 +98,7 @@ private function loadDumpfileJSON(): void */ private function autoReadJSON(string $source): void { - $this->json = AppHelper::tryReadContents($source, [ + $this->json = ContentsAutoReader::readFrom($source, [ 'loadedFile' => $this->dumpfile, ]); if (!$this->json) { diff --git a/app/Console/Controller/SqlController.php b/app/Console/Controller/SqlController.php index d8fcdc7..6fa656a 100644 --- a/app/Console/Controller/SqlController.php +++ b/app/Console/Controller/SqlController.php @@ -11,8 +11,9 @@ use Inhere\Console\Controller; use Inhere\Console\IO\Output; +use Inhere\Kite\Console\Component\ContentsAutoReader; use Inhere\Kite\Helper\AppHelper; -use Inhere\Kite\Lib\Parser\DBCreateSQL; +use Inhere\Kite\Lib\Parser\DBTable; use InvalidArgumentException; use Toolkit\PFlag\FlagsParser; @@ -25,13 +26,48 @@ class SqlController extends Controller protected static $description = 'Some useful development tool commands for SQL'; + /** + * @return string[][] + */ + protected static function commandAliases(): array + { + return [ + 'md' => ['2md', 'to-md'], + ]; + } + + /** + * convert create mysql table SQL to markdown table + * + * @options + * -s,--source string;The source code for convert. allow: FILEPATH, @clipboard;true + * -o,--output The output target. default is stdout. + * + * @param FlagsParser $fs + * @param Output $output + */ + public function mdCommand(FlagsParser $fs, Output $output): void + { + $source = $fs->getOpt('source'); + $source = ContentsAutoReader::readFrom($source); + + if (!$source) { + throw new InvalidArgumentException('empty source code for convert'); + } + + $md = DBTable::fromSchemeSQL($source)->toMDTable(); + $output->writeRaw($md); + // $cm = new CliMarkdown(); + // $output->println($cm->parse($md)); + } + /** * collect fields from create table SQL. * * @options - * -s, --source string;The source code for convert. allow: FILEPATH, @clipboard;true + * -s, --source string;The source create SQL for convert. allow: FILEPATH, @clipboard;true * -o, --output The output target. default is stdout. - * --case Convert the field case. + * --case Change the field name case. allow: camel, snake * * @param FlagsParser $fs * @param Output $output @@ -45,13 +81,11 @@ public function fieldsCommand(FlagsParser $fs, Output $output): void throw new InvalidArgumentException('empty source code for convert'); } - // $obj = new SQLMarkdown(); - // $sql = $obj->toMdTable($source); + $toCemal = $fs->getOpt('case') === 'camel'; - $p = new DBCreateSQL(); - $p->parse($source); + $dbt = DBTable::fromSchemeSQL($source); + $mp = $dbt->getFieldsComments($toCemal); - $mp = $p->getFieldsComments(); $output->aList($mp); } } diff --git a/app/Console/Controller/StringController.php b/app/Console/Controller/StringController.php index 086f477..1595c5b 100644 --- a/app/Console/Controller/StringController.php +++ b/app/Console/Controller/StringController.php @@ -12,6 +12,7 @@ use Inhere\Console\Controller; use Inhere\Console\IO\Output; use Inhere\Kite\Console\Component\Clipboard; +use Inhere\Kite\Console\Component\ContentsAutoReader; use Inhere\Kite\Helper\AppHelper; use Inhere\Kite\Kite; use Inhere\Kite\Lib\Stream\StringsStream; @@ -24,7 +25,12 @@ use function implode; use function is_file; use function preg_match; +use function str_contains; +use function str_replace; +use function strlen; +use function substr; use function trim; +use function vdump; /** * Class StringController @@ -56,6 +62,7 @@ protected static function commandAliases(): array 'join' => ['implode', 'j'], 'split' => ['s'], 'process' => ['p', 'filter', 'f'], + 'replace' => ['r'], ]; } @@ -162,7 +169,7 @@ public function joinCommand(FlagsParser $fs, Output $output): void public function splitCommand(FlagsParser $fs, Output $output): void { $text = trim($fs->getArg('text')); - $text = AppHelper::tryReadContents($text); + $text = ContentsAutoReader::readFrom($text); if (!$text) { $output->warning('empty input contents for handle'); @@ -176,7 +183,44 @@ public function splitCommand(FlagsParser $fs, Output $output): void } /** - * Filtering the input text contents + * Split text to multi line + * + * @arguments + * text The source text for handle. + * Special: + * input '@c' or '@cb' or '@clipboard' - will read from Clipboard + * input empty or '@i' or '@stdin' - will read from STDIN + * input '@l' or '@load' - will read from loaded file + * input '@FILEPATH' - will read from the filepath + * + * @options + * -f, --from The replace from char + * -t, --to The replace to chars + * + * @param FlagsParser $fs + * @param Output $output + */ + public function replaceCommand(FlagsParser $fs, Output $output): void + { + $text = trim($fs->getArg('text')); + $text = ContentsAutoReader::readFrom($text); + if (!$text) { + $output->warning('empty input contents for handle'); + return; + } + + $from = $fs->getOpt('from'); + $to = $fs->getOpt('to'); + if (!$from && !$to) { + $output->warning('the from and to cannot all empty'); + return; + } + + $output->writeRaw(str_replace($from, $to, $text)); + } + + /** + * Filtering the input multi line text contents * * @arguments * text The source text for handle. @@ -190,9 +234,11 @@ public function splitCommand(FlagsParser $fs, Output $output): void * @options * -e, --exclude array;exclude line on contains keywords. * -m, --match array;include line on contains keywords. - * -t, --trim trim the each line. + * -t, --trim bool;trim the each line text. * --wrap wrap the each line by the separator * -j, --join join the each line by the separator + * -c, --cut cut each line by the separator. cut position: L R, eg: 'L=' + * -r, --replace array;replace chars for each line * -s, --sep The separator char for split contents. defaults is newline(\n). * -f, --filter array;apply more filter for handle text. * allow filters: @@ -204,13 +250,15 @@ public function splitCommand(FlagsParser $fs, Output $output): void * @param Output $output * * @example - * {binWithCmd} -f "wrap:'" + * {binWithCmd} --cut 'L,' # cut by = and remove left + * {binWithCmd} --replace 'A/a' # replace A to a for each line + * {binWithCmd} -f "wrap:'" * */ public function processCommand(FlagsParser $fs, Output $output): void { $text = $fs->getArg('text'); - $text = AppHelper::tryReadContents($text, [ + $text = ContentsAutoReader::readFrom($text, [ 'loadedFile' => $this->dumpfile, ]); if (!$text) { @@ -222,19 +270,52 @@ public function processCommand(FlagsParser $fs, Output $output): void $in = $fs->getOpt('match'); $trim = $fs->getOpt('trim'); + $cut = $fs->getOpt('cut'); $sep = $fs->getOpt('sep', "\n"); + // + $replaces = $fs->getOpt('replace'); + + $cutPos = 'L'; + $cutChar = $cut; + if (strlen($cut) > 1) { + if ($cut[0] === 'L') { + $cutPos = 'L'; + $cutChar= substr($cut, 1); + } elseif ($cut[0] === 'R') { + $cutPos = 'R'; + $cutChar= substr($cut, 1); + } + } - $newStr = StringsStream::new(explode($sep, $text)) + $s = StringsStream::new(explode($sep, $text)) ->eachIf('trim', $trim) - ->filterIf(function (string $line) use ($ex) { + ->eachIf(function (string $line) use ($cutPos, $cutChar) { + if (!str_contains($line, $cutChar)) { + return $line; + } + + [$left, $right] = explode($cutChar, $line, 2); + return $cutPos === 'L' ? $right : $left; + }, $cut) + ->filterIf(function (string $line) use ($ex) { // exclude return !Str::has($line, $ex); }, count($ex) > 0) - ->filterIf(function (string $line) use ($in) { + ->filterIf(function (string $line) use ($in) { // include return Str::has($line, $in); }, count($in) > 0) - ->implode($sep); - - echo $newStr, "\n"; + ->eachIf(function (string $line) use ($replaces) { // replace + $froms = $tos = []; + foreach ($replaces as $replace) { + [$from, $to] = explode('/', $replace, 2); + $froms[] = $from; + $tos[] = $to; + } + + // vdump($line); + return str_replace($froms, $tos, $line); + }, $replaces); + + echo $s->implode($sep), "\n"; } /** @@ -247,7 +328,7 @@ public function processCommand(FlagsParser $fs, Output $output): void public function fieldsCommand(FlagsParser $fs, Output $output): void { $text = $fs->getArg('text'); - $text = AppHelper::tryReadContents($text, [ + $text = ContentsAutoReader::readFrom($text, [ 'loadedFile' => $this->dumpfile, ]); diff --git a/app/Console/Controller/UtilController.php b/app/Console/Controller/UtilController.php index 6735466..6563b2f 100644 --- a/app/Console/Controller/UtilController.php +++ b/app/Console/Controller/UtilController.php @@ -114,8 +114,8 @@ public function findJetBrainsCommand(FlagsParser $fs, Output $output): void /** * @options - * -r, -s, --read bool;read and show clipboard contents. - * -w, --write write input contents to clipboard. + * -r, --read bool;read and show clipboard contents. + * -w, --write write input contents to clipboard. * * @param FlagsParser $fs * @param Output $output @@ -128,6 +128,10 @@ public function clipboardCommand(FlagsParser $fs, Output $output): void } $str = $fs->getMustOpt('write'); + if ($str === '@i' || $str === '@stdin') { + $str = $this->input->readAll(); + } + $ok = Clipboard::writeString($str); if ($ok) { diff --git a/app/Helper/AppHelper.php b/app/Helper/AppHelper.php index fedcc93..d4509d5 100644 --- a/app/Helper/AppHelper.php +++ b/app/Helper/AppHelper.php @@ -5,6 +5,7 @@ use ArrayAccess; use Inhere\Console\Util\Show; use Inhere\Kite\Console\Component\Clipboard; +use Inhere\Kite\Console\Component\ContentsAutoReader; use Inhere\Kite\Kite; use Toolkit\Cli\Cli; use Toolkit\FsUtil\File; @@ -270,44 +271,6 @@ public static function getValueByNodes(array $data, array $nodes, $default = nul */ public static function tryReadContents(string $input, array $opts = []): string { - $print = $opts['print'] ?? false; - $lFile = $opts['loadedFile'] ?? ''; - - $str = $input; - if (!$input) { - $print && Cli::info('try read contents from STDIN'); - $str = Kite::cliApp()->getInput()->readAll(); - - // is one line text - } elseif (!str_contains($input, "\n")) { - if (str_starts_with($input, '@')) { - if ($input === '@c' || $input === '@cb' || $input === '@clipboard') { - $print && Cli::info('try read contents from Clipboard'); - $str = Clipboard::new()->read(); - } elseif ($input === '@i' || $input === '@stdin') { - $print && Cli::info('try read contents from STDIN'); - $str = Kite::cliApp()->getInput()->readAll(); - // $str = File::streamReadAll(STDIN); - // $str = File::readAll('php://stdin'); - // vdump($str); - // Cli::info('try read contents from STDOUT'); // error - // $str = Kite::cliApp()->getOutput()->readAll(); - } elseif (($input === '@l' || $input === '@load') && ($lFile && is_file($lFile))) { - $print && Cli::info('try read contents from file: ' . $lFile); - $str = File::readAll($lFile); - } else { - $filepath = substr($input, 1); - if (is_file($filepath)) { - $print && Cli::info('try read contents from file: ' . $filepath); - $str = File::readAll($filepath); - } - } - } elseif (is_file($input)) { - $print && Cli::info('try read contents from file: ' . $input); - $str = File::readAll($input); - } - } - - return $str; + return ContentsAutoReader::readFrom($input, $opts); } } diff --git a/app/Helper/KiteUtil.php b/app/Helper/KiteUtil.php new file mode 100644 index 0000000..2e1531c --- /dev/null +++ b/app/Helper/KiteUtil.php @@ -0,0 +1,8 @@ + comment - * ] - * - * @var array - */ - private array $fields = []; - - /** - * @param string $mdTable - * - * @return string - */ - public function toCreateSQL(string $mdTable): string - { - $lines = array_values( - array_filter( - explode("\n", trim($mdTable)) - ) - ); - - // $table = trim($lines[0], '# '); - [$tableComment, $tableName] = array_filter(explode(' ', trim($lines[0], '`:#\' '))); - - $tableName = trim($tableName, '` '); - $titleLine = trim($lines[1], '| '); - $colNumber = count(explode('|', $titleLine)); - - $this->tableName = $tableName; - - // rm name, title and split line - unset($lines[0], $lines[1], $lines[2]); - - $tpl = << INDEXES')) { - $indexes = substr($line, 9); - break; - } - - // $line eg: `sid` | `INT(11) UNSIGNED` | `No` | `0` | 下单SID - $line = str_replace(['(', ')'], ['(', ')'], trim($line, '| ')); - $nodes = array_map(static function ($value) { - return trim($value, '`\': '); - }, explode('|', $line)); - - $field = $nodes[0]; - $type = $nodes[1]; - $isInt = stripos($type, 'int') !== false; - - $nodes[0] = " `$field`"; - - // create_time - $isTimeField = stripos($field, 'time') !== false; - if ($isTimeField) { - $upType = 'INT'; - $typeNode = 'INT(10) UNSIGNED'; - } else { - $upType = $typeNode = strtoupper($type); - if ($upType === 'STRING') { - $typeNode = 'VARCHAR(128)'; - } elseif ($upType === 'INT') { - $typeNode = 'INT(11) UNSIGNED'; - } - } - - $nodes[1] = $typeNode; - - // 默认值 - if ($field === 'id') { - $nodes[1] = $upType === 'INT' ? 'INT(11) UNSIGNED' : $nodes[1]; - $nodes[3] = 'AUTO_INCREMENT'; - unset($nodes[2]); - } else { - // 是否为空 - $allowNull = in_array(strtolower($nodes[2]), ['否', 'No', 'n'], true) ? 'NOT NULL' : ''; - $nodes[2] = $allowNull; - - // default value - if (isset($nodes[3])) { - if ($this->isNoDefault($upType)) { - $nodes[3] = ''; - } else { - $defValue = $isInt ? (int)$nodes[3] : $nodes[3]; - $nodes[3] = "DEFAULT '$defValue'"; - } - } - } - - $fieldComment = ucfirst($field); - if (isset($nodes[4])) { - $fieldComment = $nodes[4]; - // comment - $nodes[4] = 'COMMENT ' . ($nodes[4] ? "'$nodes[4]'" : "'$field'"); - } - - $this->fields[$field] = $fieldComment; - - $fieldLines[] = implode(' ', $nodes); - } - - return strtr($tpl, [ - '{{TABLE}}' => $tableName, - '{{INDEXES}}' => $indexes, - '{{FIELDS}}' => implode(",\n", $fieldLines), - '{{COMMENT}}' => $tableComment, - ]); - } - - /** - * @param string $createSql Table create SQL. - * - * @return string - */ - public function toMdTable(string $createSql): string - { - # code... - $tableRows = explode("\n", trim($createSql)); - $tableName = trim(array_shift($tableRows), '( '); - $tableName = trim(substr($tableName, 12)); - - $tableComment = ''; - $tableEngine = array_pop($tableRows); - if (($pos = stripos($tableEngine, ' comment')) !== false) { - $tableComment = trim(substr($tableEngine, $pos + 9), ';\'"'); - } - - $mdNodes = [ - ' 字段名 | 类型 | 是否为空 | 默认值 | 注释 ', - '-------|------|---------|--------|-----' - ]; - - $indexes = []; - foreach ($tableRows as $row) { - $row = trim($row, ', '); - if (!$row) { - continue; - } - - if ($this->isIndexLine($row)) { - $indexes[] = $row; - continue; - } - - [$field, $other] = explode(' ', $row, 2); - [$type, $other] = explode(' ', trim($other), 2); - - if (($pos = stripos($other, 'comment ')) !== false) { - $comment = trim(substr($other, $pos + 9), '\'"'); - $other = substr($other, 0, $pos); - } else { - $comment = ucfirst(str_replace('_', ' ', $field)); - } - - $field = trim($field, '`'); - $upOther = strtoupper($other); - - $upType = strtoupper($type); - $isInt = stripos($type, 'int') !== false; - if ($isInt && str_contains($upOther, 'UNSIGNED ')) { - $upType .= ' UNSIGNED'; - } - - $allowNull = 'Yes'; - if (str_contains($other, 'NOT NULL')) { - $allowNull = 'No'; - } - - $default = ''; - if (($pos = strpos($other, 'DEFAULT ')) !== false) { - $default = trim(substr($other, $pos + 8), '\'" '); - } - - $this->fields[$field] = $comment; - $mdNodes[] = sprintf( - '`%s` | `%s` | `%s` | %s | %s', - $field, - $upType, - $allowNull, - $default !== '' ? '`' . $default . '`' : '', - $comment - ); - } - - $fmtLines = []; - - // $fmtLines[] = "Fields:"; - // $fmtLines[] = implode("\n", $fields); - - $fmtLines[] = "### $tableComment $tableName\n"; - $fmtLines[] = implode("\n", $mdNodes); - - if ($indexes) { - $fmtLines[] = "\n> INDEXES: " . implode(', ', $indexes); - } - - $this->tableName = $tableName; - return implode("\n", $fmtLines) . "\n"; - } - - private function isNoDefault(string $upperType): bool - { - if ($upperType === 'JSON') { - return true; - } - - return false; - } - - /** - * is index setting line - * - * @param string $row - * - * @return boolean - */ - private function isIndexLine(string $row): bool - { - if (str_starts_with($row, '`')) { - return false; - } - - if (stripos($row, 'PRIMARY KEY') === 0) { - return true; - } - - if (stripos($row, 'UNIQUE KEY') === 0) { - return true; - } - - if (stripos($row, 'INDEX KEY') === 0) { - return true; - } - - if (stripos($row, 'KEY ') === 0) { - return true; - } - - return false; - } - - /** - * @return array - */ - public function getFields(): array - { - return $this->fields; - } - -} \ No newline at end of file diff --git a/app/Lib/Parser/DBCreateSQL.php b/app/Lib/Parser/DBCreateSQL.php deleted file mode 100644 index 94949ad..0000000 --- a/app/Lib/Parser/DBCreateSQL.php +++ /dev/null @@ -1,147 +0,0 @@ -tableComment = $tableComment; - - $indexes = []; - foreach ($tableRows as $row) { - $row = trim($row, ', '); - if (!$row) { - continue; - } - - if ($this->isIndexLine($row)) { - $indexes[] = $row; - continue; - } - - [$field, $other] = explode(' ', $row, 2); - [$type, $other] = explode(' ', trim($other), 2); - - if (($pos = stripos($other, 'comment ')) !== false) { - $comment = trim(substr($other, $pos + 9), '\'"'); - $other = substr($other, 0, $pos); - } else { - $comment = ucfirst(str_replace('_', ' ', $field)); - } - - $field = trim($field, '`'); - $upType = strtoupper($type); - $isInt = stripos($type, 'int') !== false; - - $typeExt = ''; - $upOther = strtoupper($other); - if ($isInt && str_contains($upOther, 'UNSIGNED ')) { - $typeExt = 'UNSIGNED'; - } - - $allowNull = true; - if (str_contains($other, 'NOT NULL')) { - $allowNull = false; - } - - $default = ''; - if (($pos = strpos($other, 'DEFAULT ')) !== false) { - $default = trim(substr($other, $pos + 8), '\'" '); - } - - $this->fields[$field] = array_merge(self::FIELD_META, [ - 'name' => $field, - 'type' => $upType, - 'typeExt' => $typeExt, - 'nullable' => $allowNull, - 'default' => $default, - 'comment' => $comment, - ]); - - } - - $this->indexes = $indexes; - $this->tableName = $tableName; - return $this; - } - - /** - * @return string - */ - public function genMDTable(): string - { - $mdNodes = [ - ' 字段名 | 类型 | 是否为空 | 默认值 | 注释 ', - '-------|------|---------|--------|-----' - ]; - - $fmtLines = []; - foreach ($this->fields as $field => $meta) { - $upType = $meta['type']; - if ($meta['typeExt']) { - $upType .= ' ' . $meta['typeExt']; - } - - $allowNull = $meta['nullable'] ? 'Yes' : 'No'; - $default = $meta['default']; - - $mdNodes[] = sprintf( - '`%s` | `%s` | `%s` | %s | %s', - $field, - $upType, - $allowNull, - $default !== '' ? '`' . $default . '`' : '', - $meta['comment'] - ); - } - - $fmtLines[] = sprintf("### %s %s\n", $this->tableComment, $this->tableName); - $fmtLines[] = implode("\n", $mdNodes); - - return implode("\n", $fmtLines) . "\n"; - } - - /** - * @return string - */ - public function getSource(): string - { - return $this->source; - } -} diff --git a/app/Lib/Parser/DBMdTable.php b/app/Lib/Parser/DBMdTable.php new file mode 100644 index 0000000..4222e62 --- /dev/null +++ b/app/Lib/Parser/DBMdTable.php @@ -0,0 +1,156 @@ +setSource($mdTable); + + $lines = array_values( + array_filter( + explode("\n", trim($mdTable)) + ) + ); + + // $table = trim($lines[0], '# '); + [$tableComment, $tableName] = array_filter(explode(' ', trim($lines[0], '`:#\' '))); + + $tableName = trim($tableName, '` '); + $titleLine = trim($lines[1], '| '); + $colNumber = count(explode('|', $titleLine)); + + $dbt->setTableName($tableName); + $dbt->setTableComment($tableComment); + + // rm name, title and split line + unset($lines[0], $lines[1], $lines[2]); + + $indexesLine = ''; + foreach ($lines as $line) { + if (str_starts_with($line, '> INDEXES')) { + $indexesLine = substr($line, 10); + break; + } + + $meta = $this->parseLine($line); + $dbt->addField($meta['name'], $meta); + } + + if (!$indexesLine) { + $dbt->addIndexExpr('PRIMARY KEY (`id`)'); + } else { + $idxLines = explode('),', $indexesLine); + foreach ($idxLines as $k => $idxLine) { + $idxLine = trim($idxLine); + if (!isset($idxLines[$k + 1])) { + $dbt->addIndexExpr($idxLine); + continue; + } + $dbt->addIndexExpr($idxLine . ')'); + } + } + + return $dbt; + } + + public function parseLine(string $line): array + { + // $line eg: `sid` | `INT(11) UNSIGNED` | `No` | `0` | 下单SID + $line = str_replace(['(', ')'], ['(', ')'], trim($line, '| ')); + $nodes = array_map(static function ($value) { + return trim($value, '`\': '); + }, explode('|', $line)); + + + $typeLen = 0; + $typeExt = ''; + $typeNode = $nodes[1]; + + $field = $nodes[0]; + $isInt = stripos($typeNode, 'int') !== false; + + $typeNode1 = $typeNode; + if (str_contains($typeNode1, ' ')) { + [$typeNode1, $typeExt] = explode(' ', $typeNode1); + } + + if (str_contains($typeNode1, '(')) { + [$typeNode1, $len] = explode('(', trim($typeNode1, ') ')); + $typeLen = (int)$len; + } + + $upType = strtoupper($typeNode1); + if ($upType === 'STRING') { + $typeLen = 128; + $upType = 'VARCHAR'; + // $typeNode = 'VARCHAR(128)'; + } elseif ($upType === 'INT') { + $typeLen = 11; + // $typeNode = 'INT(11) UNSIGNED'; + } + + // create_time + $isTimeField = stripos($field, 'time') !== false; + if ($isTimeField && $typeLen < 1) { + $typeLen = 10; + } + + // 默认值 + $defValue = ''; + if ($field === 'id') { + $allowNull = 'NOT NULL'; + $defValue = 'AUTO_INCREMENT'; + } else { + // 是否为空 + $allowNull = in_array(strtolower($nodes[2]), ['否', 'no', 'n'], true) ? 'NOT NULL' : ''; + + // default value + if (isset($nodes[3])) { + if (DBTable::isNoDefault($upType)) { + $defValue = ''; + } else { + $defValue = $isInt ? (int)$nodes[3] : $nodes[3]; + } + } + } + + $fieldComment = $nodes[4] ?? ucfirst($field); + + return [ + 'name' => $field, + 'type' => $upType, + 'typeLen' => $typeLen, + 'typeExt' => $typeExt, + 'nullable' => $allowNull === '', + 'default' => $defValue, + 'comment' => $fieldComment, + ]; + } + +} diff --git a/app/Lib/Parser/DBSchemeSQL.php b/app/Lib/Parser/DBSchemeSQL.php new file mode 100644 index 0000000..51136c8 --- /dev/null +++ b/app/Lib/Parser/DBSchemeSQL.php @@ -0,0 +1,147 @@ +setSource($createSQL); + + $tableRows = explode("\n", trim($createSQL)); + $tableName = trim(array_shift($tableRows), '( '); + $tableName = trim(substr($tableName, 12)); + + $tableComment = ''; + $tableEngine = array_pop($tableRows); + if (($pos = stripos($tableEngine, ' comment')) !== false) { + $tableComment = trim(substr($tableEngine, $pos + 9), ';\'"'); + } + + $dbt->setTableName($tableName); + $dbt->setTableComment($tableComment); + + $indexes = []; + foreach ($tableRows as $row) { + $row = trim($row, ', '); + if (!$row) { + continue; + } + + if ($this->isIndexLine($row)) { + $indexes[] = $row; + continue; + } + + $meta = $this->parseLine($row); + $dbt->addField($meta['name'], $meta); + } + + $dbt->setIndexes($indexes); + + return $dbt; + } + + public function parseLine(string $row): array + { + [$field, $other] = explode(' ', $row, 2); + [$type, $other] = explode(' ', trim($other), 2); + + if (($pos = stripos($other, 'comment ')) !== false) { + $comment = trim(substr($other, $pos + 9), '\'"'); + $other = substr($other, 0, $pos); + } else { + $comment = ucfirst(str_replace('_', ' ', $field)); + } + + $field = trim($field, '`'); + $isInt = stripos($type, 'int') !== false; + + $typeLen = 0; + if (str_contains($type, '(')) { + [$type, $len] = explode('(', trim($type, ') ')); + $typeLen = (int)$len; + } + + $upType = strtoupper($type); + + $typeExt = ''; + $upOther = strtoupper($other); + if ($isInt && str_contains($upOther, 'UNSIGNED ')) { + $typeExt = 'UNSIGNED'; + } + + $allowNull = true; + if (str_contains($other, 'NOT NULL')) { + $allowNull = false; + } + + $default = ''; + if (($pos = strpos($other, 'DEFAULT ')) !== false) { + $default = trim(substr($other, $pos + 8), '\'" '); + } + + return [ + 'name' => $field, + 'type' => $upType, + 'typeLen' => $typeLen, + 'typeExt' => $typeExt, + 'nullable' => $allowNull, + 'default' => $default, + 'comment' => $comment, + ]; + } + + /** + * is index setting line + * + * @param string $row + * + * @return boolean + */ + protected function isIndexLine(string $row): bool + { + if (str_starts_with($row, '`')) { + return false; + } + + if (stripos($row, 'PRIMARY KEY') === 0) { + return true; + } + + if (stripos($row, 'UNIQUE KEY') === 0) { + return true; + } + + if (stripos($row, 'INDEX KEY') === 0) { + return true; + } + + if (stripos($row, 'KEY ') === 0) { + return true; + } + + return false; + } + +} diff --git a/app/Lib/Parser/DBTable.php b/app/Lib/Parser/DBTable.php index e8bd3cf..e5513d8 100644 --- a/app/Lib/Parser/DBTable.php +++ b/app/Lib/Parser/DBTable.php @@ -2,10 +2,13 @@ namespace Inhere\Kite\Lib\Parser; -use Inhere\Kite\Lib\Stream\ListStream; use Inhere\Kite\Lib\Stream\MapStream; -use Inhere\Kite\Lib\Stream\StringsStream; -use function stripos; +use Toolkit\Stdlib\Str; +use function array_merge; +use function implode; +use function in_array; +use function sprintf; +use function str_contains; /** * class DBTable @@ -13,90 +16,357 @@ class DBTable { public const FIELD_META = [ - 'name' => '', - 'type' => '', + 'name' => '', + 'type' => '', // INT + 'typeLen' => '', // eg: 10 'typeExt' => '', // eg: UNSIGNED 'nullable' => true, - 'default' => '', - 'comment' => '', + 'default' => '', + 'comment' => '', ]; - public string $tableName = ''; + protected string $source = ''; + + /** + * @var string + */ + protected string $tableName = ''; /** * table comments desc * * @var string */ - public string $tableComment = ''; + protected string $tableComment = ''; /** * @see FIELD_META * @var array */ - public array $fields = []; + protected array $fields = []; /** * @var array */ - public array $indexes = []; + protected array $indexes = []; + + /** + * @param string $mdTable + * + * @return static + */ + public static function fromMdTable(string $mdTable): self + { + return (new DBMdTable())->parse($mdTable); + } /** + * @param string $createSQL + * + * @return static + */ + public static function fromSchemeSQL(string $createSQL): self + { + return (new DBSchemeSQL())->parse($createSQL); + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->toCreateSQL(); + } + + /** + * @return string + */ + public function toString(): string + { + return $this->toCreateSQL(); + } + + /** + * @param bool $camelName + * * @return array */ - public function getFieldsComments(): array + public function getFieldsComments(bool $camelName = false): array { return MapStream::new($this->fields) - ->eachTo(function (array $meta) { + ->eachTo(function (array $meta, string &$key) use ($camelName) { + if ($camelName) { + $key = Str::camelCase($key, false, '_'); + } + return $meta['comment']; }, MapStream::new([])) ->toArray(); } /** - * @param string $upperType + * @param string $field + * @param array $meta * - * @return bool + * @return $this */ - protected function isNoDefault(string $upperType): bool + public function addField(string $field, array $meta): self { - if ($upperType === 'JSON') { - return true; + $meta['name'] = $field; + + $this->fields[$field] = array_merge(self::FIELD_META, $meta); + return $this; + } + + /** + * @param string $field + * + * @return array + */ + public function getField(string $field): array + { + return $this->fields[$field] ?? []; + } + + /** + * @param string $field + * @param string $nodeName + * + * @return array + */ + public function getFieldNode(string $field, string $nodeName): mixed + { + if ($this->hasField($field)) { + return null; } - return false; + return $this->fields[$field][$nodeName] ?? null; } /** - * is index setting line + * @param string $field * - * @param string $row + * @return bool + */ + public function hasField(string $field): bool + { + return isset($this->fields[$field]); + } + + /** + * @param string $expr * - * @return boolean + * @return $this + */ + public function addIndexExpr(string $expr): self + { + $this->indexes[] = $expr; + return $this; + } + + /** + * @return string */ - protected function isIndexLine(string $row): bool + public function toCreateSQL(): string { - if (str_starts_with($row, '`')) { - return false; + $tpl = <<fields as $field => $meta) { + $nodes = [ + "`$field`", + ]; + + $type = $meta['type']; + $typeNode = $type . ($meta['typeLen'] > 0 ? "({$meta['typeLen']})" : ''); + if ($meta['typeExt']) { + $typeNode .= ' ' . $meta['typeExt']; + } + + $nodes[] = $typeNode; + if (!$meta['nullable']) { + $nodes[] = 'NOT NULL'; + } + + $default = $meta['default']; + if ($default !== '') { + if ($default === 'NULL') { + $nodes[] = 'DEFAULT NULL'; + } else { + $nodes[] = "DEFAULT '$default'"; + } + // if ($this->isNoDefault($type)) { + // } else { + } elseif (!$meta['nullable'] && self::isStringType($type) ) { + $nodes[] = "DEFAULT ''"; + } + + if ($comment = $meta['comment']) { + $nodes[] = 'COMMENT ' . "'$comment'"; + } + + // `id` int(11) unsigned NOT NULL COMMENT '主键', + $fieldLines[] = implode(' ', $nodes); } - if (stripos($row, 'PRIMARY KEY') === 0) { - return true; + return strtr($tpl, [ + '{{TABLE}}' => $this->tableName, + '{{INDEXES}}' => implode(",\n ", $this->indexes), + '{{FIELDS}}' => implode(",\n ", $fieldLines), + '{{COMMENT}}' => $this->tableComment, + ]); + } + + /** + * @return string + */ + public function toMDTable(): string + { + $mdNodes = [ + ' 字段名 | 类型 | 是否为空 | 默认值 | 注释 ', + '-------|------|---------|--------|-----' + ]; + + $fmtLines = []; + foreach ($this->fields as $field => $meta) { + $upType = $meta['type']; + if ($meta['typeLen'] > 0) { + $upType .= '(' . $meta['typeLen'] . ')'; + } + + if ($meta['typeExt']) { + $upType .= ' ' . $meta['typeExt']; + } + + $allowNull = $meta['nullable'] ? 'Yes' : 'No'; + $default = $meta['default']; + + $mdNodes[] = sprintf( + '`%s` | `%s` | `%s` | %s | %s', + $field, + $upType, + $allowNull, + $default !== '' ? '`' . $default . '`' : '', + $meta['comment'] + ); } - if (stripos($row, 'UNIQUE KEY') === 0) { - return true; + $fmtLines[] = sprintf("### %s %s\n", $this->tableComment, $this->tableName); + $fmtLines[] = implode("\n", $mdNodes); + + if ($this->indexes) { + $fmtLines[] = "\n> INDEXES: " . implode(', ', $this->indexes); } - if (stripos($row, 'INDEX KEY') === 0) { - return true; + return implode("\n", $fmtLines) . "\n"; + } + + /** + * @param string $source + */ + public function setSource(string $source): void + { + $this->source = $source; + } + + /** + * @return string + */ + public function getSource(): string + { + return $this->source; + } + + /** + * @return array[] + */ + public function getFields(): array + { + return $this->fields; + } + + /** + * @return array + */ + public function getIndexes(): array + { + return $this->indexes; + } + + /** + * @param array $indexes + */ + public function setIndexes(array $indexes): void + { + $this->indexes = $indexes; + } + + /** + * @return string + */ + public function getTableName(): string + { + return $this->tableName; + } + + /** + * @param string $tableName + */ + public function setTableName(string $tableName): void + { + $this->tableName = $tableName; + } + + /** + * @return string + */ + public function getTableComment(): string + { + return $this->tableComment; + } + + /** + * @param string $tableComment + */ + public function setTableComment(string $tableComment): void + { + $this->tableComment = $tableComment; + } + + /** + * @param string $upperType + * + * @return bool + */ + public static function isStringType(string $upperType): bool + { + if (str_contains($upperType, 'TEXT')) { + return true; } - if (stripos($row, 'KEY ') === 0) { - return true; + if (str_contains($upperType, 'CHAR')) { + return true; } return false; } + /** + * @param string $upperType + * + * @return bool + */ + public static function isNoDefault(string $upperType): bool + { + if ($upperType === 'JSON') { + return true; + } + + return false; + } } diff --git a/app/Lib/Parser/MkDownTable.php b/app/Lib/Parser/MkDownTable.php deleted file mode 100644 index 851048d..0000000 --- a/app/Lib/Parser/MkDownTable.php +++ /dev/null @@ -1,20 +0,0 @@ - $str) { + foreach ($this as $key => $item) { // $new->append($func($str)); - $new->offsetSet($key, $func($str)); + $item = $func($item, $key); + $new->offsetSet($key, $item); } return $new; diff --git a/app/boot.php b/app/boot.php index 8e88ecc..58129ec 100644 --- a/app/boot.php +++ b/app/boot.php @@ -1,11 +1,20 @@ addPsr4("Toolkit\\PFlag\\", __DIR__ . '/toolkit/pflag/src/'); -$loader->addPsr4("Symfony\\Component\\Yaml\\", __DIR__ . '/symfony/yaml/'); +// $loader->addPsr4("Toolkit\\PFlag\\", __DIR__ . '/toolkit/pflag/src/'); +// $loader->addPsr4("Symfony\\Component\\Yaml\\", __DIR__ . '/symfony/yaml/'); + +Kite::setAliases([ + '@kite' => Kite::basePath(), + '@kite-user' => OS::userHomeDir('.kite'), + '@kite-custom' => OS::userHomeDir('.kite/custom'), +]); -return $loader; \ No newline at end of file +return $loader; diff --git a/resource/awesome.md b/resource/awesome.md index 0eb9038..87ea800 100644 --- a/resource/awesome.md +++ b/resource/awesome.md @@ -1,3 +1,10 @@ # awesome -- https://github.com/ziadoz/awesome-php \ No newline at end of file +- https://github.com/ziadoz/awesome-php + + +## plantUml + +**theme** + +- https://the-lum.github.io/puml-themes-gallery/#bluegray \ No newline at end of file diff --git a/test/unittest/Lib/Parser/DBTableTest.php b/test/unittest/Lib/Parser/DBTableTest.php new file mode 100644 index 0000000..ad07044 --- /dev/null +++ b/test/unittest/Lib/Parser/DBTableTest.php @@ -0,0 +1,69 @@ + INDEXES: PRIMARY KEY (`id`), UNIQUE KEY `uni_uid_orderno` (`uid`, `orderno`) +MD; + + private $createSql1 = <<mdTable1); + + // vdump($dbt->getFields()); + $this->assertNotEmpty($dbt->getSource()); + $this->assertNotEmpty($dbt->getFields()); + $this->assertNotEmpty($dbt->getIndexes()); + $this->assertEquals('user_order', $dbt->getTableName()); + $this->assertEquals('用户的订单记录表', $dbt->getTableComment()); + + $this->assertNotEmpty($genSql = $dbt->toString()); + $this->assertEquals($this->createSql1, $genSql); + // vdump($dbt->toString()); + + $this->assertNotEmpty($mdTable = $dbt->toMDTable()); + // $this->assertEquals($this->mdTable1, $mdTable); + // vdump($mdTable); + } + + public function testDbMdTable_parseLine(): void + { + $dmt = new DBMdTable(); + $ret = $dmt->parseLine('`name` | `VARCHAR(64)` | `No` | `` | 用户名称'); + + $this->assertNotEmpty($ret); + $this->assertFalse($ret['nullable']); + } +}