Passed
Push — master ( 75e5a0...04f594 )
by Paweł
05:54 queued 22s
created

Serializer::serializeDataProvider()   A

Complexity

Conditions 6
Paths 14

Size

Total Lines 27
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6.0106

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 6
eloc 16
nc 14
nop 1
dl 0
loc 27
ccs 14
cts 15
cp 0.9333
crap 6.0106
rs 9.1111
c 1
b 1
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 90
    public function init()
127
    {
128 90
        if ($this->request === null) {
129 90
            $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 90
        if ($this->response === null) {
132 90
            $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 90
    }
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 90
    public function serialize($data)
146
    {
147 90
        if ($data instanceof Model && $data->hasErrors()) {
148 1
            return $this->serializeModelErrors($data);
149 89
        } elseif ($data instanceof Arrayable) {
150 8
            return $this->serializeModel($data);
151 83
        } elseif ($data instanceof \JsonSerializable) {
152 1
            return $data->jsonSerialize();
153 82
        } elseif ($data instanceof DataProviderInterface) {
154 13
            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
        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
        ];
182
    }
183
184
    /**
185
     * Serializes a data provider.
186
     * @param DataProviderInterface $dataProvider
187
     * @return array the array representation of the data provider.
188
     */
189 13
    protected function serializeDataProvider($dataProvider)
190
    {
191 13
        if (($pagination = $dataProvider->getPagination()) !== false) {
192 13
            $this->addPaginationHeaders($pagination);
193
        }
194 13
        if ($this->request->getIsHead()) {
195 6
            return null;
196
        }
197 13
        if ($this->preserveKeys) {
198 4
            $models = $dataProvider->getModels();
199
        } else {
200 9
            $models = array_values($dataProvider->getModels());
201
        }
202 13
        $models = $this->serializeModels($models);
203
204 13
        if ($this->collectionEnvelope === null) {
205 7
            return $models;
206
        }
207
208
        $result = [
209 6
            $this->collectionEnvelope => $models,
210
        ];
211 6
        if ($pagination !== false) {
212 6
            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 6
    protected function serializePagination($pagination)
225
    {
226
        return [
227 6
            $this->linksEnvelope => Link::serialize($pagination->getLinks(true)),
228 6
            $this->metaEnvelope => [
229 6
                'totalCount' => $pagination->totalCount,
230 6
                'pageCount' => $pagination->getPageCount(),
231 6
                'currentPage' => $pagination->getPage() + 1,
232 6
                'perPage' => $pagination->getPageSize(),
233
            ],
234
        ];
235
    }
236
237
    /**
238
     * Adds HTTP headers about the pagination to the response.
239
     * @param Pagination $pagination
240
     */
241 13
    protected function addPaginationHeaders($pagination)
242
    {
243 13
        $links = [];
244 13
        foreach ($pagination->getLinks(true) as $rel => $url) {
245 13
            $links[] = "<$url>; rel=$rel";
246
        }
247
248 13
        $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 13
            ->set($this->totalCountHeader, $pagination->totalCount)
250 13
            ->set($this->pageCountHeader, $pagination->getPageCount())
251 13
            ->set($this->currentPageHeader, $pagination->getPage() + 1)
252 13
            ->set($this->perPageHeader, $pagination->pageSize)
253 13
            ->set('Link', implode(', ', $links));
254 13
    }
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
            ];
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 13
    protected function serializeModels(array $models)
296
    {
297 13
        foreach ($models as $i => $model) {
298 12
            if ($model instanceof Arrayable) {
299
                $models[$i] = $this->serializeModel($model);
300 12
            } elseif (is_array($model)) {
301 12
                $models[$i] = ArrayHelper::toArray($model);
302
            }
303
        }
304
305 13
        return $models;
306
    }
307
}
308