Passed
Pull Request — master (#35)
by Anton
07:27
created

Serializer::serializeResource()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 9
nc 3
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\ActiveDataProvider;
12
use yii\data\DataProviderInterface;
13
use yii\data\Pagination;
14
use yii\db\QueryInterface;
15
use yii\web\Link;
16
use yii\web\Linkable;
17
use yii\web\Request;
18
use yii\web\Response;
19
20
class Serializer extends Component
21
{
22
    /**
23
     * @var string the name of the query parameter containing the information about which fields should be returned
24
     * for a [[Model]] object. If the parameter is not provided or empty, the default set of fields as defined
25
     * by [[Model::fields()]] will be returned.
26
     */
27
    public $fieldsParam = 'fields';
28
    /**
29
     * @var string the name of the query parameter containing the information about which fields should be returned
30
     * in addition to those listed in [[fieldsParam]] for a resource object.
31
     */
32
    public $expandParam = 'include';
33
    /**
34
     * @var string the name of the envelope (e.g. `_links`) for returning the links objects.
35
     * It takes effect only, if `collectionEnvelope` is set.
36
     * @since 2.0.4
37
     */
38
    public $linksEnvelope = 'links';
39
    /**
40
     * @var string the name of the envelope (e.g. `_meta`) for returning the pagination object.
41
     * It takes effect only, if `collectionEnvelope` is set.
42
     * @since 2.0.4
43
     */
44
    public $metaEnvelope = 'meta';
45
    /**
46
     * @var Request the current request. If not set, the `request` application component will be used.
47
     */
48
    public $request;
49
    /**
50
     * @var Response the response to be sent. If not set, the `response` application component will be used.
51
     */
52
    public $response;
53
    /**
54
     * @var bool whether to automatically pluralize the `type` of resource.
55
     */
56
    public $pluralize = true;
57
58
    /**
59
     * Prepares the member name that should be returned.
60
     * If not set, all member names will be converted to recommended format.
61
     * For example, both 'firstName' and 'first_name' will be converted to 'first-name'.
62
     * @var callable
63
     */
64
    public $prepareMemberName = ['tuyakhov\jsonapi\Inflector', 'var2member'];
65
66
    /**
67
     * Converts a member name to an attribute name.
68
     * @var callable
69
     */
70
    public $formatMemberName = ['tuyakhov\jsonapi\Inflector', 'member2var'];
71
72
73
    /**
74
     * @inheritdoc
75
     */
76
    public function init()
77
    {
78
        if ($this->request === null) {
79
            $this->request = \Yii::$app->getRequest();
80
        }
81
        if ($this->response === null) {
82
            $this->response = \Yii::$app->getResponse();
83
        }
84
    }
85
86
    /**
87
     * Serializes the given data into a format that can be easily turned into other formats.
88
     * This method mainly converts the objects of recognized types into array representation.
89
     * It will not do conversion for unknown object types or non-object data.
90
     * @param mixed $data the data to be serialized.
91
     * @return mixed the converted data.
92
     */
93
    public function serialize($data)
94
    {
95
        if ($data instanceof Model && $data->hasErrors()) {
96
            return $this->serializeModelErrors($data);
97
        } elseif ($data instanceof ResourceInterface) {
98
            return $this->serializeResource($data);
99
        } elseif ($data instanceof ActiveDataProvider) {
100
            return $this->serializeActiveDataProvider($data);
101
        } elseif ($data instanceof DataProviderInterface) {
102
            return $this->serializeDataProvider($data);
103
        } else {
104
            return $data;
105
        }
106
    }
107
108
    /**
109
     * @param ResourceInterface $model
110
     * @return array
111
     */
112
    protected function serializeModel(ResourceInterface $model)
113
    {
114
        $fields = $this->getRequestedFields();
115
        $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...
116
        $fields = isset($fields[$type]) ? $fields[$type] : [];
117
118
        $attributes = $model->getResourceAttributes($fields);
119
        $attributes = array_combine($this->prepareMemberNames(array_keys($attributes)), array_values($attributes));
120
121
        $data = array_merge($this->serializeIdentifier($model), [
122
            'attributes' => $attributes,
123
        ]);
124
125
        $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...
126
        $relationships = $model->getResourceRelationships();
127
        if (!empty($relationships)) {
128
            foreach ($relationships as $name => $items) {
129
                $relationship = [];
130
                if (is_array($items)) {
131
                    foreach ($items as $item) {
132
                        if ($item instanceof ResourceIdentifierInterface) {
133
                            $relationship[] = $this->serializeIdentifier($item);
134
                        }
135
                    }
136
                } elseif ($items instanceof ResourceIdentifierInterface) {
137
                    $relationship = $this->serializeIdentifier($items);
138
                }
139
                if (!empty($relationship)) {
140
                    $memberName = $this->prepareMemberNames([$name]);
141
                    $memberName = reset($memberName);
142
                    if (in_array($name, $included)) {
143
                        $data['relationships'][$memberName]['data'] = $relationship;
144
                    }
145
                    if ($model instanceof LinksInterface) {
146
                        $links = $model->getRelationshipLinks($memberName);
147
                        if (!empty($links)) {
148
                            $data['relationships'][$memberName]['links'] = Link::serialize($links);
149
                        }
150
                    }
151
                }
152
            }
153
        }
154
155
        if ($model instanceof Linkable) {
156
            $data['links'] = Link::serialize($model->getLinks());
157
        }
158
159
        return $data;
160
    }
161
162
    /**
163
     * @param ResourceInterface $resource
164
     * @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...
165
     */
166
    protected function serializeResource(ResourceInterface $resource)
167
    {
168
        if ($this->request->getIsHead()) {
169
            return null;
170
        } else {
171
            $data = ['data' => $this->serializeModel($resource)];
172
173
            $included = $this->serializeIncluded($resource);
174
            if (!empty($included)) {
175
                $data['included'] = $included;
176
            }
177
178
            return $data;
179
        }
180
    }
181
182
    /**
183
     * Serialize resource identifier object and make type juggling
184
     * @link http://jsonapi.org/format/#document-resource-object-identification
185
     * @param ResourceIdentifierInterface $identifier
186
     * @return array
187
     */
188
    protected function serializeIdentifier(ResourceIdentifierInterface $identifier)
189
    {
190
        $result = [];
191
        foreach (['id', 'type'] as $key) {
192
            $getter = 'get' . ucfirst($key);
193
            $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...
194
            if ($value === null || is_array($value) || (is_object($value) && !method_exists($value, '__toString'))) {
195
                throw new InvalidValueException("The value {$key} of resource object " . get_class($identifier) . ' MUST be a string.');
196
            }
197
            if ($key === 'type' && $this->pluralize) {
198
                $value = Inflector::pluralize($value);
199
            }
200
            $result[$key] = (string) $value;
201
        }
202
        return $result;
203
    }
204
205
    /**
206
     * @param ResourceInterface|array $resources
207
     * @return array
208
     */
209
    protected function serializeIncluded($resources)
210
    {
211
        $included = $this->getIncluded();
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...
212
        $resources = is_array($resources) ? $resources : [$resources];
213
        $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...
214
215
        foreach ($resources as $resource) {
216
            if (!$resource instanceof  ResourceInterface) {
217
                continue;
218
            }
219
            $relationships = $resource->getResourceRelationships();
220
            foreach ($relationships as $name => $relationship) {
221
                if (!in_array($name, $included)) {
222
                    continue;
223
                }
224
                if (!is_array($relationship)) {
225
                    $relationship = [$relationship];
226
                }
227
                foreach ($relationship as $model) {
228
                    if ($model instanceof ResourceInterface) {
229
                        $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...
230
                        $data[$uniqueKey] = $this->serializeModel($model);
231
                    }
232
                }
233
            }
234
        }
235
236
        return array_values($data);
237
    }
238
239
    /**
240
     * Serializes a data provider.
241
     * @param ActiveDataProvider $dataProvider
242
     * @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...
243
     */
244
    protected function serializeActiveDataProvider($dataProvider)
245
    {
246
        if ($dataProvider->query instanceof QueryInterface) {
247
            $dataProvider->query
0 ignored issues
show
Bug introduced by
The call to addOrderBy() misses a required argument $columns.

This check looks for function calls that miss required arguments.

Loading history...
248
                ->andFilterWhere($this->getFiltered())
0 ignored issues
show
Documentation Bug introduced by
The method getFiltered does not exist on object<tuyakhov\jsonapi\Serializer>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
249
                ->addOrderBy();
250
        }
251
        return $this->serializeDataProvider($dataProvider);
252
    }
253
254
    /**
255
     * Serializes a data provider.
256
     * @param DataProviderInterface $dataProvider
257
     * @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...
258
     */
259
    protected function serializeDataProvider($dataProvider)
260
    {
261
        if ($this->request->getIsHead()) {
262
            return null;
263
        } else {
264
            $models = $dataProvider->getModels();
265
            $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...
266
267
            foreach ($models as $model) {
268
                if ($model instanceof ResourceInterface) {
269
                    $data[] = $this->serializeModel($model);
270
                }
271
            }
272
273
            $result = ['data' => $data];
274
275
            $included = $this->serializeIncluded($models);
276
            if (!empty($included)) {
277
                $result['included'] = $included;
278
            }
279
280
            if (($pagination = $dataProvider->getPagination()) !== false) {
281
                return array_merge($result, $this->serializePagination($pagination));
282
            }
283
284
            return $result;
285
        }
286
    }
287
288
    /**
289
     * Serializes a pagination into an array.
290
     * @param Pagination $pagination
291
     * @return array the array representation of the pagination
292
     * @see addPaginationHeaders()
293
     */
294
    protected function serializePagination($pagination)
295
    {
296
        return [
297
            $this->linksEnvelope => Link::serialize($pagination->getLinks(true)),
298
            $this->metaEnvelope => [
299
                'total-count' => $pagination->totalCount,
300
                'page-count' => $pagination->getPageCount(),
301
                'current-page' => $pagination->getPage() + 1,
302
                'per-page' => $pagination->getPageSize(),
303
            ],
304
        ];
305
    }
306
307
    /**
308
     * Serializes the validation errors in a model.
309
     * @param Model $model
310
     * @return array the array representation of the errors
311
     */
312
    protected function serializeModelErrors($model)
313
    {
314
        $this->response->setStatusCode(422, 'Data Validation Failed.');
315
        $result = [];
316
        foreach ($model->getFirstErrors() as $name => $message) {
317
            $memberName = call_user_func($this->prepareMemberName, $name);
318
            $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...
319
                'source' => ['pointer' => "/data/attributes/{$memberName}"],
320
                'detail' => $message,
321
            ];
322
        }
323
324
        return $result;
325
    }
326
327
    /**
328
     * @return array
329
     */
330
    protected function getRequestedFields()
331
    {
332
        $fields = $this->request->get($this->fieldsParam);
333
334
        if (!is_array($fields)) {
335
            $fields = [];
336
        }
337
        foreach ($fields as $key => $field) {
338
            $fields[$key] = array_map($this->formatMemberName, preg_split('/\s*,\s*/', $field, -1, PREG_SPLIT_NO_EMPTY));
339
        }
340
        return $fields;
341
    }
342
343
    protected function getIncluded()
344
    {
345
        $include = $this->request->get($this->expandParam);
346
        return is_string($include) ? array_map($this->formatMemberName, preg_split('/\s*,\s*/', $include, -1, PREG_SPLIT_NO_EMPTY)) : [];
347
    }
348
349
350
    /**
351
     * Format member names according to recommendations for JSON API implementations
352
     * @link http://jsonapi.org/format/#document-member-names
353
     * @param array $memberNames
354
     * @return array
355
     */
356
    protected function prepareMemberNames(array $memberNames = [])
357
    {
358
        return array_map($this->prepareMemberName, $memberNames);
359
    }
360
}
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...
361