50 private $instances = [];
52 private const VALID_MOUNT_REGEX =
'#^/[0-9a-z]+/([0-9a-z]+/)*$#';
71 public function mount( $prefix, $instance ) {
72 if ( !preg_match( self::VALID_MOUNT_REGEX, $prefix ) ) {
73 throw new UnexpectedValueException(
"Invalid service mount point '$prefix'." );
74 } elseif ( isset( $this->instances[$prefix] ) ) {
75 throw new UnexpectedValueException(
"A service is already mounted on '$prefix'." );
78 if ( !isset( $instance[
'class'] ) || !isset( $instance[
'config'] ) ) {
79 throw new UnexpectedValueException(
"Missing 'class' or 'config' ('$prefix')." );
82 $this->instances[$prefix] = $instance;
91 if ( !preg_match( self::VALID_MOUNT_REGEX, $prefix ) ) {
92 throw new UnexpectedValueException(
"Invalid service mount point '$prefix'." );
93 } elseif ( !isset( $this->instances[$prefix] ) ) {
94 throw new UnexpectedValueException(
"No service is mounted on '$prefix'." );
96 unset( $this->instances[$prefix] );
106 $cmpFunc =
static function ( $a, $b ) {
107 $al = substr_count( $a,
'/' );
108 $bl = substr_count( $b,
'/' );
113 foreach ( $this->instances as $prefix => $unused ) {
114 if ( strpos(
$path, $prefix ) === 0 ) {
142 public function run( array $req ) {
143 return $this->
runMulti( [ $req ] )[0];
165 foreach ( $reqs as $index => &$req ) {
166 if ( isset( $req[0] ) ) {
167 $req[
'method'] = $req[0];
170 if ( isset( $req[1] ) ) {
171 $req[
'url'] = $req[1];
179 $armoredIndexMap = [];
183 $replaceReqsByService = [];
186 foreach ( $reqs as $origIndex => $req ) {
188 $index = $curUniqueId++;
189 $armoredIndexMap[$origIndex] = $index;
190 $origPending[$index] = 1;
191 if ( preg_match(
'#^(http|ftp)s?://#', $req[
'url'] ) ) {
193 $executeReqs[$index] = $req;
198 throw new UnexpectedValueException(
"Path '{$req['url']}' has no service." );
201 $req[
'url'] = substr( $req[
'url'], strlen( $prefix ) );
202 $replaceReqsByService[$prefix][$index] = $req;
207 $idFunc =
static function () use ( &$curUniqueId ) {
208 return $curUniqueId++;
213 if ( ++$rounds > 5 ) {
214 throw new Exception(
"Too many replacement rounds detected. Aborting." );
218 $checkReqIndexesByPrefix = [];
223 $newReplaceReqsByService = [];
224 foreach ( $replaceReqsByService as $prefix => $servReqs ) {
225 $service = $this->getInstance( $prefix );
226 foreach ( $service->onRequests( $servReqs, $idFunc ) as $index => $req ) {
228 if ( isset( $servReqs[$index] ) || isset( $origPending[$index] ) ) {
232 $newReplaceReqsByService[$prefix][$index] = $req;
234 if ( isset( $req[
'response'] ) ) {
236 unset( $executeReqs[$index] );
237 unset( $origPending[$index] );
238 $doneReqs[$index] = $req;
241 $executeReqs[$index] = $req;
243 $checkReqIndexesByPrefix[$prefix][$index] = 1;
250 foreach ( $executeReqs as $index => &$req ) {
252 if ( preg_match(
'#^//#', $req[
'url'] ) ) {
261 isset( $req[
'reqTimeout'] ) &&
262 ( !isset( $opts[
'reqTimeout'] ) ||
263 $req[
'reqTimeout'] < $opts[
'reqTimeout'] )
265 $opts[
'reqTimeout'] = $req[
'reqTimeout'];
270 foreach ( $this->http->runMulti( $executeReqs, $opts ) as $index => $ranReq ) {
271 $doneReqs[$index] = $ranReq;
272 unset( $origPending[$index] );
280 $newReplaceReqsByService = [];
281 foreach ( $checkReqIndexesByPrefix as $prefix => $servReqIndexes ) {
282 $service = $this->getInstance( $prefix );
284 $servReqs = array_intersect_key( $doneReqs, $servReqIndexes );
285 foreach ( $service->onResponses( $servReqs, $idFunc ) as $index => $req ) {
287 if ( isset( $servReqs[$index] ) || isset( $origPending[$index] ) ) {
291 $newReplaceReqsByService[$prefix][$index] = $req;
293 if ( isset( $req[
'response'] ) ) {
295 unset( $origPending[$index] );
296 $doneReqs[$index] = $req;
299 $executeReqs[$index] = $req;
304 $replaceReqsByService = $newReplaceReqsByService;
305 }
while ( count( $origPending ) );
310 foreach ( $reqs as $origIndex => $req ) {
311 $index = $armoredIndexMap[$origIndex];
312 if ( !isset( $doneReqs[$index] ) ) {
313 throw new UnexpectedValueException(
"Response for request '$index' is NULL." );
315 $responses[$origIndex] = $doneReqs[$index][
'response'];
325 private function getInstance( $prefix ) {
326 if ( !isset( $this->instances[$prefix] ) ) {
327 throw new RuntimeException(
"No service registered at prefix '{$prefix}'." );
331 $config = $this->instances[$prefix][
'config'];
332 $class = $this->instances[$prefix][
'class'];
333 $service =
new $class( $config );
335 throw new UnexpectedValueException(
"Registered service has the wrong class." );
337 $this->instances[$prefix] = $service;
340 return $this->instances[$prefix];
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL using $wgServer (or one of its alternatives).
Class to handle multiple HTTP requests.
Virtual HTTP service client loosely styled after a Virtual File System.
mount( $prefix, $instance)
Map a prefix to service handler.
unmount( $prefix)
Unmap a prefix to service handler.
run(array $req)
Execute a virtual HTTP(S) request.
runMulti(array $reqs)
Execute a set of virtual HTTP(S) requests concurrently.
__construct(MultiHttpClient $http)
getMountAndService( $path)
Get the prefix and service that a virtual path is serviced by.
Virtual HTTP service instance that can be mounted on to a VirtualRESTService.