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(); |
||
0 ignored issues
–
show
|
|||
78 | } |
||
79 | if ($this->response === null) { |
||
80 | $this->response = \Yii::$app->getResponse(); |
||
0 ignored issues
–
show
It seems like
Yii::app->getResponse() can also be of type yii\console\Response . However, the property $response is declared as type yii\web\Response . Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
![]() |
|||
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(); |
||
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 |
||
166 | */ |
||
167 | protected function serializeResource(ResourceInterface $resource) |
||
168 | { |
||
169 | if ($this->request->getIsHead()) { |
||
170 | return null; |
||
171 | } else { |
||
172 | $included = $this->getIncluded(); |
||
173 | $data = [ |
||
174 | 'data' => $this->serializeModel($resource, $included) |
||
175 | ]; |
||
176 | |||
177 | $relatedResources = $this->serializeIncluded($resource, $included); |
||
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(); |
||
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 |
||
213 | * @return array |
||
214 | */ |
||
215 | protected function serializeIncluded($resources, array $included = [], $assoc = false) |
||
216 | { |
||
217 | $resources = is_array($resources) ? $resources : [$resources]; |
||
218 | $data = []; |
||
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); |
||
227 | $key = substr($path, 0, $pos); |
||
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)); |
||
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 = []; |
||
273 | |||
274 | $included = $this->getIncluded(); |
||
275 | foreach ($models as $model) { |
||
276 | if ($model instanceof ResourceInterface) { |
||
277 | $data[] = $this->serializeModel($model, $included); |
||
278 | } |
||
279 | } |
||
280 | |||
281 | $result = ['data' => $data]; |
||
282 | |||
283 | $relatedResources = $this->serializeIncluded($models, $included); |
||
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[] = [ |
||
327 | 'source' => ['pointer' => "/data/attributes/{$memberName}"], |
||
328 | 'detail' => $message, |
||
329 | 'status' => '422' |
||
330 | ]; |
||
331 | } |
||
332 | |||
333 | return $result; |
||
334 | } |
||
335 | |||
336 | /** |
||
337 | * @return array |
||
338 | */ |
||
339 | protected function getRequestedFields() |
||
340 | { |
||
341 | $fields = $this->request->get($this->fieldsParam); |
||
342 | |||
343 | if (!is_array($fields)) { |
||
344 | $fields = []; |
||
345 | } |
||
346 | foreach ($fields as $key => $field) { |
||
347 | $fields[$key] = array_map($this->formatMemberName, preg_split('/\s*,\s*/', $field, -1, PREG_SPLIT_NO_EMPTY)); |
||
348 | } |
||
349 | return $fields; |
||
350 | } |
||
351 | |||
352 | /** |
||
353 | * @return array|null |
||
354 | */ |
||
355 | protected function getIncluded() |
||
356 | { |
||
357 | $include = $this->request->get($this->expandParam); |
||
358 | return is_string($include) ? array_map($this->formatMemberName, preg_split('/\s*,\s*/', $include, -1, PREG_SPLIT_NO_EMPTY)) : []; |
||
359 | } |
||
360 | |||
361 | |||
362 | /** |
||
363 | * Format member names according to recommendations for JSON API implementations |
||
364 | * @link http://jsonapi.org/format/#document-member-names |
||
365 | * @param array $memberNames |
||
366 | * @return array |
||
367 | */ |
||
368 | protected function prepareMemberNames(array $memberNames = []) |
||
369 | { |
||
370 | return array_map($this->prepareMemberName, $memberNames); |
||
371 | } |
||
372 | } |
||
373 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.