KeyValueStringParser.php 10.5 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
<?php
/**
 * Created by PhpStorm.
 * @author: crose
 * Date: 7/8/15
 * Time: 10:12 AM
 *
 * $Id$
 */

Marc Egger's avatar
Marc Egger committed
11
namespace IMATHUZH\Qfq\Core\Helper;
Carsten  Rose's avatar
Carsten Rose committed
12

13
14
15
16
17
18
19
20
21
22
23
24
25

/**
 * Class KeyValueStringParser
 *
 * KeyValueStringParser is a parser for strings of the form
 *
 *     key1<delimiterA>value1<delimiterB>key2<delimiterA>value2<delimiterB>
 *
 * For instance
 *
 *     id=1,name=doe,firstname=john
 *
 * - Leading and trailing whitespace will be removed from keys and values.
26
 * - If a value is surrounded by quotes (`'`,  `"`), leading and trailing
27
 *   whitespace will be preserved (leading/trailing, quotes will be removed from value).
28
 * - Comments, lines starting with a '#' or ';', will be skipped.
29
30
31
32
33
34
 *
 * @package qfq
 */
class KeyValueStringParser {

    /**
35
     * Builds a string based on kvp array. Concatenate by the given delimiter.
36
     *
Carsten  Rose's avatar
Carsten Rose committed
37
     * @param array $keyValueArray
38
39
     * @param string $keyValueDelimiter
     * @param string $listDelimiter
Carsten  Rose's avatar
Carsten Rose committed
40
     *
41
     * @return string
42
     */
43
44
    public static function unparse(array $keyValueArray, $keyValueDelimiter = PARAM_KEY_VALUE_DELIMITER, $listDelimiter = PARAM_LIST_DELIMITER, $flagEscape = false) {

45
46
47
48
49
        array_walk($keyValueArray, function (&$value) {
            if (!is_string($value) || $value === "" || strlen($value) === 1) {
                return;
            }

50
            if ($value[0] === " " && self::isFirstAndLastCharacterIdentical($value[0])) {
51
52
53
54
55
56
                $value = '"' . $value . '"';
            }
        });

        $newKeyValuePairImploded = array();
        foreach ($keyValueArray as $key => $value) {
57
58
59
60
61
62
63
            if ($flagEscape) {
                $key = str_replace($keyValueDelimiter, '\\' . $keyValueDelimiter, $key);
                $key = str_replace($listDelimiter, '\\' . $listDelimiter, $key);

                $value = str_replace($keyValueDelimiter, '\\' . $keyValueDelimiter, $value);
                $value = str_replace($listDelimiter, '\\' . $listDelimiter, $value);
            }
64
            $newKeyValuePairImploded[] = trim($key) . $keyValueDelimiter . $value;
65
66
        }

67
        return implode($listDelimiter, $newKeyValuePairImploded);
68
69
    }

70
71
    /**
     * @param $string
Carsten  Rose's avatar
Carsten Rose committed
72
     *
73
74
     * @return bool
     */
75
    private static function isFirstAndLastCharacterIdentical($string) {
76
77
78
79
80
81
82
83
        if ($string === "") {
            return false;
        }

        return $string[0] === $string[strlen($string) - 1];
    }

    /**
84
     * Parse key/value pairs string and returns them as an assoc array. Respects escape '\'.
85
     *
Carsten  Rose's avatar
Carsten Rose committed
86
     * Hint $keyValueString: "a:1,b:2,c:,d",  "," (empty key AND empty value)
87
     *
Carsten  Rose's avatar
Carsten Rose committed
88
     * @param string $keyValueString string of key/value pairs. E.g.: 'a=100,b=test'
89
90
     * @param string $keyValueDelimiter
     * @param string $listDelimiter
91
     * @param string $valueMode
Carsten  Rose's avatar
Carsten Rose committed
92
93
94
95
96
     *                               * KVP_VALUE_GIVEN: If only a key is given, the value is ''.  E.G. 'a,b' >> [ 'a'
     *                               => '', 'b' => '' ]
     *                               * KVP_IF_VALUE_EMPTY_COPY_KEY: If only a key is given, the value is the same as
     *                               the key. E.G. 'a,b' >> [ 'a' => 'a', 'b' => 'b' ].
     *
97
     * @return array  associative array indexed by keys
Marc Egger's avatar
Marc Egger committed
98
     * @throws \UserFormException Thrown if there is a value but no key.
99
100
     */

101
    public static function parse($keyValueString, $keyValueDelimiter = ":", $listDelimiter = ",", $valueMode = KVP_VALUE_GIVEN) {
102
103
104
105
        if ($keyValueString === "") {
            return array();
        }

106
107
108
        // Clean any "\r\n" to "\n"
        $keyValueString = str_replace("\r\n", "\n", $keyValueString);

109
110
111
        // Allow {{ }} expressions to span several lines (replace \n inside these expressions)
        $keyValueString = OnString::removeNewlinesInNestedExpression($keyValueString);

112
113
        // Check if there are 'escaped delimiter', If yes, use the more expensive explodeEscape().
        if (strpos($keyValueString, '\\' . $listDelimiter) !== false) {
114
            $keyValuePairs = self::explodeEscape($listDelimiter, $keyValueString);
115
116
117
        } else {
            $keyValuePairs = explode($listDelimiter, $keyValueString);
        }
118
119
120
121

        $returnValue = array();
        foreach ($keyValuePairs as $keyValuePairString) {

122
            if (trim($keyValuePairString) === "") {
123
124
125
                continue;
            }

126
127
128
129
130
131
            // Check if there are 'escaped delimiter', If yes, use the more expensive explodeEscape().
            if (strpos($keyValueString, '\\' . $keyValueDelimiter) !== false) {
                $keyValueArray = self::explodeEscape($keyValueDelimiter, $keyValuePairString, 2);
            } else {
                $keyValueArray = explode($keyValueDelimiter, $keyValuePairString, 2);
            }
132
133
134
135

            $key = trim($keyValueArray[0]);

            // skip comments
136
            if (substr($key, 0, 1) == '#' || substr($key, 0, 1) == ';') {
137
138
139
140
141
                continue;
            }

            if ($key === '') {
                // ":", ":1"
Marc Egger's avatar
Marc Egger committed
142
                throw new \UserFormException(json_encode(
143
                    [ERROR_MESSAGE_TO_USER => "Value has no key: '$keyValuePairString'",
Marc Egger's avatar
Marc Egger committed
144
                        ERROR_MESSAGE_TO_DEVELOPER => "KVP='$keyValueString', keyValueDelimiter='$keyValueDelimiter', listDelimiter='$listDelimiter'"]),
145
                    ERROR_KVP_VALUE_HAS_NO_KEY);
146
147
148
149
            }

            if (count($keyValueArray) === 2) {
                // "a:1", "a:"
150
                $returnValue[$key] = self::quoteUnwrap(trim($keyValueArray[1]));
151
152
            } else {
                // no Value given: "a"
153
                $returnValue[$key] = ($valueMode === KVP_VALUE_GIVEN) ? "" : $key;
154
155
156
157
158
159
            }
        }

        return $returnValue;
    }

160
161
162
163
164
165
    /**
     * Explode string by delimiter and respects escaped delimiter. The result is replaced with escaped delimiter.
     * E.g. 'a,b\,c,d' becomes [ 'a', 'b,c', 'd']
     *
     * @param string $delimiter
     * @param string $data
Carsten  Rose's avatar
Carsten Rose committed
166
     * @param int $max
Carsten  Rose's avatar
Carsten Rose committed
167
     *
168
169
170
171
172
173
174
175
176
177
178
179
     * @return array
     */
    public static function explodeEscape($delimiter, $data, $max = 0) {

        // If the delimiter is a reserved regex char, it has to be escaped.
        $delimiterEscaped = preg_quote($delimiter);

        $items = preg_split('#(?<!\\\)' . $delimiterEscaped . '#', $data);

        // In case there is an upper limit of array elements
        if ($max > 0 && count($items) > $max) {
            $remain = array_slice($items, 0, $max);
180
181
182
183
184
            $remain[$max - 1] .= $delimiter . implode($delimiter, array_slice($items, $max));
            # Take care, that the 'escape' in last element is not cleaned!
            for ($ii = 0; $ii <= $max - 2; $ii++) {
                $remain[$ii] = str_replace('\\' . $delimiter, $delimiter, $remain[$ii]);
            }
185
            $items = $remain;
186
187
188
        } else {
            // Escaped tokens: the escape character needs to be replaced.
            $items = OnArray::arrayValueReplace($items, '\\' . $delimiter, $delimiter);
189
190
        }

191
        return $items;
192
193
    }

194
195
    /**
     * @param $string
Carsten  Rose's avatar
Carsten Rose committed
196
     *
197
198
     * @return string
     */
199
    public static function quoteUnwrap($string) {
200
201
202
203
204
205
        $quotes = ['\'', '"'];

        if ($string === "" || strlen($string) === 1) {
            return $string;
        }

206
        if (in_array($string[0], $quotes) === true && self::isFirstAndLastCharacterIdentical($string)) {
207
208
            return substr($string, 1, strlen($string) - 2);
        }
Carsten  Rose's avatar
Carsten Rose committed
209

210
211
        return $string;
    }
212
213

    /**
Carsten  Rose's avatar
Carsten Rose committed
214
215
     * Works like PHP 'explode()', but respects $delimeter wrapped in ticks (single or double): those are not
     * interpreted as delimiter.
216
217
218
     *
     * E.g.: "a,b,'c,d',e" with delimiter ',' will result in [ 'a', 'b', 'c,d', 'e' ]
     *
219
     * @param     $delimiter
Carsten  Rose's avatar
Carsten Rose committed
220
     * @param     $str
221
     * @param int $limit
Carsten  Rose's avatar
Carsten Rose committed
222
     *
223
     * @return array|bool
Marc Egger's avatar
Marc Egger committed
224
     * @throws \CodeException
225
     */
226
    public static function explodeWrapped($delimiter, $str, $limit = PHP_INT_MAX) {
227

228
        if ($delimiter == '') {
229
230
231
            return false;
        }

232
        if ($limit < 0) {
Marc Egger's avatar
Marc Egger committed
233
            throw new \CodeException("Not Implemented: limit<0", ERROR_NOT_IMPLEMENTED);
234
235
        }

236
        if ($limit == 0) {
237
238
239
240
            $limit = 1;
        }

        $final = array();
241
        $startToken = '';
242
243
244
        $onHold = '';

        $cnt = 0;
245
        $arr = explode($delimiter, $str, PHP_INT_MAX);
246
247
248
        foreach ($arr as $value) {
            $trimmed = trim($value);
            if ($value == '' && $startToken == '') {
249

250
                if ($cnt < $limit) {
251
252
253
254
255
256
                    $final[] = '';
                    $cnt++;
                }
                continue;
            }

257
            if ($startToken == '') {
258
259
260
                switch ($trimmed[0]) {
                    case SINGLE_TICK:
                    case DOUBLE_TICK:
261
                        if ($trimmed[0] == substr($trimmed, -1)) {
262
263
264
265
266
267
268
269
270
                            break; // In case start and end token is in one exploded item
                        }
                        $startToken = $trimmed[0];
                        $onHold = $value;
                        continue 2;
                    default:
                        break;
                }

271
                if ($cnt >= $limit) {
272
                    $final[$cnt - 1] .= $delimiter . $value;
273
274
275
276
277
278
                } else {
                    $final[] = $value;
                    $cnt++;
                }
                continue;
            } else {
279
                $onHold .= $delimiter . $value;
280
281
                $lastChar = substr($trimmed, -1);
                if ($startToken == $lastChar) {
282

283
                    if ($cnt >= $limit) {
284
                        $final[$cnt - 1] .= $delimiter . $onHold;
285
286
287
288
289
290
291
292
293
294
295
296
                    } else {
                        $final[] = $onHold;
                        $cnt++;
                    }
                    $startToken = '';
                    $onHold = '';
                }
            }
        }

        return $final;
    }
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318

    /**
     * Parse kvp string. Key has to be uniq otherwise only the last will be taken. Escaping not supported.
     *
     * p:{{pageAlias:T}}|q:Delete?:yes:no|download:file.pdf
     * @param $str
     * @param string $keyValueDelimiter
     * @param string $listDelimiter
     */
    public static function explodeKvpSimple($str, $keyValueDelimiter = ":", $listDelimiter = ",") {
        $result = array();

        $items = explode($listDelimiter, $str);
        foreach ($items as $item) {
            if ($item == '') {
                continue;
            }
            $arg = explode($keyValueDelimiter, $item, 2);
            $result[$arg[0]] = $arg[1] ?? '';
        }
        return $result;
    }
319
}