Test Failed
Pull Request — main (#64)
by Lode
08:15
created

RequestParser   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 320
Duplicated Lines 6.25 %

Coupling/Cohesion

Components 2
Dependencies 4

Importance

Changes 0
Metric Value
wmc 45
lcom 2
cbo 4
dl 20
loc 320
rs 8.8
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
B fromSuperglobals() 0 24 9
A fromPsrRequest() 0 24 4
A getSelfLink() 0 3 1
A hasIncludePaths() 0 3 1
A getIncludePaths() 0 23 4
A hasSparseFieldset() 0 3 1
A getSparseFieldset() 0 7 2
A hasSortFields() 0 3 1
A getSortFields() 0 25 4
A hasPagination() 0 3 1
A getPagination() 0 3 1
A hasFilter() 0 3 1
A getFilter() 0 3 1
A hasAttribute() 10 10 3
A getAttribute() 0 3 1
A hasRelationship() 10 10 3
A getRelationship() 0 3 1
A hasMeta() 0 10 3
A getMeta() 0 3 1
A getDocument() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like RequestParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RequestParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace alsvanzelf\jsonapi\helpers;
4
5
use alsvanzelf\jsonapi\Document;
6
use Psr\Http\Message\RequestInterface;
7
use Psr\Http\Message\ServerRequestInterface;
8
9
class RequestParser {
0 ignored issues
show
Coding Style introduced by
Missing doc comment for class RequestParser
Loading history...
10
	const SORT_ASCENDING  = 'ascending';
11
	const SORT_DESCENDING = 'descending';
12
	
13
	/** @var array */
14
	protected static $defaults = [
15
		/**
16
		 * reformat the include query parameter paths to nested arrays
17
		 * this allows easier processing on each step of the chain
18
		 */
19
		'useNestedIncludePaths' => true,
20
		
21
		/**
22
		 * reformat the sort query parameter paths to separate the sort order
23
		 * this allows easier processing of sort orders and field names
24
		 */
25
		'useAnnotatedSortFields' => true,
26
	];
27
	/** @var string */
28
	private $selfLink = '';
29
	/** @var array */
30
	private $queryParameters = [];
31
	/** @var array */
32
	private $document = [];
33
	
34
	/**
35
	 * @param string $selfLink        the uri used to make this request {@see getSelfLink()}
36
	 * @param array  $queryParameters all query parameters defined by the specification
37
	 * @param array  $document        the request jsonapi document
38
	 */
39
	public function __construct($selfLink='', array $queryParameters=[], array $document=[]) {
40
		$this->selfLink        = $selfLink;
41
		$this->queryParameters = $queryParameters;
42
		$this->document        = $document;
43
	}
44
	
45
	/**
46
	 * @return self
47
	 */
48
	public static function fromSuperglobals() {
49
		$selfLink = '';
50
		if (isset($_SERVER['REQUEST_SCHEME']) && isset($_SERVER['HTTP_HOST']) && isset($_SERVER['REQUEST_URI'])) {
51
			$selfLink = $_SERVER['REQUEST_SCHEME'].'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
52
		}
53
		
54
		$queryParameters = $_GET;
55
		
56
		$document = $_POST;
57
		if ($document === [] && isset($_SERVER['CONTENT_TYPE'])) {
58
			$documentIsJsonapi = (strpos($_SERVER['CONTENT_TYPE'], Document::CONTENT_TYPE_OFFICIAL) !== false);
59
			$documentIsJson    = (strpos($_SERVER['CONTENT_TYPE'], Document::CONTENT_TYPE_DEBUG)    !== false);
60
			
61
			if ($documentIsJsonapi || $documentIsJson) {
62
				$document = json_decode(file_get_contents('php://input'), true);
63
				
64
				if ($document === null) {
65
					$document = [];
66
				}
67
			}
68
		}
69
		
70
		return new self($selfLink, $queryParameters, $document);
71
	}
72
	
73
	/**
74
	 * @param  ServerRequestInterface|RequestInterface $request
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
75
	 * @return self
76
	 */
77
	public static function fromPsrRequest(RequestInterface $request) {
78
		$selfLink = (string) $request->getUri();
79
		
80
		if ($request instanceof ServerRequestInterface) {
81
			$queryParameters = $request->getQueryParams();
82
		}
83
		else {
84
			$queryParameters = [];
85
			parse_str($request->getUri()->getQuery(), $queryParameters);
86
		}
87
		
88
		if ($request->getBody()->getContents() === '') {
89
			$document = [];
90
		}
91
		else {
92
			$document = json_decode($request->getBody()->getContents(), true);
93
			
94
			if ($document === null) {
95
				$document = [];
96
			}
97
		}
98
		
99
		return new self($selfLink, $queryParameters, $document);
0 ignored issues
show
Bug introduced by
It seems like $queryParameters can also be of type null; however, alsvanzelf\jsonapi\helpe...stParser::__construct() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
100
	}
101
	
102
	/**
103
	 * the full link used to make this request
104
	 * 
105
	 * this is not a bare self link of a resource and includes query parameters if used
106
	 * 
107
	 * @return string
108
	 */
109
	public function getSelfLink() {
110
		return $this->selfLink;
111
	}
112
	
113
	/**
114
	 * @return boolean
115
	 */
116
	public function hasIncludePaths() {
117
		return isset($this->queryParameters['include']);
118
	}
119
	
120
	/**
121
	 * returns a nested array based on the path, or the raw paths
122
	 * 
123
	 * the nested format allows easier processing on each step of the chain
124
	 * the raw format allows for custom processing
125
	 * 
126
	 * @param  array $options optional {@see RequestParser::$defaults}
127
	 * @return string[]|array
128
	 */
129
	public function getIncludePaths(array $options=[]) {
130
		$includePaths = explode(',', $this->queryParameters['include']);
131
		
132
		$options = array_merge(self::$defaults, $options);
133
		if ($options['useNestedIncludePaths'] === false) {
134
			return $includePaths;
135
		}
136
		
137
		$restructured = [];
138
		foreach ($includePaths as $path) {
139
			$steps = explode('.', $path);
140
			
141
			$wrapped = [];
142
			while ($steps !== []) {
143
				$lastStep = array_pop($steps);
144
				$wrapped  = [$lastStep => $wrapped];
145
			}
146
			
147
			$restructured = array_merge_recursive($restructured, $wrapped);
148
		}
149
		
150
		return $restructured;
151
	}
152
	
153
	/**
154
	 * @param  string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
155
	 * @return boolean
156
	 */
157
	public function hasSparseFieldset($type) {
158
		return isset($this->queryParameters['fields'][$type]);
159
	}
160
	
161
	/**
162
	 * @param  string $type
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
163
	 * @return string[]
164
	 */
165
	public function getSparseFieldset($type) {
166
		if ($this->queryParameters['fields'][$type] === '') {
167
			return [];
168
		}
169
		
170
		return explode(',', $this->queryParameters['fields'][$type]);
171
	}
172
	
173
	/**
174
	 * @return boolean
175
	 */
176
	public function hasSortFields() {
177
		return isset($this->queryParameters['sort']);
178
	}
179
	
180
	/**
181
	 * returns an array with sort order annotations, or the raw sort fields with minus signs
182
	 * 
183
	 * the annotated format allows easier processing of sort orders and field names
184
	 * the raw format allows for custom processing
185
	 * 
186
	 * @todo return some kind of SortFieldObject
187
	 * 
188
	 * @param  array $options optional {@see RequestParser::$defaults}
189
	 * @return string[]|array[] {
190
	 *         @var string $field the sort field, without any minus sign for descending sort order
191
	 *         @var string $order one of the RequestParser::SORT_* constants
192
	 * }
193
	 */
194
	public function getSortFields(array $options=[]) {
195
		$fields = explode(',', $this->queryParameters['sort']);
196
		
197
		$options = array_merge(self::$defaults, $options);
198
		if ($options['useAnnotatedSortFields'] === false) {
199
			return $fields;
200
		}
201
		
202
		$sort = [];
203
		foreach ($fields as $field) {
204
			$order = RequestParser::SORT_ASCENDING;
205
			
206
			if (strpos($field, '-') === 0) {
207
				$field = substr($field, 1);
208
				$order = RequestParser::SORT_DESCENDING;
209
			}
210
			
211
			$sort[] = [
212
				'field' => $field,
213
				'order' => $order,
214
			];
215
		}
216
		
217
		return $sort;
218
	}
219
	
220
	/**
221
	 * @return boolean
222
	 */
223
	public function hasPagination() {
224
		return isset($this->queryParameters['page']);
225
	}
226
	
227
	/**
228
	 * @todo return some kind of PaginatorObject which recognizes the strategy of pagination used
229
	 *       e.g. page-based, offset-based, cursor-based, or unknown
230
	 * 
231
	 * @return array
232
	 */
233
	public function getPagination() {
234
		return $this->queryParameters['page'];
235
	}
236
	
237
	/**
238
	 * @return boolean
239
	 */
240
	public function hasFilter() {
241
		return isset($this->queryParameters['filter']);
242
	}
243
	
244
	/**
245
	 * @return array
246
	 */
247
	public function getFilter() {
248
		return $this->queryParameters['filter'];
249
	}
250
	
251
	/**
252
	 * @param  string $attributeName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
253
	 * @return boolean
254
	 */
255 View Code Duplication
	public function hasAttribute($attributeName) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
256
		if (isset($this->document['data']['attributes']) === false) {
257
			return false;
258
		}
259
		if (array_key_exists($attributeName, $this->document['data']['attributes']) === false) {
260
			return false;
261
		}
262
		
263
		return true;
264
	}
265
	
266
	/**
267
	 * @param  string $attributeName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
268
	 * @return mixed
269
	 */
270
	public function getAttribute($attributeName) {
271
		return $this->document['data']['attributes'][$attributeName];
272
	}
273
	
274
	/**
275
	 * @param  string $relationshipName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
276
	 * @return boolean
277
	 */
278 View Code Duplication
	public function hasRelationship($relationshipName) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
279
		if (isset($this->document['data']['relationships']) === false) {
280
			return false;
281
		}
282
		if (array_key_exists($relationshipName, $this->document['data']['relationships']) === false) {
283
			return false;
284
		}
285
		
286
		return true;
287
	}
288
	
289
	/**
290
	 * @todo return some kind of read-only ResourceIdentifierObject
291
	 * 
292
	 * @param  string $relationshipName
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
293
	 * @return array
294
	 */
295
	public function getRelationship($relationshipName) {
296
		return $this->document['data']['relationships'][$relationshipName];
297
	}
298
	
299
	/**
300
	 * @param  string $metaKey
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
301
	 * @return boolean
302
	 */
303
	public function hasMeta($metaKey) {
304
		if (isset($this->document['meta']) === false) {
305
			return false;
306
		}
307
		if (array_key_exists($metaKey, $this->document['meta']) === false) {
308
			return false;
309
		}
310
		
311
		return true;
312
	}
313
	
314
	/**
315
	 * @param  string $metaKey
0 ignored issues
show
Coding Style introduced by
Missing parameter comment
Loading history...
316
	 * @return mixed
317
	 */
318
	public function getMeta($metaKey) {
319
		return $this->document['meta'][$metaKey];
320
	}
321
	
322
	/**
323
	 * @return array
324
	 */
325
	public function getDocument() {
326
		return $this->document;
327
	}
328
}
329