pictcode / lib / Cake / Utility / Hash.php @ 635eef61
履歴 | 表示 | アノテート | ダウンロード (31.303 KB)
1 | 635eef61 | spyder1211 | <?php
|
---|---|---|---|
2 | /**
|
||
3 | * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
|
||
4 | * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||
5 | *
|
||
6 | * Licensed under The MIT License
|
||
7 | * For full copyright and license information, please see the LICENSE.txt
|
||
8 | * Redistributions of files must retain the above copyright notice.
|
||
9 | *
|
||
10 | * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
|
||
11 | * @link http://cakephp.org CakePHP(tm) Project
|
||
12 | * @package Cake.Utility
|
||
13 | * @since CakePHP(tm) v 2.2.0
|
||
14 | * @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||
15 | */
|
||
16 | |||
17 | App::uses('CakeText', 'Utility'); |
||
18 | |||
19 | /**
|
||
20 | * Library of array functions for manipulating and extracting data
|
||
21 | * from arrays or 'sets' of data.
|
||
22 | *
|
||
23 | * `Hash` provides an improved interface, more consistent and
|
||
24 | * predictable set of features over `Set`. While it lacks the spotty
|
||
25 | * support for pseudo Xpath, its more fully featured dot notation provides
|
||
26 | * similar features in a more consistent implementation.
|
||
27 | *
|
||
28 | * @package Cake.Utility
|
||
29 | */
|
||
30 | class Hash { |
||
31 | |||
32 | /**
|
||
33 | * Get a single value specified by $path out of $data.
|
||
34 | * Does not support the full dot notation feature set,
|
||
35 | * but is faster for simple read operations.
|
||
36 | *
|
||
37 | * @param array $data Array of data to operate on.
|
||
38 | * @param string|array $path The path being searched for. Either a dot
|
||
39 | * separated string, or an array of path segments.
|
||
40 | * @param mixed $default The return value when the path does not exist
|
||
41 | * @throws InvalidArgumentException
|
||
42 | * @return mixed The value fetched from the array, or null.
|
||
43 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::get
|
||
44 | */
|
||
45 | public static function get(array $data, $path, $default = null) { |
||
46 | if (empty($data) || $path === '' || $path === null) { |
||
47 | return $default; |
||
48 | } |
||
49 | if (is_string($path) || is_numeric($path)) { |
||
50 | $parts = explode('.', $path); |
||
51 | } else {
|
||
52 | if (!is_array($path)) { |
||
53 | throw new InvalidArgumentException(__d('cake_dev', |
||
54 | 'Invalid Parameter %s, should be dot separated path or array.',
|
||
55 | $path
|
||
56 | )); |
||
57 | } |
||
58 | $parts = $path; |
||
59 | } |
||
60 | |||
61 | foreach ($parts as $key) { |
||
62 | if (is_array($data) && isset($data[$key])) { |
||
63 | $data =& $data[$key]; |
||
64 | } else {
|
||
65 | return $default; |
||
66 | } |
||
67 | } |
||
68 | |||
69 | return $data; |
||
70 | } |
||
71 | |||
72 | /**
|
||
73 | * Gets the values from an array matching the $path expression.
|
||
74 | * The path expression is a dot separated expression, that can contain a set
|
||
75 | * of patterns and expressions:
|
||
76 | *
|
||
77 | * - `{n}` Matches any numeric key, or integer.
|
||
78 | * - `{s}` Matches any string key.
|
||
79 | * - `{*}` Matches any value.
|
||
80 | * - `Foo` Matches any key with the exact same value.
|
||
81 | *
|
||
82 | * There are a number of attribute operators:
|
||
83 | *
|
||
84 | * - `=`, `!=` Equality.
|
||
85 | * - `>`, `<`, `>=`, `<=` Value comparison.
|
||
86 | * - `=/.../` Regular expression pattern match.
|
||
87 | *
|
||
88 | * Given a set of User array data, from a `$User->find('all')` call:
|
||
89 | *
|
||
90 | * - `1.User.name` Get the name of the user at index 1.
|
||
91 | * - `{n}.User.name` Get the name of every user in the set of users.
|
||
92 | * - `{n}.User[id]` Get the name of every user with an id key.
|
||
93 | * - `{n}.User[id>=2]` Get the name of every user with an id key greater than or equal to 2.
|
||
94 | * - `{n}.User[username=/^paul/]` Get User elements with username matching `^paul`.
|
||
95 | *
|
||
96 | * @param array $data The data to extract from.
|
||
97 | * @param string $path The path to extract.
|
||
98 | * @return array An array of the extracted values. Returns an empty array
|
||
99 | * if there are no matches.
|
||
100 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::extract
|
||
101 | */
|
||
102 | public static function extract(array $data, $path) { |
||
103 | if (empty($path)) { |
||
104 | return $data; |
||
105 | } |
||
106 | |||
107 | // Simple paths.
|
||
108 | if (!preg_match('/[{\[]/', $path)) { |
||
109 | return (array)static::get($data, $path); |
||
110 | } |
||
111 | |||
112 | if (strpos($path, '[') === false) { |
||
113 | $tokens = explode('.', $path); |
||
114 | } else {
|
||
115 | $tokens = CakeText::tokenize($path, '.', '[', ']'); |
||
116 | } |
||
117 | |||
118 | $_key = '__set_item__'; |
||
119 | |||
120 | $context = array($_key => array($data)); |
||
121 | |||
122 | foreach ($tokens as $token) { |
||
123 | $next = array(); |
||
124 | |||
125 | list($token, $conditions) = static::_splitConditions($token); |
||
126 | |||
127 | foreach ($context[$_key] as $item) { |
||
128 | foreach ((array)$item as $k => $v) { |
||
129 | if (static::_matchToken($k, $token)) { |
||
130 | $next[] = $v; |
||
131 | } |
||
132 | } |
||
133 | } |
||
134 | |||
135 | // Filter for attributes.
|
||
136 | if ($conditions) { |
||
137 | $filter = array(); |
||
138 | foreach ($next as $item) { |
||
139 | if (is_array($item) && static::_matches($item, $conditions)) { |
||
140 | $filter[] = $item; |
||
141 | } |
||
142 | } |
||
143 | $next = $filter; |
||
144 | } |
||
145 | $context = array($_key => $next); |
||
146 | |||
147 | } |
||
148 | return $context[$_key]; |
||
149 | } |
||
150 | /**
|
||
151 | * Split token conditions
|
||
152 | *
|
||
153 | * @param string $token the token being splitted.
|
||
154 | * @return array array(token, conditions) with token splitted
|
||
155 | */
|
||
156 | protected static function _splitConditions($token) { |
||
157 | $conditions = false; |
||
158 | $position = strpos($token, '['); |
||
159 | if ($position !== false) { |
||
160 | $conditions = substr($token, $position); |
||
161 | $token = substr($token, 0, $position); |
||
162 | } |
||
163 | |||
164 | return array($token, $conditions); |
||
165 | } |
||
166 | |||
167 | /**
|
||
168 | * Check a key against a token.
|
||
169 | *
|
||
170 | * @param string $key The key in the array being searched.
|
||
171 | * @param string $token The token being matched.
|
||
172 | * @return bool
|
||
173 | */
|
||
174 | protected static function _matchToken($key, $token) { |
||
175 | switch ($token) { |
||
176 | case '{n}': |
||
177 | return is_numeric($key); |
||
178 | case '{s}': |
||
179 | return is_string($key); |
||
180 | case '{*}': |
||
181 | return true; |
||
182 | default:
|
||
183 | return is_numeric($token) ? ($key == $token) : $key === $token; |
||
184 | } |
||
185 | } |
||
186 | |||
187 | /**
|
||
188 | * Checks whether or not $data matches the attribute patterns
|
||
189 | *
|
||
190 | * @param array $data Array of data to match.
|
||
191 | * @param string $selector The patterns to match.
|
||
192 | * @return bool Fitness of expression.
|
||
193 | */
|
||
194 | protected static function _matches(array $data, $selector) { |
||
195 | preg_match_all(
|
||
196 | '/(\[ (?P<attr>[^=><!]+?) (\s* (?P<op>[><!]?[=]|[><]) \s* (?P<val>(?:\/.*?\/ | [^\]]+)) )? \])/x',
|
||
197 | $selector,
|
||
198 | $conditions,
|
||
199 | PREG_SET_ORDER
|
||
200 | ); |
||
201 | |||
202 | foreach ($conditions as $cond) { |
||
203 | $attr = $cond['attr']; |
||
204 | $op = isset($cond['op']) ? $cond['op'] : null; |
||
205 | $val = isset($cond['val']) ? $cond['val'] : null; |
||
206 | |||
207 | // Presence test.
|
||
208 | if (empty($op) && empty($val) && !isset($data[$attr])) { |
||
209 | return false; |
||
210 | } |
||
211 | |||
212 | // Empty attribute = fail.
|
||
213 | if (!(isset($data[$attr]) || array_key_exists($attr, $data))) { |
||
214 | return false; |
||
215 | } |
||
216 | |||
217 | $prop = null; |
||
218 | if (isset($data[$attr])) { |
||
219 | $prop = $data[$attr]; |
||
220 | } |
||
221 | $isBool = is_bool($prop); |
||
222 | if ($isBool && is_numeric($val)) { |
||
223 | $prop = $prop ? '1' : '0'; |
||
224 | } elseif ($isBool) { |
||
225 | $prop = $prop ? 'true' : 'false'; |
||
226 | } |
||
227 | |||
228 | // Pattern matches and other operators.
|
||
229 | if ($op === '=' && $val && $val[0] === '/') { |
||
230 | if (!preg_match($val, $prop)) { |
||
231 | return false; |
||
232 | } |
||
233 | } elseif (($op === '=' && $prop != $val) || |
||
234 | ($op === '!=' && $prop == $val) || |
||
235 | ($op === '>' && $prop <= $val) || |
||
236 | ($op === '<' && $prop >= $val) || |
||
237 | ($op === '>=' && $prop < $val) || |
||
238 | ($op === '<=' && $prop > $val) |
||
239 | ) { |
||
240 | return false; |
||
241 | } |
||
242 | |||
243 | } |
||
244 | return true; |
||
245 | } |
||
246 | |||
247 | /**
|
||
248 | * Insert $values into an array with the given $path. You can use
|
||
249 | * `{n}` and `{s}` elements to insert $data multiple times.
|
||
250 | *
|
||
251 | * @param array $data The data to insert into.
|
||
252 | * @param string $path The path to insert at.
|
||
253 | * @param mixed $values The values to insert.
|
||
254 | * @return array The data with $values inserted.
|
||
255 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::insert
|
||
256 | */
|
||
257 | public static function insert(array $data, $path, $values = null) { |
||
258 | if (strpos($path, '[') === false) { |
||
259 | $tokens = explode('.', $path); |
||
260 | } else {
|
||
261 | $tokens = CakeText::tokenize($path, '.', '[', ']'); |
||
262 | } |
||
263 | |||
264 | if (strpos($path, '{') === false && strpos($path, '[') === false) { |
||
265 | return static::_simpleOp('insert', $data, $tokens, $values); |
||
266 | } |
||
267 | |||
268 | $token = array_shift($tokens); |
||
269 | $nextPath = implode('.', $tokens); |
||
270 | |||
271 | list($token, $conditions) = static::_splitConditions($token); |
||
272 | |||
273 | foreach ($data as $k => $v) { |
||
274 | if (static::_matchToken($k, $token)) { |
||
275 | if ($conditions && static::_matches($v, $conditions)) { |
||
276 | $data[$k] = array_merge($v, $values); |
||
277 | continue;
|
||
278 | } |
||
279 | if (!$conditions) { |
||
280 | $data[$k] = static::insert($v, $nextPath, $values); |
||
281 | } |
||
282 | } |
||
283 | } |
||
284 | return $data; |
||
285 | } |
||
286 | |||
287 | /**
|
||
288 | * Perform a simple insert/remove operation.
|
||
289 | *
|
||
290 | * @param string $op The operation to do.
|
||
291 | * @param array $data The data to operate on.
|
||
292 | * @param array $path The path to work on.
|
||
293 | * @param mixed $values The values to insert when doing inserts.
|
||
294 | * @return array data.
|
||
295 | */
|
||
296 | protected static function _simpleOp($op, $data, $path, $values = null) { |
||
297 | $_list =& $data; |
||
298 | |||
299 | $count = count($path); |
||
300 | $last = $count - 1; |
||
301 | foreach ($path as $i => $key) { |
||
302 | if ((is_numeric($key) && intval($key) > 0 || $key === '0') && strpos($key, '0') !== 0) { |
||
303 | $key = (int)$key; |
||
304 | } |
||
305 | if ($op === 'insert') { |
||
306 | if ($i === $last) { |
||
307 | $_list[$key] = $values; |
||
308 | return $data; |
||
309 | } |
||
310 | if (!isset($_list[$key])) { |
||
311 | $_list[$key] = array(); |
||
312 | } |
||
313 | $_list =& $_list[$key]; |
||
314 | if (!is_array($_list)) { |
||
315 | $_list = array(); |
||
316 | } |
||
317 | } elseif ($op === 'remove') { |
||
318 | if ($i === $last) { |
||
319 | unset($_list[$key]); |
||
320 | return $data; |
||
321 | } |
||
322 | if (!isset($_list[$key])) { |
||
323 | return $data; |
||
324 | } |
||
325 | $_list =& $_list[$key]; |
||
326 | } |
||
327 | } |
||
328 | } |
||
329 | |||
330 | /**
|
||
331 | * Remove data matching $path from the $data array.
|
||
332 | * You can use `{n}` and `{s}` to remove multiple elements
|
||
333 | * from $data.
|
||
334 | *
|
||
335 | * @param array $data The data to operate on
|
||
336 | * @param string $path A path expression to use to remove.
|
||
337 | * @return array The modified array.
|
||
338 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::remove
|
||
339 | */
|
||
340 | public static function remove(array $data, $path) { |
||
341 | if (strpos($path, '[') === false) { |
||
342 | $tokens = explode('.', $path); |
||
343 | } else {
|
||
344 | $tokens = CakeText::tokenize($path, '.', '[', ']'); |
||
345 | } |
||
346 | |||
347 | if (strpos($path, '{') === false && strpos($path, '[') === false) { |
||
348 | return static::_simpleOp('remove', $data, $tokens); |
||
349 | } |
||
350 | |||
351 | $token = array_shift($tokens); |
||
352 | $nextPath = implode('.', $tokens); |
||
353 | |||
354 | list($token, $conditions) = static::_splitConditions($token); |
||
355 | |||
356 | foreach ($data as $k => $v) { |
||
357 | $match = static::_matchToken($k, $token); |
||
358 | if ($match && is_array($v)) { |
||
359 | if ($conditions && static::_matches($v, $conditions)) { |
||
360 | unset($data[$k]); |
||
361 | continue;
|
||
362 | } |
||
363 | $data[$k] = static::remove($v, $nextPath); |
||
364 | if (empty($data[$k])) { |
||
365 | unset($data[$k]); |
||
366 | } |
||
367 | } elseif ($match && empty($nextPath)) { |
||
368 | unset($data[$k]); |
||
369 | } |
||
370 | } |
||
371 | return $data; |
||
372 | } |
||
373 | |||
374 | /**
|
||
375 | * Creates an associative array using `$keyPath` as the path to build its keys, and optionally
|
||
376 | * `$valuePath` as path to get the values. If `$valuePath` is not specified, all values will be initialized
|
||
377 | * to null (useful for Hash::merge). You can optionally group the values by what is obtained when
|
||
378 | * following the path specified in `$groupPath`.
|
||
379 | *
|
||
380 | * @param array $data Array from where to extract keys and values
|
||
381 | * @param string $keyPath A dot-separated string.
|
||
382 | * @param string $valuePath A dot-separated string.
|
||
383 | * @param string $groupPath A dot-separated string.
|
||
384 | * @return array Combined array
|
||
385 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::combine
|
||
386 | * @throws CakeException CakeException When keys and values count is unequal.
|
||
387 | */
|
||
388 | public static function combine(array $data, $keyPath, $valuePath = null, $groupPath = null) { |
||
389 | if (empty($data)) { |
||
390 | return array(); |
||
391 | } |
||
392 | |||
393 | if (is_array($keyPath)) { |
||
394 | $format = array_shift($keyPath); |
||
395 | $keys = static::format($data, $keyPath, $format); |
||
396 | } else {
|
||
397 | $keys = static::extract($data, $keyPath); |
||
398 | } |
||
399 | if (empty($keys)) { |
||
400 | return array(); |
||
401 | } |
||
402 | |||
403 | if (!empty($valuePath) && is_array($valuePath)) { |
||
404 | $format = array_shift($valuePath); |
||
405 | $vals = static::format($data, $valuePath, $format); |
||
406 | } elseif (!empty($valuePath)) { |
||
407 | $vals = static::extract($data, $valuePath); |
||
408 | } |
||
409 | if (empty($vals)) { |
||
410 | $vals = array_fill(0, count($keys), null); |
||
411 | } |
||
412 | |||
413 | if (count($keys) !== count($vals)) { |
||
414 | throw new CakeException(__d( |
||
415 | 'cake_dev',
|
||
416 | 'Hash::combine() needs an equal number of keys + values.'
|
||
417 | )); |
||
418 | } |
||
419 | |||
420 | if ($groupPath !== null) { |
||
421 | $group = static::extract($data, $groupPath); |
||
422 | if (!empty($group)) { |
||
423 | $c = count($keys); |
||
424 | for ($i = 0; $i < $c; $i++) { |
||
425 | if (!isset($group[$i])) { |
||
426 | $group[$i] = 0; |
||
427 | } |
||
428 | if (!isset($out[$group[$i]])) { |
||
429 | $out[$group[$i]] = array(); |
||
430 | } |
||
431 | $out[$group[$i]][$keys[$i]] = $vals[$i]; |
||
432 | } |
||
433 | return $out; |
||
434 | } |
||
435 | } |
||
436 | if (empty($vals)) { |
||
437 | return array(); |
||
438 | } |
||
439 | return array_combine($keys, $vals); |
||
440 | } |
||
441 | |||
442 | /**
|
||
443 | * Returns a formatted series of values extracted from `$data`, using
|
||
444 | * `$format` as the format and `$paths` as the values to extract.
|
||
445 | *
|
||
446 | * Usage:
|
||
447 | *
|
||
448 | * ```
|
||
449 | * $result = Hash::format($users, array('{n}.User.id', '{n}.User.name'), '%s : %s');
|
||
450 | * ```
|
||
451 | *
|
||
452 | * The `$format` string can use any format options that `vsprintf()` and `sprintf()` do.
|
||
453 | *
|
||
454 | * @param array $data Source array from which to extract the data
|
||
455 | * @param string $paths An array containing one or more Hash::extract()-style key paths
|
||
456 | * @param string $format Format string into which values will be inserted, see sprintf()
|
||
457 | * @return array An array of strings extracted from `$path` and formatted with `$format`
|
||
458 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::format
|
||
459 | * @see sprintf()
|
||
460 | * @see Hash::extract()
|
||
461 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::format
|
||
462 | */
|
||
463 | public static function format(array $data, array $paths, $format) { |
||
464 | $extracted = array(); |
||
465 | $count = count($paths); |
||
466 | |||
467 | if (!$count) { |
||
468 | return null; |
||
469 | } |
||
470 | |||
471 | for ($i = 0; $i < $count; $i++) { |
||
472 | $extracted[] = static::extract($data, $paths[$i]); |
||
473 | } |
||
474 | $out = array(); |
||
475 | $data = $extracted; |
||
476 | $count = count($data[0]); |
||
477 | |||
478 | $countTwo = count($data); |
||
479 | for ($j = 0; $j < $count; $j++) { |
||
480 | $args = array(); |
||
481 | for ($i = 0; $i < $countTwo; $i++) { |
||
482 | if (array_key_exists($j, $data[$i])) { |
||
483 | $args[] = $data[$i][$j]; |
||
484 | } |
||
485 | } |
||
486 | $out[] = vsprintf($format, $args); |
||
487 | } |
||
488 | return $out; |
||
489 | } |
||
490 | |||
491 | /**
|
||
492 | * Determines if one array contains the exact keys and values of another.
|
||
493 | *
|
||
494 | * @param array $data The data to search through.
|
||
495 | * @param array $needle The values to file in $data
|
||
496 | * @return bool true if $data contains $needle, false otherwise
|
||
497 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::contains
|
||
498 | */
|
||
499 | public static function contains(array $data, array $needle) { |
||
500 | if (empty($data) || empty($needle)) { |
||
501 | return false; |
||
502 | } |
||
503 | $stack = array(); |
||
504 | |||
505 | while (!empty($needle)) { |
||
506 | $key = key($needle); |
||
507 | $val = $needle[$key]; |
||
508 | unset($needle[$key]); |
||
509 | |||
510 | if (array_key_exists($key, $data) && is_array($val)) { |
||
511 | $next = $data[$key]; |
||
512 | unset($data[$key]); |
||
513 | |||
514 | if (!empty($val)) { |
||
515 | $stack[] = array($val, $next); |
||
516 | } |
||
517 | } elseif (!array_key_exists($key, $data) || $data[$key] != $val) { |
||
518 | return false; |
||
519 | } |
||
520 | |||
521 | if (empty($needle) && !empty($stack)) { |
||
522 | list($needle, $data) = array_pop($stack); |
||
523 | } |
||
524 | } |
||
525 | return true; |
||
526 | } |
||
527 | |||
528 | /**
|
||
529 | * Test whether or not a given path exists in $data.
|
||
530 | * This method uses the same path syntax as Hash::extract()
|
||
531 | *
|
||
532 | * Checking for paths that could target more than one element will
|
||
533 | * make sure that at least one matching element exists.
|
||
534 | *
|
||
535 | * @param array $data The data to check.
|
||
536 | * @param string $path The path to check for.
|
||
537 | * @return bool Existence of path.
|
||
538 | * @see Hash::extract()
|
||
539 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::check
|
||
540 | */
|
||
541 | public static function check(array $data, $path) { |
||
542 | $results = static::extract($data, $path); |
||
543 | if (!is_array($results)) { |
||
544 | return false; |
||
545 | } |
||
546 | return count($results) > 0; |
||
547 | } |
||
548 | |||
549 | /**
|
||
550 | * Recursively filters a data set.
|
||
551 | *
|
||
552 | * @param array $data Either an array to filter, or value when in callback
|
||
553 | * @param callable $callback A function to filter the data with. Defaults to
|
||
554 | * `static::_filter()` Which strips out all non-zero empty values.
|
||
555 | * @return array Filtered array
|
||
556 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::filter
|
||
557 | */
|
||
558 | public static function filter(array $data, $callback = array('self', '_filter')) { |
||
559 | foreach ($data as $k => $v) { |
||
560 | if (is_array($v)) { |
||
561 | $data[$k] = static::filter($v, $callback); |
||
562 | } |
||
563 | } |
||
564 | return array_filter($data, $callback); |
||
565 | } |
||
566 | |||
567 | /**
|
||
568 | * Callback function for filtering.
|
||
569 | *
|
||
570 | * @param array $var Array to filter.
|
||
571 | * @return bool
|
||
572 | */
|
||
573 | protected static function _filter($var) { |
||
574 | if ($var === 0 || $var === '0' || !empty($var)) { |
||
575 | return true; |
||
576 | } |
||
577 | return false; |
||
578 | } |
||
579 | |||
580 | /**
|
||
581 | * Collapses a multi-dimensional array into a single dimension, using a delimited array path for
|
||
582 | * each array element's key, i.e. array(array('Foo' => array('Bar' => 'Far'))) becomes
|
||
583 | * array('0.Foo.Bar' => 'Far').)
|
||
584 | *
|
||
585 | * @param array $data Array to flatten
|
||
586 | * @param string $separator String used to separate array key elements in a path, defaults to '.'
|
||
587 | * @return array
|
||
588 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::flatten
|
||
589 | */
|
||
590 | public static function flatten(array $data, $separator = '.') { |
||
591 | $result = array(); |
||
592 | $stack = array(); |
||
593 | $path = null; |
||
594 | |||
595 | reset($data); |
||
596 | while (!empty($data)) { |
||
597 | $key = key($data); |
||
598 | $element = $data[$key]; |
||
599 | unset($data[$key]); |
||
600 | |||
601 | if (is_array($element) && !empty($element)) { |
||
602 | if (!empty($data)) { |
||
603 | $stack[] = array($data, $path); |
||
604 | } |
||
605 | $data = $element; |
||
606 | reset($data); |
||
607 | $path .= $key . $separator; |
||
608 | } else {
|
||
609 | $result[$path . $key] = $element; |
||
610 | } |
||
611 | |||
612 | if (empty($data) && !empty($stack)) { |
||
613 | list($data, $path) = array_pop($stack); |
||
614 | reset($data); |
||
615 | } |
||
616 | } |
||
617 | return $result; |
||
618 | } |
||
619 | |||
620 | /**
|
||
621 | * Expands a flat array to a nested array.
|
||
622 | *
|
||
623 | * For example, unflattens an array that was collapsed with `Hash::flatten()`
|
||
624 | * into a multi-dimensional array. So, `array('0.Foo.Bar' => 'Far')` becomes
|
||
625 | * `array(array('Foo' => array('Bar' => 'Far')))`.
|
||
626 | *
|
||
627 | * @param array $data Flattened array
|
||
628 | * @param string $separator The delimiter used
|
||
629 | * @return array
|
||
630 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::expand
|
||
631 | */
|
||
632 | public static function expand($data, $separator = '.') { |
||
633 | $result = array(); |
||
634 | |||
635 | $stack = array(); |
||
636 | |||
637 | foreach ($data as $flat => $value) { |
||
638 | $keys = explode($separator, $flat); |
||
639 | $keys = array_reverse($keys); |
||
640 | $child = array( |
||
641 | $keys[0] => $value |
||
642 | ); |
||
643 | array_shift($keys); |
||
644 | foreach ($keys as $k) { |
||
645 | $child = array( |
||
646 | $k => $child |
||
647 | ); |
||
648 | } |
||
649 | |||
650 | $stack[] = array($child, &$result); |
||
651 | |||
652 | while (!empty($stack)) { |
||
653 | foreach ($stack as $curKey => &$curMerge) { |
||
654 | foreach ($curMerge[0] as $key => &$val) { |
||
655 | if (!empty($curMerge[1][$key]) && (array)$curMerge[1][$key] === $curMerge[1][$key] && (array)$val === $val) { |
||
656 | $stack[] = array(&$val, &$curMerge[1][$key]); |
||
657 | } elseif ((int)$key === $key && isset($curMerge[1][$key])) { |
||
658 | $curMerge[1][] = $val; |
||
659 | } else {
|
||
660 | $curMerge[1][$key] = $val; |
||
661 | } |
||
662 | } |
||
663 | unset($stack[$curKey]); |
||
664 | } |
||
665 | unset($curMerge); |
||
666 | } |
||
667 | } |
||
668 | return $result; |
||
669 | } |
||
670 | |||
671 | /**
|
||
672 | * This function can be thought of as a hybrid between PHP's `array_merge` and `array_merge_recursive`.
|
||
673 | *
|
||
674 | * The difference between this method and the built-in ones, is that if an array key contains another array, then
|
||
675 | * Hash::merge() will behave in a recursive fashion (unlike `array_merge`). But it will not act recursively for
|
||
676 | * keys that contain scalar values (unlike `array_merge_recursive`).
|
||
677 | *
|
||
678 | * Note: This function will work with an unlimited amount of arguments and typecasts non-array parameters into arrays.
|
||
679 | *
|
||
680 | * @param array $data Array to be merged
|
||
681 | * @param mixed $merge Array to merge with. The argument and all trailing arguments will be array cast when merged
|
||
682 | * @return array Merged array
|
||
683 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::merge
|
||
684 | */
|
||
685 | public static function merge(array $data, $merge) { |
||
686 | $args = array_slice(func_get_args(), 1); |
||
687 | $return = $data; |
||
688 | |||
689 | foreach ($args as &$curArg) { |
||
690 | $stack[] = array((array)$curArg, &$return); |
||
691 | } |
||
692 | unset($curArg); |
||
693 | |||
694 | while (!empty($stack)) { |
||
695 | foreach ($stack as $curKey => &$curMerge) { |
||
696 | foreach ($curMerge[0] as $key => &$val) { |
||
697 | if (!empty($curMerge[1][$key]) && (array)$curMerge[1][$key] === $curMerge[1][$key] && (array)$val === $val) { |
||
698 | $stack[] = array(&$val, &$curMerge[1][$key]); |
||
699 | } elseif ((int)$key === $key && isset($curMerge[1][$key])) { |
||
700 | $curMerge[1][] = $val; |
||
701 | } else {
|
||
702 | $curMerge[1][$key] = $val; |
||
703 | } |
||
704 | } |
||
705 | unset($stack[$curKey]); |
||
706 | } |
||
707 | unset($curMerge); |
||
708 | } |
||
709 | return $return; |
||
710 | } |
||
711 | |||
712 | /**
|
||
713 | * Checks to see if all the values in the array are numeric
|
||
714 | *
|
||
715 | * @param array $data The array to check.
|
||
716 | * @return bool true if values are numeric, false otherwise
|
||
717 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::numeric
|
||
718 | */
|
||
719 | public static function numeric(array $data) { |
||
720 | if (empty($data)) { |
||
721 | return false; |
||
722 | } |
||
723 | return $data === array_filter($data, 'is_numeric'); |
||
724 | } |
||
725 | |||
726 | /**
|
||
727 | * Counts the dimensions of an array.
|
||
728 | * Only considers the dimension of the first element in the array.
|
||
729 | *
|
||
730 | * If you have an un-even or heterogenous array, consider using Hash::maxDimensions()
|
||
731 | * to get the dimensions of the array.
|
||
732 | *
|
||
733 | * @param array $data Array to count dimensions on
|
||
734 | * @return int The number of dimensions in $data
|
||
735 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::dimensions
|
||
736 | */
|
||
737 | public static function dimensions(array $data) { |
||
738 | if (empty($data)) { |
||
739 | return 0; |
||
740 | } |
||
741 | reset($data); |
||
742 | $depth = 1; |
||
743 | while ($elem = array_shift($data)) { |
||
744 | if (is_array($elem)) { |
||
745 | $depth += 1; |
||
746 | $data =& $elem; |
||
747 | } else {
|
||
748 | break;
|
||
749 | } |
||
750 | } |
||
751 | return $depth; |
||
752 | } |
||
753 | |||
754 | /**
|
||
755 | * Counts the dimensions of *all* array elements. Useful for finding the maximum
|
||
756 | * number of dimensions in a mixed array.
|
||
757 | *
|
||
758 | * @param array $data Array to count dimensions on
|
||
759 | * @return int The maximum number of dimensions in $data
|
||
760 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::maxDimensions
|
||
761 | */
|
||
762 | public static function maxDimensions($data) { |
||
763 | $depth = array(); |
||
764 | if (is_array($data) && reset($data) !== false) { |
||
765 | foreach ($data as $value) { |
||
766 | $depth[] = static::maxDimensions($value) + 1; |
||
767 | } |
||
768 | } |
||
769 | return empty($depth) ? 0 : max($depth); |
||
770 | } |
||
771 | |||
772 | /**
|
||
773 | * Map a callback across all elements in a set.
|
||
774 | * Can be provided a path to only modify slices of the set.
|
||
775 | *
|
||
776 | * @param array $data The data to map over, and extract data out of.
|
||
777 | * @param string $path The path to extract for mapping over.
|
||
778 | * @param callable $function The function to call on each extracted value.
|
||
779 | * @return array An array of the modified values.
|
||
780 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::map
|
||
781 | */
|
||
782 | public static function map(array $data, $path, $function) { |
||
783 | $values = (array)static::extract($data, $path); |
||
784 | return array_map($function, $values); |
||
785 | } |
||
786 | |||
787 | /**
|
||
788 | * Reduce a set of extracted values using `$function`.
|
||
789 | *
|
||
790 | * @param array $data The data to reduce.
|
||
791 | * @param string $path The path to extract from $data.
|
||
792 | * @param callable $function The function to call on each extracted value.
|
||
793 | * @return mixed The reduced value.
|
||
794 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::reduce
|
||
795 | */
|
||
796 | public static function reduce(array $data, $path, $function) { |
||
797 | $values = (array)static::extract($data, $path); |
||
798 | return array_reduce($values, $function); |
||
799 | } |
||
800 | |||
801 | /**
|
||
802 | * Apply a callback to a set of extracted values using `$function`.
|
||
803 | * The function will get the extracted values as the first argument.
|
||
804 | *
|
||
805 | * ### Example
|
||
806 | *
|
||
807 | * You can easily count the results of an extract using apply().
|
||
808 | * For example to count the comments on an Article:
|
||
809 | *
|
||
810 | * `$count = Hash::apply($data, 'Article.Comment.{n}', 'count');`
|
||
811 | *
|
||
812 | * You could also use a function like `array_sum` to sum the results.
|
||
813 | *
|
||
814 | * `$total = Hash::apply($data, '{n}.Item.price', 'array_sum');`
|
||
815 | *
|
||
816 | * @param array $data The data to reduce.
|
||
817 | * @param string $path The path to extract from $data.
|
||
818 | * @param callable $function The function to call on each extracted value.
|
||
819 | * @return mixed The results of the applied method.
|
||
820 | */
|
||
821 | public static function apply(array $data, $path, $function) { |
||
822 | $values = (array)static::extract($data, $path); |
||
823 | return call_user_func($function, $values); |
||
824 | } |
||
825 | |||
826 | /**
|
||
827 | * Sorts an array by any value, determined by a Set-compatible path
|
||
828 | *
|
||
829 | * ### Sort directions
|
||
830 | *
|
||
831 | * - `asc` Sort ascending.
|
||
832 | * - `desc` Sort descending.
|
||
833 | *
|
||
834 | * ## Sort types
|
||
835 | *
|
||
836 | * - `regular` For regular sorting (don't change types)
|
||
837 | * - `numeric` Compare values numerically
|
||
838 | * - `string` Compare values as strings
|
||
839 | * - `natural` Compare items as strings using "natural ordering" in a human friendly way.
|
||
840 | * Will sort foo10 below foo2 as an example. Requires PHP 5.4 or greater or it will fallback to 'regular'
|
||
841 | *
|
||
842 | * @param array $data An array of data to sort
|
||
843 | * @param string $path A Set-compatible path to the array value
|
||
844 | * @param string $dir See directions above. Defaults to 'asc'.
|
||
845 | * @param string $type See direction types above. Defaults to 'regular'.
|
||
846 | * @return array Sorted array of data
|
||
847 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::sort
|
||
848 | */
|
||
849 | public static function sort(array $data, $path, $dir = 'asc', $type = 'regular') { |
||
850 | if (empty($data)) { |
||
851 | return array(); |
||
852 | } |
||
853 | $originalKeys = array_keys($data); |
||
854 | $numeric = is_numeric(implode('', $originalKeys)); |
||
855 | if ($numeric) { |
||
856 | $data = array_values($data); |
||
857 | } |
||
858 | $sortValues = static::extract($data, $path); |
||
859 | $sortCount = count($sortValues); |
||
860 | $dataCount = count($data); |
||
861 | |||
862 | // Make sortValues match the data length, as some keys could be missing
|
||
863 | // the sorted value path.
|
||
864 | if ($sortCount < $dataCount) { |
||
865 | $sortValues = array_pad($sortValues, $dataCount, null); |
||
866 | } |
||
867 | $result = static::_squash($sortValues); |
||
868 | $keys = static::extract($result, '{n}.id'); |
||
869 | $values = static::extract($result, '{n}.value'); |
||
870 | |||
871 | $dir = strtolower($dir); |
||
872 | $type = strtolower($type); |
||
873 | if ($type === 'natural' && version_compare(PHP_VERSION, '5.4.0', '<')) { |
||
874 | $type = 'regular'; |
||
875 | } |
||
876 | if ($dir === 'asc') { |
||
877 | $dir = SORT_ASC; |
||
878 | } else {
|
||
879 | $dir = SORT_DESC; |
||
880 | } |
||
881 | if ($type === 'numeric') { |
||
882 | $type = SORT_NUMERIC; |
||
883 | } elseif ($type === 'string') { |
||
884 | $type = SORT_STRING; |
||
885 | } elseif ($type === 'natural') { |
||
886 | $type = SORT_NATURAL; |
||
887 | } else {
|
||
888 | $type = SORT_REGULAR; |
||
889 | } |
||
890 | array_multisort($values, $dir, $type, $keys, $dir, $type); |
||
891 | $sorted = array(); |
||
892 | $keys = array_unique($keys); |
||
893 | |||
894 | foreach ($keys as $k) { |
||
895 | if ($numeric) { |
||
896 | $sorted[] = $data[$k]; |
||
897 | continue;
|
||
898 | } |
||
899 | if (isset($originalKeys[$k])) { |
||
900 | $sorted[$originalKeys[$k]] = $data[$originalKeys[$k]]; |
||
901 | } else {
|
||
902 | $sorted[$k] = $data[$k]; |
||
903 | } |
||
904 | } |
||
905 | return $sorted; |
||
906 | } |
||
907 | |||
908 | /**
|
||
909 | * Helper method for sort()
|
||
910 | * Squashes an array to a single hash so it can be sorted.
|
||
911 | *
|
||
912 | * @param array $data The data to squash.
|
||
913 | * @param string $key The key for the data.
|
||
914 | * @return array
|
||
915 | */
|
||
916 | protected static function _squash($data, $key = null) { |
||
917 | $stack = array(); |
||
918 | foreach ($data as $k => $r) { |
||
919 | $id = $k; |
||
920 | if ($key !== null) { |
||
921 | $id = $key; |
||
922 | } |
||
923 | if (is_array($r) && !empty($r)) { |
||
924 | $stack = array_merge($stack, static::_squash($r, $id)); |
||
925 | } else {
|
||
926 | $stack[] = array('id' => $id, 'value' => $r); |
||
927 | } |
||
928 | } |
||
929 | return $stack; |
||
930 | } |
||
931 | |||
932 | /**
|
||
933 | * Computes the difference between two complex arrays.
|
||
934 | * This method differs from the built-in array_diff() in that it will preserve keys
|
||
935 | * and work on multi-dimensional arrays.
|
||
936 | *
|
||
937 | * @param array $data First value
|
||
938 | * @param array $compare Second value
|
||
939 | * @return array Returns the key => value pairs that are not common in $data and $compare
|
||
940 | * The expression for this function is ($data - $compare) + ($compare - ($data - $compare))
|
||
941 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::diff
|
||
942 | */
|
||
943 | public static function diff(array $data, $compare) { |
||
944 | if (empty($data)) { |
||
945 | return (array)$compare; |
||
946 | } |
||
947 | if (empty($compare)) { |
||
948 | return (array)$data; |
||
949 | } |
||
950 | $intersection = array_intersect_key($data, $compare); |
||
951 | while (($key = key($intersection)) !== null) { |
||
952 | if ($data[$key] == $compare[$key]) { |
||
953 | unset($data[$key]); |
||
954 | unset($compare[$key]); |
||
955 | } |
||
956 | next($intersection); |
||
957 | } |
||
958 | return $data + $compare; |
||
959 | } |
||
960 | |||
961 | /**
|
||
962 | * Merges the difference between $data and $compare onto $data.
|
||
963 | *
|
||
964 | * @param array $data The data to append onto.
|
||
965 | * @param array $compare The data to compare and append onto.
|
||
966 | * @return array The merged array.
|
||
967 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::mergeDiff
|
||
968 | */
|
||
969 | public static function mergeDiff(array $data, $compare) { |
||
970 | if (empty($data) && !empty($compare)) { |
||
971 | return $compare; |
||
972 | } |
||
973 | if (empty($compare)) { |
||
974 | return $data; |
||
975 | } |
||
976 | foreach ($compare as $key => $value) { |
||
977 | if (!array_key_exists($key, $data)) { |
||
978 | $data[$key] = $value; |
||
979 | } elseif (is_array($value)) { |
||
980 | $data[$key] = static::mergeDiff($data[$key], $compare[$key]); |
||
981 | } |
||
982 | } |
||
983 | return $data; |
||
984 | } |
||
985 | |||
986 | /**
|
||
987 | * Normalizes an array, and converts it to a standard format.
|
||
988 | *
|
||
989 | * @param array $data List to normalize
|
||
990 | * @param bool $assoc If true, $data will be converted to an associative array.
|
||
991 | * @return array
|
||
992 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::normalize
|
||
993 | */
|
||
994 | public static function normalize(array $data, $assoc = true) { |
||
995 | $keys = array_keys($data); |
||
996 | $count = count($keys); |
||
997 | $numeric = true; |
||
998 | |||
999 | if (!$assoc) { |
||
1000 | for ($i = 0; $i < $count; $i++) { |
||
1001 | if (!is_int($keys[$i])) { |
||
1002 | $numeric = false; |
||
1003 | break;
|
||
1004 | } |
||
1005 | } |
||
1006 | } |
||
1007 | if (!$numeric || $assoc) { |
||
1008 | $newList = array(); |
||
1009 | for ($i = 0; $i < $count; $i++) { |
||
1010 | if (is_int($keys[$i])) { |
||
1011 | $newList[$data[$keys[$i]]] = null; |
||
1012 | } else {
|
||
1013 | $newList[$keys[$i]] = $data[$keys[$i]]; |
||
1014 | } |
||
1015 | } |
||
1016 | $data = $newList; |
||
1017 | } |
||
1018 | return $data; |
||
1019 | } |
||
1020 | |||
1021 | /**
|
||
1022 | * Takes in a flat array and returns a nested array
|
||
1023 | *
|
||
1024 | * ### Options:
|
||
1025 | *
|
||
1026 | * - `children` The key name to use in the resultset for children.
|
||
1027 | * - `idPath` The path to a key that identifies each entry. Should be
|
||
1028 | * compatible with Hash::extract(). Defaults to `{n}.$alias.id`
|
||
1029 | * - `parentPath` The path to a key that identifies the parent of each entry.
|
||
1030 | * Should be compatible with Hash::extract(). Defaults to `{n}.$alias.parent_id`
|
||
1031 | * - `root` The id of the desired top-most result.
|
||
1032 | *
|
||
1033 | * @param array $data The data to nest.
|
||
1034 | * @param array $options Options are:
|
||
1035 | * @return array of results, nested
|
||
1036 | * @see Hash::extract()
|
||
1037 | * @throws InvalidArgumentException When providing invalid data.
|
||
1038 | * @link http://book.cakephp.org/2.0/en/core-utility-libraries/hash.html#Hash::nest
|
||
1039 | */
|
||
1040 | public static function nest(array $data, $options = array()) { |
||
1041 | if (!$data) { |
||
1042 | return $data; |
||
1043 | } |
||
1044 | |||
1045 | $alias = key(current($data)); |
||
1046 | $options += array( |
||
1047 | 'idPath' => "{n}.$alias.id", |
||
1048 | 'parentPath' => "{n}.$alias.parent_id", |
||
1049 | 'children' => 'children', |
||
1050 | 'root' => null |
||
1051 | ); |
||
1052 | |||
1053 | $return = $idMap = array(); |
||
1054 | $ids = static::extract($data, $options['idPath']); |
||
1055 | |||
1056 | $idKeys = explode('.', $options['idPath']); |
||
1057 | array_shift($idKeys); |
||
1058 | |||
1059 | $parentKeys = explode('.', $options['parentPath']); |
||
1060 | array_shift($parentKeys); |
||
1061 | |||
1062 | foreach ($data as $result) { |
||
1063 | $result[$options['children']] = array(); |
||
1064 | |||
1065 | $id = static::get($result, $idKeys); |
||
1066 | $parentId = static::get($result, $parentKeys); |
||
1067 | |||
1068 | if (isset($idMap[$id][$options['children']])) { |
||
1069 | $idMap[$id] = array_merge($result, (array)$idMap[$id]); |
||
1070 | } else {
|
||
1071 | $idMap[$id] = array_merge($result, array($options['children'] => array())); |
||
1072 | } |
||
1073 | if (!$parentId || !in_array($parentId, $ids)) { |
||
1074 | $return[] =& $idMap[$id]; |
||
1075 | } else {
|
||
1076 | $idMap[$parentId][$options['children']][] =& $idMap[$id]; |
||
1077 | } |
||
1078 | } |
||
1079 | |||
1080 | if (!$return) { |
||
1081 | throw new InvalidArgumentException(__d('cake_dev', |
||
1082 | 'Invalid data array to nest.'
|
||
1083 | )); |
||
1084 | } |
||
1085 | |||
1086 | if ($options['root']) { |
||
1087 | $root = $options['root']; |
||
1088 | } else {
|
||
1089 | $root = static::get($return[0], $parentKeys); |
||
1090 | } |
||
1091 | |||
1092 | foreach ($return as $i => $result) { |
||
1093 | $id = static::get($result, $idKeys); |
||
1094 | $parentId = static::get($result, $parentKeys); |
||
1095 | if ($id !== $root && $parentId != $root) { |
||
1096 | unset($return[$i]); |
||
1097 | } |
||
1098 | } |
||
1099 | return array_values($return); |
||
1100 | } |
||
1101 | |||
1102 | } |