| 1 | <?php |
|---|
| 2 | /** |
|---|
| 3 | * WordPress List utility class |
|---|
| 4 | * |
|---|
| 5 | * @package WordPress |
|---|
| 6 | * @since 4.7.0 |
|---|
| 7 | */ |
|---|
| 8 | |
|---|
| 9 | /** |
|---|
| 10 | * List utility. |
|---|
| 11 | * |
|---|
| 12 | * Utility class to handle operations on an array of objects or arrays. |
|---|
| 13 | * |
|---|
| 14 | * @since 4.7.0 |
|---|
| 15 | */ |
|---|
| 16 | #[AllowDynamicProperties] |
|---|
| 17 | class WP_List_Util { |
|---|
| 18 | /** |
|---|
| 19 | * The input array. |
|---|
| 20 | * |
|---|
| 21 | * @since 4.7.0 |
|---|
| 22 | * @var array |
|---|
| 23 | */ |
|---|
| 24 | private $input = array(); |
|---|
| 25 | |
|---|
| 26 | /** |
|---|
| 27 | * The output array. |
|---|
| 28 | * |
|---|
| 29 | * @since 4.7.0 |
|---|
| 30 | * @var array |
|---|
| 31 | */ |
|---|
| 32 | private $output = array(); |
|---|
| 33 | |
|---|
| 34 | /** |
|---|
| 35 | * Temporary arguments for sorting. |
|---|
| 36 | * |
|---|
| 37 | * @since 4.7.0 |
|---|
| 38 | * @var string[] |
|---|
| 39 | */ |
|---|
| 40 | private $orderby = array(); |
|---|
| 41 | |
|---|
| 42 | /** |
|---|
| 43 | * Constructor. |
|---|
| 44 | * |
|---|
| 45 | * Sets the input array. |
|---|
| 46 | * |
|---|
| 47 | * @since 4.7.0 |
|---|
| 48 | * |
|---|
| 49 | * @param array $input Array to perform operations on. |
|---|
| 50 | */ |
|---|
| 51 | public function __construct( $input ) { |
|---|
| 52 | $this->output = $input; |
|---|
| 53 | $this->input = $input; |
|---|
| 54 | } |
|---|
| 55 | |
|---|
| 56 | /** |
|---|
| 57 | * Returns the original input array. |
|---|
| 58 | * |
|---|
| 59 | * @since 4.7.0 |
|---|
| 60 | * |
|---|
| 61 | * @return array The input array. |
|---|
| 62 | */ |
|---|
| 63 | public function get_input() { |
|---|
| 64 | return $this->input; |
|---|
| 65 | } |
|---|
| 66 | |
|---|
| 67 | /** |
|---|
| 68 | * Returns the output array. |
|---|
| 69 | * |
|---|
| 70 | * @since 4.7.0 |
|---|
| 71 | * |
|---|
| 72 | * @return array The output array. |
|---|
| 73 | */ |
|---|
| 74 | public function get_output() { |
|---|
| 75 | return $this->output; |
|---|
| 76 | } |
|---|
| 77 | |
|---|
| 78 | /** |
|---|
| 79 | * Filters the list, based on a set of key => value arguments. |
|---|
| 80 | * |
|---|
| 81 | * Retrieves the objects from the list that match the given arguments. |
|---|
| 82 | * Key represents property name, and value represents property value. |
|---|
| 83 | * |
|---|
| 84 | * If an object has more properties than those specified in arguments, |
|---|
| 85 | * that will not disqualify it. When using the 'AND' operator, |
|---|
| 86 | * any missing properties will disqualify it. |
|---|
| 87 | * |
|---|
| 88 | * @since 4.7.0 |
|---|
| 89 | * |
|---|
| 90 | * @param array $args Optional. An array of key => value arguments to match |
|---|
| 91 | * against each object. Default empty array. |
|---|
| 92 | * @param string $operator Optional. The logical operation to perform. 'AND' means |
|---|
| 93 | * all elements from the array must match. 'OR' means only |
|---|
| 94 | * one element needs to match. 'NOT' means no elements may |
|---|
| 95 | * match. Default 'AND'. |
|---|
| 96 | * @return array Array of found values. |
|---|
| 97 | */ |
|---|
| 98 | public function filter( $args = array(), $operator = 'AND' ) { |
|---|
| 99 | if ( empty( $args ) ) { |
|---|
| 100 | return $this->output; |
|---|
| 101 | } |
|---|
| 102 | |
|---|
| 103 | $operator = strtoupper( $operator ); |
|---|
| 104 | |
|---|
| 105 | if ( ! in_array( $operator, array( 'AND', 'OR', 'NOT' ), true ) ) { |
|---|
| 106 | $this->output = array(); |
|---|
| 107 | return $this->output; |
|---|
| 108 | } |
|---|
| 109 | |
|---|
| 110 | $count = count( $args ); |
|---|
| 111 | $filtered = array(); |
|---|
| 112 | |
|---|
| 113 | foreach ( $this->output as $key => $obj ) { |
|---|
| 114 | $matched = 0; |
|---|
| 115 | |
|---|
| 116 | foreach ( $args as $m_key => $m_value ) { |
|---|
| 117 | if ( is_array( $obj ) ) { |
|---|
| 118 | // Treat object as an array. |
|---|
| 119 | if ( array_key_exists( $m_key, $obj ) && ( $m_value == $obj[ $m_key ] ) ) { |
|---|
| 120 | $matched++; |
|---|
| 121 | } |
|---|
| 122 | } elseif ( is_object( $obj ) ) { |
|---|
| 123 | // Treat object as an object. |
|---|
| 124 | if ( isset( $obj->{$m_key} ) && ( $m_value == $obj->{$m_key} ) ) { |
|---|
| 125 | $matched++; |
|---|
| 126 | } |
|---|
| 127 | } |
|---|
| 128 | } |
|---|
| 129 | |
|---|
| 130 | if ( ( 'AND' === $operator && $matched === $count ) |
|---|
| 131 | || ( 'OR' === $operator && $matched > 0 ) |
|---|
| 132 | || ( 'NOT' === $operator && 0 === $matched ) |
|---|
| 133 | ) { |
|---|
| 134 | $filtered[ $key ] = $obj; |
|---|
| 135 | } |
|---|
| 136 | } |
|---|
| 137 | |
|---|
| 138 | $this->output = $filtered; |
|---|
| 139 | |
|---|
| 140 | return $this->output; |
|---|
| 141 | } |
|---|
| 142 | |
|---|
| 143 | /** |
|---|
| 144 | * Plucks a certain field out of each element in the input array. |
|---|
| 145 | * |
|---|
| 146 | * This has the same functionality and prototype of |
|---|
| 147 | * array_column() (PHP 5.5) but also supports objects. |
|---|
| 148 | * |
|---|
| 149 | * @since 4.7.0 |
|---|
| 150 | * |
|---|
| 151 | * @param int|string $field Field to fetch from the object or array. |
|---|
| 152 | * @param int|string $index_key Optional. Field from the element to use as keys for the new array. |
|---|
| 153 | * Default null. |
|---|
| 154 | * @return array Array of found values. If `$index_key` is set, an array of found values with keys |
|---|
| 155 | * corresponding to `$index_key`. If `$index_key` is null, array keys from the original |
|---|
| 156 | * `$list` will be preserved in the results. |
|---|
| 157 | */ |
|---|
| 158 | public function pluck( $field, $index_key = null ) { |
|---|
| 159 | $newlist = array(); |
|---|
| 160 | |
|---|
| 161 | if ( ! $index_key ) { |
|---|
| 162 | /* |
|---|
| 163 | * This is simple. Could at some point wrap array_column() |
|---|
| 164 | * if we knew we had an array of arrays. |
|---|
| 165 | */ |
|---|
| 166 | foreach ( $this->output as $key => $value ) { |
|---|
| 167 | if ( is_object( $value ) ) { |
|---|
| 168 | $newlist[ $key ] = isset( $value->$field ) ? $value->$field : array(); |
|---|
| 169 | } elseif ( is_array( $value ) ) { |
|---|
| 170 | $newlist[ $key ] = isset( $value[ $field ] ) ? $value[ $field ] : array(); |
|---|
| 171 | } else { |
|---|
| 172 | _doing_it_wrong( |
|---|
| 173 | __METHOD__, |
|---|
| 174 | __( 'Values for the input array must be either objects or arrays.' ), |
|---|
| 175 | '6.2.0' |
|---|
| 176 | ); |
|---|
| 177 | } |
|---|
| 178 | } |
|---|
| 179 | |
|---|
| 180 | $this->output = $newlist; |
|---|
| 181 | |
|---|
| 182 | return $this->output; |
|---|
| 183 | } |
|---|
| 184 | |
|---|
| 185 | /* |
|---|
| 186 | * When index_key is not set for a particular item, push the value |
|---|
| 187 | * to the end of the stack. This is how array_column() behaves. |
|---|
| 188 | */ |
|---|
| 189 | foreach ( $this->output as $value ) { |
|---|
| 190 | if ( is_object( $value ) ) { |
|---|
| 191 | if ( isset( $value->$index_key ) ) { |
|---|
| 192 | $newlist[ $value->$index_key ] = $value->$field; |
|---|
| 193 | } else { |
|---|
| 194 | $newlist[] = $value->$field; |
|---|
| 195 | } |
|---|
| 196 | } elseif ( is_array( $value ) ) { |
|---|
| 197 | if ( isset( $value[ $index_key ] ) ) { |
|---|
| 198 | $newlist[ $value[ $index_key ] ] = $value[ $field ]; |
|---|
| 199 | } else { |
|---|
| 200 | $newlist[] = $value[ $field ]; |
|---|
| 201 | } |
|---|
| 202 | } else { |
|---|
| 203 | _doing_it_wrong( |
|---|
| 204 | __METHOD__, |
|---|
| 205 | __( 'Values for the input array must be either objects or arrays.' ), |
|---|
| 206 | '6.2.0' |
|---|
| 207 | ); |
|---|
| 208 | } |
|---|
| 209 | } |
|---|
| 210 | |
|---|
| 211 | $this->output = $newlist; |
|---|
| 212 | |
|---|
| 213 | return $this->output; |
|---|
| 214 | } |
|---|
| 215 | |
|---|
| 216 | /** |
|---|
| 217 | * Sorts the input array based on one or more orderby arguments. |
|---|
| 218 | * |
|---|
| 219 | * @since 4.7.0 |
|---|
| 220 | * |
|---|
| 221 | * @param string|array $orderby Optional. Either the field name to order by or an array |
|---|
| 222 | * of multiple orderby fields as `$orderby => $order`. |
|---|
| 223 | * Default empty array. |
|---|
| 224 | * @param string $order Optional. Either 'ASC' or 'DESC'. Only used if `$orderby` |
|---|
| 225 | * is a string. Default 'ASC'. |
|---|
| 226 | * @param bool $preserve_keys Optional. Whether to preserve keys. Default false. |
|---|
| 227 | * @return array The sorted array. |
|---|
| 228 | */ |
|---|
| 229 | public function sort( $orderby = array(), $order = 'ASC', $preserve_keys = false ) { |
|---|
| 230 | if ( empty( $orderby ) ) { |
|---|
| 231 | return $this->output; |
|---|
| 232 | } |
|---|
| 233 | |
|---|
| 234 | if ( is_string( $orderby ) ) { |
|---|
| 235 | $orderby = array( $orderby => $order ); |
|---|
| 236 | } |
|---|
| 237 | |
|---|
| 238 | foreach ( $orderby as $field => $direction ) { |
|---|
| 239 | $orderby[ $field ] = 'DESC' === strtoupper( $direction ) ? 'DESC' : 'ASC'; |
|---|
| 240 | } |
|---|
| 241 | |
|---|
| 242 | $this->orderby = $orderby; |
|---|
| 243 | |
|---|
| 244 | if ( $preserve_keys ) { |
|---|
| 245 | uasort( $this->output, array( $this, 'sort_callback' ) ); |
|---|
| 246 | } else { |
|---|
| 247 | usort( $this->output, array( $this, 'sort_callback' ) ); |
|---|
| 248 | } |
|---|
| 249 | |
|---|
| 250 | $this->orderby = array(); |
|---|
| 251 | |
|---|
| 252 | return $this->output; |
|---|
| 253 | } |
|---|
| 254 | |
|---|
| 255 | /** |
|---|
| 256 | * Callback to sort an array by specific fields. |
|---|
| 257 | * |
|---|
| 258 | * @since 4.7.0 |
|---|
| 259 | * |
|---|
| 260 | * @see WP_List_Util::sort() |
|---|
| 261 | * |
|---|
| 262 | * @param object|array $a One object to compare. |
|---|
| 263 | * @param object|array $b The other object to compare. |
|---|
| 264 | * @return int 0 if both objects equal. -1 if second object should come first, 1 otherwise. |
|---|
| 265 | */ |
|---|
| 266 | private function sort_callback( $a, $b ) { |
|---|
| 267 | if ( empty( $this->orderby ) ) { |
|---|
| 268 | return 0; |
|---|
| 269 | } |
|---|
| 270 | |
|---|
| 271 | $a = (array) $a; |
|---|
| 272 | $b = (array) $b; |
|---|
| 273 | |
|---|
| 274 | foreach ( $this->orderby as $field => $direction ) { |
|---|
| 275 | if ( ! isset( $a[ $field ] ) || ! isset( $b[ $field ] ) ) { |
|---|
| 276 | continue; |
|---|
| 277 | } |
|---|
| 278 | |
|---|
| 279 | if ( $a[ $field ] == $b[ $field ] ) { |
|---|
| 280 | continue; |
|---|
| 281 | } |
|---|
| 282 | |
|---|
| 283 | $results = 'DESC' === $direction ? array( 1, -1 ) : array( -1, 1 ); |
|---|
| 284 | |
|---|
| 285 | if ( is_numeric( $a[ $field ] ) && is_numeric( $b[ $field ] ) ) { |
|---|
| 286 | return ( $a[ $field ] < $b[ $field ] ) ? $results[0] : $results[1]; |
|---|
| 287 | } |
|---|
| 288 | |
|---|
| 289 | return 0 > strcmp( $a[ $field ], $b[ $field ] ) ? $results[0] : $results[1]; |
|---|
| 290 | } |
|---|
| 291 | |
|---|
| 292 | return 0; |
|---|
| 293 | } |
|---|
| 294 | } |
|---|