Passed
Push — main ( f8d78a...6afd83 )
by Lode
01:12 queued 12s
created

Document::toJson()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 10
c 1
b 0
f 0
nc 12
nop 1
dl 0
loc 19
ccs 11
cts 11
cp 1
crap 5
rs 9.6111
1
<?php
2
3
namespace alsvanzelf\jsonapi;
4
5
use alsvanzelf\jsonapi\exceptions\DuplicateException;
6
use alsvanzelf\jsonapi\exceptions\Exception;
7
use alsvanzelf\jsonapi\exceptions\InputException;
8
use alsvanzelf\jsonapi\helpers\AtMemberManager;
9
use alsvanzelf\jsonapi\helpers\Converter;
10
use alsvanzelf\jsonapi\helpers\ExtensionMemberManager;
11
use alsvanzelf\jsonapi\helpers\HttpStatusCodeManager;
12
use alsvanzelf\jsonapi\helpers\LinksManager;
13
use alsvanzelf\jsonapi\helpers\Validator;
14
use alsvanzelf\jsonapi\interfaces\DocumentInterface;
15
use alsvanzelf\jsonapi\interfaces\ExtensionInterface;
16
use alsvanzelf\jsonapi\interfaces\ProfileInterface;
17
use alsvanzelf\jsonapi\objects\JsonapiObject;
18
use alsvanzelf\jsonapi\objects\LinkObject;
19
use alsvanzelf\jsonapi\objects\LinksObject;
20
use alsvanzelf\jsonapi\objects\MetaObject;
21
22
/**
23
 * @see ResourceDocument, CollectionDocument, ErrorsDocument or MetaDocument
24
 */
25
abstract class Document implements DocumentInterface, \JsonSerializable {
26
	use AtMemberManager, ExtensionMemberManager, HttpStatusCodeManager, LinksManager {
27
		LinksManager::addLink as linkManagerAddLink;
28
	}
29
	
30
	const JSONAPI_VERSION_1_0 = '1.0';
31
	const JSONAPI_VERSION_1_1 = '1.1';
32
	const JSONAPI_VERSION_LATEST = Document::JSONAPI_VERSION_1_1;
33
	
34
	const CONTENT_TYPE_OFFICIAL = 'application/vnd.api+json';
35
	const CONTENT_TYPE_DEBUG    = 'application/json';
36
	const CONTENT_TYPE_JSONP    = 'application/javascript';
37
	
38
	const LEVEL_ROOT     = 'root';
39
	const LEVEL_JSONAPI  = 'jsonapi';
40
	const LEVEL_RESOURCE = 'resource';
41
	
42
	/** @var MetaObject */
43
	protected $meta;
44
	/** @var JsonapiObject */
45
	protected $jsonapi;
46
	/** @var ExtensionInterface[] */
47
	protected $extensions = [];
48
	/** @var ProfileInterface[] */
49
	protected $profiles = [];
50
	/** @var array */
51
	protected static $defaults = [
52
		/**
53
		 * encode to json with these default options
54
		 */
55
		'encodeOptions' => JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE,
56
		
57
		/**
58
		 * encode to human-readable json, useful when debugging
59
		 */
60
		'prettyPrint' => false,
61
		
62
		/**
63
		 * send out the official jsonapi content-type header
64
		 * overwrite for jsonp or if clients don't support it
65
		 */
66
		'contentType' => Document::CONTENT_TYPE_OFFICIAL,
67
		
68
		/**
69
		 * overwrite the array to encode to json
70
		 */
71
		'array' => null,
72
		
73
		/**
74
		 * overwrite the json to send as response
75
		 */
76
		'json' => null,
77
		
78
		/**
79
		 * set the callback for jsonp responses
80
		 */
81
		'jsonpCallback' => null,
82
	];
83
	
84 122
	public function __construct() {
85 122
		$this->setHttpStatusCode(200);
86 122
		$this->setJsonapiObject(new JsonapiObject());
87 122
	}
88
	
89
	/**
90
	 * human api
91
	 */
92
	
93
	/**
94
	 * @param string $key
95
	 * @param string $href
96
	 * @param array  $meta optional, if given a LinkObject is added, otherwise a link string is added
97
	 * @param string $level one of the Document::LEVEL_* constants, optional, defaults to Document::LEVEL_ROOT
98
	 * 
99
	 * @throws InputException if the $level is Document::LEVEL_JSONAPI, Document::LEVEL_RESOURCE, or unknown
100
	 */
101 20
	public function addLink($key, $href, array $meta=[], $level=Document::LEVEL_ROOT) {
102 20
		if ($level === Document::LEVEL_ROOT) {
103 17
			$this->linkManagerAddLink($key, $href, $meta);
104
		}
105 3
		elseif ($level === Document::LEVEL_JSONAPI) {
106 1
			throw new InputException('level "jsonapi" can not be used for links');
107
		}
108 2
		elseif ($level === Document::LEVEL_RESOURCE) {
109 1
			throw new InputException('level "resource" can only be set on a ResourceDocument');
110
		}
111
		else {
112 1
			throw new InputException('unknown level "'.$level.'"');
113
		}
114 17
	}
115
	
116
	/**
117
	 * set the self link on the document
118
	 * 
119
	 * @note a LinkObject is added when extensions or profiles are applied
120
	 * 
121
	 * @param string $href
122
	 * @param array  $meta optional, if given a LinkObject is added, otherwise a link string is added
123
	 * @param string $level one of the Document::LEVEL_* constants, optional, defaults to Document::LEVEL_ROOT
124
	 */
125 7
	public function setSelfLink($href, array $meta=[], $level=Document::LEVEL_ROOT) {
126 7
		if ($level === Document::LEVEL_ROOT && ($this->extensions !== [] || $this->profiles !== [])) {
127 3
			$contentType = Converter::prepareContentType(Document::CONTENT_TYPE_OFFICIAL, $this->extensions, $this->profiles);
128
			
129 3
			$linkObject = new LinkObject($href, $meta);
130 3
			$linkObject->setMediaType($contentType);
131
			
132 3
			$this->addLinkObject('self', $linkObject);
133
		}
134
		else {
135 4
			$this->addLink('self', $href, $meta, $level);
136
		}
137 7
	}
138
	
139
	/**
140
	 * set a link describing the current document
141
	 * 
142
	 * for example this could link to an OpenAPI or JSON Schema document
143
	 * 
144
	 * @note according to the spec, this can only be set to Document::LEVEL_ROOT
145
	 * 
146
	 * @param string $href
147
	 * @param array  $meta optional, if given a LinkObject is added, otherwise a link string is added
148
	 */
149 2
	public function setDescribedByLink($href, array $meta=[]) {
150 2
		$this->addLink('describedby', $href, $meta, $level=Document::LEVEL_ROOT);
151 2
	}
152
	
153
	/**
154
	 * @param string $key
155
	 * @param mixed  $value
156
	 * @param string $level one of the Document::LEVEL_* constants, optional, defaults to Document::LEVEL_ROOT
157
	 * 
158
	 * @throws InputException if the $level is unknown
159
	 * @throws InputException if the $level is Document::LEVEL_RESOURCE
160
	 */
161 15
	public function addMeta($key, $value, $level=Document::LEVEL_ROOT) {
162 15
		if ($level === Document::LEVEL_ROOT) {
163 11
			if ($this->meta === null) {
164 11
				$this->setMetaObject(new MetaObject());
165
			}
166
			
167 11
			$this->meta->add($key, $value);
168
		}
169 5
		elseif ($level === Document::LEVEL_JSONAPI) {
170 3
			if ($this->jsonapi === null) {
171 1
				$this->setJsonapiObject(new JsonapiObject());
172
			}
173
			
174 3
			$this->jsonapi->addMeta($key, $value);
175
		}
176 2
		elseif ($level === Document::LEVEL_RESOURCE) {
177 1
			throw new InputException('level "resource" can only be set on a ResourceDocument');
178
		}
179
		else {
180 1
			throw new InputException('unknown level "'.$level.'"');
181
		}
182 13
	}
183
	
184
	/**
185
	 * spec api
186
	 */
187
	
188
	/**
189
	 * @param MetaObject $metaObject
190
	 */
191 16
	public function setMetaObject(MetaObject $metaObject) {
192 16
		$this->meta = $metaObject;
193 16
	}
194
	
195
	/**
196
	 * @param JsonapiObject $jsonapiObject
197
	 */
198 122
	public function setJsonapiObject(JsonapiObject $jsonapiObject) {
199 122
		$this->jsonapi = $jsonapiObject;
200 122
	}
201
	
202
	/**
203
	 * hide that this api supports jsonapi, or which version it is using
204
	 */
205 2
	public function unsetJsonapiObject() {
206 2
		$this->jsonapi = null;
207 2
	}
208
	
209
	/**
210
	 * apply a extension which adds the link and sets a correct content-type
211
	 * 
212
	 * note that the rules from the extension are not automatically enforced
213
	 * applying the rules, and applying them correctly, is manual
214
	 * however the $extension could have custom methods to help
215
	 * 
216
	 * @see https://jsonapi.org/extensions/#extensions
217
	 * 
218
	 * @param ExtensionInterface $extension
219
	 * 
220
	 * @throws Exception if namespace uses illegal characters
221
	 * @throws DuplicateException if namespace conflicts with another applied extension
222
	 */
223 8
	public function applyExtension(ExtensionInterface $extension) {
224 8
		$namespace = $extension->getNamespace();
225 8
		if (strlen($namespace) < 1 || preg_match('{[^a-zA-Z0-9]}', $namespace) === 1) {
226 1
			throw new Exception('invalid namespace "'.$namespace.'"');
227
		}
228 7
		if (isset($this->extensions[$namespace])) {
229 1
			throw new DuplicateException('an extension with namespace "'.$namespace.'" is already applied');
230
		}
231
		
232 7
		$this->extensions[$namespace] = $extension;
233
		
234 7
		if ($this->jsonapi !== null) {
235 7
			$this->jsonapi->addExtension($extension);
236
		}
237 7
	}
238
	
239
	/**
240
	 * apply a profile which adds the link and sets a correct content-type
241
	 * 
242
	 * note that the rules from the profile are not automatically enforced
243
	 * applying the rules, and applying them correctly, is manual
244
	 * however the $profile could have custom methods to help
245
	 * 
246
	 * @see https://jsonapi.org/extensions/#profiles
247
	 * 
248
	 * @param ProfileInterface $profile
249
	 */
250 11
	public function applyProfile(ProfileInterface $profile) {
251 11
		$this->profiles[] = $profile;
252
		
253 11
		if ($this->jsonapi !== null) {
254 11
			$this->jsonapi->addProfile($profile);
255
		}
256 11
	}
257
	
258
	/**
259
	 * DocumentInterface
260
	 */
261
	
262
	/**
263
	 * @inheritDoc
264
	 */
265 87
	public function toArray() {
266 87
		$array = [];
267
		
268 87
		if ($this->hasAtMembers()) {
269 2
			$array = array_merge($array, $this->getAtMembers());
270
		}
271 87
		if ($this->hasExtensionMembers()) {
272 4
			$array = array_merge($array, $this->getExtensionMembers());
273
		}
274
		
275 87
		if ($this->jsonapi !== null && $this->jsonapi->isEmpty() === false) {
276 86
			$array['jsonapi'] = $this->jsonapi->toArray();
277
		}
278 87
		if ($this->links !== null && $this->links->isEmpty() === false) {
279 29
			$array['links'] = $this->links->toArray();
280
		}
281 87
		if ($this->meta !== null && $this->meta->isEmpty() === false) {
282 16
			$array['meta'] = $this->meta->toArray();
283
		}
284
		
285 87
		return $array;
286
	}
287
	
288
	/**
289
	 * @inheritDoc
290
	 */
291 34
	public function toJson(array $options=[]) {
292 34
		$options = array_merge(self::$defaults, $options);
293
		
294 34
		$array = ($options['array'] !== null) ? $options['array'] : $this->toArray();
295
		
296 34
		if ($options['prettyPrint']) {
297 22
			$options['encodeOptions'] |= JSON_PRETTY_PRINT;
298
		}
299
		
300 34
		$json = json_encode($array, $options['encodeOptions']);
301 34
		if ($json === false) {
302 1
			throw new Exception('failed to generate json: '.json_last_error_msg());
303
		}
304
		
305 33
		if ($options['jsonpCallback'] !== null) {
306 1
			$json = $options['jsonpCallback'].'('.$json.')';
307
		}
308
		
309 33
		return $json;
310
	}
311
	
312
	/**
313
	 * @inheritDoc
314
	 */
315 7
	public function sendResponse(array $options=[]) {
316 7
		$options = array_merge(self::$defaults, $options);
317
		
318 7
		if ($this->httpStatusCode === 204) {
319 1
			http_response_code($this->httpStatusCode);
320 1
			return;
321
		}
322
		
323 6
		$json = ($options['json'] !== null) ? $options['json'] : $this->toJson($options);
324
		
325 6
		http_response_code($this->httpStatusCode);
326
		
327 6
		$contentType = Converter::prepareContentType($options['contentType'], $this->extensions, $this->profiles);
328 6
		header('Content-Type: '.$contentType);
329
		
330 6
		echo $json;
331 6
	}
332
	
333
	/**
334
	 * JsonSerializable
335
	 */
336
	
337 1
	public function jsonSerialize() {
338 1
		return $this->toArray();
339
	}
340
}
341