Passed
Pull Request — master (#36)
by Anton
03:15
created

Serializer::serialize()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 9
nc 4
nop 1
1
<?php
2
/**
3
 * @author Anton Tuyakhov <[email protected]>
4
 */
5
6
namespace tuyakhov\jsonapi;
7
8
use yii\base\Component;
9
use yii\base\InvalidValueException;
10
use yii\base\Model;
11
use yii\data\DataProviderInterface;
12
use yii\data\Pagination;
13
use yii\web\Link;
14
use yii\web\Linkable;
15
use yii\web\Request;
16
use yii\web\Response;
17
18
class Serializer extends Component
19
{
20
    /**
21
     * @var string the name of the query parameter containing the information about which fields should be returned
22
     * for a [[Model]] object. If the parameter is not provided or empty, the default set of fields as defined
23
     * by [[Model::fields()]] will be returned.
24
     */
25
    public $fieldsParam = 'fields';
26
    /**
27
     * @var string the name of the query parameter containing the information about which fields should be returned
28
     * in addition to those listed in [[fieldsParam]] for a resource object.
29
     */
30
    public $expandParam = 'include';
31
    /**
32
     * @var string the name of the envelope (e.g. `_links`) for returning the links objects.
33
     * It takes effect only, if `collectionEnvelope` is set.
34
     * @since 2.0.4
35
     */
36
    public $linksEnvelope = 'links';
37
    /**
38
     * @var string the name of the envelope (e.g. `_meta`) for returning the pagination object.
39
     * It takes effect only, if `collectionEnvelope` is set.
40
     * @since 2.0.4
41
     */
42
    public $metaEnvelope = 'meta';
43
    /**
44
     * @var Request the current request. If not set, the `request` application component will be used.
45
     */
46
    public $request;
47
    /**
48
     * @var Response the response to be sent. If not set, the `response` application component will be used.
49
     */
50
    public $response;
51
    /**
52
     * @var bool whether to automatically pluralize the `type` of resource.
53
     */
54
    public $pluralize = true;
55
56
    /**
57
     * Prepares the member name that should be returned.
58
     * If not set, all member names will be converted to recommended format.
59
     * For example, both 'firstName' and 'first_name' will be converted to 'first-name'.
60
     * @var callable
61
     */
62
    public $prepareMemberName = ['tuyakhov\jsonapi\Inflector', 'var2member'];
63
64
    /**
65
     * Converts a member name to an attribute name.
66
     * @var callable
67
     */
68
    public $formatMemberName = ['tuyakhov\jsonapi\Inflector', 'member2var'];
69
70
71
    /**
72
     * @inheritdoc
73
     */
74
    public function init()
75
    {
76
        if ($this->request === null) {
77
            $this->request = \Yii::$app->getRequest();
78
        }
79
        if ($this->response === null) {
80
            $this->response = \Yii::$app->getResponse();
81
        }
82
    }
83
84
    /**
85
     * Serializes the given data into a format that can be easily turned into other formats.
86
     * This method mainly converts the objects of recognized types into array representation.
87
     * It will not do conversion for unknown object types or non-object data.
88
     * @param mixed $data the data to be serialized.
89
     * @return mixed the converted data.
90
     */
91
    public function serialize($data)
92
    {
93
        if ($data instanceof Model && $data->hasErrors()) {
94
            return $this->serializeModelErrors($data);
95
        } elseif ($data instanceof ResourceInterface) {
96
            return $this->serializeResource($data);
97
        } elseif ($data instanceof DataProviderInterface) {
98
            return $this->serializeDataProvider($data);
99
        } else {
100
            return $data;
101
        }
102
    }
103
104
    /**
105
     * @param ResourceInterface $model
106
     * @return array
107
     */
108
    protected function serializeModel(ResourceInterface $model, array $included = null)
109
    {
110
        $fields = $this->getRequestedFields();
111
        $type = $this->pluralize ? Inflector::pluralize($model->getType()) : $model->getType();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
112
        $fields = isset($fields[$type]) ? $fields[$type] : [];
113
114
        $attributes = $model->getResourceAttributes($fields);
115
        $attributes = array_combine($this->prepareMemberNames(array_keys($attributes)), array_values($attributes));
116
117
        $data = array_merge($this->serializeIdentifier($model), [
118
            'attributes' => $attributes,
119
        ]);
120
121
        $relationships = $model->getResourceRelationships($included);
0 ignored issues
show
Bug introduced by
It seems like $included defined by parameter $included on line 108 can also be of type null; however, tuyakhov\jsonapi\Resourc...ResourceRelationships() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
122
        if (!empty($relationships)) {
123
            foreach ($relationships as $name => $items) {
124
                $relationship = [];
125
                if (is_array($items)) {
126
                    foreach ($items as $item) {
127
                        if ($item instanceof ResourceIdentifierInterface) {
128
                            $relationship[] = $this->serializeIdentifier($item);
129
                        }
130
                    }
131
                } elseif ($items instanceof ResourceIdentifierInterface) {
132
                    $relationship = $this->serializeIdentifier($items);
133
                }
134
                $memberName = $this->prepareMemberNames([$name]);
135
                $memberName = reset($memberName);
136
                if (!empty($relationship)) {
137
                    $data['relationships'][$memberName]['data'] = $relationship;
138
                }
139
                if ($model instanceof LinksInterface) {
140
                    $links = $model->getRelationshipLinks($memberName);
141
                    if (!empty($links)) {
142
                        $data['relationships'][$memberName]['links'] = Link::serialize($links);
143
                    }
144
                }
145
            }
146
        }
147
148
        if ($model instanceof Linkable) {
149
            $data['links'] = Link::serialize($model->getLinks());
150
        }
151
152
        return $data;
153
    }
154
155
    /**
156
     * @param ResourceInterface $resource
157
     * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be null|array<string,array>?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
158
     */
159
    protected function serializeResource(ResourceInterface $resource)
160
    {
161
        if ($this->request->getIsHead()) {
162
            return null;
163
        } else {
164
            $included = $topLevel = $this->getIncluded();
165
166
            if ($included !== null) {
167
                $topLevel = array_map(function($item) {
168
                    if (($pos = strrpos($item, '.')) !== false) {
169
                        return substr($item, 0, $pos);
170
                    }
171
                    return $item;
172
                }, $included);
173
            }
174
175
            $data = [
176
                'data' => $this->serializeModel($resource, $topLevel)
177
            ];
178
179
            $relatedResources = $this->serializeIncluded($resource, $included);
180
            if (!empty($relatedResources)) {
181
                $data['included'] = $relatedResources;
182
            }
183
184
            return $data;
185
        }
186
    }
187
188
    /**
189
     * Serialize resource identifier object and make type juggling
190
     * @link http://jsonapi.org/format/#document-resource-object-identification
191
     * @param ResourceIdentifierInterface $identifier
192
     * @return array
193
     */
194
    protected function serializeIdentifier(ResourceIdentifierInterface $identifier)
195
    {
196
        $result = [];
197
        foreach (['id', 'type'] as $key) {
198
            $getter = 'get' . ucfirst($key);
199
            $value = $identifier->$getter();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
200
            if ($value === null || is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
201
                throw new InvalidValueException("The value {$key} of resource object " . get_class($identifier) . ' MUST be a string.');
202
            }
203
            if ($key === 'type' && $this->pluralize) {
204
                $value = Inflector::pluralize($value);
205
            }
206
            $result[$key] = (string) $value;
207
        }
208
        return $result;
209
    }
210
211
    /**
212
     * @param ResourceInterface|array $resources
213
     * @param array $included
0 ignored issues
show
Documentation introduced by
Should the type for parameter $included not be null|array?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
214
     * @return array
215
     */
216
    protected function serializeIncluded($resources, array $included = null)
217
    {
218
        $resources = is_array($resources) ? $resources : [$resources];
219
        $data = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
220
221
        if ($included === null) {
222
            return [];
223
        }
224
225
        $inclusion = [];
226
        $linked = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
227
        foreach ($included as $path) {
228
            if (($pos = strrpos($path, '.')) === false) {
229
                $linked[] = $path;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 9 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
230
                $inclusion[$path] = [];
231
                continue;
232
            }
233
            $name = substr($path, $pos + 1);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 14 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
234
            $key = substr($path, 0, $pos);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 15 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
235
            $inclusion[$key][] = $name;
236
            $linked[] = $key;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 10 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
237
        }
238
239
        foreach ($resources as $resource) {
240
            if (!$resource instanceof  ResourceInterface) {
241
                continue;
242
            }
243
            $relationships = $resource->getResourceRelationships($linked);
244
            foreach ($relationships as $name => $relationship) {
245
                if ($relationship === null) {
246
                    continue;
247
                }
248
                if (!is_array($relationship)) {
249
                    $relationship = [$relationship];
250
                }
251
                foreach ($relationship as $model) {
252
                    if ($model instanceof ResourceInterface) {
253
                        $uniqueKey = $model->getType() . '/' . $model->getId();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
254
                        $data[$uniqueKey] = $this->serializeModel($model, $inclusion[$name]);
255
                        if (!empty($inclusion[$name])) {
256
                            $data = array_merge($data, $this->serializeIncluded($model, $inclusion[$name]));
257
                        }
258
                    }
259
                }
260
            }
261
        }
262
263
        return array_values($data);
264
    }
265
266
    /**
267
     * Serializes a data provider.
268
     * @param DataProviderInterface $dataProvider
269
     * @return array the array representation of the data provider.
0 ignored issues
show
Documentation introduced by
Should the return type not be null|array<string,array>?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
270
     */
271
    protected function serializeDataProvider($dataProvider)
272
    {
273
        if ($this->request->getIsHead()) {
274
            return null;
275
        } else {
276
            $models = $dataProvider->getModels();
277
            $data = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
278
279
            $included = $this->getIncluded();
280
            foreach ($models as $model) {
281
                if ($model instanceof ResourceInterface) {
282
                    $data[] = $this->serializeModel($model, $included);
283
                }
284
            }
285
286
            $result = ['data' => $data];
287
288
            $relatedResources = $this->serializeIncluded($models, $included);
289
            if (!empty($relatedResources)) {
290
                $result['included'] = $relatedResources;
291
            }
292
293
            if (($pagination = $dataProvider->getPagination()) !== false) {
294
                return array_merge($result, $this->serializePagination($pagination));
295
            }
296
297
            return $result;
298
        }
299
    }
300
301
    /**
302
     * Serializes a pagination into an array.
303
     * @param Pagination $pagination
304
     * @return array the array representation of the pagination
305
     * @see addPaginationHeaders()
306
     */
307
    protected function serializePagination($pagination)
308
    {
309
        return [
310
            $this->linksEnvelope => Link::serialize($pagination->getLinks(true)),
311
            $this->metaEnvelope => [
312
                'total-count' => $pagination->totalCount,
313
                'page-count' => $pagination->getPageCount(),
314
                'current-page' => $pagination->getPage() + 1,
315
                'per-page' => $pagination->getPageSize(),
316
            ],
317
        ];
318
    }
319
320
    /**
321
     * Serializes the validation errors in a model.
322
     * @param Model $model
323
     * @return array the array representation of the errors
324
     */
325
    protected function serializeModelErrors($model)
326
    {
327
        $this->response->setStatusCode(422, 'Data Validation Failed.');
328
        $result = [];
329
        foreach ($model->getFirstErrors() as $name => $message) {
330
            $memberName = call_user_func($this->prepareMemberName, $name);
331
            $result[] = [
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
332
                'source' => ['pointer' => "/data/attributes/{$memberName}"],
333
                'detail' => $message,
334
            ];
335
        }
336
337
        return $result;
338
    }
339
340
    /**
341
     * @return array
342
     */
343
    protected function getRequestedFields()
344
    {
345
        $fields = $this->request->get($this->fieldsParam);
346
347
        if (!is_array($fields)) {
348
            $fields = [];
349
        }
350
        foreach ($fields as $key => $field) {
351
            $fields[$key] = array_map($this->formatMemberName, preg_split('/\s*,\s*/', $field, -1, PREG_SPLIT_NO_EMPTY));
352
        }
353
        return $fields;
354
    }
355
356
    /**
357
     * @return array|null
358
     */
359
    protected function getIncluded()
360
    {
361
        $include = $this->request->get($this->expandParam);
362
        if (!$this->request->isGet) {
363
            return null;
364
        }
365
        return is_string($include) ? array_map($this->formatMemberName, preg_split('/\s*,\s*/', $include, -1, PREG_SPLIT_NO_EMPTY)) : [];
366
    }
367
368
369
    /**
370
     * Format member names according to recommendations for JSON API implementations
371
     * @link http://jsonapi.org/format/#document-member-names
372
     * @param array $memberNames
373
     * @return array
374
     */
375
    protected function prepareMemberNames(array $memberNames = [])
376
    {
377
        return array_map($this->prepareMemberName, $memberNames);
378
    }
379
}
0 ignored issues
show
Coding Style introduced by
As per coding style, files should not end with a newline character.

This check marks files that end in a newline character, i.e. an empy line.

Loading history...
380