Passed
Push — master ( 6d8250...ba51dd )
by Alexander
17:09 queued 16:02
created

ArrayableTrait::extractFieldsFor()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 2
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Arrays;
6
7
/**
8
 * ArrayableTrait provides a common implementation of the {@see ArrayableInterface} interface.
9
 *
10
 * ArrayableTrait implements {@see ArrayableInterface::toArray()} by respecting the field definitions as declared
11
 * in {@see ArrayableInterface::fields()} and {@see ArrayableInterface::extraFields()}.
12
 */
13
trait ArrayableTrait
14
{
15
    /**
16
     * Returns the list of fields that should be returned by default by {@see ArrayableInterface::toArray()} when no specific fields are specified.
17
     *
18
     * A field is a named element in the returned array by {@see ArrayableInterface::toArray()}.
19
     *
20
     * This method should return an array of field names or field definitions.
21
     * If the former, the field name will be treated as an object property name whose value will be used
22
     * as the field value. If the latter, the array key should be the field name while the array value should be
23
     * the corresponding field definition which can be either an object property name or a PHP callable
24
     * returning the corresponding field value. The signature of the callable should be:
25
     *
26
     * ```php
27
     * function ($model, $field) {
28
     *     // return field value
29
     * }
30
     * ```
31
     *
32
     * For example, the following code declares four fields:
33
     *
34
     * - `email`: the field name is the same as the property name `email`;
35
     * - `firstName` and `lastName`: the field names are `firstName` and `lastName`, and their
36
     *   values are obtained from the `first_name` and `last_name` properties;
37
     * - `fullName`: the field name is `fullName`. Its value is obtained by concatenating `first_name`
38
     *   and `last_name`.
39
     *
40
     * ```php
41
     * return [
42
     *     'email',
43
     *     'firstName' => 'first_name',
44
     *     'lastName' => 'last_name',
45
     *     'fullName' => function () {
46
     *         return $this->first_name . ' ' . $this->last_name;
47
     *     },
48
     * ];
49
     * ```
50
     *
51
     * In this method, you may also want to return different lists of fields based on some context
52
     * information. For example, depending on the privilege of the current application user,
53
     * you may return different sets of visible fields or filter out some fields.
54
     *
55
     * The default implementation of this method returns the public object member variables indexed by themselves.
56
     *
57
     * @return array the list of field names or field definitions.
58
     * @see toArray()
59
     */
60 2
    public function fields(): array
61
    {
62 2
        $fields = array_keys(ArrayHelper::getObjectVars($this));
63 2
        return array_combine($fields, $fields);
0 ignored issues
show
Bug Best Practice introduced by
The expression return array_combine($fields, $fields) could return the type false which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
64
    }
65
66
    /**
67
     * Returns the list of fields that can be expanded further and returned by {@see ArrayableInterface::toArray()}.
68
     *
69
     * This method is similar to {@see ArrayableInterface::fields()} except that the list of fields returned
70
     * by this method are not returned by default by {@see ArrayableInterface::toArray()}]. Only when field names
71
     * to be expanded are explicitly specified when calling {@see ArrayableInterface::toArray()}, will their values
72
     * be exported.
73
     *
74
     * The default implementation returns an empty array.
75
     *
76
     * You may override this method to return a list of expandable fields based on some context information
77
     * (e.g. the current application user).
78
     *
79
     * @return array the list of expandable field names or field definitions. Please refer
80
     * to {@see ArrayableInterface::fields()} on the format of the return value.
81
     * @see toArray()
82
     * @see fields()
83
     */
84 1
    public function extraFields(): array
85
    {
86 1
        return [];
87
    }
88
89
    /**
90
     * Converts the model into an array.
91
     *
92
     * This method will first identify which fields to be included in the resulting array by calling {@see resolveFields()}.
93
     * It will then turn the model into an array with these fields. If `$recursive` is true,
94
     * any embedded objects will also be converted into arrays.
95
     * When embedded objects are {@see ArrayableInterface}, their respective nested fields will be extracted and passed to {@see ArrayableInterface::toArray()}.
96
     *
97
     * @param array $fields the fields being requested.
98
     * If empty or if it contains '*', all fields as specified by {@see ArrayableInterface::fields()} will be returned.
99
     * Fields can be nested, separated with dots (.). e.g.: item.field.sub-field
100
     * `$recursive` must be true for nested fields to be extracted. If `$recursive` is false, only the root fields will be extracted.
101
     * @param array $expand the additional fields being requested for exporting. Only fields declared in {@see ArrayableInterface::extraFields()}
102
     * will be considered.
103
     * Expand can also be nested, separated with dots (.). e.g.: item.expand1.expand2
104
     * `$recursive` must be true for nested expands to be extracted. If `$recursive` is false, only the root expands will be extracted.
105
     * @param bool $recursive whether to recursively return array representation of embedded objects.
106
     * @return array the array representation of the object
107
     */
108 1
    public function toArray(array $fields = [], array $expand = [], bool $recursive = true): array
109
    {
110 1
        $data = [];
111 1
        foreach ($this->resolveFields($fields, $expand) as $field => $definition) {
112 1
            $attribute = is_string($definition) ? $this->$definition : $definition($this, $field);
113
114 1
            if ($recursive) {
115 1
                $nestedFields = $this->extractFieldsFor($fields, $field);
116 1
                $nestedExpand = $this->extractFieldsFor($expand, $field);
117 1
                if ($attribute instanceof ArrayableInterface) {
118 1
                    $attribute = $attribute->toArray($nestedFields, $nestedExpand);
119 1
                } elseif (is_array($attribute)) {
120 1
                    $attribute = $this->filterAndExpand($attribute, $nestedFields, $nestedExpand);
121
                }
122
            }
123 1
            $data[$field] = $attribute;
124
        }
125
126 1
        return $recursive ? ArrayHelper::toArray($data) : $data;
127
    }
128
129 1
    private function filterAndExpand(array $array, array $fields = [], array $expand = []): array
130
    {
131 1
        $data = [];
132 1
        $rootFields = $this->extractRootFields($fields);
133 1
        $rootExpand = $this->extractRootFields($expand);
134 1
        foreach (array_merge($rootFields, $rootExpand) as $field) {
135 1
            if (array_key_exists($field, $array)) {
136 1
                $attribute = $array[$field];
137 1
                $nestedFields = $this->extractFieldsFor($fields, $field);
138 1
                $nestedExpand = $this->extractFieldsFor($expand, $field);
139 1
                if ($attribute instanceof ArrayableInterface) {
140 1
                    $attribute = $attribute->toArray($nestedFields, $nestedExpand, true);
141 1
                } elseif (is_array($attribute) && ($nestedExpand || $nestedFields)) {
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
142 1
                    $attribute = $this->filterAndExpand($attribute, $nestedFields, $nestedExpand);
143
                }
144 1
                $data[$field] = $attribute;
145
            }
146
        }
147 1
        return $data;
148
    }
149
150
    /**
151
     * Extracts the root field names from nested fields.
152
     * Nested fields are separated with dots (.). e.g: "item.id"
153
     * The previous example would extract "item".
154
     *
155
     * @param array $fields The fields requested for extraction
156
     * @return array root fields extracted from the given nested fields
157
     */
158 1
    protected function extractRootFields(array $fields): array
159
    {
160 1
        $result = [];
161
162 1
        foreach ($fields as $field) {
163 1
            $result[] = current(explode('.', $field, 2));
164
        }
165
166 1
        if (in_array('*', $result, true)) {
167 1
            $result = [];
168
        }
169
170 1
        return array_unique($result);
171
    }
172
173
    /**
174
     * Extract nested fields from a fields collection for a given root field
175
     * Nested fields are separated with dots (.). e.g: "item.id"
176
     * The previous example would extract "id".
177
     *
178
     * @param array $fields The fields requested for extraction
179
     * @param string $rootField The root field for which we want to extract the nested fields
180
     * @return array nested fields extracted for the given field
181
     */
182 1
    protected function extractFieldsFor(array $fields, string $rootField): array
183
    {
184 1
        $result = [];
185
186 1
        foreach ($fields as $field) {
187 1
            if (0 === strpos($field, "{$rootField}.")) {
188 1
                $result[] = preg_replace('/^' . preg_quote($rootField, '/') . '\./i', '', $field);
189
            }
190
        }
191
192 1
        return array_unique($result);
193
    }
194
195
    /**
196
     * Determines which fields can be returned by {@see ArrayableInterface::toArray()}.
197
     * This method will first extract the root fields from the given fields.
198
     * Then it will check the requested root fields against those declared in {@see ArrayableInterface::fields()} and {@see ArrayableInterface::extraFields()}
199
     * to determine which fields can be returned.
200
     * @param array $fields the fields being requested for exporting
201
     * @param array $expand the additional fields being requested for exporting
202
     * @return array the list of fields to be exported. The array keys are the field names, and the array values
203
     * are the corresponding object property names or PHP callables returning the field values.
204
     */
205 1
    protected function resolveFields(array $fields, array $expand): array
206
    {
207 1
        $fields = $this->extractRootFields($fields);
208 1
        $expand = $this->extractRootFields($expand);
209 1
        $result = [];
210
211 1
        foreach ($this->fields() as $field => $definition) {
212 1
            if (is_int($field)) {
213 1
                $field = $definition;
214
            }
215 1
            if (empty($fields) || in_array($field, $fields, true)) {
216 1
                $result[$field] = $definition;
217
            }
218
        }
219
220 1
        if (empty($expand)) {
221 1
            return $result;
222
        }
223
224 1
        foreach ($this->extraFields() as $field => $definition) {
225 1
            if (is_int($field)) {
226 1
                $field = $definition;
227
            }
228 1
            if (in_array($field, $expand, true)) {
229 1
                $result[$field] = $definition;
230
            }
231
        }
232
233 1
        return $result;
234
    }
235
}
236