Created
April 20, 2023 15:19
-
-
Save g105b/73468b46f0aa2266d8f0a3f837eb72be to your computer and use it in GitHub Desktop.
Promise implementation and basic HTTP client in one file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
$http = new HHttp(); | |
//$http->fetch("https://api.github.com/orgs/phpgt/repos") | |
$http->fetch("https://raw.githubusercontent.com/PhpGt/Fetch/master/broken.json") | |
->then(function(RResponse $response) { | |
echo "Got a response!", PHP_EOL; | |
sleep(1); | |
return $response->json(); | |
})->then(function(object|array $json) { | |
$repoList = []; | |
foreach($json as $obj) { | |
array_push($repoList, $obj->name); | |
} | |
echo "Success! Repositories: ", PHP_EOL; | |
echo implode(", ", $repoList); | |
})->catch(function(Throwable $reason) { | |
echo "Caught the error: ", $reason->getMessage(), PHP_EOL; | |
}); | |
$http->run(); | |
class HHttp { | |
/** @var array<DDeferred> */ | |
private array $deferredArray; | |
public function __construct() { | |
$this->deferredArray = []; | |
} | |
/** @noinspection PhpComposerExtensionStubsInspection */ | |
public function fetch(string $url):PPromise { | |
$deferred = new DDeferred(); | |
$promise = $deferred->getPromise(); | |
$curl = curl_init($url); | |
$process = new PProcess(function()use($deferred, $curl) { | |
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); | |
curl_setopt($curl, CURLOPT_USERAGENT, "php.gt/fetch"); | |
$responseText = curl_exec($curl); | |
$deferred->resolve(new RResponse($responseText, $deferred)); | |
}); | |
$deferred->setProcess($process); | |
array_push($this->deferredArray, $deferred); | |
return $promise; | |
} | |
public function run():void { | |
do { | |
$numComplete = 0; | |
foreach($this->deferredArray as $deferred) { | |
$process = $deferred->getProcess(); | |
$process->tick(); | |
if($deferred->getState() !== PPromise::STATE_PENDING) { | |
$numComplete++; | |
} | |
} | |
echo "."; | |
usleep(100_000); | |
} | |
while($numComplete < count($this->deferredArray)); | |
} | |
} | |
class RResponse { | |
public function __construct( | |
private readonly string $responseText, | |
private readonly DDeferred $deferred, | |
) {} | |
public function json():PPromise { | |
try { | |
$obj = json_decode($this->responseText, flags: JSON_THROW_ON_ERROR); | |
$this->deferred->resolve($obj); | |
} | |
catch(Exception $e) { | |
$this->deferred->reject($e); | |
} | |
return $this->deferred->getPromise(); | |
} | |
} | |
class PPromise { | |
const STATE_RESOLVED = "resolved"; | |
const STATE_REJECTED = "rejected"; | |
const STATE_PENDING = "pending"; | |
private mixed $resolvedValue; | |
private Throwable $rejectedReason; | |
/** @var callable */ | |
private $executor; | |
/** @var array<TThen|CCatch|FFinally> */ | |
private array $chain; | |
// TODO: Enum this. | |
public string $state = self::STATE_PENDING; | |
public function __construct(callable $executor) { | |
$this->executor = $executor; | |
$this->chain = []; | |
$this->callExecutor(); | |
} | |
public function then(callable $onResolved):PPromise { | |
array_push($this->chain, new TThen($onResolved)); | |
$this->tryComplete(); | |
return $this; | |
} | |
public function catch(callable $onRejected):PPromise { | |
array_push($this->chain, new CCatch($onRejected)); | |
$this->tryComplete(); | |
return $this; | |
} | |
public function finally(callable $onComplete):PPromise { | |
array_push($this->chain, new FFinally($onComplete)); | |
$this->tryComplete(); | |
return $this; | |
} | |
private function callExecutor():void { | |
call_user_func( | |
$this->executor, | |
function(mixed $value = null):void { | |
$this->resolve($value); | |
}, | |
function(Throwable $reason):void { | |
$this->reject($reason); | |
}, | |
function():void { | |
$this->complete(); | |
} | |
); | |
} | |
private function resolve(mixed $value):void { | |
// TODO: The resolvedValue cannot be an instance of PPromise | |
$this->state = self::STATE_RESOLVED; | |
$this->resolvedValue = $value; | |
} | |
private function reject(Throwable $reason):void { | |
$this->state = self::STATE_REJECTED; | |
$this->rejectedReason = $reason; | |
} | |
protected function tryComplete():void { | |
if(isset($this->resolvedValue) || isset($this->rejectedReason)) { | |
$this->complete(); | |
} | |
} | |
private function complete():void { | |
usort( | |
$this->chain, | |
fn($a, $b) => $a instanceof FFinally ? 1 : 0 | |
); | |
while($chainItem = array_shift($this->chain)) { | |
try { | |
if($chainItem instanceof TThen && $this->state === self::STATE_RESOLVED) { | |
$chainItem->call($this->resolvedValue); | |
} | |
if($chainItem instanceof CCatch && $this->state === self::STATE_REJECTED) { | |
$chainItem->call($this->rejectedReason); | |
} | |
if($chainItem instanceof FFinally) { | |
$chainItem->call($this->resolvedValue ?? null, $this->rejectedReason ?? null); | |
} | |
} | |
catch(Throwable $exception) { | |
$this->reject($exception); | |
} | |
} | |
} | |
} | |
abstract class Chainable { | |
/** @var callable */ | |
protected $callback; | |
private bool $called; | |
public function __construct(callable $callback) { | |
$this->callback = $callback; | |
$this->called = false; | |
} | |
public function call(mixed...$parameters):void { | |
if($this->called) { | |
return; | |
} | |
call_user_func($this->callback, ...$parameters); | |
$this->called = true; | |
} | |
} | |
class TThen extends Chainable {} | |
class CCatch extends Chainable {} | |
class FFinally extends Chainable {} | |
class DDeferred { | |
private PPromise $promise; | |
private PProcess $process; | |
private mixed $resolvedValue; | |
private Throwable $rejectedReason; | |
/** @var callable */ | |
private $resolveCallback; | |
/** @var callable */ | |
private $rejectCallback; | |
/** @var callable */ | |
private $completeCallback; | |
private bool $completed; | |
/** @var array<callable> */ | |
private array $eventListenersOnComplete; | |
public function __construct() { | |
$this->completed = false; | |
$this->eventListenersOnComplete = []; | |
} | |
public function addOnCompleteCallback(callable $callback):void { | |
array_push($this->eventListenersOnComplete, $callback); | |
} | |
public function getPromise():PPromise { | |
$this->promise = new PPromise(function(callable $resolve, callable $reject, callable $complete):void { | |
$this->resolveCallback = $resolve; | |
$this->rejectCallback = $reject; | |
$this->completeCallback = $complete; | |
}); | |
return $this->promise; | |
} | |
public function setProcess(PProcess $process):void { | |
$this->process = $process; | |
} | |
public function getProcess():PProcess { | |
return $this->process; | |
} | |
public function getState():string { | |
return $this->promise->state; | |
} | |
public function resolve(mixed $resolvedValue):void { | |
call_user_func($this->resolveCallback, $resolvedValue); | |
$this->complete(); | |
} | |
public function reject(Throwable $rejectedReason):void { | |
call_user_func($this->rejectCallback, $rejectedReason); | |
$this->complete(); | |
} | |
private function complete():void { | |
if($this->completed) { | |
return; | |
} | |
call_user_func($this->completeCallback); | |
$this->completed = true; | |
foreach($this->eventListenersOnComplete as $callback) { | |
call_user_func($callback); | |
} | |
} | |
} | |
class PProcess { | |
/** @var callable */ | |
private $callback; | |
public function __construct(callable $callback) { | |
$this->callback = $callback; | |
} | |
public function tick():void { | |
call_user_func($this->callback); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment