首页 > 解决方案 > PHP parse_str() 函数允许传递速记数组

问题描述

我们接受从模板到标记引擎的字符串,它允许以“简单”形式传递配置。

引擎通过 PHP 解析字符串,使用 parse_str() 函数的改编版本 - 因此我们可以解析以下字符串的任意组合:

config=posts_per_page:"5",default:"No questions yet -- once created they will appear here."&markup->template="{{ questions }}"

给出:

Array(
[config] => Array
    (
        [posts_per_page] => 5
        [default] => No questions yet -- once created they will appear here.
    )

[markup] => Array
    (
        [template] => {{ questions }}
    )
)

或者:

config->default=all:"<p class='ml-3'>No members here yet...</p>"

要得到:

Array 
[config] => Array
    (
        [default] => Array
            (
                [all] => <p class='ml-3'>No members here yet...</p>
            )

    )
)

其他:

config=>handle:"medium"

回报:

Array (
[config] => Array
    (
        [>handle] => medium
    )
)

字符串可以用空格(和多行空格)传递,字符串参数应该在“双引号”之间传递以保持自然间距 - 我们在传递给 parse_str 方法之前在字符串上运行以下 preg_replace :

// strip white spaces from data that is not passed inside double quotes ( "data" ) ##
$string = preg_replace( '~"[^"]*"(*SKIP)(*F)|\s+~', "", $string );

到目前为止,一切都很好 - 直到我们尝试在字符串值中传递一个“分隔符”,然后它才会被逐字处理 - 例如以下字符串返回一个损坏的数组:

config=posts_per_page:"5",default:"No questions yet -- once created, they will appear here."&markup->template="{{ questions }}"

返回以下数组:

Array (
[config] => Array
    (
        [posts_per_page] => 5
        [default] => No questions yet -- once created
        [ they will appear here."] => 
    )

[markup] => Array
    (
        [template] => {{ questions }}
    )
)

"," 被逐字处理,字符串被分解成一个额外的数组部分。

一种简单的解决方案是创建与字符串值冲突的可能性较低的分隔符和运算符 - 例如将“,”更改为“@@@” - 但使用的标记的一个重要部分是易于编写和阅读 -它的预期用例是前端开发人员将简单的参数传递给模板解析器——这是我们试图避免使用 JSON 的原因之一——这当然很适合传递数据,但它很难阅读和写-当然,该陈述是主观的并且可以发表意见:)

这是 parse_str 方法:

public static function parse_str( $string = null ) {

    // h::log($string);

    // delimiters ##
    $operator_assign = '=';
    $operator_array = '->';
    $delimiter_key = ':';
    $delimiter_and_property = ',';
    $delimiter_and_key = '&';

    // check for "=" delimiter ##
    if( false === strpos( $string, $operator_assign ) ){

        h::log( 'e:>Passed string format does not include asssignment operator "'.$operator_assign.'" -- '.$string );

        return false;

    }

    # result array
    $array = [];
  
    # split on outer delimiter
    $pairs = explode( $delimiter_and_key, $string );
  
    # loop through each pair
    foreach ( $pairs as $i ) {

        # split into name and value
        list( $key, $value ) = explode( $operator_assign, $i, 2 );

        // what about array values ##
        // example -- sm:medium, lg:large
        if( false !== strpos( $value, $delimiter_key ) ){

            // temp array ##
            $value_array = [];  

            // split value into an array at "," ##
            $value_pairs = explode( $delimiter_and_property, $value );

            // h::log( $value_pairs );

            # loop through each pair
            foreach ( $value_pairs as $v_pair ) {

                // h::log( $v_pair ); // 'sm:medium'

                # split into name and value
                list( $value_key, $value_value ) = explode( $delimiter_key, $v_pair, 2 );

                $value_array[ $value_key ] = $value_value;

            }

            // check if we have an array ##
            if ( is_array( $value_array ) ){

                $value = $value_array;

            }

        }
     
        // $key might be in part__part format, so check ##
        if( false !== strpos( $key, $operator_array ) ){

            // explode, max 2 parts ##
            $md_key = explode( $operator_array, $key, 2 );

            # if name already exists
            if( isset( $array[ $md_key[0] ][ $md_key[1] ] ) ) {

                # stick multiple values into an array
                if( is_array( $array[ $md_key[0] ][ $md_key[1] ] ) ) {
                
                    $array[ $md_key[0] ][ $md_key[1] ][] = $value;
                
                } else {
                
                    $array[ $md_key[0] ][ $md_key[1] ] = array( $array[ $md_key[0] ][ $md_key[1] ], $value );
                
                }

            # otherwise, simply stick it in a scalar
            } else {

                $array[ $md_key[0] ][ $md_key[1] ] = $value;

            }

        } else {

            # if name already exists
            if( isset($array[$key]) ) {

                # stick multiple values into an array
                if( is_array($array[$key]) ) {
                
                    $array[$key][] = $value;
                
                } else {
                
                    $array[$key] = array($array[$key], $value);
                
                }

            # otherwise, simply stick it in a scalar
            } else {

                $array[$key] = $value;

            }
          
        }
    }

    // h::log( $array );
  
    # return result array
    return $array;

  }

我将尝试跳过“双引号”之间的拆分字符串 - 可能通过另一个正则表达式,但也许还有其他潜在的陷阱可能无法使这种方法长期可行 - 任何帮助都接受了!

标签: phparraysregexparsingconfiguration

解决方案


一种解决方案是更改以下内容:

从:

$value_pairs = explode( $delimiter_and_property, $value );

至:

$value_pairs = self::quoted_explode( $value, $delimiter_and_property, '"' );

它调用在另一个 SO 答案上找到的新方法(在评论块中链接):

/**
 * Regex Escape values 
*/
public static function regex_escape( $subject ) {

    return str_replace( array( '\\', '^', '-', ']' ), array( '\\\\', '\\^', '\\-', '\\]' ), $subject );

}

/**
 * Explode string, while respecting delimiters
 * 
 * @link https://stackoverflow.com/questions/3264775/an-explode-function-that-ignores-characters-inside-quotes/13755505#13755505
*/
public static function quoted_explode( $subject, $delimiter = ',', $quotes = '\"' )
{
    $clauses[] = '[^'.self::regex_escape( $delimiter.$quotes ).']';

    foreach( str_split( $quotes) as $quote ) {

        $quote = self::regex_escape( $quote );
        $clauses[] = "[$quote][^$quote]*[$quote]";

    }

    $regex = '(?:'.implode('|', $clauses).')+';
    
    preg_match_all( '/'.str_replace('/', '\\/', $regex).'/', $subject, $matches );

    return $matches[0];

}

推荐阅读