yiisoft /
arrays
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | namespace Yiisoft\Arrays; |
||
| 6 | |||
| 7 | use function array_key_exists; |
||
| 8 | use function in_array; |
||
| 9 | use function is_array; |
||
| 10 | use function is_int; |
||
| 11 | use function is_string; |
||
| 12 | |||
| 13 | /** |
||
| 14 | * `ArrayableTrait` provides a common implementation of the {@see ArrayableInterface} interface. |
||
| 15 | * |
||
| 16 | * `ArrayableTrait` implements {@see ArrayableInterface::toArray()} by respecting the field definitions as declared |
||
| 17 | * in {@see ArrayableInterface::fields()} and {@see ArrayableInterface::extraFields()}. |
||
| 18 | */ |
||
| 19 | trait ArrayableTrait |
||
| 20 | { |
||
| 21 | /** |
||
| 22 | * Returns the list of fields that should be returned by default by {@see ArrayableInterface::toArray()} |
||
| 23 | * when no specific fields are specified. |
||
| 24 | * |
||
| 25 | * A field is a named element in the returned array by {@see ArrayableInterface::toArray()}. |
||
| 26 | * |
||
| 27 | * This method should return an array of field names or field definitions. |
||
| 28 | * If the former, the field name will be treated as an object property name whose value will be used |
||
| 29 | * as the field value. If the latter, the array key should be the field name while the array value should be |
||
| 30 | * the corresponding field definition which can be either an object property name or a PHP callable |
||
| 31 | * returning the corresponding field value. The signature of the callable should be: |
||
| 32 | * |
||
| 33 | * ```php |
||
| 34 | * function ($model, $field) { |
||
| 35 | * // return field value |
||
| 36 | * } |
||
| 37 | * ``` |
||
| 38 | * |
||
| 39 | * For example, the following code declares four fields: |
||
| 40 | * |
||
| 41 | * - `email`: the field name is the same as the property name `email`; |
||
| 42 | * - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their |
||
| 43 | * values are obtained from the `first_name` and `last_name` properties; |
||
| 44 | * - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name` |
||
| 45 | * and `last_name`. |
||
| 46 | * |
||
| 47 | * ```php |
||
| 48 | * return [ |
||
| 49 | * 'email', |
||
| 50 | * 'firstName' => 'first_name', |
||
| 51 | * 'lastName' => 'last_name', |
||
| 52 | * 'fullName' => function () { |
||
| 53 | * return $this->first_name . ' ' . $this->last_name; |
||
| 54 | * }, |
||
| 55 | * ]; |
||
| 56 | * ``` |
||
| 57 | * |
||
| 58 | * In this method, you may also want to return different lists of fields based on some context |
||
| 59 | * information. For example, depending on the privilege of the current application user, |
||
| 60 | * you may return different sets of visible fields or filter out some fields. |
||
| 61 | * |
||
| 62 | * The default implementation of this method returns the public object member variables indexed by themselves. |
||
| 63 | * |
||
| 64 | * @return array The list of field names or field definitions. |
||
| 65 | * |
||
| 66 | * @see toArray() |
||
| 67 | */ |
||
| 68 | 3 | public function fields(): array |
|
| 69 | { |
||
| 70 | 3 | $fields = array_keys(ArrayHelper::getObjectVars($this)); |
|
| 71 | 3 | return array_combine($fields, $fields); |
|
| 72 | } |
||
| 73 | |||
| 74 | /** |
||
| 75 | * Returns the list of fields that can be expanded further and returned by {@see ArrayableInterface::toArray()}. |
||
| 76 | * |
||
| 77 | * This method is similar to {@see ArrayableInterface::fields()} except that the list of fields returned |
||
| 78 | * by this method are not returned by default by {@see ArrayableInterface::toArray()}]. Only when field names |
||
| 79 | * to be expanded are explicitly specified when calling {@see ArrayableInterface::toArray()}, will their values |
||
| 80 | * be exported. |
||
| 81 | * |
||
| 82 | * The default implementation returns an empty array. |
||
| 83 | * |
||
| 84 | * You may override this method to return a list of expandable fields based on some context information |
||
| 85 | * (e.g. the current application user). |
||
| 86 | * |
||
| 87 | * @return array The list of expandable field names or field definitions. Please refer |
||
| 88 | * to {@see ArrayableInterface::fields()} on the format of the return value. |
||
| 89 | * |
||
| 90 | * @see toArray() |
||
| 91 | * @see fields() |
||
| 92 | */ |
||
| 93 | 1 | public function extraFields(): array |
|
| 94 | { |
||
| 95 | 1 | return []; |
|
| 96 | } |
||
| 97 | |||
| 98 | /** |
||
| 99 | * Converts the model into an array. |
||
| 100 | * |
||
| 101 | * This method will first identify which fields to be included in the resulting array |
||
| 102 | * by calling {@see resolveFields()}. It will then turn the model into an array with these fields. |
||
| 103 | * If `$recursive` is true, any embedded objects will also be converted into arrays. |
||
| 104 | * When embedded objects are {@see ArrayableInterface}, their respective nested fields |
||
| 105 | * will be extracted and passed to {@see ArrayableInterface::toArray()}. |
||
| 106 | * |
||
| 107 | * @param array $fields The fields being requested. |
||
| 108 | * If empty or if it contains '*', all fields as specified by {@see ArrayableInterface::fields()} will be returned. |
||
| 109 | * Fields can be nested, separated with dots (.). e.g.: item.field.sub-field |
||
| 110 | * `$recursive` must be true for nested fields to be extracted. If `$recursive` is false, only the root fields |
||
| 111 | * will be extracted. |
||
| 112 | * @param array $expand The additional fields being requested for exporting. Only fields declared |
||
| 113 | * in {@see ArrayableInterface::extraFields()} will be considered. |
||
| 114 | * Expand can also be nested, separated with dots (.). e.g.: item.expand1.expand2 |
||
| 115 | * `$recursive` must be true for nested expands to be extracted. If `$recursive` is false, only the root expands |
||
| 116 | * will be extracted. |
||
| 117 | * @param bool $recursive Whether to recursively return array representation of embedded objects. |
||
| 118 | * |
||
| 119 | * @return array The array representation of the object. |
||
| 120 | */ |
||
| 121 | 4 | public function toArray(array $fields = [], array $expand = [], bool $recursive = true): array |
|
| 122 | { |
||
| 123 | 4 | $data = []; |
|
| 124 | 4 | foreach ($this->resolveFields($fields, $expand) as $field => $definition) { |
|
| 125 | 4 | $attribute = is_string($definition) ? $this->$definition : $definition($this, $field); |
|
| 126 | |||
| 127 | 4 | if ($recursive) { |
|
| 128 | 4 | $nestedFields = $this->extractFieldsFor($fields, $field); |
|
| 129 | 4 | $nestedExpand = $this->extractFieldsFor($expand, $field); |
|
| 130 | 4 | if ($attribute instanceof ArrayableInterface) { |
|
| 131 | 1 | $attribute = $attribute->toArray($nestedFields, $nestedExpand); |
|
| 132 | 4 | } elseif (is_array($attribute) && ($nestedExpand || $nestedFields)) { |
|
|
0 ignored issues
–
show
The expression
$nestedFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using Loading history...
|
|||
| 133 | 1 | $attribute = $this->filterAndExpand($attribute, $nestedFields, $nestedExpand); |
|
| 134 | } |
||
| 135 | } |
||
| 136 | 4 | $data[$field] = $attribute; |
|
| 137 | } |
||
| 138 | |||
| 139 | 4 | return $recursive ? ArrayHelper::toArray($data) : $data; |
|
| 140 | } |
||
| 141 | |||
| 142 | 1 | private function filterAndExpand(array $array, array $fields = [], array $expand = []): array |
|
| 143 | { |
||
| 144 | 1 | $data = []; |
|
| 145 | 1 | $rootFields = $this->extractRootFields($fields); |
|
| 146 | 1 | $rootExpand = $this->extractRootFields($expand); |
|
| 147 | 1 | foreach (array_merge($rootFields, $rootExpand) as $field) { |
|
| 148 | 1 | if (array_key_exists($field, $array)) { |
|
| 149 | 1 | $attribute = $array[$field]; |
|
| 150 | 1 | $nestedFields = $this->extractFieldsFor($fields, $field); |
|
| 151 | 1 | $nestedExpand = $this->extractFieldsFor($expand, $field); |
|
| 152 | 1 | if ($attribute instanceof ArrayableInterface) { |
|
| 153 | 1 | $attribute = $attribute->toArray($nestedFields, $nestedExpand); |
|
| 154 | 1 | } elseif (is_array($attribute) && ($nestedExpand || $nestedFields)) { |
|
|
0 ignored issues
–
show
The expression
$nestedFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using Loading history...
The expression
$nestedExpand of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using Loading history...
|
|||
| 155 | 1 | $attribute = $this->filterAndExpand($attribute, $nestedFields, $nestedExpand); |
|
| 156 | } |
||
| 157 | 1 | $data[$field] = $attribute; |
|
| 158 | } |
||
| 159 | } |
||
| 160 | 1 | return $data; |
|
| 161 | } |
||
| 162 | |||
| 163 | /** |
||
| 164 | * Extracts the root field names from nested fields. |
||
| 165 | * Nested fields are separated with dots (.). e.g: "item.id" |
||
| 166 | * The previous example would extract "item". |
||
| 167 | * |
||
| 168 | * @param array $fields The fields requested for extraction |
||
| 169 | * |
||
| 170 | * @return array root Fields extracted from the given nested fields. |
||
| 171 | */ |
||
| 172 | 4 | protected function extractRootFields(array $fields): array |
|
| 173 | { |
||
| 174 | 4 | $result = []; |
|
| 175 | |||
| 176 | 4 | foreach ($fields as $field) { |
|
| 177 | 1 | $result[] = strstr($field . '.', '.', true); |
|
| 178 | } |
||
| 179 | |||
| 180 | 4 | if (in_array('*', $result, true)) { |
|
| 181 | 1 | $result = []; |
|
| 182 | } |
||
| 183 | |||
| 184 | 4 | return array_unique($result); |
|
| 185 | } |
||
| 186 | |||
| 187 | /** |
||
| 188 | * Extract nested fields from a fields collection for a given root field |
||
| 189 | * Nested fields are separated with dots (.). e.g: "item.id" |
||
| 190 | * The previous example would extract "id". |
||
| 191 | * |
||
| 192 | * @param array $fields The fields requested for extraction. |
||
| 193 | * @param string $rootField The root field for which we want to extract the nested fields. |
||
| 194 | * |
||
| 195 | * @return array Nested fields extracted for the given field. |
||
| 196 | */ |
||
| 197 | 4 | protected function extractFieldsFor(array $fields, string $rootField): array |
|
| 198 | { |
||
| 199 | 4 | $result = []; |
|
| 200 | |||
| 201 | 4 | foreach ($fields as $field) { |
|
| 202 | 1 | if (str_starts_with($field, "{$rootField}.")) { |
|
| 203 | 1 | $result[] = preg_replace('/^' . preg_quote($rootField, '/') . '\./i', '', $field); |
|
| 204 | } |
||
| 205 | } |
||
| 206 | |||
| 207 | 4 | return array_unique($result); |
|
| 208 | } |
||
| 209 | |||
| 210 | /** |
||
| 211 | * Determines which fields can be returned by {@see ArrayableInterface::toArray()}. |
||
| 212 | * This method will first extract the root fields from the given fields. |
||
| 213 | * Then it will check the requested root fields against those declared in {@see ArrayableInterface::fields()} |
||
| 214 | * and {@see ArrayableInterface::extraFields()} to determine which fields can be returned. |
||
| 215 | * |
||
| 216 | * @param array $fields The fields being requested for exporting. |
||
| 217 | * @param array $expand The additional fields being requested for exporting. |
||
| 218 | * |
||
| 219 | * @return array The list of fields to be exported. The array keys are the field names, and the array values |
||
| 220 | * are the corresponding object property names or PHP callables returning the field values. |
||
| 221 | */ |
||
| 222 | 4 | protected function resolveFields(array $fields, array $expand): array |
|
| 223 | { |
||
| 224 | 4 | $fields = $this->extractRootFields($fields); |
|
| 225 | 4 | $expand = $this->extractRootFields($expand); |
|
| 226 | 4 | $result = []; |
|
| 227 | |||
| 228 | 4 | foreach ($this->fields() as $field => $definition) { |
|
| 229 | 4 | if (is_int($field)) { |
|
| 230 | 3 | $field = $definition; |
|
| 231 | } |
||
| 232 | 4 | if (empty($fields) || in_array($field, $fields, true)) { |
|
| 233 | 4 | $result[$field] = $definition; |
|
| 234 | } |
||
| 235 | } |
||
| 236 | |||
| 237 | 4 | if (empty($expand)) { |
|
| 238 | 4 | return $result; |
|
| 239 | } |
||
| 240 | |||
| 241 | 1 | foreach ($this->extraFields() as $field => $definition) { |
|
| 242 | 1 | if (is_int($field)) { |
|
| 243 | 1 | $field = $definition; |
|
| 244 | } |
||
| 245 | 1 | if (in_array($field, $expand, true)) { |
|
| 246 | 1 | $result[$field] = $definition; |
|
| 247 | } |
||
| 248 | } |
||
| 249 | |||
| 250 | 1 | return $result; |
|
| 251 | } |
||
| 252 | } |
||
| 253 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)or! empty(...)instead.