Passed
Pull Request — 2.2 (#20357)
by Wilmer
13:33 queued 05:55
created

Serializer   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Test Coverage

Coverage 77.65%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 85
c 2
b 1
f 0
dl 0
loc 273
ccs 66
cts 85
cp 0.7765
rs 9.92
wmc 31

9 Methods

Rating   Name   Duplication   Size   Complexity  
A serializeModels() 0 11 4
A serializeModel() 0 8 2
A addPaginationHeaders() 0 13 2
A init() 0 7 3
B serialize() 0 19 8
A getRequestedFields() 0 8 3
A serializeModelErrors() 0 12 2
A serializePagination() 0 9 1
A serializeDataProvider() 0 27 6
1
<?php
2
3
/**
4
 * @link https://www.yiiframework.com/
5
 * @copyright Copyright (c) 2008 Yii Software LLC
6
 * @license https://www.yiiframework.com/license/
7
 */
8
9
namespace yii\rest;
10
11
use Yii;
12
use yii\base\Arrayable;
13
use yii\base\Component;
14
use yii\base\Model;
15
use yii\data\DataProviderInterface;
16
use yii\data\Pagination;
17
use yii\helpers\ArrayHelper;
18
use yii\web\Link;
19
use yii\web\Request;
20
use yii\web\Response;
21
22
/**
23
 * Serializer converts resource objects and collections into array representation.
24
 *
25
 * Serializer is mainly used by REST controllers to convert different objects into array representation
26
 * so that they can be further turned into different formats, such as JSON, XML, by response formatters.
27
 *
28
 * The default implementation handles resources as [[Model]] objects and collections as objects
29
 * implementing [[DataProviderInterface]]. You may override [[serialize()]] to handle more types.
30
 *
31
 * @author Qiang Xue <[email protected]>
32
 * @since 2.0
33
 */
34
class Serializer extends Component
35
{
36
    /**
37
     * @var string the name of the query parameter containing the information about which fields should be returned
38
     * for a [[Model]] object. If the parameter is not provided or empty, the default set of fields as defined
39
     * by [[Model::fields()]] will be returned.
40
     */
41
    public $fieldsParam = 'fields';
42
    /**
43
     * @var string the name of the query parameter containing the information about which fields should be returned
44
     * in addition to those listed in [[fieldsParam]] for a resource object.
45
     */
46
    public $expandParam = 'expand';
47
    /**
48
     * @var string the name of the HTTP header containing the information about total number of data items.
49
     * This is used when serving a resource collection with pagination.
50
     */
51
    public $totalCountHeader = 'X-Pagination-Total-Count';
52
    /**
53
     * @var string the name of the HTTP header containing the information about total number of pages of data.
54
     * This is used when serving a resource collection with pagination.
55
     */
56
    public $pageCountHeader = 'X-Pagination-Page-Count';
57
    /**
58
     * @var string the name of the HTTP header containing the information about the current page number (1-based).
59
     * This is used when serving a resource collection with pagination.
60
     */
61
    public $currentPageHeader = 'X-Pagination-Current-Page';
62
    /**
63
     * @var string the name of the HTTP header containing the information about the number of data items in each page.
64
     * This is used when serving a resource collection with pagination.
65
     */
66
    public $perPageHeader = 'X-Pagination-Per-Page';
67
    /**
68
     * @var string|null the name of the envelope (e.g. `items`) for returning the resource objects in a collection.
69
     * This is used when serving a resource collection. When this is set and pagination is enabled, the serializer
70
     * will return a collection in the following format:
71
     *
72
     * ```php
73
     * [
74
     *     'items' => [...],  // assuming collectionEnvelope is "items"
75
     *     '_links' => {  // pagination links as returned by Pagination::getLinks()
76
     *         'self' => '...',
77
     *         'next' => '...',
78
     *         'last' => '...',
79
     *     },
80
     *     '_meta' => {  // meta information as returned by Pagination::toArray()
81
     *         'totalCount' => 100,
82
     *         'pageCount' => 5,
83
     *         'currentPage' => 1,
84
     *         'perPage' => 20,
85
     *     },
86
     * ]
87
     * ```
88
     *
89
     * If this property is not set, the resource arrays will be directly returned without using envelope.
90
     * The pagination information as shown in `_links` and `_meta` can be accessed from the response HTTP headers.
91
     */
92
    public $collectionEnvelope;
93
    /**
94
     * @var string the name of the envelope (e.g. `_links`) for returning the links objects.
95
     * It takes effect only, if `collectionEnvelope` is set.
96
     * @since 2.0.4
97
     */
98
    public $linksEnvelope = '_links';
99
    /**
100
     * @var string the name of the envelope (e.g. `_meta`) for returning the pagination object.
101
     * It takes effect only, if `collectionEnvelope` is set.
102
     * @since 2.0.4
103
     */
104
    public $metaEnvelope = '_meta';
105
    /**
106
     * @var Request|null the current request. If not set, the `request` application component will be used.
107
     */
108
    public $request;
109
    /**
110
     * @var Response|null the response to be sent. If not set, the `response` application component will be used.
111
     */
112
    public $response;
113
    /**
114
     * @var bool whether to preserve array keys when serializing collection data.
115
     * Set this to `true` to allow serialization of a collection as a JSON object where array keys are
116
     * used to index the model objects. The default is to serialize all collections as array, regardless
117
     * of how the array is indexed.
118
     * @see serializeDataProvider()
119
     * @since 2.0.10
120
     */
121
    public $preserveKeys = false;
122
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 84
    public function init()
128
    {
129 84
        if ($this->request === null) {
130 84
            $this->request = Yii::$app->getRequest();
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::app->getRequest() can also be of type yii\console\Request. However, the property $request is declared as type null|yii\web\Request. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
131
        }
132 84
        if ($this->response === null) {
133 84
            $this->response = Yii::$app->getResponse();
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::app->getResponse() can also be of type yii\console\Response. However, the property $response is declared as type null|yii\web\Response. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
134
        }
135
    }
136
137
    /**
138
     * Serializes the given data into a format that can be easily turned into other formats.
139
     * This method mainly converts the objects of recognized types into array representation.
140
     * It will not do conversion for unknown object types or non-object data.
141
     * The default implementation will handle [[Model]], [[DataProviderInterface]] and [\JsonSerializable](https://www.php.net/manual/en/class.jsonserializable.php).
142
     * You may override this method to support more object types.
143
     * @param mixed $data the data to be serialized.
144
     * @return mixed the converted data.
145
     */
146 84
    public function serialize($data)
147
    {
148 84
        if ($data instanceof Model && $data->hasErrors()) {
149 1
            return $this->serializeModelErrors($data);
150 83
        } elseif ($data instanceof Arrayable) {
151 8
            return $this->serializeModel($data);
152 77
        } elseif ($data instanceof \JsonSerializable) {
153 1
            return $data->jsonSerialize();
154 76
        } elseif ($data instanceof DataProviderInterface) {
155 7
            return $this->serializeDataProvider($data);
156 69
        } elseif (is_array($data)) {
157 1
            $serializedArray = [];
158 1
            foreach ($data as $key => $value) {
159 1
                $serializedArray[$key] = $this->serialize($value);
160
            }
161 1
            return $serializedArray;
162
        }
163
164 68
        return $data;
165
    }
166
167
    /**
168
     * @return array the names of the requested fields. The first element is an array
169
     * representing the list of default fields requested, while the second element is
170
     * an array of the extra fields requested in addition to the default fields.
171
     * @see Model::fields()
172
     * @see Model::extraFields()
173
     */
174 8
    protected function getRequestedFields()
175
    {
176 8
        $fields = $this->request->get($this->fieldsParam);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

176
        /** @scrutinizer ignore-call */ 
177
        $fields = $this->request->get($this->fieldsParam);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
177 8
        $expand = $this->request->get($this->expandParam);
178
179 8
        return [
180 8
            is_string($fields) ? preg_split('/\s*,\s*/', $fields, -1, PREG_SPLIT_NO_EMPTY) : [],
181 8
            is_string($expand) ? preg_split('/\s*,\s*/', $expand, -1, PREG_SPLIT_NO_EMPTY) : [],
182 8
        ];
183
    }
184
185
    /**
186
     * Serializes a data provider.
187
     * @param DataProviderInterface $dataProvider
188
     * @return array the array representation of the data provider.
189
     */
190 7
    protected function serializeDataProvider($dataProvider)
191
    {
192 7
        if ($this->preserveKeys) {
193 2
            $models = $dataProvider->getModels();
194
        } else {
195 5
            $models = array_values($dataProvider->getModels());
196
        }
197 7
        $models = $this->serializeModels($models);
198
199 7
        if (($pagination = $dataProvider->getPagination()) !== false) {
200 7
            $this->addPaginationHeaders($pagination);
201
        }
202
203 7
        if ($this->request->getIsHead()) {
204
            return null;
205 7
        } elseif ($this->collectionEnvelope === null) {
206 7
            return $models;
207
        }
208
209
        $result = [
210
            $this->collectionEnvelope => $models,
211
        ];
212
        if ($pagination !== false) {
213
            return array_merge($result, $this->serializePagination($pagination));
214
        }
215
216
        return $result;
217
    }
218
219
    /**
220
     * Serializes a pagination into an array.
221
     * @param Pagination $pagination
222
     * @return array the array representation of the pagination
223
     * @see addPaginationHeaders()
224
     */
225
    protected function serializePagination($pagination)
226
    {
227
        return [
228
            $this->linksEnvelope => Link::serialize($pagination->getLinks(true)),
229
            $this->metaEnvelope => [
230
                'totalCount' => $pagination->totalCount,
231
                'pageCount' => $pagination->getPageCount(),
232
                'currentPage' => $pagination->getPage() + 1,
233
                'perPage' => $pagination->getPageSize(),
234
            ],
235
        ];
236
    }
237
238
    /**
239
     * Adds HTTP headers about the pagination to the response.
240
     * @param Pagination $pagination
241
     */
242 7
    protected function addPaginationHeaders($pagination)
243
    {
244 7
        $links = [];
245 7
        foreach ($pagination->getLinks(true) as $rel => $url) {
246 7
            $links[] = "<$url>; rel=$rel";
247
        }
248
249 7
        $this->response->getHeaders()
0 ignored issues
show
Bug introduced by
The method getHeaders() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

249
        $this->response->/** @scrutinizer ignore-call */ 
250
                         getHeaders()

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
250 7
            ->set($this->totalCountHeader, $pagination->totalCount)
251 7
            ->set($this->pageCountHeader, $pagination->getPageCount())
252 7
            ->set($this->currentPageHeader, $pagination->getPage() + 1)
253 7
            ->set($this->perPageHeader, $pagination->pageSize)
254 7
            ->set('Link', implode(', ', $links));
255
    }
256
257
    /**
258
     * Serializes a model object.
259
     * @param Arrayable $model
260
     * @return array the array representation of the model
261
     */
262 8
    protected function serializeModel($model)
263
    {
264 8
        if ($this->request->getIsHead()) {
265
            return null;
266
        }
267
268 8
        list($fields, $expand) = $this->getRequestedFields();
269 8
        return $model->toArray($fields, $expand);
270
    }
271
272
    /**
273
     * Serializes the validation errors in a model.
274
     * @param Model $model
275
     * @return array the array representation of the errors
276
     */
277 1
    protected function serializeModelErrors($model)
278
    {
279 1
        $this->response->setStatusCode(422, 'Data Validation Failed.');
280 1
        $result = [];
281 1
        foreach ($model->getFirstErrors() as $name => $message) {
282 1
            $result[] = [
283 1
                'field' => $name,
284 1
                'message' => $message,
285 1
            ];
286
        }
287
288 1
        return $result;
289
    }
290
291
    /**
292
     * Serializes a set of models.
293
     * @param array $models
294
     * @return array the array representation of the models
295
     */
296 7
    protected function serializeModels(array $models)
297
    {
298 7
        foreach ($models as $i => $model) {
299 6
            if ($model instanceof Arrayable) {
300
                $models[$i] = $this->serializeModel($model);
301 6
            } elseif (is_array($model)) {
302 6
                $models[$i] = ArrayHelper::toArray($model);
303
            }
304
        }
305
306 7
        return $models;
307
    }
308
}
309