Passed
Pull Request — master (#14)
by Anton
02:12
created

Serializer::serializeModel()   C

Complexity

Conditions 13
Paths 8

Size

Total Lines 47
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 47
rs 5.0999
c 0
b 0
f 0
cc 13
eloc 28
nc 8
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\helpers\Inflector;
14
use yii\web\Link;
15
use yii\web\Linkable;
16
use yii\web\Request;
17
use yii\web\Response;
18
19
class Serializer extends Component
20
{
21
    /**
22
     * @var string the name of the query parameter containing the information about which fields should be returned
23
     * for a [[Model]] object. If the parameter is not provided or empty, the default set of fields as defined
24
     * by [[Model::fields()]] will be returned.
25
     */
26
    public $fieldsParam = 'fields';
27
    /**
28
     * @var string the name of the query parameter containing the information about which fields should be returned
29
     * in addition to those listed in [[fieldsParam]] for a resource object.
30
     */
31
    public $expandParam = 'include';
32
    /**
33
     * @var string the name of the envelope (e.g. `_links`) for returning the links objects.
34
     * It takes effect only, if `collectionEnvelope` is set.
35
     * @since 2.0.4
36
     */
37
    public $linksEnvelope = 'links';
38
    /**
39
     * @var string the name of the envelope (e.g. `_meta`) for returning the pagination object.
40
     * It takes effect only, if `collectionEnvelope` is set.
41
     * @since 2.0.4
42
     */
43
    public $metaEnvelope = 'meta';
44
    /**
45
     * @var Request the current request. If not set, the `request` application component will be used.
46
     */
47
    public $request;
48
    /**
49
     * @var Response the response to be sent. If not set, the `response` application component will be used.
50
     */
51
    public $response;
52
53
    /**
54
     * Prepares the member name that should be returned.
55
     * If not set, all member names will be converted to recommended format.
56
     * For example, both 'firstName' and 'first_name' will be converted to 'first-name'.
57
     * @var callable
58
     */
59
    public $prepareMemberName;
60
61
62
    /**
63
     * @inheritdoc
64
     */
65
    public function init()
66
    {
67
        if ($this->request === null) {
68
            $this->request = \Yii::$app->getRequest();
69
        }
70
        if ($this->response === null) {
71
            $this->response = \Yii::$app->getResponse();
72
        }
73
    }
74
75
    /**
76
     * Serializes the given data into a format that can be easily turned into other formats.
77
     * This method mainly converts the objects of recognized types into array representation.
78
     * It will not do conversion for unknown object types or non-object data.
79
     * @param mixed $data the data to be serialized.
80
     * @return mixed the converted data.
81
     */
82
    public function serialize($data)
83
    {
84
        if ($data instanceof Model && $data->hasErrors()) {
85
            return $this->serializeModelErrors($data);
86
        } elseif ($data instanceof ResourceInterface) {
87
            return $this->serializeResource($data);
88
        } elseif ($data instanceof DataProviderInterface) {
89
            return $this->serializeDataProvider($data);
90
        } else {
91
            return $data;
92
        }
93
    }
94
95
    /**
96
     * @param ResourceInterface $model
97
     * @return array
98
     */
99
    protected function serializeModel(ResourceInterface $model)
100
    {
101
        $fields = $this->getRequestedFields();
102
        $fields = isset($fields[$model->getType()]) ? $fields[$model->getType()] : [];
103
104
        $attributes = $model->getResourceAttributes($fields);
105
        $attributes = array_combine($this->prepareMemberNames(array_keys($attributes)), array_values($attributes));
106
107
        $data = array_merge($this->serializeIdentifier($model), [
108
            'attributes' => $attributes,
109
        ]);
110
111
        $included = $this->getIncluded();
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...
112
        $relationships = $model->getResourceRelationships();
113
        if (!empty($relationships)) {
114
            foreach ($relationships as $name => $items) {
115
                $relationship = [];
116
                if (is_array($items)) {
117
                    foreach ($items as $item) {
118
                        if ($item instanceof ResourceIdentifierInterface) {
119
                            $relationship[] = $this->serializeIdentifier($item);
120
                        }
121
                    }
122
                } elseif ($items instanceof ResourceIdentifierInterface) {
123
                    $relationship = $this->serializeIdentifier($items);
124
                }
125
126
                if (!empty($relationship)) {
127
                    if (in_array($name, $included)) {
128
                        $data['relationships'][$name]['data'] = $relationship;   
129
                    }
130
                    if ($model instanceof LinksInterface) {
131
                        $links = $model->getRelationshipLinks($name);
132
                        if (!empty($links)) {
133
                            $data['relationships'][$name]['links'] = Link::serialize($links);
134
                        }
135
                    }
136
                }
137
            }
138
        }
139
140
        if ($model instanceof Linkable) {
141
            $data['links'] = Link::serialize($model->getLinks());
142
        }
143
144
        return $data;
145
    }
146
147
    /**
148
     * @param ResourceInterface $resource
149
     * @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...
150
     */
151
    protected function serializeResource(ResourceInterface $resource)
152
    {
153
        if ($this->request->getIsHead()) {
154
            return null;
155
        } else {
156
            $data = ['data' => $this->serializeModel($resource)];
157
158
            $included = $this->serializeIncluded($resource);
159
            if (!empty($included)) {
160
                $data['included'] = $included;
161
            }
162
163
            return $data;
164
        }
165
    }
166
167
    /**
168
     * Serialize resource identifier object and make type juggling
169
     * @link http://jsonapi.org/format/#document-resource-object-identification
170
     * @param ResourceIdentifierInterface $identifier
171
     * @return array
172
     */
173
    protected function serializeIdentifier(ResourceIdentifierInterface $identifier)
174
    {
175
        $result = [];
176
        foreach (['id', 'type'] as $key) {
177
            $getter = 'get' . ucfirst($key);
178
            $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...
179
            if ($value === null || is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
180
                throw new InvalidValueException("The value {$key} of resource object " . get_class($identifier) . ' MUST be a string.');
181
            }
182
            $result[$key] = (string) $value;
183
        }
184
        return $result;
185
    }
186
187
    /**
188
     * @param ResourceInterface $resource
189
     * @return array
190
     */
191
    protected function serializeIncluded($resource)
192
    {
193
        $included = $this->getIncluded();
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...
194
        $relationships = $resource->getResourceRelationships();
195
        $data = [];
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...
196
        foreach ($relationships as $name => $relationship) {
197
            if (!in_array($name, $included)) {
198
                continue;
199
            }
200
            if (!is_array($relationship)) {
201
                $relationship = [$relationship];
202
            }
203
            foreach ($relationship as $model) {
204
                if ($model instanceof ResourceInterface) {
205
                    $data[] = $this->serializeModel($model);
206
                }
207
            }
208
        }
209
        return $data;
210
    }
211
212
    /**
213
     * Serializes a data provider.
214
     * @param DataProviderInterface $dataProvider
215
     * @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...
216
     */
217
    protected function serializeDataProvider($dataProvider)
218
    {
219
        if ($this->request->getIsHead()) {
220
            return null;
221
        } else {
222
            $models = [];
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...
223
            $includedModels = [];
224
225
            foreach ($dataProvider->getModels() as $model) {
226
                if ($model instanceof ResourceInterface) {
227
                    $models[] = $this->serializeModel($model);
228
229
                    $included = $this->serializeIncluded($model);
230
                    foreach ($included as $document) {
231
                        $includedModels[] = $document;
232
                    }
233
                }
234
            }
235
236
            $result = ['data' => $models];
237
238
            if (!empty($includedModels)) {
239
                $result['included'] = $includedModels;
240
            }
241
242
            if (($pagination = $dataProvider->getPagination()) !== false) {
243
                return array_merge($result, $this->serializePagination($pagination));
244
            }
245
246
            return $result;
247
        }
248
    }
249
250
    /**
251
     * Serializes a pagination into an array.
252
     * @param Pagination $pagination
253
     * @return array the array representation of the pagination
254
     * @see addPaginationHeaders()
255
     */
256
    protected function serializePagination($pagination)
257
    {
258
        return [
259
            $this->linksEnvelope => Link::serialize($pagination->getLinks(true)),
260
            $this->metaEnvelope => [
261
                'total-count' => $pagination->totalCount,
262
                'page-count' => $pagination->getPageCount(),
263
                'current-page' => $pagination->getPage() + 1,
264
                'per-page' => $pagination->getPageSize(),
265
            ],
266
        ];
267
    }
268
269
    /**
270
     * Serializes the validation errors in a model.
271
     * @param Model $model
272
     * @return array the array representation of the errors
273
     */
274
    protected function serializeModelErrors($model)
275
    {
276
        $this->response->setStatusCode(422, 'Data Validation Failed.');
277
        $result = [];
278
        foreach ($model->getFirstErrors() as $name => $message) {
279
            $result[] = [
280
                'source' => ['pointer' => "/data/attributes/{$name}"],
281
                'detail' => $message,
282
            ];
283
        }
284
285
        return $result;
286
    }
287
288
    /**
289
     * @return array
290
     */
291
    protected function getRequestedFields()
292
    {
293
        $fields = $this->request->get($this->fieldsParam);
294
295
        if (!is_array($fields)) {
296
            $fields = [];
297
        }
298
        foreach ($fields as $key => $field) {
299
            $fields[$key] = preg_split('/\s*,\s*/', $field, -1, PREG_SPLIT_NO_EMPTY);
300
        }
301
        return $fields;
302
    }
303
304
    protected function getIncluded()
305
    {
306
        $include = $this->request->get($this->expandParam);
307
        return is_string($include) ? preg_split('/\s*,\s*/', $include, -1, PREG_SPLIT_NO_EMPTY) : [];
308
    }
309
310
311
    /**
312
     * Format member names according to recommendations for JSON API implementations
313
     * @link http://jsonapi.org/format/#document-member-names
314
     * @param array $memberNames
315
     * @return array
316
     */
317
    protected function prepareMemberNames(array $memberNames = [])
318
    {
319
        $callback = $this->prepareMemberName !== null ? $this->prepareMemberName : function($name) {
320
            return Inflector::camel2id(Inflector::variablize($name));
321
        };
322
        return array_map($callback, $memberNames);
323
    }
324
}
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...
325