Skip to content

Instantly share code, notes, and snippets.

@g105b
Created April 20, 2023 15:19
Show Gist options
  • Save g105b/73468b46f0aa2266d8f0a3f837eb72be to your computer and use it in GitHub Desktop.
Save g105b/73468b46f0aa2266d8f0a3f837eb72be to your computer and use it in GitHub Desktop.
Promise implementation and basic HTTP client in one file
<?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