1
|
|
|
<?php namespace Neomerx\JsonApi\Document; |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Copyright 2015 [email protected] (www.neomerx.com) |
5
|
|
|
* |
6
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
7
|
|
|
* you may not use this file except in compliance with the License. |
8
|
|
|
* You may obtain a copy of the License at |
9
|
|
|
* |
10
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0 |
11
|
|
|
* |
12
|
|
|
* Unless required by applicable law or agreed to in writing, software |
13
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, |
14
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
15
|
|
|
* See the License for the specific language governing permissions and |
16
|
|
|
* limitations under the License. |
17
|
|
|
*/ |
18
|
|
|
|
19
|
|
|
use \Closure; |
20
|
|
|
use \Psr\Log\LoggerAwareTrait; |
21
|
|
|
use \Psr\Log\LoggerAwareInterface; |
22
|
|
|
use \Neomerx\JsonApi\Factories\Exceptions; |
23
|
|
|
use \Neomerx\JsonApi\Contracts\Document\ErrorInterface; |
24
|
|
|
use \Neomerx\JsonApi\Contracts\Document\DocumentInterface; |
25
|
|
|
use \Neomerx\JsonApi\Document\Presenters\ElementPresenter; |
26
|
|
|
use \Neomerx\JsonApi\Contracts\Schema\ResourceObjectInterface; |
27
|
|
|
use \Neomerx\JsonApi\Contracts\Schema\RelationshipObjectInterface; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @package Neomerx\JsonApi |
31
|
|
|
*/ |
32
|
|
|
class Document implements DocumentInterface, LoggerAwareInterface |
33
|
|
|
{ |
34
|
|
|
use LoggerAwareTrait; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var array |
38
|
|
|
*/ |
39
|
|
|
private $errors; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var array|object|null |
43
|
|
|
*/ |
44
|
|
|
private $meta; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @var array|null|string |
48
|
|
|
*/ |
49
|
|
|
private $links; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var array |
53
|
|
|
*/ |
54
|
|
|
private $hasBeenMetAlready; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var array|null |
58
|
|
|
*/ |
59
|
|
|
private $included; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var int |
63
|
|
|
*/ |
64
|
|
|
private $includedIndex = 0; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @var array |
68
|
|
|
*/ |
69
|
|
|
private $includedResources = []; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @var array|null |
73
|
|
|
*/ |
74
|
|
|
private $version; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @var array|null |
78
|
|
|
*/ |
79
|
|
|
private $data; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* @var array |
83
|
|
|
*/ |
84
|
|
|
private $bufferForData = []; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @var array |
88
|
|
|
*/ |
89
|
|
|
private $bufferForIncluded = []; |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* If original data were in array. |
93
|
|
|
* |
94
|
|
|
* @var bool|null |
95
|
|
|
*/ |
96
|
|
|
private $isDataArrayed; |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* @var ElementPresenter |
100
|
|
|
*/ |
101
|
|
|
private $presenter; |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* @var bool |
105
|
|
|
*/ |
106
|
|
|
private $showData = true; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* @var string|null |
110
|
|
|
*/ |
111
|
|
|
private $urlPrefix; |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Constructor. |
115
|
|
|
*/ |
116
|
94 |
|
public function __construct() |
117
|
|
|
{ |
118
|
94 |
|
$this->presenter = new ElementPresenter($this); |
119
|
94 |
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* @inheritdoc |
123
|
|
|
*/ |
124
|
8 |
|
public function setDocumentLinks($links) |
125
|
|
|
{ |
126
|
8 |
|
$this->links = $this->presenter->getLinksRepresentation($this->urlPrefix, $links); |
|
|
|
|
127
|
8 |
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* @inheritdoc |
131
|
|
|
*/ |
132
|
6 |
|
public function setMetaToDocument($meta) |
133
|
|
|
{ |
134
|
6 |
|
(is_object($meta) === true || is_array($meta) === true) ?: Exceptions::throwInvalidArgument('meta', $meta); |
135
|
6 |
|
$this->meta = $meta; |
136
|
6 |
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* @inheritdoc |
140
|
|
|
*/ |
141
|
27 |
|
public function addToIncluded(ResourceObjectInterface $resource) |
142
|
|
|
{ |
143
|
27 |
|
$idx = $resource->getId(); |
144
|
27 |
|
$type = $resource->getType(); |
145
|
27 |
|
if (isset($this->hasBeenMetAlready[$type][$idx]) === false) { |
146
|
26 |
|
$this->bufferForIncluded[$type][$idx] = $this->presenter->convertIncludedResourceToArray($resource); |
147
|
26 |
|
$this->hasBeenMetAlready[$type][$idx] = true; |
148
|
26 |
|
} |
149
|
27 |
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* @inheritdoc |
153
|
|
|
*/ |
154
|
66 |
|
public function addToData(ResourceObjectInterface $resource) |
155
|
|
|
{ |
156
|
|
|
// check if 'not-arrayed' data were added you cannot add to 'non-array' data section anymore |
157
|
66 |
|
($this->isDataArrayed === true || $this->isDataArrayed === null) ?: Exceptions::throwLogicException(); |
158
|
|
|
|
159
|
66 |
|
$this->isDataArrayed !== null ?: $this->isDataArrayed = $resource->isInArray(); |
160
|
|
|
|
161
|
|
|
// check all resources have the same isInArray flag |
162
|
66 |
|
($this->isDataArrayed === $resource->isInArray()) ?: Exceptions::throwLogicException(); |
163
|
|
|
|
164
|
66 |
|
$idx = $resource->getId(); |
165
|
66 |
|
$type = $resource->getType(); |
166
|
|
|
|
167
|
66 |
|
isset($this->bufferForData[$type][$idx]) === false ?: Exceptions::throwLogicException(); |
168
|
|
|
|
169
|
66 |
|
$this->bufferForData[$type][$idx] = $this->presenter->convertDataResourceToArray($resource, true); |
170
|
64 |
|
$this->hasBeenMetAlready[$type][$idx] = true; |
171
|
|
|
|
172
|
|
|
// check if resource has already been added to included |
173
|
|
|
// (for example as related resource of one of the previous main resources) |
174
|
64 |
|
if (isset($this->includedResources[$type][$idx]) === true) { |
175
|
1 |
|
$includedIndex = $this->includedResources[$type][$idx]; |
176
|
|
|
|
177
|
|
|
// remove duplicate from 'included' (leave only in main resources) |
178
|
1 |
|
unset($this->included[$includedIndex]); |
179
|
1 |
|
} |
180
|
64 |
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* @inheritdoc |
184
|
|
|
*/ |
185
|
6 |
|
public function setEmptyData() |
186
|
|
|
{ |
187
|
6 |
|
$this->data = []; |
188
|
6 |
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* @inheritdoc |
192
|
|
|
*/ |
193
|
3 |
|
public function setNullData() |
194
|
|
|
{ |
195
|
3 |
|
$this->data = null; |
196
|
3 |
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* @inheritdoc |
200
|
|
|
*/ |
201
|
30 |
|
public function addRelationshipToData( |
202
|
|
|
ResourceObjectInterface $parent, |
203
|
|
|
RelationshipObjectInterface $relationship, |
204
|
|
|
ResourceObjectInterface $resource |
205
|
|
|
) { |
206
|
30 |
|
$this->presenter->addRelationshipTo($this->bufferForData, $parent, $relationship, $resource); |
207
|
30 |
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* @inheritdoc |
211
|
|
|
*/ |
212
|
17 |
|
public function addRelationshipToIncluded( |
213
|
|
|
ResourceObjectInterface $parent, |
214
|
|
|
RelationshipObjectInterface $relationship, |
215
|
|
|
ResourceObjectInterface $resource |
216
|
|
|
) { |
217
|
17 |
|
$this->presenter->addRelationshipTo($this->bufferForIncluded, $parent, $relationship, $resource); |
218
|
17 |
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* @inheritdoc |
222
|
|
|
*/ |
223
|
5 |
|
public function addEmptyRelationshipToData( |
224
|
|
|
ResourceObjectInterface $parent, |
225
|
|
|
RelationshipObjectInterface $relationship |
226
|
|
|
) { |
227
|
5 |
|
$this->presenter->setRelationshipTo($this->bufferForData, $parent, $relationship, []); |
228
|
4 |
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* @inheritdoc |
232
|
|
|
*/ |
233
|
9 |
|
public function addNullRelationshipToData( |
234
|
|
|
ResourceObjectInterface $parent, |
235
|
|
|
RelationshipObjectInterface $relationship |
236
|
|
|
) { |
237
|
9 |
|
$this->presenter->setRelationshipTo($this->bufferForData, $parent, $relationship, null); |
238
|
9 |
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* @inheritdoc |
242
|
|
|
*/ |
243
|
3 |
|
public function addEmptyRelationshipToIncluded( |
244
|
|
|
ResourceObjectInterface $parent, |
245
|
|
|
RelationshipObjectInterface $relationship |
246
|
|
|
) { |
247
|
3 |
|
$this->presenter->setRelationshipTo($this->bufferForIncluded, $parent, $relationship, []); |
248
|
3 |
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* @inheritdoc |
252
|
|
|
*/ |
253
|
6 |
|
public function addNullRelationshipToIncluded( |
254
|
|
|
ResourceObjectInterface $parent, |
255
|
|
|
RelationshipObjectInterface $relationship |
256
|
|
|
) { |
257
|
6 |
|
$this->presenter->setRelationshipTo($this->bufferForIncluded, $parent, $relationship, null); |
258
|
6 |
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* @inheritdoc |
262
|
|
|
*/ |
263
|
69 |
|
public function setResourceCompleted(ResourceObjectInterface $resource) |
264
|
|
|
{ |
265
|
69 |
|
$idx = $resource->getId(); |
266
|
69 |
|
$type = $resource->getType(); |
267
|
|
|
|
268
|
69 |
|
$foundInData = isset($this->bufferForData[$type][$idx]); |
269
|
69 |
|
$foundInIncluded = isset($this->bufferForIncluded[$type][$idx]); |
270
|
|
|
|
271
|
|
|
$addMeta = function (array $representation, Closure $getMetaClosure) { |
272
|
69 |
|
if (empty($representation[self::KEYWORD_RELATIONSHIPS]) === true) { |
273
|
|
|
// if no relationships have been added remove empty placeholder |
274
|
30 |
|
unset($representation[self::KEYWORD_RELATIONSHIPS]); |
275
|
30 |
|
} else { |
276
|
|
|
// relationship might have meta |
277
|
47 |
|
$relShipsMeta = $getMetaClosure(); |
278
|
47 |
|
if (empty($relShipsMeta) === false) { |
279
|
3 |
|
$representation[self::KEYWORD_RELATIONSHIPS][self::KEYWORD_META] = $relShipsMeta; |
280
|
3 |
|
} |
281
|
|
|
} |
282
|
|
|
|
283
|
69 |
|
return $representation; |
284
|
69 |
|
}; |
285
|
|
|
|
286
|
69 |
|
if ($foundInData === true) { |
287
|
62 |
|
$representation = $this->bufferForData[$type][$idx]; |
288
|
62 |
|
unset($this->bufferForData[$type][$idx]); |
289
|
|
|
|
290
|
|
|
$this->data[] = $addMeta($representation, function () use ($resource) { |
291
|
39 |
|
return $resource->getRelationshipsPrimaryMeta(); |
292
|
62 |
|
}); |
293
|
62 |
|
} |
294
|
|
|
|
295
|
69 |
|
if ($foundInIncluded === true) { |
296
|
25 |
|
$representation = $this->bufferForIncluded[$type][$idx]; |
297
|
25 |
|
unset($this->bufferForIncluded[$type][$idx]); |
298
|
|
|
|
299
|
|
|
$this->included[] = $addMeta($representation, function () use ($resource) { |
300
|
21 |
|
return $resource->getRelationshipsInclusionMeta(); |
301
|
25 |
|
}); |
302
|
|
|
// remember we added (type, id) at index |
303
|
25 |
|
$this->includedResources[$type][$idx] = $this->includedIndex; |
304
|
25 |
|
$this->includedIndex++; |
305
|
25 |
|
} |
306
|
69 |
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* @inheritdoc |
310
|
|
|
*/ |
311
|
89 |
|
public function getDocument() |
312
|
|
|
{ |
313
|
89 |
|
if ($this->errors !== null) { |
314
|
7 |
|
return [self::KEYWORD_ERRORS => $this->errors]; |
315
|
|
|
} |
316
|
|
|
|
317
|
82 |
|
$document = array_filter([ |
318
|
82 |
|
self::KEYWORD_JSON_API => $this->version, |
319
|
82 |
|
self::KEYWORD_META => $this->meta, |
320
|
82 |
|
self::KEYWORD_LINKS => $this->links, |
321
|
82 |
|
self::KEYWORD_DATA => true, // this field wont be filtered |
322
|
82 |
|
self::KEYWORD_INCLUDED => empty($this->included) === true ? null : array_values($this->included), |
323
|
|
|
], function ($value) { |
324
|
82 |
|
return $value !== null; |
325
|
82 |
|
}); |
326
|
|
|
|
327
|
82 |
|
if ($this->showData === true) { |
328
|
79 |
|
$isDataNotArray = ($this->isDataArrayed === false && empty($this->data) === false); |
329
|
79 |
|
$document[self::KEYWORD_DATA] = ($isDataNotArray ? $this->data[0] : $this->data); |
330
|
79 |
|
} else { |
331
|
3 |
|
unset($document[self::KEYWORD_DATA]); |
332
|
|
|
} |
333
|
|
|
|
334
|
82 |
|
return $document; |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
/** |
338
|
|
|
* @inheritdoc |
339
|
|
|
*/ |
340
|
2 |
|
public function addJsonApiVersion($version, $meta = null) |
341
|
|
|
{ |
342
|
2 |
|
$this->version = $meta === null ? |
343
|
2 |
|
[self::KEYWORD_VERSION => $version] : [self::KEYWORD_VERSION => $version, self::KEYWORD_META => $meta]; |
344
|
2 |
|
} |
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* @inheritdoc |
348
|
|
|
*/ |
349
|
3 |
|
public function unsetData() |
350
|
|
|
{ |
351
|
3 |
|
$this->showData = false; |
352
|
3 |
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* @inheritdoc |
356
|
|
|
*/ |
357
|
6 |
|
public function addError(ErrorInterface $error) |
358
|
|
|
{ |
359
|
6 |
|
$errorId = (($errorId = $error->getId()) === null ? null : (string)$errorId); |
360
|
|
|
|
361
|
6 |
|
$representation = array_filter([ |
362
|
6 |
|
self::KEYWORD_ERRORS_ID => $errorId, |
363
|
6 |
|
self::KEYWORD_ERRORS_LINKS => $this->presenter |
364
|
6 |
|
->getLinksRepresentation($this->urlPrefix, $error->getLinks()), |
365
|
6 |
|
self::KEYWORD_ERRORS_STATUS => $error->getStatus(), |
366
|
6 |
|
self::KEYWORD_ERRORS_CODE => $error->getCode(), |
367
|
6 |
|
self::KEYWORD_ERRORS_TITLE => $error->getTitle(), |
368
|
6 |
|
self::KEYWORD_ERRORS_DETAIL => $error->getDetail(), |
369
|
6 |
|
self::KEYWORD_ERRORS_SOURCE => $error->getSource(), |
370
|
6 |
|
self::KEYWORD_ERRORS_META => $error->getMeta(), |
371
|
6 |
|
], function ($value) { |
372
|
6 |
|
return $value !== null; |
373
|
6 |
|
}); |
374
|
|
|
|
375
|
6 |
|
$this->errors[] = (object)$representation; |
376
|
6 |
|
} |
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* @inheritdoc |
380
|
|
|
*/ |
381
|
3 |
|
public function addErrors($errors) |
382
|
|
|
{ |
383
|
3 |
|
empty($this->errors) === false ?: $this->errors = []; |
384
|
|
|
|
385
|
3 |
|
foreach ($errors as $error) { |
386
|
2 |
|
$this->addError($error); |
387
|
3 |
|
} |
388
|
3 |
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* @inheritdoc |
392
|
|
|
*/ |
393
|
45 |
|
public function setUrlPrefix($prefix) |
394
|
|
|
{ |
395
|
45 |
|
$this->urlPrefix = (string)$prefix; |
396
|
45 |
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* Get URL prefix. |
400
|
|
|
* |
401
|
|
|
* @return null|string |
402
|
|
|
*/ |
403
|
64 |
|
public function getUrlPrefix() |
404
|
|
|
{ |
405
|
64 |
|
return $this->urlPrefix; |
406
|
|
|
} |
407
|
|
|
} |
408
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.