Passed
Pull Request — main (#66)
by Lode
02:46
created

Document::addLink()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

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