Passed
Push — master ( 5f5ef6...a6257d )
by Alexander
08:20
created

Serializer::addPaginationHeaders()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 2
nop 1
dl 0
loc 13
ccs 10
cts 10
cp 1
crap 2
rs 9.9666
c 0
b 0
f 0
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\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|null 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|null the current request. If not set, the `request` application component will be used.
106
     */
107
    public $request;
108
    /**
109
     * @var Response|null 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 84
    public function init()
127
    {
128 84
        if ($this->request === null) {
129 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...
130
        }
131 84
        if ($this->response === null) {
132 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...
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 84
    public function serialize($data)
146
    {
147 84
        if ($data instanceof Model && $data->hasErrors()) {
148 1
            return $this->serializeModelErrors($data);
149 83
        } elseif ($data instanceof Arrayable) {
150 8
            return $this->serializeModel($data);
151 77
        } elseif ($data instanceof \JsonSerializable) {
152 1
            return $data->jsonSerialize();
153 76
        } elseif ($data instanceof DataProviderInterface) {
154 7
            return $this->serializeDataProvider($data);
155 69
        } elseif (is_array($data)) {
156 1
            $serializedArray = [];
157 1
            foreach ($data as $key => $value) {
158 1
                $serializedArray[$key] = $this->serialize($value);
159
            }
160 1
            return $serializedArray;
161
        }
162
163 68
        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 8
    protected function getRequestedFields()
174
    {
175 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

175
        /** @scrutinizer ignore-call */ 
176
        $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...
176 8
        $expand = $this->request->get($this->expandParam);
177
178 8
        return [
179 8
            is_string($fields) ? preg_split('/\s*,\s*/', $fields, -1, PREG_SPLIT_NO_EMPTY) : [],
180 8
            is_string($expand) ? preg_split('/\s*,\s*/', $expand, -1, PREG_SPLIT_NO_EMPTY) : [],
181 8
        ];
182
    }
183
184
    /**
185
     * Serializes a data provider.
186
     * @param DataProviderInterface $dataProvider
187
     * @return array the array representation of the data provider.
188
     */
189 7
    protected function serializeDataProvider($dataProvider)
190
    {
191 7
        if ($this->preserveKeys) {
192 2
            $models = $dataProvider->getModels();
193
        } else {
194 5
            $models = array_values($dataProvider->getModels());
195
        }
196 7
        $models = $this->serializeModels($models);
197
198 7
        if (($pagination = $dataProvider->getPagination()) !== false) {
199 7
            $this->addPaginationHeaders($pagination);
200
        }
201
202 7
        if ($this->request->getIsHead()) {
203
            return null;
204 7
        } elseif ($this->collectionEnvelope === null) {
205 7
            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 7
    protected function addPaginationHeaders($pagination)
242
    {
243 7
        $links = [];
244 7
        foreach ($pagination->getLinks(true) as $rel => $url) {
245 7
            $links[] = "<$url>; rel=$rel";
246
        }
247
248 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

248
        $this->response->/** @scrutinizer ignore-call */ 
249
                         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...
249 7
            ->set($this->totalCountHeader, $pagination->totalCount)
250 7
            ->set($this->pageCountHeader, $pagination->getPageCount())
251 7
            ->set($this->currentPageHeader, $pagination->getPage() + 1)
252 7
            ->set($this->perPageHeader, $pagination->pageSize)
253 7
            ->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 8
    protected function serializeModel($model)
262
    {
263 8
        if ($this->request->getIsHead()) {
264
            return null;
265
        }
266
267 8
        list($fields, $expand) = $this->getRequestedFields();
268 8
        return $model->toArray($fields, $expand);
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 1
    protected function serializeModelErrors($model)
277
    {
278 1
        $this->response->setStatusCode(422, 'Data Validation Failed.');
279 1
        $result = [];
280 1
        foreach ($model->getFirstErrors() as $name => $message) {
281 1
            $result[] = [
282 1
                'field' => $name,
283 1
                'message' => $message,
284 1
            ];
285
        }
286
287 1
        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 7
    protected function serializeModels(array $models)
296
    {
297 7
        foreach ($models as $i => $model) {
298 6
            if ($model instanceof Arrayable) {
299
                $models[$i] = $this->serializeModel($model);
300 6
            } elseif (is_array($model)) {
301 6
                $models[$i] = ArrayHelper::toArray($model);
302
            }
303
        }
304
305 7
        return $models;
306
    }
307
}
308