diff --git a/src/codegen.inc.php b/src/codegen.inc.php index 34c6fed..22fc6fa 100644 --- a/src/codegen.inc.php +++ b/src/codegen.inc.php @@ -84,15 +84,21 @@ function _add_twig_support(\Twig\Environment $twig) } )); $twig->addFunction(new \Twig\TwigFunction('data2value', - function($name, \mtgType $type, $buf, $prefix = '', $tokens = array(), $as_is = false, $postfix = '') + function($val, \mtgType $type, $data, $tokens) { - return data2value($name, $type, $buf, $prefix, $tokens, $as_is, $postfix); + return data2value($val, $type, $data, $tokens); } )); $twig->addFunction(new \Twig\TwigFunction('value2data', + function($val, \mtgType $type, $data, $tokens = array()) + { + return value2data($val, $type, $data, $tokens); + } + )); + $twig->addFunction(new \Twig\TwigFunction('data_normalize', function($name, \mtgType $type, $buf, $prefix = '', $tokens = array()) { - return value2data($name, $type, $buf, $prefix, $tokens); + return data_normalize($name, $type, $buf, $prefix, $tokens); } )); } @@ -147,6 +153,41 @@ function apply_filters($token_prefix, $field_name, array $tokens, $val, $add_ass return $str; } +function apply_filters_v2($token_prefix, $field_name, array $tokens, $val) : string +{ + $str = $val; + foreach($tokens as $token => $args_json) + { + if(strpos($token, $token_prefix) === false) + continue; + + $filter_func = '\metagen_php\\' . $token; + + if(function_exists($filter_func)) + { + $args = null; + if($args_json && + ((strpos($args_json, '[') === 0 && strlen($args_json) > 1 && $args_json[strlen($args_json)-1] === ']') || + (strpos($args_json, '{') === 0 && strlen($args_json) > 1 && $args_json[strlen($args_json)-1] === '}'))) + { + $args = json_decode($args_json, true); + if(!is_array($args)) + throw new Exception("Bad filter '$token' args: '$args_json'"); + } + else + $args = $args_json; + + $str = "$filter_func($str, '$field_name', \$data, " . str_replace("\n", "", var_export($args, true)) . ")"; + } + else + throw new Exception("No such filter '$filter_func'"); + } + if($str !== $val) + return "{$val} = " . $str; + else + return ''; +} + function apply_array_filters($field_name, array $tokens, $val, $add_assoc_check = true) { return apply_filters("fltarr_", $field_name, $tokens, $val, $add_assoc_check); @@ -157,148 +198,100 @@ function apply_value_filters($field_name, array $tokens, $val, $add_assoc_check return apply_filters("flt_", $field_name, $tokens, $val, $add_assoc_check); } +function apply_array_filters_v2($field_name, array $tokens, $val) : string +{ + return apply_filters_v2("fltarr_", $field_name, $tokens, $val); +} + +function apply_value_filters_v2($field_name, array $tokens, $val) : string +{ + return apply_filters_v2("flt_", $field_name, $tokens, $val); +} + //TODO: move it to template -function data2value($name, \mtgType $type, $buf, $prefix = '', $tokens = array(), $as_is = false, $postfix = '', $indent = '') +function data2value(string $val, \mtgType $type, string $data, array $tokens, bool $inside_loop = false) { $str = ''; $tmp_val = '$tmp_val__'; - $pname = $prefix.$name; - $cond_indent = $indent." "; - - $default_value_arg = array_key_exists('default', $tokens) ? ', '.default_value($tokens['default']) : ''; - if($type instanceof \mtgMetaStruct) + if(!$inside_loop) { - $default = array_key_exists('default', $tokens) ? $tokens['default'] : null; - if($default) - { - $default = json_decode($default, true); - if(is_array($default)) - { - $default = str_replace("\n", "", var_export($default, true)); - $default_value_arg = ", \$assoc ? $default : array_values($default)"; - } - else if($default === null) - { - $default_value_arg = ", null"; - } - else - throw new Exception("Bad default struct: " . $tokens['default']); - } + $str .= "{$tmp_val} = current({$data});\n"; + $str .= "if({$tmp_val} === false && key({$data}) === null) throw new Exception(\"No more data\");\n"; + $str .= "next({$data});\n\n"; } - $str .= "\n"; - - if($as_is) - $tmp_val = $buf; - else - $str .= $indent."{$tmp_val} = \metagen_php\array_extract_val({$buf}, \$assoc, '{$name}' {$default_value_arg});\n"; - - if($type instanceof \mtgBuiltinType) + if($type instanceof \mtgBuiltinType || $type instanceof \mtgMetaEnum) { - $str .= $cond_indent."{$tmp_val} = " . apply_value_filters($name, $tokens, "{$tmp_val}"). ";\n"; - $str .= $cond_indent."{$pname} = \metagen_php\\val_{$type}({$tmp_val});\n"; + $str .= "{$val} = {$tmp_val};\n"; } else if($type instanceof \mtgMetaStruct) { - $str .= $cond_indent."if({$tmp_val} === null) {\n"; - $str .= $cond_indent." {$pname} = null; \n"; - $str .= $cond_indent."} else {\n"; - $str .= $cond_indent."{$tmp_val} = " . apply_value_filters($name, $tokens, "{$tmp_val}"). ";\n"; - $str .= $cond_indent."\$tmp_sub_arr__ = \metagen_php\\val_arr({$tmp_val});\n"; + $str .= "if({$tmp_val} === null)\n"; + $str .= " {$val} = null;\n"; + $str .= "else\n"; + $str .= "{\n"; if(array_key_exists('virtual', $tokens)) { - $str .= $cond_indent."\$vclass__ = AutogenBundle::getClassName(\metagen_php\\val_uint32(\metagen_php\array_extract_val(\$tmp_sub_arr__, \$assoc, '\$id', {$type->getClassId()})));\n"; - $str .= $cond_indent."if(!is_a(\$vclass__, '{$type->getName()}', true)) throw new Exception(\"'\$vclass__' is not subclass of '{$type->getName()}'\");\n"; - $str .= $cond_indent."{$pname} = new \$vclass__(\$tmp_sub_arr__, \$assoc);\n"; + $str .= " \$vclass__ = AutogenBundle::getClassName(array_shift({$tmp_val}));\n"; + $str .= " if(!is_a(\$vclass__, '{$type->getName()}', true)) throw new Exception(\"'\$vclass__' is not subclass of '{$type->getName()}'\");\n"; + $str .= " {$val} = new \$vclass__();\n"; + $str .= " {$val}->import({$tmp_val});\n"; } else { - $str .= $cond_indent." {$pname} = new {$type}(\$tmp_sub_arr__, \$assoc);\n"; + $str .= " {$val} = new {$type}();\n"; + $str .= " {$val}->import({$tmp_val});\n"; } - $str .= "}"; + $str .= "}\n"; } else if($type instanceof \mtgArrType) { - //TODO: maybe filters should be applied to the whole array as well? - $str .= $cond_indent."{$tmp_val} = " . apply_array_filters($name, $tokens, "{$tmp_val}"). ";\n"; - $str .= $cond_indent."\$tmp_arr__ = \metagen_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"; - $str .= $cond_indent."{$pname} = \metagen_php\\val_enum('$type', {$tmp_val}, false);\n"; + $str .= "\$tmp_arr__ = {$tmp_val};\n"; + $str .= "foreach(\$tmp_arr__ as {$tmp_val})\n"; + $str .= "{\n"; + $str .= data2value('$tmp__', $type->getValue(), "\$tmp_arr_item__", $tokens, true) . "\n"; + $str .= "{$val}[] = \$tmp__;\n"; + $str .= "}\n"; } else throw new Exception("Unknown type '{$type->getName()}'"); - 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 = '') +function value2data(string $val, \mtgType $type, string $data, $tokens = array()) { - $pname = $prefix.$name; - $str = ''; - $str .= "\$__last_var = '$name';"; - $str .= "\$__last_val = $pname;"; if($type instanceof \mtgBuiltinType) { - if($type->isNumeric()) - $str .= $indent."\metagen_php\array_set_value({$buf}, \$assoc, '$name', 1*{$pname});"; - else if($type->isString()) - $str .= $indent."\metagen_php\array_set_value({$buf}, \$assoc, '$name', ''.{$pname});"; - else if($type->isBool()) - $str .= $indent."\metagen_php\array_set_value({$buf}, \$assoc, '$name', (bool){$pname});"; - else if($type->isBlob()) - $str .= $indent."\metagen_php\array_set_value({$buf}, \$assoc, '$name', {$pname});"; - else - throw new Exception("Unknown type '$type'"); + $str .= "{$data}[] = $val;\n"; } else if($type instanceof \mtgMetaStruct) { if(array_key_exists('virtual', $tokens)) - $str .= $indent."\metagen_php\array_set_value({$buf}, \$assoc, '$name', !is_object({$pname}) ? {$pname} : {$pname}->export(\$assoc, true/*virtual*/));"; + $str .= "{$data}[] = {$val}->export(true/*virtual*/);\n"; else - $str .= $indent."\metagen_php\array_set_value({$buf}, \$assoc, '$name', !is_object({$pname}) ? {$pname} : {$pname}->export(\$assoc));"; + $str .= "{$data}[] = {$val}->export();\n"; } else if($type instanceof \mtgMetaEnum) { - $str .= $indent."\metagen_php\array_set_value({$buf}, \$assoc, '$name', 1*{$pname});"; + $str .= "{$data}[] = $val;\n"; } else if($type instanceof \mtgArrType) { - $str .= $indent."\$arr_tmp__ = array();\n"; + $str .= "\$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 .= " \metagen_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."\metagen_php\array_set_value({$buf}, \$assoc, '$name', \$arr_tmp__);\n"; + $str .= "if({$val} && is_array(current({$val})))\n"; + $str .= " \$arr_tmp__ = {$val};\n"; + $str .= "else\n"; + $str .= " foreach({$val} as \$arr_tmp_item__)\n"; + $str .= " {\n"; + $str .= value2data("\$arr_tmp_item__", $type->getValue(), "\$arr_tmp__", $tokens) . "\n"; + $str .= " }\n"; + $str .= "{$data}[] = \$arr_tmp__;\n"; } else throw new Exception("Unknown type '{$type->getName()}'"); @@ -310,3 +303,90 @@ function is_null_str($default) { return is_string($default) && json_decode($default, true) === null; } + +function data_normalize($name, \mtgType $type, $buf, $dst, $tokens = array(), $tmp_val = '$tmp_val__') +{ + $str = ''; + + $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)) + { + $default = str_replace("\n", "", var_export($default, true)); + $default_value_arg = "$default"; + } + else if($default === null) + { + $default_value_arg = "null"; + } + else + throw new Exception("Bad default struct: " . $tokens['default']); + } + } + + $str .= "\n"; + + if($name !== null) + $str .= "{$tmp_val} = {$buf}['{$name}'] ?? {$default_value_arg};\n"; + + if($type instanceof \mtgBuiltinType) + { + $str .= apply_value_filters_v2($name, $tokens, "{$tmp_val}"). ";\n"; + $str .= "{$dst}[] = \metagen_php\\val_{$type}({$tmp_val});\n"; + } + else if($type instanceof \mtgMetaStruct) + { + $str .= "if({$tmp_val} === null) {\n"; + $str .= " {$dst}[] = null; \n"; + $str .= "} else {\n"; + $str .= apply_value_filters_v2($name, $tokens, "{$tmp_val}"). ";\n"; + $str .= "\$tmp_sub_arr__ = \metagen_php\\val_arr({$tmp_val});\n"; + if(array_key_exists('virtual', $tokens)) + { + $str .= "\$vclass_id__ = \metagen_php\\val_uint32(\$tmp_sub_arr__['\$id'] ?? {$type->getClassId()});\n"; + $str .= "\$vclass__ = AutogenBundle::getClassName(\$vclass_id__);\n"; + $str .= "if(!is_a(\$vclass__, '{$type->getName()}', true)) throw new Exception(\"'\$vclass__' is not subclass of '{$type->getName()}'\");\n"; + $str .= "\$tmp_sub_mapped__ = array(\$vclass_id__);\n"; + $str .= "call_user_func_array([\$vclass__, 'normalize'], array(&\$tmp_sub_arr__, &\$tmp_sub_mapped__));\n"; + $str .= "{$dst}[] = \$tmp_sub_mapped__;\n"; + } + else + { + $str .= "\$tmp_sub_mapped__ = array();\n"; + $str .= "{$type}::normalize(\$tmp_sub_arr__, \$tmp_sub_mapped__);\n"; + $str .= "{$dst}[] = \$tmp_sub_mapped__;\n"; + } + $str .= "}"; + } + else if($type instanceof \mtgArrType) + { + //TODO: maybe filters should be applied to the whole array as well? + $str .= apply_array_filters_v2($name, $tokens, "{$tmp_val}"). ";\n"; + $str .= "\$tmp_arr__ = \metagen_php\\val_arr({$tmp_val});\n"; + $str .= "\$tmp_dst_arr__ = array();\n"; + + $str .= "foreach(\$tmp_arr__ as \$tmp_arr_item__)\n"; + $str .= "{\n"; + //NOTE: removing default for field + unset($tokens['default']); + $str .= data_normalize(null, $type->getValue(), "\$tmp_arr_item__", "\$tmp_dst_arr__", $tokens, "\$tmp_arr_item__") . "\n"; + $str .= "}\n"; + $str .= "{$dst}[] = \$tmp_dst_arr__;\n"; + } + else if($type instanceof \mtgMetaEnum) + { + $str .= apply_value_filters_v2($name, $tokens, "{$tmp_val}"). ";\n"; + $str .= "{$dst}[] = \metagen_php\\val_enum('$type', {$tmp_val}, false);\n"; + } + else + throw new Exception("Unknown type '{$type->getName()}'"); + + return $str; +} + diff --git a/src/data_utils.inc.php b/src/data_utils.inc.php index c494dad..4c12c66 100644 --- a/src/data_utils.inc.php +++ b/src/data_utils.inc.php @@ -2,6 +2,36 @@ namespace metagen_php; use Exception; +function data_get_val(&$arr_or_obj, $name) +{ + if(is_array($arr_or_obj)) + return $arr_or_obj[$name]; + else if(is_object($arr_or_obj)) + return $arr_or_obj->$name; + else + throw new Exception("Not an array nor an object"); +} + +function data_set_val(&$arr_or_obj, $name, $val) +{ + if(is_array($arr_or_obj)) + $arr_or_obj[$name] = $val; + else if(is_object($arr_or_obj)) + $arr_or_obj->$name = $val; + else + throw new Exception("Not an array nor an object"); +} + +function data_get_keys(&$arr_or_obj) : array +{ + if(is_array($arr_or_obj)) + return array_keys($arr_or_obj); + else if(is_object($arr_or_obj)) + return array_keys(get_object_vars($arr_or_obj)); + else + throw new Exception("Not an array nor an object"); +} + function array_extract_val(&$arr, $assoc, $name) { if(!is_array($arr)) diff --git a/tpl/macro.twig b/tpl/macro.twig index 6abfe5f..f8bd38c 100644 --- a/tpl/macro.twig +++ b/tpl/macro.twig @@ -48,12 +48,9 @@ class {{o.name}} {{o.parent ? 'extends ' ~ o.parent.name}} return $flds; } - function __construct(&$data = null, $assoc = false) + function __construct() { {{_self.fields_init(o)}} - - if(!is_null($data)) - $this->import($data, $assoc); } function getClassId() @@ -61,57 +58,72 @@ class {{o.name}} {{o.parent ? 'extends ' ~ o.parent.name}} return self::CLASS_ID; } - function import(&$data, $assoc = false, $root = true) + function import(array &$data) + { + $this->_import($data, true); + } + + function _import(array &$data, $root = true) : int + { + if(!is_array($data)) + throw new Exception("Bad data: $data"); + + if($root) + reset($data); + + $IDX = 0; + + try + { + {{_self.import_fields(o)}} + } + catch(Exception $e) + { + $FIELDS = self::CLASS_FIELDS(); + throw new Exception($e->getMessage() . " < {$FIELDS[$IDX]} < ({{o.name}})"); + } + return $IDX; + } + + function export($virtual = false) : array + { + $data = array(); + $this->_export($data, $virtual); + return $data; + } + + function _export(array &$data, $virtual = false) + { + if($virtual) + $data[] = $this->getClassId(); + + {{_self.export_fields(o)}} + } + + static function normalize(array &$data, array &$mapped, bool $root = true) : int { $IDX = -1; try { - if(!is_array($data)) - throw new Exception("Bad data: $data"); - try - { - $IDX = 0; - {{_self.import_fields(o)}} - } - catch(Exception $e) - { - $FIELDS = self::CLASS_FIELDS(); - throw new Exception($e->getMessage() . " < {$FIELDS[$IDX]}"); - } - if($root && $assoc && sizeof($data) > 0) - throw new Exception("Junk fields: " . implode(',', array_keys($data))); + $IDX = 0; +{%- if o.parent ~%} + $IDX = parent::normalize($data, $mapped, false); +{%- endif ~%} + +{%- for f in o.fields ~%} + {{data_normalize(f.name, f.type, '$data', '$mapped', f.tokens)}} + ++$IDX; +{%- endfor ~%} } catch(Exception $e) { - throw new Exception($e->getMessage() . " < ({{o.name}})"); + $FIELDS = self::CLASS_FIELDS(); + throw new Exception($e->getMessage() . " < {$FIELDS[$IDX]} < ({{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) - \metagen_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("[{{o.name}}]\n->$__last_var\n" . serialize($__last_val) . "\n\t" . $e->getMessage()); - } - } } {% endmacro %} @@ -231,22 +243,20 @@ array_merge(parent::CLASS_FIELDS_PROPS(), {% macro import_fields(o) %} {%- if o.parent -%} - $IDX = parent::import($data, $assoc, false); + $IDX = parent::_import($data, false); {%- endif -%} {%- for f in o.fields ~%} - {{data2value(f.name, f.type, '$data', '$this->', f.tokens)}} + {{data2value('$this->'~f.name, f.type, '$data', f.tokens)}} ++$IDX; {%- endfor -%} {% endmacro %} {% macro export_fields(o) %} {%- if o.parent -%} - $__last_var = 'parent'; - $__last_val = '<>'; - parent::fill($data, $assoc, false); + parent::_export($data, false); {%- endif -%} {%- for f in o.fields ~%} - {{value2data(f.name, f.type, '$data', '$this->', f.tokens)}} + {{value2data('$this->'~f.name, f.type, '$data', f.tokens)}} {%- endfor -%} {% endmacro %}