Passed
Push — master ( 9efe7c...a42e22 )
by Anton
35s
created

Serializer   C

Complexity

Total Complexity 62

Size/Duplication

Total Lines 354
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 62
lcom 1
cbo 11
dl 0
loc 354
rs 5.9493
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 9 3
B serialize() 0 12 5
C serializeModel() 0 53 14
A serializeResource() 0 18 3
B serializeIdentifier() 0 16 8
C serializeIncluded() 0 45 14
B serializeDataProvider() 0 29 6
A serializePagination() 0 12 1
A serializeModelErrors() 0 14 2
A getRequestedFields() 0 12 3
A getIncluded() 0 5 2
A prepareMemberNames() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Serializer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Serializer, and based on these observations, apply Extract Interface, too.

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 array $included
106
     * @param ResourceInterface $model
107
     * @return array
108
     */
109
    protected function serializeModel(ResourceInterface $model, array $included = [])
110
    {
111
        $fields = $this->getRequestedFields();
112
        $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...
113
        $fields = isset($fields[$type]) ? $fields[$type] : [];
114
115
        $topLevel = array_map(function($item) {
116
            if (($pos = strrpos($item, '.')) !== false) {
117
                return substr($item, 0, $pos);
118
            }
119
            return $item;
120
        }, $included);
121
122
        $attributes = $model->getResourceAttributes($fields);
123
        $attributes = array_combine($this->prepareMemberNames(array_keys($attributes)), array_values($attributes));
124
125
        $data = array_merge($this->serializeIdentifier($model), [
126
            'attributes' => $attributes,
127
        ]);
128
129
        $relationships = $model->getResourceRelationships($topLevel);
130
        if (!empty($relationships)) {
131
            foreach ($relationships as $name => $items) {
132
                $relationship = [];
133
                if (is_array($items)) {
134
                    foreach ($items as $item) {
135
                        if ($item instanceof ResourceIdentifierInterface) {
136
                            $relationship[] = $this->serializeIdentifier($item);
137
                        }
138
                    }
139
                } elseif ($items instanceof ResourceIdentifierInterface) {
140
                    $relationship = $this->serializeIdentifier($items);
141
                }
142
                $memberName = $this->prepareMemberNames([$name]);
143
                $memberName = reset($memberName);
144
                if (!empty($relationship)) {
145
                    $data['relationships'][$memberName]['data'] = $relationship;
146
                }
147
                if ($model instanceof LinksInterface) {
148
                    $links = $model->getRelationshipLinks($memberName);
149
                    if (!empty($links)) {
150
                        $data['relationships'][$memberName]['links'] = Link::serialize($links);
151
                    }
152
                }
153
            }
154
        }
155
156
        if ($model instanceof Linkable) {
157
            $data['links'] = Link::serialize($model->getLinks());
158
        }
159
160
        return $data;
161
    }
162
163
    /**
164
     * @param ResourceInterface $resource
165
     * @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...
166
     */
167
    protected function serializeResource(ResourceInterface $resource)
168
    {
169
        if ($this->request->getIsHead()) {
170
            return null;
171
        } else {
172
            $included = $this->getIncluded();
173
            $data = [
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 5 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...
174
                'data' => $this->serializeModel($resource, $included)
0 ignored issues
show
Bug introduced by
It seems like $included defined by $this->getIncluded() on line 172 can also be of type null; however, tuyakhov\jsonapi\Serializer::serializeModel() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
175
            ];
176
177
            $relatedResources = $this->serializeIncluded($resource, $included);
0 ignored issues
show
Bug introduced by
It seems like $included defined by $this->getIncluded() on line 172 can also be of type null; however, tuyakhov\jsonapi\Serializer::serializeIncluded() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
178
            if (!empty($relatedResources)) {
179
                $data['included'] = $relatedResources;
180
            }
181
182
            return $data;
183
        }
184
    }
185
186
    /**
187
     * Serialize resource identifier object and make type juggling
188
     * @link http://jsonapi.org/format/#document-resource-object-identification
189
     * @param ResourceIdentifierInterface $identifier
190
     * @return array
191
     */
192
    protected function serializeIdentifier(ResourceIdentifierInterface $identifier)
193
    {
194
        $result = [];
195
        foreach (['id', 'type'] as $key) {
196
            $getter = 'get' . ucfirst($key);
197
            $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...
198
            if ($value === null || is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
199
                throw new InvalidValueException("The value {$key} of resource object " . get_class($identifier) . ' MUST be a string.');
200
            }
201
            if ($key === 'type' && $this->pluralize) {
202
                $value = Inflector::pluralize($value);
203
            }
204
            $result[$key] = (string) $value;
205
        }
206
        return $result;
207
    }
208
209
    /**
210
     * @param ResourceInterface|array $resources
211
     * @param array $included
212
     * @param true $assoc
0 ignored issues
show
Documentation introduced by
Should the type for parameter $assoc not be false|true?

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...
213
     * @return array
214
     */
215
    protected function serializeIncluded($resources, array $included = [], $assoc = false)
216
    {
217
        $resources = is_array($resources) ? $resources : [$resources];
218
        $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...
219
220
        $inclusion = [];
221
        foreach ($included as $path) {
222
            if (($pos = strrpos($path, '.')) === false) {
223
                $inclusion[$path] = [];
224
                continue;
225
            }
226
            $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...
227
            $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...
228
            $inclusion[$key][] = $name;
229
        }
230
231
        foreach ($resources as $resource) {
232
            if (!$resource instanceof  ResourceInterface) {
233
                continue;
234
            }
235
            $relationships = $resource->getResourceRelationships(array_keys($inclusion));
236
            foreach ($relationships as $name => $relationship) {
237
                if ($relationship === null) {
238
                    continue;
239
                }
240
                if (!is_array($relationship)) {
241
                    $relationship = [$relationship];
242
                }
243
                foreach ($relationship as $model) {
244
                    if (!$model instanceof ResourceInterface) {
245
                        continue;
246
                    }
247
                    $uniqueKey = $model->getType() . '/' . $model->getId();
248
                    if (!isset($data[$uniqueKey])) {
249
                        $data[$uniqueKey] = $this->serializeModel($model, $inclusion[$name]);
250
                    }
251
                    if (!empty($inclusion[$name])) {
252
                        $data = array_merge($data, $this->serializeIncluded($model, $inclusion[$name], true));
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a false|object<tuyakhov\jsonapi\true>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
253
                    }
254
                }
255
            }
256
        }
257
258
        return $assoc ? $data : array_values($data);
259
    }
260
261
    /**
262
     * Serializes a data provider.
263
     * @param DataProviderInterface $dataProvider
264
     * @return null|array the array representation of the data provider.
265
     */
266
    protected function serializeDataProvider($dataProvider)
267
    {
268
        if ($this->request->getIsHead()) {
269
            return null;
270
        } else {
271
            $models = $dataProvider->getModels();
272
            $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...
273
274
            $included = $this->getIncluded();
275
            foreach ($models as $model) {
276
                if ($model instanceof ResourceInterface) {
277
                    $data[] = $this->serializeModel($model, $included);
0 ignored issues
show
Bug introduced by
It seems like $included defined by $this->getIncluded() on line 274 can also be of type null; however, tuyakhov\jsonapi\Serializer::serializeModel() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
278
                }
279
            }
280
281
            $result = ['data' => $data];
282
283
            $relatedResources = $this->serializeIncluded($models, $included);
0 ignored issues
show
Bug introduced by
It seems like $included defined by $this->getIncluded() on line 274 can also be of type null; however, tuyakhov\jsonapi\Serializer::serializeIncluded() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
284
            if (!empty($relatedResources)) {
285
                $result['included'] = $relatedResources;
286
            }
287
288
            if (($pagination = $dataProvider->getPagination()) !== false) {
289
                return array_merge($result, $this->serializePagination($pagination));
290
            }
291
292
            return $result;
293
        }
294
    }
295
296
    /**
297
     * Serializes a pagination into an array.
298
     * @param Pagination $pagination
299
     * @return array the array representation of the pagination
300
     * @see addPaginationHeaders()
301
     */
302
    protected function serializePagination($pagination)
303
    {
304
        return [
305
            $this->linksEnvelope => Link::serialize($pagination->getLinks(true)),
306
            $this->metaEnvelope => [
307
                'total-count' => $pagination->totalCount,
308
                'page-count' => $pagination->getPageCount(),
309
                'current-page' => $pagination->getPage() + 1,
310
                'per-page' => $pagination->getPageSize(),
311
            ],
312
        ];
313
    }
314
315
    /**
316
     * Serializes the validation errors in a model.
317
     * @param Model $model
318
     * @return array the array representation of the errors
319
     */
320
    protected function serializeModelErrors($model)
321
    {
322
        $this->response->setStatusCode(422, 'Data Validation Failed.');
323
        $result = [];
324
        foreach ($model->getFirstErrors() as $name => $message) {
325
            $memberName = call_user_func($this->prepareMemberName, $name);
326
            $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...
327
                'source' => ['pointer' => "/data/attributes/{$memberName}"],
328
                'detail' => $message,
329
            ];
330
        }
331
332
        return $result;
333
    }
334
335
    /**
336
     * @return array
337
     */
338
    protected function getRequestedFields()
339
    {
340
        $fields = $this->request->get($this->fieldsParam);
341
342
        if (!is_array($fields)) {
343
            $fields = [];
344
        }
345
        foreach ($fields as $key => $field) {
346
            $fields[$key] = array_map($this->formatMemberName, preg_split('/\s*,\s*/', $field, -1, PREG_SPLIT_NO_EMPTY));
347
        }
348
        return $fields;
349
    }
350
351
    /**
352
     * @return array|null
353
     */
354
    protected function getIncluded()
355
    {
356
        $include = $this->request->get($this->expandParam);
357
        return is_string($include) ? array_map($this->formatMemberName, preg_split('/\s*,\s*/', $include, -1, PREG_SPLIT_NO_EMPTY)) : [];
358
    }
359
360
361
    /**
362
     * Format member names according to recommendations for JSON API implementations
363
     * @link http://jsonapi.org/format/#document-member-names
364
     * @param array $memberNames
365
     * @return array
366
     */
367
    protected function prepareMemberNames(array $memberNames = [])
368
    {
369
        return array_map($this->prepareMemberName, $memberNames);
370
    }
371
}
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...
372