Passed
Pull Request — master (#19434)
by Fedonyuk
13:15 queued 05:05
created

ArrayableTrait   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Test Coverage

Coverage 91.55%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 66
c 2
b 1
f 0
dl 0
loc 240
ccs 65
cts 71
cp 0.9155
rs 9.84
wmc 32

6 Methods

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