268 lines
6.2 KiB
PHP
268 lines
6.2 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Vvveb
|
|
*
|
|
* Copyright (C) 2022 Ziadin Givan
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
namespace Vvveb\System\Sqlp;
|
|
|
|
class Lexer {
|
|
protected $regex;
|
|
|
|
protected $offsetToToken;
|
|
|
|
protected $macroMap;
|
|
|
|
public function __construct($tokenMap, $macroMap) {
|
|
$this->regex = '/(' . implode(')|(', array_keys($tokenMap)) . ')/As';
|
|
$this->offsetToToken = array_values($tokenMap);
|
|
$this->macroMap = $macroMap;
|
|
}
|
|
|
|
public function lex($string) {
|
|
$tokens = [];
|
|
|
|
$offset = 0;
|
|
$sql = '';
|
|
|
|
while (isset($string[$offset])) {
|
|
if (! preg_match($this->regex, $string, $matches, 0, $offset)) {
|
|
throw new \Exception(sprintf('Unexpected character "%s" at offset %d', $string[$offset], $offset));
|
|
}
|
|
|
|
// find the first non-empty element (but skipping $matches[0]) using a quick for loop
|
|
for ($i = 1; '' === $matches[$i]; ++$i);
|
|
$token = $this->offsetToToken[$i - 1];
|
|
//gather all sql tokens into one continuous string
|
|
if ($token == 'T_SQL') {
|
|
$sql .= $matches[0];
|
|
} else {
|
|
if ($sql) {
|
|
$tokens[] = [$sql, 'T_SQL', $offset];
|
|
$sql = '';
|
|
}
|
|
$tokens[] = [$matches[0], $token, $offset];
|
|
}
|
|
|
|
$offset += strlen($matches[0]);
|
|
}
|
|
//add remaining sql if any left
|
|
if ($sql) {
|
|
$tokens[] = [$sql, 'T_SQL', $offset];
|
|
}
|
|
|
|
return $tokens;
|
|
}
|
|
|
|
// a recursive function to build the ast structure
|
|
function tree(&$structure, $k=0) {
|
|
$output = [];
|
|
$count = count($structure);
|
|
|
|
for ($i= $k; $i < $count; $i++) {
|
|
list($element, $type, $offset) = $structure[$i];
|
|
|
|
$node = $structure[$i];
|
|
|
|
if ($type == 'T_IF_START') {
|
|
$ret = $this->tree($structure, $i + 1);
|
|
$node['children'] = $ret[0];
|
|
$i = $ret[1];
|
|
} else {
|
|
if ($type == 'T_IF_END' || $type == 'T_EACH_END') {
|
|
$output[] = $node;
|
|
|
|
return [$output, $i];
|
|
} else {
|
|
if ($type == 'T_SQL') {
|
|
}
|
|
}
|
|
}
|
|
|
|
$output[] = $node;
|
|
}
|
|
|
|
return $output;
|
|
}
|
|
|
|
function parseError($structure, $node_idx) {
|
|
$node = $structure[$node_idx];
|
|
|
|
throw(
|
|
new \Exception(
|
|
sprintf('sqlP parse error, expecting close tag for `%s` at offset %d at `%s`',
|
|
$node[0] , $node[2], $this->treeToSql($structure))
|
|
));
|
|
}
|
|
|
|
// a recursive function to build the ast structure
|
|
function treeMacro(&$structure, $k=0, $level = 0, $startTag = false) {
|
|
$output = [];
|
|
$count = count($structure);
|
|
|
|
for ($i = $k; $i < $count; $i++) {
|
|
list($element, $type, $offset) = $structure[$i];
|
|
|
|
$node = $structure[$i];
|
|
|
|
if ($type == 'T_EACH') {
|
|
$ret = $this->treeMacro($structure, $i + 1, $level + 1, $type);
|
|
|
|
if (is_array($ret) && count($ret) == 2) {
|
|
$node['children'] = $ret[0];
|
|
$i = $ret[1];
|
|
} else {
|
|
$this->parseError($structure, $i);
|
|
}
|
|
} else {
|
|
if ($type == 'T_ELSE') {
|
|
$ret = $this->treeMacro($structure, $i + 1, $level + 1, $type);
|
|
|
|
if (is_array($ret) && count($ret) == 2) {
|
|
$node['children'] = $ret[0];
|
|
$i = $ret[1];
|
|
} else {
|
|
$this->parseError($structure, $i);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($type == 'T_IF_START') {
|
|
$ret = $this->treeMacro($structure, $i + 1, $level + 1, $type);
|
|
|
|
if (is_array($ret) && count($ret) == 2) {
|
|
$node['children'] = $ret[0];
|
|
$i = $ret[1];
|
|
} else {
|
|
$this->parseError($structure, $i);
|
|
}
|
|
} else {
|
|
if ($level > 0 && (
|
|
($type == 'T_ELSE' && $startTag == 'T_IF_START') ||
|
|
($type == 'T_IF_END' && $startTag == 'T_ELSE') ||
|
|
($type == 'T_IF_END' && $startTag == 'T_IF_START') ||
|
|
($type == 'T_EACH_END' && $startTag == 'T_EACH')
|
|
)) {
|
|
$output[] = $node;
|
|
|
|
return [$output, $i];
|
|
} else {
|
|
if ($type == 'T_SQL') {
|
|
}
|
|
}
|
|
}
|
|
|
|
$output[] = $node;
|
|
}
|
|
|
|
return $output;
|
|
}
|
|
|
|
function treeToSql(&$tree) {
|
|
$sql = '';
|
|
|
|
foreach ($tree as $token) {
|
|
$sql .= $token[0];
|
|
|
|
if (isset($token['children'])) {
|
|
$sql .= $this->treeToSql($token['children']);
|
|
}
|
|
}
|
|
|
|
return $sql;
|
|
}
|
|
|
|
function parseMacro($statement, $regex, $template) {
|
|
$macro = $template;
|
|
|
|
if (preg_match_all($regex, $statement, $matches, PREG_SET_ORDER)) {
|
|
foreach ($matches as $match) {
|
|
$macro = $template;
|
|
|
|
//replace macro template variables %$variable
|
|
$macro = preg_replace_callback(
|
|
'@\$%(\w+)@',
|
|
function ($varMatch) use ($match) {
|
|
return
|
|
preg_replace_callback(
|
|
'/:([\w\.]+)/ms',
|
|
function ($matches) {
|
|
return '$' . \Vvveb\dotToArrayKey('params.' . $matches[1]);
|
|
//return '$params[\'' . $matches[1] . '\']';
|
|
},
|
|
$match[$varMatch[1]]);
|
|
},
|
|
$macro);
|
|
|
|
//@result
|
|
//replace result variables
|
|
$macro =
|
|
preg_replace_callback(
|
|
'/@result\.([\w\.]+)/',
|
|
function ($matches) {
|
|
return \Vvveb\dotToArrayKey('$results.' . $matches[1]);
|
|
},
|
|
$macro);
|
|
|
|
//replace macro template placeholders %placeholder
|
|
$macro = preg_replace_callback(
|
|
'@\%(\w+)@',
|
|
function ($varMatch) use ($match) {
|
|
return $match[$varMatch[1]];
|
|
},
|
|
$macro);
|
|
|
|
$statement = str_replace($match[0], $macro, $statement);
|
|
}
|
|
}
|
|
|
|
return $statement;
|
|
}
|
|
|
|
function treeToPhp(&$tree, &$macroMap) {
|
|
$sql = '';
|
|
|
|
foreach ($tree as $token) {
|
|
$name = $token[1];
|
|
$code = $token[0];
|
|
|
|
if ($name == 'T_SQL') {
|
|
$sql .= $code;
|
|
} elseif (isset($macroMap[$name])) {
|
|
$macro = $macroMap[$name];
|
|
|
|
if (is_array($macro)) {
|
|
$regex = $macro[0];
|
|
$string = $macro[1];
|
|
|
|
$sql .= $this->parseMacro($code, $regex, $string);
|
|
} else {
|
|
$sql .= $macro;
|
|
}
|
|
}
|
|
|
|
if (isset($token['children'])) {
|
|
$sql .= $this->treeToPhp($token['children'], $macroMap);
|
|
}
|
|
}
|
|
|
|
return $sql;
|
|
}
|
|
}
|