These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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(); |
||
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 |
||
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(); |
||
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 = []; |
||
220 | |||
221 | if ($included === null) { |
||
222 | return []; |
||
223 | } |
||
224 | |||
225 | $inclusion = []; |
||
226 | $linked = []; |
||
0 ignored issues
–
show
|
|||
227 | foreach ($included as $path) { |
||
228 | if (($pos = strrpos($path, '.')) === false) { |
||
229 | $linked[] = $path; |
||
0 ignored issues
–
show
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); |
||
234 | $key = substr($path, 0, $pos); |
||
235 | $inclusion[$key][] = $name; |
||
236 | $linked[] = $key; |
||
0 ignored issues
–
show
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
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 = []; |
||
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[] = [ |
||
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 | } |
||
380 |
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
will produce issues in the first and second line, while this second example
will produce no issues.