Passed
Pull Request — main (#54)
by Lode
03:44
created

Document::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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