Passed
Push — 17667-sqlite-create-index-with... ( 21cdd8...b9d5ca )
by Alexander
84:30 queued 44:33
created

Serializer::serialize()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 14
nc 7
nop 1
dl 0
loc 19
rs 8.4444
c 0
b 0
f 0
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\rest;
9
10
use Yii;
11
use yii\base\Arrayable;
12
use yii\base\Component;
13
use yii\base\Model;
14
use yii\data\DataProviderInterface;
15
use yii\data\Pagination;
16
use yii\helpers\ArrayHelper;
17
use yii\web\Link;
18
use yii\web\Request;
19
use yii\web\Response;
20
21
/**
22
 * Serializer converts resource objects and collections into array representation.
23
 *
24
 * Serializer is mainly used by REST controllers to convert different objects into array representation
25
 * so that they can be further turned into different formats, such as JSON, XML, by response formatters.
26
 *
27
 * The default implementation handles resources as [[Model]] objects and collections as objects
28
 * implementing [[DataProviderInterface]]. You may override [[serialize()]] to handle more types.
29
 *
30
 * @author Qiang Xue <[email protected]>
31
 * @since 2.0
32
 */
33
class Serializer extends Component
34
{
35
    /**
36
     * @var string the name of the query parameter containing the information about which fields should be returned
37
     * for a [[Model]] object. If the parameter is not provided or empty, the default set of fields as defined
38
     * by [[Model::fields()]] will be returned.
39
     */
40
    public $fieldsParam = 'fields';
41
    /**
42
     * @var string the name of the query parameter containing the information about which fields should be returned
43
     * in addition to those listed in [[fieldsParam]] for a resource object.
44
     */
45
    public $expandParam = 'expand';
46
    /**
47
     * @var string the name of the HTTP header containing the information about total number of data items.
48
     * This is used when serving a resource collection with pagination.
49
     */
50
    public $totalCountHeader = 'X-Pagination-Total-Count';
51
    /**
52
     * @var string the name of the HTTP header containing the information about total number of pages of data.
53
     * This is used when serving a resource collection with pagination.
54
     */
55
    public $pageCountHeader = 'X-Pagination-Page-Count';
56
    /**
57
     * @var string the name of the HTTP header containing the information about the current page number (1-based).
58
     * This is used when serving a resource collection with pagination.
59
     */
60
    public $currentPageHeader = 'X-Pagination-Current-Page';
61
    /**
62
     * @var string the name of the HTTP header containing the information about the number of data items in each page.
63
     * This is used when serving a resource collection with pagination.
64
     */
65
    public $perPageHeader = 'X-Pagination-Per-Page';
66
    /**
67
     * @var string the name of the envelope (e.g. `items`) for returning the resource objects in a collection.
68
     * This is used when serving a resource collection. When this is set and pagination is enabled, the serializer
69
     * will return a collection in the following format:
70
     *
71
     * ```php
72
     * [
73
     *     'items' => [...],  // assuming collectionEnvelope is "items"
74
     *     '_links' => {  // pagination links as returned by Pagination::getLinks()
75
     *         'self' => '...',
76
     *         'next' => '...',
77
     *         'last' => '...',
78
     *     },
79
     *     '_meta' => {  // meta information as returned by Pagination::toArray()
80
     *         'totalCount' => 100,
81
     *         'pageCount' => 5,
82
     *         'currentPage' => 1,
83
     *         'perPage' => 20,
84
     *     },
85
     * ]
86
     * ```
87
     *
88
     * If this property is not set, the resource arrays will be directly returned without using envelope.
89
     * The pagination information as shown in `_links` and `_meta` can be accessed from the response HTTP headers.
90
     */
91
    public $collectionEnvelope;
92
    /**
93
     * @var string the name of the envelope (e.g. `_links`) for returning the links objects.
94
     * It takes effect only, if `collectionEnvelope` is set.
95
     * @since 2.0.4
96
     */
97
    public $linksEnvelope = '_links';
98
    /**
99
     * @var string the name of the envelope (e.g. `_meta`) for returning the pagination object.
100
     * It takes effect only, if `collectionEnvelope` is set.
101
     * @since 2.0.4
102
     */
103
    public $metaEnvelope = '_meta';
104
    /**
105
     * @var Request the current request. If not set, the `request` application component will be used.
106
     */
107
    public $request;
108
    /**
109
     * @var Response the response to be sent. If not set, the `response` application component will be used.
110
     */
111
    public $response;
112
    /**
113
     * @var bool whether to preserve array keys when serializing collection data.
114
     * Set this to `true` to allow serialization of a collection as a JSON object where array keys are
115
     * used to index the model objects. The default is to serialize all collections as array, regardless
116
     * of how the array is indexed.
117
     * @see serializeDataProvider()
118
     * @since 2.0.10
119
     */
120
    public $preserveKeys = false;
121
122
123
    /**
124
     * {@inheritdoc}
125
     */
126
    public function init()
127
    {
128
        if ($this->request === null) {
129
            $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 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...
130
        }
131
        if ($this->response === null) {
132
            $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 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...
133
        }
134
    }
135
136
    /**
137
     * Serializes the given data into a format that can be easily turned into other formats.
138
     * This method mainly converts the objects of recognized types into array representation.
139
     * It will not do conversion for unknown object types or non-object data.
140
     * The default implementation will handle [[Model]], [[DataProviderInterface]] and [\JsonSerializable](https://www.php.net/manual/en/class.jsonserializable.php).
141
     * You may override this method to support more object types.
142
     * @param mixed $data the data to be serialized.
143
     * @return mixed the converted data.
144
     */
145
    public function serialize($data)
146
    {
147
        if ($data instanceof Model && $data->hasErrors()) {
148
            return $this->serializeModelErrors($data);
149
        } elseif ($data instanceof \JsonSerializable) {
150
            return $data->jsonSerialize();
151
        } elseif ($data instanceof Arrayable) {
152
            return $this->serializeModel($data);
153
        } elseif ($data instanceof DataProviderInterface) {
154
            return $this->serializeDataProvider($data);
155
        } elseif (is_array($data)) {
156
            $serializedArray = [];
157
            foreach ($data as $key => $value) {
158
                $serializedArray[$key] = $this->serialize($value);
159
            }
160
            return $serializedArray;
161
        }
162
163
        return $data;
164
    }
165
166
    /**
167
     * @return array the names of the requested fields. The first element is an array
168
     * representing the list of default fields requested, while the second element is
169
     * an array of the extra fields requested in addition to the default fields.
170
     * @see Model::fields()
171
     * @see Model::extraFields()
172
     */
173
    protected function getRequestedFields()
174
    {
175
        $fields = $this->request->get($this->fieldsParam);
176
        $expand = $this->request->get($this->expandParam);
177
178
        return [
179
            is_string($fields) ? preg_split('/\s*,\s*/', $fields, -1, PREG_SPLIT_NO_EMPTY) : [],
180
            is_string($expand) ? preg_split('/\s*,\s*/', $expand, -1, PREG_SPLIT_NO_EMPTY) : [],
181
        ];
182
    }
183
184
    /**
185
     * Serializes a data provider.
186
     * @param DataProviderInterface $dataProvider
187
     * @return array the array representation of the data provider.
188
     */
189
    protected function serializeDataProvider($dataProvider)
190
    {
191
        if ($this->preserveKeys) {
192
            $models = $dataProvider->getModels();
193
        } else {
194
            $models = array_values($dataProvider->getModels());
195
        }
196
        $models = $this->serializeModels($models);
197
198
        if (($pagination = $dataProvider->getPagination()) !== false) {
199
            $this->addPaginationHeaders($pagination);
200
        }
201
202
        if ($this->request->getIsHead()) {
203
            return null;
204
        } elseif ($this->collectionEnvelope === null) {
205
            return $models;
206
        }
207
208
        $result = [
209
            $this->collectionEnvelope => $models,
210
        ];
211
        if ($pagination !== false) {
212
            return array_merge($result, $this->serializePagination($pagination));
213
        }
214
215
        return $result;
216
    }
217
218
    /**
219
     * Serializes a pagination into an array.
220
     * @param Pagination $pagination
221
     * @return array the array representation of the pagination
222
     * @see addPaginationHeaders()
223
     */
224
    protected function serializePagination($pagination)
225
    {
226
        return [
227
            $this->linksEnvelope => Link::serialize($pagination->getLinks(true)),
228
            $this->metaEnvelope => [
229
                'totalCount' => $pagination->totalCount,
230
                'pageCount' => $pagination->getPageCount(),
231
                'currentPage' => $pagination->getPage() + 1,
232
                'perPage' => $pagination->getPageSize(),
233
            ],
234
        ];
235
    }
236
237
    /**
238
     * Adds HTTP headers about the pagination to the response.
239
     * @param Pagination $pagination
240
     */
241
    protected function addPaginationHeaders($pagination)
242
    {
243
        $links = [];
244
        foreach ($pagination->getLinks(true) as $rel => $url) {
245
            $links[] = "<$url>; rel=$rel";
246
        }
247
248
        $this->response->getHeaders()
249
            ->set($this->totalCountHeader, $pagination->totalCount)
250
            ->set($this->pageCountHeader, $pagination->getPageCount())
251
            ->set($this->currentPageHeader, $pagination->getPage() + 1)
252
            ->set($this->perPageHeader, $pagination->pageSize)
253
            ->set('Link', implode(', ', $links));
254
    }
255
256
    /**
257
     * Serializes a model object.
258
     * @param Arrayable $model
259
     * @return array the array representation of the model
260
     */
261
    protected function serializeModel($model)
262
    {
263
        if ($this->request->getIsHead()) {
264
            return null;
265
        }
266
267
        list($fields, $expand) = $this->getRequestedFields();
268
        return $model->toArray($fields, $expand);
0 ignored issues
show
Bug introduced by
It seems like $fields can also be of type false; however, parameter $fields of yii\base\Arrayable::toArray() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

268
        return $model->toArray(/** @scrutinizer ignore-type */ $fields, $expand);
Loading history...
Bug introduced by
It seems like $expand can also be of type false; however, parameter $expand of yii\base\Arrayable::toArray() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

268
        return $model->toArray($fields, /** @scrutinizer ignore-type */ $expand);
Loading history...
269
    }
270
271
    /**
272
     * Serializes the validation errors in a model.
273
     * @param Model $model
274
     * @return array the array representation of the errors
275
     */
276
    protected function serializeModelErrors($model)
277
    {
278
        $this->response->setStatusCode(422, 'Data Validation Failed.');
279
        $result = [];
280
        foreach ($model->getFirstErrors() as $name => $message) {
281
            $result[] = [
282
                'field' => $name,
283
                'message' => $message,
284
            ];
285
        }
286
287
        return $result;
288
    }
289
290
    /**
291
     * Serializes a set of models.
292
     * @param array $models
293
     * @return array the array representation of the models
294
     */
295
    protected function serializeModels(array $models)
296
    {
297
        list($fields, $expand) = $this->getRequestedFields();
298
        foreach ($models as $i => $model) {
299
            if ($model instanceof Arrayable) {
300
                $models[$i] = $model->toArray($fields, $expand);
0 ignored issues
show
Bug introduced by
It seems like $fields can also be of type false; however, parameter $fields of yii\base\Arrayable::toArray() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

300
                $models[$i] = $model->toArray(/** @scrutinizer ignore-type */ $fields, $expand);
Loading history...
Bug introduced by
It seems like $expand can also be of type false; however, parameter $expand of yii\base\Arrayable::toArray() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

300
                $models[$i] = $model->toArray($fields, /** @scrutinizer ignore-type */ $expand);
Loading history...
301
            } elseif (is_array($model)) {
302
                $models[$i] = ArrayHelper::toArray($model);
303
            }
304
        }
305
306
        return $models;
307
    }
308
}
309