commit da3f1f78e7862a39bfdec10978e1b3f652848f96 Author: Pavel Shevaev Date: Mon Dec 5 18:21:32 2022 +0300 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e92f57 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tags diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..fd2536d --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "name": "bit/metagen_php", + "version" : "v0.0.9", + "description": "PHP codgen support from meta descriptions", + "homepage": "https://git.bit5.ru/bit/metagen_php", + "require": { + "php": ">=7.4", + "twig/twig" : "v3.4.3" + }, + "autoload": { + "files": [ + "src/codegen.inc.php" + ] + } +} diff --git a/src/codegen.inc.php b/src/codegen.inc.php new file mode 100644 index 0000000..ccfb63c --- /dev/null +++ b/src/codegen.inc.php @@ -0,0 +1,265 @@ + true, + 'autoescape' => false, + 'strict_variables' => true] + ); + $twig->addExtension(new \Twig\Extension\DebugExtension()); + + _add_twig_support($twig); + + return $twig; +} + +function _add_twig_support(\Twig\Environment $twig) +{ + $twig->addTest(new \Twig\TwigTest('instanceof', + function($obj, $class) + { + return (new \ReflectionClass($class))->isInstance($obj); + } + )); + $twig->addFilter(new \Twig\TwigFilter('var_export', + function($v) + { + return str_replace("\n", "", var_export($v, true)); + } + )); + $twig->addFunction(new \Twig\TwigFunction('Error', + function($e) + { + throw new Exception($e); + } + )); + $twig->addFunction(new \Twig\TwigFunction('has_token', + function($o, $token) + { + return $o->hasToken($token); + } + )); + $twig->addFunction(new \Twig\TwigFunction('token', + function($o, $name) + { + return $o->getToken($name); + } + )); + $twig->addFilter(new \Twig\TwigFilter('default_value', + function($default) + { + return _default_value($default); + } + )); + $twig->addFunction(new \Twig\TwigFunction('apply_value_filters', + function ($field_name, array $tokens, $val, $add_assoc_check = true) + { + return _apply_value_filters($field_name, $tokens, $val, $add_assoc_check); + } + )); + $twig->addFunction(new \Twig\TwigFunction('data2value', + function($name, \mtgType $type, $buf, $prefix = '', $tokens = array(), $as_is = false, $postfix = '') + { + return _data2value($name, $type, $buf, $prefix, $tokens, $as_is, $postfix); + } + )); + $twig->addFunction(new \Twig\TwigFunction('value2data', + function($name, \mtgType $type, $buf, $prefix = '', $tokens = array()) + { + return _value2data($name, $type, $buf, $prefix, $tokens); + } + )); +} + +function _default_value($default) +{ + if($default == "[]") + return 'array()'; + + if(strlen($default) > 2 && strpos($default, '[') === 0 && strpos($default, ']') === strlen($default)-1) + { + $str = trim($default, ']['); + if(strpos($str, "{") !== false ) + $str = var_export(json_decode(trim($default, ']['), true), true); + return 'array(' . $str . ')'; + } + return str_replace('$', '\$', $default); +} + +function _apply_value_filters($field_name, array $tokens, $val, $add_assoc_check = true) +{ + $str = $val; + foreach($tokens as $token => $args_json) + { + if(strpos($token, 'flt_') === false) + continue; + + $filter_func = 'mtg_' . $token; + + if(function_exists($filter_func)) + { + $args = array(); + if($args_json) + { + $args = json_decode($args_json, true); + if(!is_array($args)) + throw new Exception("Bad filter args: $args_json"); + } + if($add_assoc_check) + $str = "(\$assoc ? $filter_func($str, '$field_name', \$this, " . str_replace("\n", "", var_export($args, true)) . ") : $str)"; + else + $str = "$filter_func($str, '$field_name', \$this, " . str_replace("\n", "", var_export($args, true)) . ")"; + } + else + throw new Exception("No such filter '$filter_func'"); + } + return $str; +} + +//TODO: move it to template +function _data2value($name, \mtgType $type, $buf, $prefix = '', $tokens = array(), $as_is = false, $postfix = '', $indent = '') +{ + $str = ''; + $tmp_val = '$tmp_val__'; + $pname = $prefix.$name; + + $cond_indent = $indent." "; + + $default_value_arg = array_key_exists('default', $tokens) ? _default_value($tokens['default']) : 'null'; + if($type instanceof \mtgMetaStruct) + { + $default = array_key_exists('default', $tokens) ? $tokens['default'] : null; + if($default) + { + $default = json_decode($default, true); + if(!is_array($default)) + throw new Exception("Bad default struct: " . $tokens['default']); + $default = str_replace("\n", "", var_export($default, true)); + $default_value_arg = "\$assoc ? $default : array_values($default)"; + } + else + $default_value_arg = "null"; + } + + $str .= "\n"; + + if($as_is) + $tmp_val = $buf; + else + $str .= $indent."{$tmp_val} = mtg_php_array_extract_val({$buf}, \$assoc, '{$name}', {$default_value_arg});\n"; + + if($type instanceof \mtgBuiltinType) + { + $str .= $cond_indent."{$tmp_val} = " . _apply_value_filters($name, $tokens, "{$tmp_val}"). ";\n"; + $str .= $cond_indent."{$pname} = mtg_php_val_{$type}({$tmp_val});\n"; + } + else if($type instanceof \mtgMetaStruct) + { + if(array_key_exists('virtual', $tokens)) + { + $str .= $cond_indent."{$tmp_val} = " . _apply_value_filters($name, $tokens, "{$tmp_val}"). ";\n"; + $str .= $cond_indent."\$tmp_sub_arr__ = mtg_php_val_arr({$tmp_val});\n"; + $str .= $cond_indent."\$vclass__ = AutogenBundle::getClassName(mtg_php_val_uint32(mtg_php_array_extract_val(\$tmp_sub_arr__, \$assoc, 'vclass__')));\n"; + $str .= $cond_indent."{$pname} = new \$vclass__(\$tmp_sub_arr__, \$assoc);\n"; + } + else + { + $str .= $cond_indent."{$tmp_val} = " . _apply_value_filters($name, $tokens, "{$tmp_val}"). ";\n"; + $str .= $cond_indent."\$tmp_sub_arr__ = mtg_php_val_arr({$tmp_val});\n"; + $str .= $cond_indent."{$pname} = new {$type}(\$tmp_sub_arr__, \$assoc);\n"; + } + } + else if($type instanceof \mtgArrType) + { + //TODO: maybe filters should be applied to the whole array as well? + $str .= $cond_indent."\$tmp_arr__ = mtg_php_val_arr({$tmp_val});\n"; + $str .= $cond_indent."foreach(\$tmp_arr__ as \$tmp_arr_item__)\n"; + $str .= $cond_indent."{\n"; + //NOTE: removing default for field + unset($tokens['default']); + $str .= _data2value('$tmp__', $type->getValue(), "\$tmp_arr_item__", '', $tokens, true, "{$pname}[] = \$tmp__;", $cond_indent." ") . "\n"; + $str .= $cond_indent."}\n"; + } + else if($type instanceof \mtgMetaEnum) + { + $str .= $cond_indent."{$tmp_val} = " . _apply_value_filters($name, $tokens, "{$tmp_val}"). ";\n"; + $check_enum_validity = array_key_exists('is_enum_mask', $tokens) ? 'false' : 'true'; + $str .= $cond_indent."{$pname} = mtg_php_val_enum('$type', {$tmp_val}, {$check_enum_validity});\n"; + } + else + throw new Exception("Unknown type '{$type}'"); + + if($postfix) + $str .= $cond_indent.$postfix."\n"; + + //replacing some garbage + $str = str_replace('$tmp_val__ = $tmp_val__;', '', $str); + return $str; +} + +//TODO: move it to template +function _value2data($name, \mtgType $type, $buf, $prefix = '', $tokens = array(), $indent = '') +{ + $pname = $prefix.$name; + + $str = ''; + $str .= "\$__last_var = '$name';"; + $str .= "\$__last_val = $pname;"; + + if($type instanceof \mtgBuiltinType) + { + if($type->isNumeric()) + $str .= $indent."mtg_php_array_set_value({$buf}, \$assoc, '$name', 1*{$pname});"; + else if($type->isString()) + $str .= $indent."mtg_php_array_set_value({$buf}, \$assoc, '$name', ''.{$pname});"; + else if($type->isBool()) + $str .= $indent."mtg_php_array_set_value({$buf}, \$assoc, '$name', (bool){$pname});"; + else if($type->isBlob()) + $str .= $indent."mtg_php_array_set_value({$buf}, \$assoc, '$name', {$pname});"; + else + throw new Exception("Unknown type '$type'"); + } + else if($type instanceof \mtgMetaStruct) + { + if(array_key_exists('virtual', $tokens)) + $str .= $indent."mtg_php_array_set_value({$buf}, \$assoc, '$name', is_array({$pname}) ? {$pname} : {$pname}->export(\$assoc, true/*virtual*/));"; + else + $str .= $indent."mtg_php_array_set_value({$buf}, \$assoc, '$name', is_array({$pname}) ? {$pname} : {$pname}->export(\$assoc));"; + } + else if($type instanceof \mtgMetaEnum) + { + $str .= $indent."mtg_php_array_set_value({$buf}, \$assoc, '$name', 1*{$pname});"; + } + else if($type instanceof \mtgArrType) + { + $str .= $indent."\$arr_tmp__ = array();\n"; + //NOTE: adding optimization, checking if the first item is array + $str .= $indent."if(!\$assoc && {$pname} && is_array(current({$pname})))\n"; + $str .= $indent."{\n"; + //$str .= " mtg_php_debug('BULK:' . get_class(\$this));\n"; + $str .= $indent." \$arr_tmp__ = {$pname};\n"; + $str .= $indent."}\n"; + $str .= $indent."else\n"; + $str .= $indent." foreach({$pname} as \$idx__ => \$arr_tmp_item__)\n"; + $str .= $indent." {\n"; + $str .= _value2data('$arr_tmp_item__', $type->getValue(), "\$arr_tmp__", "", $tokens, $indent." ") . "\n"; + $str .= $indent." if(\$assoc)\n"; + $str .= $indent." {\n"; + $str .= $indent." \$arr_tmp__[] = \$arr_tmp__['\$arr_tmp_item__'];\n"; + $str .= $indent." unset(\$arr_tmp__['\$arr_tmp_item__']);\n"; + $str .= $indent." }\n"; + $str .= $indent." }\n"; + $str .= $indent."mtg_php_array_set_value({$buf}, \$assoc, '$name', \$arr_tmp__);\n"; + } + else + throw new Exception("Unknown type '{$type}'"); + + return $str; +} + diff --git a/tpl/codegen_bundle.twig b/tpl/codegen_bundle.twig new file mode 100644 index 0000000..3a06cdd --- /dev/null +++ b/tpl/codegen_bundle.twig @@ -0,0 +1,31 @@ +import($data, $assoc); + } + + function getClassId() + { + return self::CLASS_ID; + } + + function import(&$data, $assoc = false, $root = true) + { + $IDX = -1; + + try + { + if(!is_array($data)) + throw new Exception("Bad data: $data"); + try + { + $IDX = 0; + {{_self.import_fields(o)}} + } + catch(Exception $e) + { + if($IDX > -1) + { + $FIELDS = self::CLASS_FIELDS(); + throw new Exception($e->getMessage() . " < {$FIELDS[$IDX]}"); + } + else + throw $e; + } + if($root && $assoc && sizeof($data) > 0) + throw new Exception("Junk fields: " . implode(',', array_keys($data))); + } + catch(Exception $e) + { + throw new Exception($e->getMessage() . " < ({{o.name}})"); + } + return $IDX; + } + + function export($assoc = false, $virtual = false) + { + $data = array(); + $this->fill($data, $assoc, $virtual); + return $data; + } + + function fill(&$data, $assoc = false, $virtual = false) + { + if($virtual) + mtg_php_array_set_value($data, $assoc, 'vclass__', $this->getClassId()); + try + { + $__last_var = null; + $__last_val = null; + + {{_self.export_fields(o)}} + } + catch(Exception $e) + { + throw new Exception("[%class%]\n->$__last_var\n" . serialize($__last_val) . "\n\t" . $e->getMessage()); + } + } +} + +{% endmacro %} + +{% macro struct_fields(o) %} +{%- for f in o.fields ~%} + {{ _self.decl_struct_field(o, f) }} +{%- endfor -%} +{% endmacro %} + +{% macro decl_struct_field(o, f) ~%} + /** @var {{f.type}}*/ + {%- if f.type is instanceof('\\mtgBuiltinType') and f.type.isstring ~%} + public ${{f.name}} = ''; + {%- elseif f.type is instanceof('\\mtgArrType') ~%} + public ${{f.name}} = []; + {%- else ~%} + public ${{f.name}}; + {%- endif -%} +{% endmacro %} + +{% macro fields_names(o) %} +{%- if o.parent -%} +array_merge(parent::CLASS_FIELDS(), +{%- endif -%} + + [ +{%- for f in o.fields -%} + '{{f.name}}' {{not loop.last ? ','}} +{%- endfor -%} + ] + +{%- if o.parent -%} +) +{%- endif -%} + +{% endmacro %} + +{% macro fields_types(o) %} +{%- if o.parent -%} +array_merge(parent::CLASS_FIELDS_TYPES(), +{%- endif -%} + + [ +{%- for f in o.fields -%} + '{{f.name}}' => '{{f.type}}' {{not loop.last ? ','}} +{%- endfor -%} + ] + +{%- if o.parent -%} +) +{%- endif -%} + +{% endmacro %} + +{% macro fields_props(o) %} +{%- if o.parent -%} +array_merge(parent::CLASS_FIELDS_PROPS(), +{%- endif -%} + + [ +{%- for f in o.fields -%} + '{{f.name}}' => {{f.tokens|var_export}} {{not loop.last ? ','}} +{%- endfor -%} + ] + +{%- if o.parent -%} +) +{%- endif -%} + +{% endmacro %} + +{% macro field_init_value(o, f) %} + {%- if f.type is instanceof('\\mtgMetaStruct') ~%} + new {{f.type}}() + {%- elseif f.type is instanceof('\\mtgMetaEnum') ~%} + {%- if has_token(f, 'default') -%} + {{f.type}}::{{token(f, 'default')|default_value}} + {%- else -%} + {{f.type}}::DEFAULT_VALUE + {%- endif -%} + {%- elseif f.type is instanceof('\\mtgArrType') ~%} + [] + {%- elseif f.type is instanceof('\\mtgBuiltinType') ~%} + {%- if has_token(f, 'default') -%} + mtg_php_val_{{f.type}}({{apply_value_filters(f.name, f.tokens, token(f, 'default')|default_value, false)}}) + {%- elseif f.type.isstring -%} + '' + {%- else -%} + 0 + {%- endif -%} + {%- else -%} + {{Error("Uknown type: " ~ f.type)}} + {%- endif -%} +{% endmacro %} + +{% macro field_init(o, f) %} + $this->{{f.name}} = {{_self.field_init_value(o, f)}}; +{% endmacro %} + +{% macro fields_init(o) %} +{%- if o.parent -%} + parent::__construct(); +{%- endif -%} +{%- for f in o.fields ~%} + {{_self.field_init(o, f)}} +{%- endfor -%} +{% endmacro %} + +{% macro import_fields(o) %} +{%- if o.parent -%} + $IDX = parent::import($data, $assoc, false); +{%- endif -%} +{%- for f in o.fields ~%} + {{data2value(f.name, f.type, '$data', '$this->', f.tokens)}} + ++$IDX; +{%- endfor -%} +{% endmacro %} + +{% macro export_fields(o) %} +{%- if o.parent -%} + $__last_var = 'parent'; + $__last_val = '<>'; + parent::fill($data, $assoc, false); +{%- endif -%} +{%- for f in o.fields ~%} + {{value2data(f.name, f.type, '$data', '$this->', f.tokens)}} +{%- endfor -%} +{% endmacro %}