Test Failed
Pull Request — main (#64)
by Lode
03:16
created

Document::addMeta()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 12
c 0
b 0
f 0
nc 6
nop 3
dl 0
loc 20
rs 9.2222
1
<?php
2
3
namespace alsvanzelf\jsonapi;
4
5
use alsvanzelf\jsonapi\exceptions\Exception;
1 ignored issue
show
Bug introduced by
This use statement conflicts with another class in this namespace, alsvanzelf\jsonapi\Exception. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
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
	public function __construct() {
79
		$this->setHttpStatusCode(200);
80
		$this->setJsonapiObject(new JsonapiObject());
81
	}
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
	public function addLink($key, $href, array $meta=[], $level=Document::LEVEL_ROOT) {
96
		if ($level === Document::LEVEL_ROOT) {
97
			if ($this->links === null) {
98
				$this->setLinksObject(new LinksObject());
99
			}
100
			
101
			$this->links->add($key, $href, $meta);
102
		}
103
		elseif ($level === Document::LEVEL_JSONAPI) {
104
			throw new InputException('level "jsonapi" can not be used for links');
105
		}
106
		elseif ($level === Document::LEVEL_RESOURCE) {
107
			throw new InputException('level "resource" can only be set on a ResourceDocument');
108
		}
109
		else {
110
			throw new InputException('unknown level "'.$level.'"');
111
		}
112
	}
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
	public function setSelfLink($href, array $meta=[], $level=Document::LEVEL_ROOT) {
122
		$this->addLink('self', $href, $meta, $level);
123
	}
124
	
125
	/**
126
	 * @param string $key
127
	 * @param mixed  $value
128
	 * @param string $level one of the Document::LEVEL_* constants, optional, defaults to Document::LEVEL_ROOT
129
	 * 
130
	 * @throws InputException if the $level is unknown
131
	 * @throws InputException if the $level is Document::LEVEL_RESOURCE
132
	 */
133
	public function addMeta($key, $value, $level=Document::LEVEL_ROOT) {
134
		if ($level === Document::LEVEL_ROOT) {
135
			if ($this->meta === null) {
136
				$this->setMetaObject(new MetaObject());
137
			}
138
			
139
			$this->meta->add($key, $value);
140
		}
141
		elseif ($level === Document::LEVEL_JSONAPI) {
142
			if ($this->jsonapi === null) {
143
				$this->setJsonapiObject(new JsonapiObject());
144
			}
145
			
146
			$this->jsonapi->addMeta($key, $value);
147
		}
148
		elseif ($level === Document::LEVEL_RESOURCE) {
149
			throw new InputException('level "resource" can only be set on a ResourceDocument');
150
		}
151
		else {
152
			throw new InputException('unknown level "'.$level.'"');
153
		}
154
	}
155
	
156
	/**
157
	 * spec api
158
	 */
159
	
160
	/**
161
	 * @param MetaObject $metaObject
162
	 */
163
	public function setMetaObject(MetaObject $metaObject) {
164
		$this->meta = $metaObject;
165
	}
166
	
167
	/**
168
	 * @param JsonapiObject $jsonapiObject
169
	 */
170
	public function setJsonapiObject(JsonapiObject $jsonapiObject) {
171
		$this->jsonapi = $jsonapiObject;
172
	}
173
	
174
	/**
175
	 * hide that this api supports jsonapi, or which version it is using
176
	 */
177
	public function unsetJsonapiObject() {
178
		$this->jsonapi = null;
179
	}
180
	
181
	/**
182
	 * apply a profile which adds the link and sets a correct content-type
183
	 * 
184
	 * note that the rules from the profile are not automatically enforced
185
	 * applying the rules, and applying them correctly, is manual
186
	 * however the $profile could have custom methods to help
187
	 * 
188
	 * @see https://jsonapi.org/format/1.1/#profiles
189
	 * 
190
	 * @param ProfileInterface $profile
191
	 */
192
	public function applyProfile(ProfileInterface $profile) {
193
		$this->profiles[] = $profile;
194
		
195
		if ($this->links === null) {
196
			$this->setLinksObject(new LinksObject());
197
		}
198
		
199
		$link = $profile->getAliasedLink();
200
		if ($link instanceof LinkObject) {
201
			$this->links->appendLinkObject('profile', $link);
202
		}
203
		else {
204
			$this->links->append('profile', $link);
205
		}
206
	}
207
	
208
	/**
209
	 * DocumentInterface
210
	 */
211
	
212
	/**
213
	 * @inheritDoc
214
	 */
215
	public function toArray() {
216
		$array = $this->getAtMembers();
217
		
218
		if ($this->jsonapi !== null && $this->jsonapi->isEmpty() === false) {
219
			$array['jsonapi'] = $this->jsonapi->toArray();
220
		}
221
		if ($this->links !== null && $this->links->isEmpty() === false) {
222
			$array['links'] = $this->links->toArray();
223
		}
224
		if ($this->meta !== null && $this->meta->isEmpty() === false) {
225
			$array['meta'] = $this->meta->toArray();
226
		}
227
		
228
		return $array;
229
	}
230
	
231
	/**
232
	 * @inheritDoc
233
	 */
234
	public function toJson(array $options=[]) {
235
		$options = array_merge(self::$defaults, $options);
236
		
237
		$array = ($options['array'] !== null) ? $options['array'] : $this->toArray();
238
		
239
		if ($options['prettyPrint']) {
240
			$options['encodeOptions'] |= JSON_PRETTY_PRINT;
241
		}
242
		
243
		$json = json_encode($array, $options['encodeOptions']);
244
		if ($json === false) {
245
			throw new Exception('failed to generate json: '.json_last_error_msg());
246
		}
247
		
248
		if ($options['jsonpCallback'] !== null) {
249
			$json = $options['jsonpCallback'].'('.$json.')';
250
		}
251
		
252
		return $json;
253
	}
254
	
255
	/**
256
	 * @inheritDoc
257
	 */
258
	public function sendResponse(array $options=[]) {
259
		$options = array_merge(self::$defaults, $options);
260
		
261
		if ($this->httpStatusCode === 204) {
262
			http_response_code($this->httpStatusCode);
263
			return;
264
		}
265
		
266
		$json = ($options['json'] !== null) ? $options['json'] : $this->toJson($options);
267
		
268
		http_response_code($this->httpStatusCode);
269
		
270
		$contentType = Converter::mergeProfilesInContentType($options['contentType'], $this->profiles);
271
		header('Content-Type: '.$contentType);
272
		
273
		echo $json;
274
	}
275
	
276
	/**
277
	 * JsonSerializable
278
	 */
279
	
280
	public function jsonSerialize() {
281
		return $this->toArray();
282
	}
283
}
284