Completed
Branch master (62f6c6)
by
unknown
21:31
created

VirtualRESTServiceClient   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 248
Duplicated Lines 21.37 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
dl 53
loc 248
rs 8.8
c 0
b 0
f 0
wmc 36
lcom 1
cbo 2

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A mount() 8 8 3
A unmount() 8 8 3
B getMountAndService() 0 23 6
A run() 0 3 1
F runMulti() 37 135 22

How to fix   Duplicated Code   

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:

1
<?php
2
/**
3
 * Virtual HTTP service client
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
23
/**
24
 * Virtual HTTP service client loosely styled after a Virtual File System
25
 *
26
 * Services can be mounted on path prefixes so that virtual HTTP operations
27
 * against sub-paths will map to those services. Operations can actually be
28
 * done using HTTP messages over the wire or may simple be emulated locally.
29
 *
30
 * Virtual HTTP request maps are arrays that use the following format:
31
 *   - method   : GET/HEAD/PUT/POST/DELETE
32
 *   - url      : HTTP/HTTPS URL or virtual service path with a registered prefix
33
 *   - query    : <query parameter field/value associative array> (uses RFC 3986)
34
 *   - headers  : <header name/value associative array>
35
 *   - body     : source to get the HTTP request body from;
36
 *                this can simply be a string (always), a resource for
37
 *                PUT requests, and a field/value array for POST request;
38
 *                array bodies are encoded as multipart/form-data and strings
39
 *                use application/x-www-form-urlencoded (headers sent automatically)
40
 *   - stream   : resource to stream the HTTP response body to
41
 * Request maps can use integer index 0 instead of 'method' and 1 instead of 'url'.
42
 *
43
 * @author Aaron Schulz
44
 * @since 1.23
45
 */
46
class VirtualRESTServiceClient {
47
	/** @var MultiHttpClient */
48
	protected $http;
49
	/** @var VirtualRESTService[] Map of (prefix => VirtualRESTService) */
50
	protected $instances = [];
51
52
	const VALID_MOUNT_REGEX = '#^/[0-9a-z]+/([0-9a-z]+/)*$#';
53
54
	/**
55
	 * @param MultiHttpClient $http
56
	 */
57
	public function __construct( MultiHttpClient $http ) {
58
		$this->http = $http;
59
	}
60
61
	/**
62
	 * Map a prefix to service handler
63
	 *
64
	 * @param string $prefix Virtual path
65
	 * @param VirtualRESTService $instance
66
	 */
67 View Code Duplication
	public function mount( $prefix, VirtualRESTService $instance ) {
68
		if ( !preg_match( self::VALID_MOUNT_REGEX, $prefix ) ) {
69
			throw new UnexpectedValueException( "Invalid service mount point '$prefix'." );
70
		} elseif ( isset( $this->instances[$prefix] ) ) {
71
			throw new UnexpectedValueException( "A service is already mounted on '$prefix'." );
72
		}
73
		$this->instances[$prefix] = $instance;
74
	}
75
76
	/**
77
	 * Unmap a prefix to service handler
78
	 *
79
	 * @param string $prefix Virtual path
80
	 */
81 View Code Duplication
	public function unmount( $prefix ) {
82
		if ( !preg_match( self::VALID_MOUNT_REGEX, $prefix ) ) {
83
			throw new UnexpectedValueException( "Invalid service mount point '$prefix'." );
84
		} elseif ( !isset( $this->instances[$prefix] ) ) {
85
			throw new UnexpectedValueException( "No service is mounted on '$prefix'." );
86
		}
87
		unset( $this->instances[$prefix] );
88
	}
89
90
	/**
91
	 * Get the prefix and service that a virtual path is serviced by
92
	 *
93
	 * @param string $path
94
	 * @return array (prefix,VirtualRESTService) or (null,null) if none found
95
	 */
96
	public function getMountAndService( $path ) {
97
		$cmpFunc = function( $a, $b ) {
98
			$al = substr_count( $a, '/' );
99
			$bl = substr_count( $b, '/' );
100
			if ( $al === $bl ) {
101
				return 0; // should not actually happen
102
			}
103
			return ( $al < $bl ) ? 1 : -1; // largest prefix first
104
		};
105
106
		$matches = []; // matching prefixes (mount points)
107
		foreach ( $this->instances as $prefix => $service ) {
108
			if ( strpos( $path, $prefix ) === 0 ) {
109
				$matches[] = $prefix;
110
			}
111
		}
112
		usort( $matches, $cmpFunc );
113
114
		// Return the most specific prefix and corresponding service
115
		return isset( $matches[0] )
116
			? [ $matches[0], $this->instances[$matches[0]] ]
117
			: [ null, null ];
118
	}
119
120
	/**
121
	 * Execute a virtual HTTP(S) request
122
	 *
123
	 * This method returns a response map of:
124
	 *   - code    : HTTP response code or 0 if there was a serious cURL error
125
	 *   - reason  : HTTP response reason (empty if there was a serious cURL error)
126
	 *   - headers : <header name/value associative array>
127
	 *   - body    : HTTP response body or resource (if "stream" was set)
128
	 *   - error   : Any cURL error string
129
	 * The map also stores integer-indexed copies of these values. This lets callers do:
130
	 * @code
131
	 *     list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $client->run( $req );
132
	 * @endcode
133
	 * @param array $req Virtual HTTP request maps
134
	 * @return array Response array for request
135
	 */
136
	public function run( array $req ) {
137
		return $this->runMulti( [ $req ] )[0];
138
	}
139
140
	/**
141
	 * Execute a set of virtual HTTP(S) requests concurrently
142
	 *
143
	 * A map of requests keys to response maps is returned. Each response map has:
144
	 *   - code    : HTTP response code or 0 if there was a serious cURL error
145
	 *   - reason  : HTTP response reason (empty if there was a serious cURL error)
146
	 *   - headers : <header name/value associative array>
147
	 *   - body    : HTTP response body or resource (if "stream" was set)
148
	 *   - error   : Any cURL error string
149
	 * The map also stores integer-indexed copies of these values. This lets callers do:
150
	 * @code
151
	 *     list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $responses[0];
152
	 * @endcode
153
	 *
154
	 * @param array $reqs Map of Virtual HTTP request maps
155
	 * @return array $reqs Map of corresponding response values with the same keys/order
156
	 * @throws Exception
157
	 */
158
	public function runMulti( array $reqs ) {
159
		foreach ( $reqs as $index => &$req ) {
160 View Code Duplication
			if ( isset( $req[0] ) ) {
161
				$req['method'] = $req[0]; // short-form
162
				unset( $req[0] );
163
			}
164 View Code Duplication
			if ( isset( $req[1] ) ) {
165
				$req['url'] = $req[1]; // short-form
166
				unset( $req[1] );
167
			}
168
			$req['chain'] = []; // chain or list of replaced requests
169
		}
170
		unset( $req ); // don't assign over this by accident
171
172
		$curUniqueId = 0;
173
		$armoredIndexMap = []; // (original index => new index)
174
175
		$doneReqs = []; // (index => request)
176
		$executeReqs = []; // (index => request)
177
		$replaceReqsByService = []; // (prefix => index => request)
178
		$origPending = []; // (index => 1) for original requests
179
180
		foreach ( $reqs as $origIndex => $req ) {
181
			// Re-index keys to consecutive integers (they will be swapped back later)
182
			$index = $curUniqueId++;
183
			$armoredIndexMap[$origIndex] = $index;
184
			$origPending[$index] = 1;
185
			if ( preg_match( '#^(http|ftp)s?://#', $req['url'] ) ) {
186
				// Absolute FTP/HTTP(S) URL, run it as normal
187
				$executeReqs[$index] = $req;
188
			} else {
189
				// Must be a virtual HTTP URL; resolve it
190
				list( $prefix, $service ) = $this->getMountAndService( $req['url'] );
191
				if ( !$service ) {
192
					throw new UnexpectedValueException( "Path '{$req['url']}' has no service." );
193
				}
194
				// Set the URL to the mount-relative portion
195
				$req['url'] = substr( $req['url'], strlen( $prefix ) );
196
				$replaceReqsByService[$prefix][$index] = $req;
197
			}
198
		}
199
200
		// Function to get IDs that won't collide with keys in $armoredIndexMap
201
		$idFunc = function() use ( &$curUniqueId ) {
202
			return $curUniqueId++;
203
		};
204
205
		$rounds = 0;
206
		do {
207
			if ( ++$rounds > 5 ) { // sanity
208
				throw new Exception( "Too many replacement rounds detected. Aborting." );
209
			}
210
			// Track requests executed this round that have a prefix/service.
211
			// Note that this also includes requests where 'response' was forced.
212
			$checkReqIndexesByPrefix = [];
213
			// Resolve the virtual URLs valid and qualified HTTP(S) URLs
214
			// and add any required authentication headers for the backend.
215
			// Services can also replace requests with new ones, either to
216
			// defer the original or to set a proxy response to the original.
217
			$newReplaceReqsByService = [];
218
			foreach ( $replaceReqsByService as $prefix => $servReqs ) {
219
				$service = $this->instances[$prefix];
220
				foreach ( $service->onRequests( $servReqs, $idFunc ) as $index => $req ) {
221
					// Services use unique IDs for replacement requests
222 View Code Duplication
					if ( isset( $servReqs[$index] ) || isset( $origPending[$index] ) ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
223
						// A current or original request which was not modified
224
					} else {
225
						// Replacement request that will convert to original requests
226
						$newReplaceReqsByService[$prefix][$index] = $req;
227
					}
228 View Code Duplication
					if ( isset( $req['response'] ) ) {
229
						// Replacement requests with pre-set responses should not execute
230
						unset( $executeReqs[$index] );
231
						unset( $origPending[$index] );
232
						$doneReqs[$index] = $req;
233
					} else {
234
						// Original or mangled request included
235
						$executeReqs[$index] = $req;
236
					}
237
					$checkReqIndexesByPrefix[$prefix][$index] = 1;
238
				}
239
			}
240
			// Update index of requests to inspect for replacement
241
			$replaceReqsByService = $newReplaceReqsByService;
0 ignored issues
show
Unused Code introduced by
$replaceReqsByService is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
242
			// Run the actual work HTTP requests
243
			foreach ( $this->http->runMulti( $executeReqs ) as $index => $ranReq ) {
244
				$doneReqs[$index] = $ranReq;
245
				unset( $origPending[$index] );
246
			}
247
			$executeReqs = [];
248
			// Services can also replace requests with new ones, either to
249
			// defer the original or to set a proxy response to the original.
250
			// Any replacement requests executed above will need to be replaced
251
			// with new requests (eventually the original). The responses can be
252
			// forced by setting 'response' rather than actually be sent over the wire.
253
			$newReplaceReqsByService = [];
254
			foreach ( $checkReqIndexesByPrefix as $prefix => $servReqIndexes ) {
255
				$service = $this->instances[$prefix];
256
				// $doneReqs actually has the requests (with 'response' set)
257
				$servReqs = array_intersect_key( $doneReqs, $servReqIndexes );
258
				foreach ( $service->onResponses( $servReqs, $idFunc ) as $index => $req ) {
259
					// Services use unique IDs for replacement requests
260 View Code Duplication
					if ( isset( $servReqs[$index] ) || isset( $origPending[$index] ) ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
261
						// A current or original request which was not modified
262
					} else {
263
						// Replacement requests with pre-set responses should not execute
264
						$newReplaceReqsByService[$prefix][$index] = $req;
265
					}
266 View Code Duplication
					if ( isset( $req['response'] ) ) {
267
						// Replacement requests with pre-set responses should not execute
268
						unset( $origPending[$index] );
269
						$doneReqs[$index] = $req;
270
					} else {
271
						// Update the request in case it was mangled
272
						$executeReqs[$index] = $req;
273
					}
274
				}
275
			}
276
			// Update index of requests to inspect for replacement
277
			$replaceReqsByService = $newReplaceReqsByService;
278
		} while ( count( $origPending ) );
279
280
		$responses = [];
281
		// Update $reqs to include 'response' and normalized request 'headers'.
282
		// This maintains the original order of $reqs.
283
		foreach ( $reqs as $origIndex => $req ) {
284
			$index = $armoredIndexMap[$origIndex];
285
			if ( !isset( $doneReqs[$index] ) ) {
286
				throw new UnexpectedValueException( "Response for request '$index' is NULL." );
287
			}
288
			$responses[$origIndex] = $doneReqs[$index]['response'];
289
		}
290
291
		return $responses;
292
	}
293
}
294