Passed
Push — master ( f2bee3...d16bab )
by Anton
03:49
created

Serializer::serializeResource()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 28
c 0
b 0
f 0
rs 8.439
cc 5
eloc 17
nc 5
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);
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 null|array $included
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 null|array the array representation of the data provider.
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