1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of JSON-API. |
5
|
|
|
* |
6
|
|
|
* (c) Toby Zerner <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Tobscure\JsonApi; |
13
|
|
|
|
14
|
|
|
use JsonSerializable; |
15
|
|
|
|
16
|
|
|
class Document implements JsonSerializable |
17
|
|
|
{ |
18
|
|
|
use LinksTrait, SelfLinkTrait, PaginationLinksTrait, MetaTrait; |
19
|
|
|
|
20
|
|
|
const MEDIA_TYPE = 'application/vnd.api+json'; |
21
|
|
|
|
22
|
|
|
private $data; |
23
|
|
|
private $errors; |
24
|
|
|
private $jsonapi; |
25
|
|
|
|
26
|
|
|
private $include = []; |
27
|
|
|
private $fields = []; |
28
|
|
|
|
29
|
24 |
|
private function __construct() |
30
|
|
|
{ |
31
|
24 |
|
} |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @param ResourceInterface|ResourceInterface[] $data |
35
|
|
|
* |
36
|
|
|
* @return self |
37
|
|
|
*/ |
38
|
15 |
|
public static function fromData($data) |
39
|
|
|
{ |
40
|
15 |
|
$document = new self; |
41
|
15 |
|
$document->setData($data); |
42
|
|
|
|
43
|
15 |
|
return $document; |
44
|
|
|
} |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @param array $meta |
48
|
|
|
* |
49
|
|
|
* @return self |
50
|
|
|
*/ |
51
|
6 |
|
public static function fromMeta(array $meta) |
52
|
|
|
{ |
53
|
6 |
|
$document = new self; |
54
|
6 |
|
$document->setMeta($meta); |
55
|
|
|
|
56
|
6 |
|
return $document; |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @param Error[] $errors |
61
|
|
|
* |
62
|
|
|
* @return self |
63
|
|
|
*/ |
64
|
3 |
|
public static function fromErrors(array $errors) |
65
|
|
|
{ |
66
|
3 |
|
$document = new self; |
67
|
3 |
|
$document->setErrors($errors); |
68
|
|
|
|
69
|
3 |
|
return $document; |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Set the primary data. |
74
|
|
|
* |
75
|
|
|
* @param ResourceInterface|ResourceInterface[]|null $data |
76
|
|
|
*/ |
77
|
15 |
|
public function setData($data) |
78
|
|
|
{ |
79
|
15 |
|
$this->data = $data; |
80
|
15 |
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Set the errors array. |
84
|
|
|
* |
85
|
|
|
* @param Error[]|null $errors |
86
|
|
|
*/ |
87
|
3 |
|
public function setErrors(array $errors = null) |
88
|
|
|
{ |
89
|
3 |
|
$this->errors = $errors; |
90
|
3 |
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Set the jsonapi version. |
94
|
|
|
* |
95
|
|
|
* @param string $version |
96
|
|
|
*/ |
97
|
|
|
public function setApiVersion($version) |
98
|
|
|
{ |
99
|
|
|
$this->jsonapi['version'] = $version; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Set the jsonapi meta information. |
104
|
|
|
* |
105
|
|
|
* @param array $meta |
106
|
|
|
*/ |
107
|
|
|
public function setApiMeta(array $meta) |
108
|
|
|
{ |
109
|
|
|
$this->jsonapi['meta'] = $meta; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Set the relationship paths to include. |
114
|
|
|
* |
115
|
|
|
* @param string[] $include |
116
|
|
|
*/ |
117
|
3 |
|
public function setInclude($include) |
118
|
|
|
{ |
119
|
3 |
|
$this->include = $include; |
120
|
3 |
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Set the sparse fieldsets. |
124
|
|
|
* |
125
|
|
|
* @param array $fields |
126
|
|
|
*/ |
127
|
3 |
|
public function setFields($fields) |
128
|
|
|
{ |
129
|
3 |
|
$this->fields = $fields; |
130
|
3 |
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Serialize for JSON usage. |
134
|
|
|
* |
135
|
|
|
* @return array |
136
|
|
|
*/ |
137
|
24 |
|
public function jsonSerialize() |
138
|
|
|
{ |
139
|
|
|
$document = [ |
140
|
24 |
|
'links' => $this->links, |
141
|
24 |
|
'meta' => $this->meta, |
142
|
24 |
|
'errors' => $this->errors, |
143
|
24 |
|
'jsonapi' => $this->jsonapi |
144
|
24 |
|
]; |
145
|
|
|
|
146
|
24 |
|
if ($this->data) { |
147
|
15 |
|
$isCollection = is_array($this->data); |
148
|
15 |
|
$resources = $isCollection ? $this->data : [$this->data]; |
149
|
|
|
|
150
|
15 |
|
$map = $this->buildResourceMap($resources); |
151
|
|
|
|
152
|
15 |
|
$primary = $this->extractResourcesFromMap($map, $resources); |
153
|
|
|
|
154
|
15 |
|
$document['data'] = $isCollection ? $primary : $primary[0]; |
155
|
|
|
|
156
|
15 |
|
if ($map) { |
|
|
|
|
157
|
15 |
|
$document['included'] = call_user_func_array('array_merge', $map); |
158
|
15 |
|
} |
159
|
15 |
|
} |
160
|
|
|
|
161
|
24 |
|
return (object) array_filter($document); |
162
|
|
|
} |
163
|
|
|
|
164
|
15 |
|
private function buildResourceMap(array $resources) |
165
|
|
|
{ |
166
|
15 |
|
$map = []; |
167
|
|
|
|
168
|
15 |
|
$include = $this->buildRelationshipTree($this->include); |
169
|
|
|
|
170
|
15 |
|
$this->mergeResources($map, $resources, $include); |
171
|
|
|
|
172
|
15 |
|
return $map; |
173
|
|
|
} |
174
|
|
|
|
175
|
15 |
|
private function mergeResources(array &$map, array $resources, array $include) |
176
|
|
|
{ |
177
|
15 |
|
foreach ($resources as $resource) { |
178
|
15 |
|
$relationships = []; |
179
|
|
|
|
180
|
15 |
|
foreach ($include as $name => $nested) { |
181
|
3 |
|
if (! ($relationship = $resource->getRelationship($name))) { |
182
|
|
|
continue; |
183
|
|
|
} |
184
|
|
|
|
185
|
3 |
|
$relationships[$name] = $relationship; |
186
|
|
|
|
187
|
3 |
|
if ($data = $relationship->getData()) { |
188
|
3 |
|
$children = is_array($data) ? $data : [$data]; |
189
|
|
|
|
190
|
3 |
|
$this->mergeResources($map, $children, $nested); |
191
|
3 |
|
} |
192
|
15 |
|
} |
193
|
|
|
|
194
|
15 |
|
$this->mergeResource($map, $resource, $relationships); |
195
|
15 |
|
} |
196
|
15 |
|
} |
197
|
|
|
|
198
|
15 |
|
private function mergeResource(array &$map, ResourceInterface $resource, array $relationships) |
199
|
|
|
{ |
200
|
15 |
|
$type = $resource->getType(); |
201
|
15 |
|
$id = $resource->getId(); |
202
|
15 |
|
$links = $resource->getLinks(); |
203
|
15 |
|
$meta = $resource->getMeta(); |
204
|
|
|
|
205
|
15 |
|
$fields = isset($this->fields[$type]) ? $this->fields[$type] : null; |
206
|
|
|
|
207
|
15 |
|
$attributes = $resource->getAttributes($fields); |
208
|
|
|
|
209
|
15 |
|
if ($fields) { |
210
|
3 |
|
$keys = array_flip($fields); |
211
|
|
|
|
212
|
3 |
|
$attributes = array_intersect_key($attributes, $keys); |
213
|
3 |
|
$relationships = array_intersect_key($relationships, $keys); |
214
|
3 |
|
} |
215
|
|
|
|
216
|
15 |
|
if (empty($map[$type][$id])) { |
217
|
15 |
|
$map[$type][$id] = new ResourceObject($type, $id); |
218
|
15 |
|
} |
219
|
|
|
|
220
|
15 |
|
array_map([$map[$type][$id], 'setAttribute'], array_keys($attributes), $attributes); |
221
|
15 |
|
array_map([$map[$type][$id], 'setRelationship'], array_keys($relationships), $relationships); |
222
|
15 |
|
array_map([$map[$type][$id], 'setLink'], array_keys($links), $links); |
223
|
15 |
|
array_map([$map[$type][$id], 'setMetaItem'], array_keys($meta), $meta); |
224
|
15 |
|
} |
225
|
|
|
|
226
|
15 |
|
private function extractResourcesFromMap(array &$map, array $resources) |
227
|
|
|
{ |
228
|
15 |
|
return array_filter( |
229
|
15 |
|
array_map(function ($resource) use (&$map) { |
230
|
15 |
|
$type = $resource->getType(); |
231
|
15 |
|
$id = $resource->getId(); |
232
|
|
|
|
233
|
15 |
|
if (isset($map[$type][$id])) { |
234
|
15 |
|
$resource = $map[$type][$id]; |
235
|
15 |
|
unset($map[$type][$id]); |
236
|
|
|
|
237
|
15 |
|
return $resource; |
238
|
|
|
} |
239
|
15 |
|
}, $resources) |
240
|
15 |
|
); |
241
|
|
|
} |
242
|
|
|
|
243
|
15 |
|
private function buildRelationshipTree(array $paths) |
244
|
|
|
{ |
245
|
15 |
|
$tree = []; |
246
|
|
|
|
247
|
15 |
|
foreach ($paths as $path) { |
248
|
3 |
|
$keys = explode('.', $path); |
249
|
3 |
|
$array = &$tree; |
250
|
|
|
|
251
|
3 |
|
foreach ($keys as $key) { |
252
|
3 |
|
if (! isset($array[$key])) { |
253
|
3 |
|
$array[$key] = []; |
254
|
3 |
|
} |
255
|
|
|
|
256
|
3 |
|
$array = &$array[$key]; |
257
|
3 |
|
} |
258
|
15 |
|
} |
259
|
|
|
|
260
|
15 |
|
return $tree; |
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.