ArrayUtil.php
1 year ago
DiscountsUtil.php
2 years ago
FeaturesUtil.php
10 months ago
I18nUtil.php
3 years ago
LoggingUtil.php
1 year ago
NumberUtil.php
10 months ago
OrderUtil.php
8 months ago
PluginUtil.php
7 months ago
RestApiUtil.php
2 years ago
ShippingUtil.php
1 year ago
StringUtil.php
1 year ago
TimeUtil.php
2 years ago
ArrayUtil.php
371 lines
| 1 | <?php |
| 2 | /** |
| 3 | * A class of utilities for dealing with arrays. |
| 4 | */ |
| 5 | |
| 6 | declare( strict_types = 1 ); |
| 7 | namespace Automattic\WooCommerce\Utilities; |
| 8 | |
| 9 | /** |
| 10 | * A class of utilities for dealing with arrays. |
| 11 | */ |
| 12 | class ArrayUtil { |
| 13 | |
| 14 | /** |
| 15 | * Automatic selector type for the 'select' method. |
| 16 | */ |
| 17 | public const SELECT_BY_AUTO = 0; |
| 18 | |
| 19 | /** |
| 20 | * Object method selector type for the 'select' method. |
| 21 | */ |
| 22 | public const SELECT_BY_OBJECT_METHOD = 1; |
| 23 | |
| 24 | /** |
| 25 | * Object property selector type for the 'select' method. |
| 26 | */ |
| 27 | public const SELECT_BY_OBJECT_PROPERTY = 2; |
| 28 | |
| 29 | /** |
| 30 | * Array key selector type for the 'select' method. |
| 31 | */ |
| 32 | public const SELECT_BY_ARRAY_KEY = 3; |
| 33 | |
| 34 | /** |
| 35 | * Get a value from an nested array by specifying the entire key hierarchy with '::' as separator. |
| 36 | * |
| 37 | * E.g. for [ 'foo' => [ 'bar' => [ 'fizz' => 'buzz' ] ] ] the value for key 'foo::bar::fizz' would be 'buzz'. |
| 38 | * |
| 39 | * @param array $items The array to get the value from. |
| 40 | * @param string $key The complete key hierarchy, using '::' as separator. |
| 41 | * @param mixed $default_value The value to return if the key doesn't exist in the array. |
| 42 | * |
| 43 | * @return mixed The retrieved value, or the supplied default value. |
| 44 | * @throws \Exception $array is not an array. |
| 45 | */ |
| 46 | public static function get_nested_value( array $items, string $key, $default_value = null ) { |
| 47 | $key_stack = explode( '::', $key ); |
| 48 | $subkey = array_shift( $key_stack ); |
| 49 | |
| 50 | if ( isset( $items[ $subkey ] ) ) { |
| 51 | $value = $items[ $subkey ]; |
| 52 | |
| 53 | if ( count( $key_stack ) ) { |
| 54 | foreach ( $key_stack as $subkey ) { |
| 55 | if ( is_array( $value ) && isset( $value[ $subkey ] ) ) { |
| 56 | $value = $value[ $subkey ]; |
| 57 | } else { |
| 58 | $value = $default_value; |
| 59 | break; |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | } else { |
| 64 | $value = $default_value; |
| 65 | } |
| 66 | |
| 67 | return $value; |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Checks if a given key exists in an array and its value can be evaluated as 'true'. |
| 72 | * |
| 73 | * @param array $items The array to check. |
| 74 | * @param string $key The key for the value to check. |
| 75 | * @return bool True if the key exists in the array and the value can be evaluated as 'true'. |
| 76 | */ |
| 77 | public static function is_truthy( array $items, string $key ) { |
| 78 | return isset( $items[ $key ] ) && $items[ $key ]; |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * Gets the value for a given key from an array, or a default value if the key doesn't exist in the array. |
| 83 | * |
| 84 | * This is equivalent to "$array[$key] ?? $default" except in one case: |
| 85 | * when they key exists, has a null value, and a non-null default is supplied: |
| 86 | * |
| 87 | * $array = ['key' => null] |
| 88 | * $array['key'] ?? 'default' => 'default' |
| 89 | * ArrayUtil::get_value_or_default($array, 'key', 'default') => null |
| 90 | * |
| 91 | * @param array $items The array to get the value from. |
| 92 | * @param string $key The key to use to retrieve the value. |
| 93 | * @param null $default_value The default value to return if the key doesn't exist in the array. |
| 94 | * @return mixed|null The value for the key, or the default value passed. |
| 95 | */ |
| 96 | public static function get_value_or_default( array $items, string $key, $default_value = null ) { |
| 97 | return array_key_exists( $key, $items ) ? $items[ $key ] : $default_value; |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Converts an array of numbers to a human-readable range, such as "1,2,3,5" to "1-3, 5". It also supports |
| 102 | * floating point numbers, however with some perhaps unexpected / undefined behaviour if used within a range. |
| 103 | * Source: https://stackoverflow.com/a/34254663/4574 |
| 104 | * |
| 105 | * @param array $items An array (in any order, see $sort) of individual numbers. |
| 106 | * @param string $item_separator The string that separates sequential range groups. Defaults to ', '. |
| 107 | * @param string $range_separator The string that separates ranges. Defaults to '-'. A plausible example otherwise would be ' to '. |
| 108 | * @param bool|true $sort Sort the array prior to iterating? You'll likely always want to sort, but if not, you can set this to false. |
| 109 | * |
| 110 | * @return string |
| 111 | */ |
| 112 | public static function to_ranges_string( array $items, string $item_separator = ', ', string $range_separator = '-', bool $sort = true ): string { |
| 113 | if ( $sort ) { |
| 114 | sort( $items ); |
| 115 | } |
| 116 | |
| 117 | $point = null; |
| 118 | $range = false; |
| 119 | $str = ''; |
| 120 | |
| 121 | foreach ( $items as $i ) { |
| 122 | if ( null === $point ) { |
| 123 | $str .= $i; |
| 124 | } elseif ( ( $point + 1 ) === $i ) { |
| 125 | $range = true; |
| 126 | } else { |
| 127 | if ( $range ) { |
| 128 | $str .= $range_separator . $point; |
| 129 | $range = false; |
| 130 | } |
| 131 | $str .= $item_separator . $i; |
| 132 | } |
| 133 | $point = $i; |
| 134 | } |
| 135 | |
| 136 | if ( $range ) { |
| 137 | $str .= $range_separator . $point; |
| 138 | } |
| 139 | |
| 140 | return $str; |
| 141 | } |
| 142 | |
| 143 | /** |
| 144 | * Helper function to generate a callback which can be executed on an array to select a value from each item. |
| 145 | * |
| 146 | * @param string $selector_name Field/property/method name to select. |
| 147 | * @param int $selector_type Selector type. |
| 148 | * |
| 149 | * @return \Closure Callback to select the value. |
| 150 | */ |
| 151 | private static function get_selector_callback( string $selector_name, int $selector_type = self::SELECT_BY_AUTO ): \Closure { |
| 152 | if ( self::SELECT_BY_OBJECT_METHOD === $selector_type ) { |
| 153 | $callback = function ( $item ) use ( $selector_name ) { |
| 154 | return $item->$selector_name(); |
| 155 | }; |
| 156 | } elseif ( self::SELECT_BY_OBJECT_PROPERTY === $selector_type ) { |
| 157 | $callback = function ( $item ) use ( $selector_name ) { |
| 158 | return $item->$selector_name; |
| 159 | }; |
| 160 | } elseif ( self::SELECT_BY_ARRAY_KEY === $selector_type ) { |
| 161 | $callback = function ( $item ) use ( $selector_name ) { |
| 162 | return $item[ $selector_name ]; |
| 163 | }; |
| 164 | } else { |
| 165 | $callback = function ( $item ) use ( $selector_name ) { |
| 166 | if ( is_array( $item ) ) { |
| 167 | return $item[ $selector_name ]; |
| 168 | } elseif ( method_exists( $item, $selector_name ) ) { |
| 169 | return $item->$selector_name(); |
| 170 | } else { |
| 171 | return $item->$selector_name; |
| 172 | } |
| 173 | }; |
| 174 | } |
| 175 | return $callback; |
| 176 | } |
| 177 | |
| 178 | /** |
| 179 | * Select one single value from all the items in an array of either arrays or objects based on a selector. |
| 180 | * For arrays, the selector is a key name; for objects, the selector can be either a method name or a property name. |
| 181 | * |
| 182 | * @param array $items Items to apply the selection to. |
| 183 | * @param string $selector_name Key, method or property name to use as a selector. |
| 184 | * @param int $selector_type Selector type, one of the SELECT_BY_* constants. |
| 185 | * @return array The selected values. |
| 186 | */ |
| 187 | public static function select( array $items, string $selector_name, int $selector_type = self::SELECT_BY_AUTO ): array { |
| 188 | $callback = self::get_selector_callback( $selector_name, $selector_type ); |
| 189 | return array_map( $callback, $items ); |
| 190 | } |
| 191 | |
| 192 | /** |
| 193 | * Returns a new assoc array with format [ $key1 => $item1, $key2 => $item2, ... ] where $key is the value of the selector and items are original items passed. |
| 194 | * |
| 195 | * @param array $items Items to use for conversion. |
| 196 | * @param string $selector_name Key, method or property name to use as a selector. |
| 197 | * @param int $selector_type Selector type, one of the SELECT_BY_* constants. |
| 198 | * |
| 199 | * @return array The converted assoc array. |
| 200 | */ |
| 201 | public static function select_as_assoc( array $items, string $selector_name, int $selector_type = self::SELECT_BY_AUTO ): array { |
| 202 | $selector_callback = self::get_selector_callback( $selector_name, $selector_type ); |
| 203 | $result = array(); |
| 204 | foreach ( $items as $item ) { |
| 205 | $key = $selector_callback( $item ); |
| 206 | self::ensure_key_is_array( $result, $key ); |
| 207 | $result[ $key ][] = $item; |
| 208 | } |
| 209 | return $result; |
| 210 | } |
| 211 | |
| 212 | /** |
| 213 | * Returns whether two assoc array are same. The comparison is done recursively by keys, and the functions returns on first difference found. |
| 214 | * |
| 215 | * @param array $array1 First array to compare. |
| 216 | * @param array $array2 Second array to compare. |
| 217 | * @param bool $strict Whether to use strict comparison. |
| 218 | * |
| 219 | * @return bool Whether the arrays are different. |
| 220 | */ |
| 221 | public static function deep_compare_array_diff( array $array1, array $array2, bool $strict = true ) { |
| 222 | return self::deep_compute_or_compare_array_diff( $array1, $array2, true, $strict ); |
| 223 | } |
| 224 | |
| 225 | /** |
| 226 | * Computes difference between two assoc arrays recursively. Similar to PHP's native assoc_array_diff, but also supports nested arrays. |
| 227 | * |
| 228 | * @param array $array1 First array. |
| 229 | * @param array $array2 Second array. |
| 230 | * @param bool $strict Whether to also match type of values. |
| 231 | * |
| 232 | * @return array The difference between the two arrays. |
| 233 | */ |
| 234 | public static function deep_assoc_array_diff( array $array1, array $array2, bool $strict = true ): array { |
| 235 | return self::deep_compute_or_compare_array_diff( $array1, $array2, false, $strict ); |
| 236 | } |
| 237 | |
| 238 | /** |
| 239 | * Helper method to compare to compute difference between two arrays. Comparison is done recursively. |
| 240 | * |
| 241 | * @param array $array1 First array. |
| 242 | * @param array $array2 Second array. |
| 243 | * @param bool $compare Whether to compare the arrays. If true, then function will return false on first difference, in order to be slightly more efficient. |
| 244 | * @param bool $strict Whether to do string comparison. |
| 245 | * |
| 246 | * @return array|bool The difference between the two arrays, or if array are same, depending upon $compare param. |
| 247 | */ |
| 248 | private static function deep_compute_or_compare_array_diff( array $array1, array $array2, bool $compare, bool $strict = true ) { |
| 249 | $diff = array(); |
| 250 | foreach ( $array1 as $key => $value ) { |
| 251 | if ( is_array( $value ) ) { |
| 252 | if ( ! array_key_exists( $key, $array2 ) || ! is_array( $array2[ $key ] ) ) { |
| 253 | if ( $compare ) { |
| 254 | return true; |
| 255 | } |
| 256 | $diff[ $key ] = $value; |
| 257 | continue; |
| 258 | } |
| 259 | $new_diff = self::deep_assoc_array_diff( $value, $array2[ $key ], $strict ); |
| 260 | if ( ! empty( $new_diff ) ) { |
| 261 | if ( $compare ) { |
| 262 | return true; |
| 263 | } |
| 264 | $diff[ $key ] = $new_diff; |
| 265 | } |
| 266 | } elseif ( $strict ) { |
| 267 | if ( ! array_key_exists( $key, $array2 ) || $value !== $array2[ $key ] ) { |
| 268 | if ( $compare ) { |
| 269 | return true; |
| 270 | } |
| 271 | $diff[ $key ] = $value; |
| 272 | } |
| 273 | // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- Intentional when $strict is false. |
| 274 | } elseif ( ! array_key_exists( $key, $array2 ) || $value != $array2[ $key ] ) { |
| 275 | if ( $compare ) { |
| 276 | return true; |
| 277 | } |
| 278 | $diff[ $key ] = $value; |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | return $compare ? false : $diff; |
| 283 | } |
| 284 | |
| 285 | /** |
| 286 | * Push a value to an array, but only if the value isn't in the array already. |
| 287 | * |
| 288 | * @param array $items The array. |
| 289 | * @param mixed $value The value to maybe push. |
| 290 | * @return bool True if the value has been added to the array, false if the value was already in the array. |
| 291 | */ |
| 292 | public static function push_once( array &$items, $value ): bool { |
| 293 | if ( in_array( $value, $items, true ) ) { |
| 294 | return false; |
| 295 | } |
| 296 | |
| 297 | $items[] = $value; |
| 298 | return true; |
| 299 | } |
| 300 | |
| 301 | /** |
| 302 | * Ensure that an associative array has a given key, and if not, set the key to an empty array. |
| 303 | * |
| 304 | * @param array $items The array to check. |
| 305 | * @param string $key The key to check. |
| 306 | * @param bool $throw_if_existing_is_not_array If true, an exception will be thrown if the key already exists in the array but the value is not an array. |
| 307 | * @return bool True if the key has been added to the array, false if not (the key already existed). |
| 308 | * @throws \Exception The key already exists in the array but the value is not an array. |
| 309 | */ |
| 310 | public static function ensure_key_is_array( array &$items, string $key, bool $throw_if_existing_is_not_array = false ): bool { |
| 311 | if ( ! isset( $items[ $key ] ) ) { |
| 312 | $items[ $key ] = array(); |
| 313 | return true; |
| 314 | } |
| 315 | |
| 316 | if ( $throw_if_existing_is_not_array && ! is_array( $items[ $key ] ) ) { |
| 317 | $type = is_object( $items[ $key ] ) ? get_class( $items[ $key ] ) : gettype( $items[ $key ] ); |
| 318 | // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped |
| 319 | throw new \Exception( "Array key exists but it's not an array, it's a {$type}" ); |
| 320 | } |
| 321 | |
| 322 | return false; |
| 323 | } |
| 324 | |
| 325 | /** |
| 326 | * Given an array of associative arrays, all having a shared key name ("column"), generates a new array in which |
| 327 | * keys are the distinct column values found, and values are arrays with all the matches found |
| 328 | * (or only the last matching array found, if $single_values is true). |
| 329 | * See ArrayUtilTest for examples. |
| 330 | * |
| 331 | * @param array $items The array to process. |
| 332 | * @param string $column The name of the key to group by. |
| 333 | * @param bool $single_values True to only return the last suitable array found for each column value. |
| 334 | * @return array The grouped array. |
| 335 | */ |
| 336 | public static function group_by_column( array $items, string $column, bool $single_values = false ): array { |
| 337 | if ( $single_values ) { |
| 338 | return array_combine( array_column( $items, $column ), array_values( $items ) ); |
| 339 | } |
| 340 | |
| 341 | $distinct_column_values = array_unique( array_column( $items, $column ), SORT_REGULAR ); |
| 342 | $result = array_fill_keys( $distinct_column_values, array() ); |
| 343 | |
| 344 | foreach ( $items as $value ) { |
| 345 | $result[ $value[ $column ] ][] = $value; |
| 346 | } |
| 347 | |
| 348 | return $result; |
| 349 | } |
| 350 | |
| 351 | /** |
| 352 | * Check if all items in an array pass a callback. |
| 353 | * |
| 354 | * @param array $items The array to check. |
| 355 | * @param callable $callback The callback to check each item. |
| 356 | * |
| 357 | * @return bool true if all items pass the callback, false otherwise. |
| 358 | */ |
| 359 | public static function array_all( array $items, callable $callback ): bool { |
| 360 | if ( function_exists( 'array_all' ) ) { |
| 361 | return array_all( $items, $callback ); |
| 362 | } |
| 363 | foreach ( $items as $item ) { |
| 364 | if ( ! $callback( $item ) ) { |
| 365 | return false; |
| 366 | } |
| 367 | } |
| 368 | return true; |
| 369 | } |
| 370 | } |
| 371 |