From ac1c9254906b7e2ef1260c2a6ec715949fb73da2 Mon Sep 17 00:00:00 2001 From: SQKo <87897282+SQKo@users.noreply.github.com> Date: Sat, 9 Dec 2023 21:42:35 +0700 Subject: [PATCH] Add react/promise v2 to v3 transition helper --- composer.json | 5 +- src/Discord/DriverInterface.php | 6 +- src/Discord/Drivers/Guzzle.php | 6 +- src/Discord/Drivers/React.php | 9 +- src/Discord/Http.php | 40 ++-- .../CancellablePromiseInterface.php | 26 +++ src/Discord/PromiseHelpers/Deferred.php | 139 ++++++++++++++ .../ExtendedPromiseInterface.php | 33 ++++ .../PolyfillPromiseInterface.php | 175 ++++++++++++++++++ .../PromiseHelpers/PromisorInterface.php | 26 +++ src/Discord/PromiseHelpers/bootstrap.php | 28 +++ src/Discord/Request.php | 2 +- 12 files changed, 463 insertions(+), 32 deletions(-) create mode 100644 src/Discord/PromiseHelpers/CancellablePromiseInterface.php create mode 100644 src/Discord/PromiseHelpers/Deferred.php create mode 100644 src/Discord/PromiseHelpers/ExtendedPromiseInterface.php create mode 100644 src/Discord/PromiseHelpers/PolyfillPromiseInterface.php create mode 100644 src/Discord/PromiseHelpers/PromisorInterface.php create mode 100644 src/Discord/PromiseHelpers/bootstrap.php diff --git a/composer.json b/composer.json index e4f2868..c9bfe25 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,9 @@ } ], "autoload": { + "files": [ + "src/Discord/PromiseHelpers/bootstrap.php" + ], "psr-4": { "Discord\\Http\\": "src/Discord", "Tests\\Discord\\Http\\": "tests/Discord" @@ -19,7 +22,7 @@ "php": "^7.4|^8.0", "react/http": "^1.2", "psr/log": "^1.1 || ^2.0 || ^3.0", - "react/promise": "^2.2" + "react/promise": "^2.8 || ^3.1" }, "suggest": { "guzzlehttp/guzzle": "For alternative to ReactPHP/Http Browser" diff --git a/src/Discord/DriverInterface.php b/src/Discord/DriverInterface.php index c83e809..777a70e 100644 --- a/src/Discord/DriverInterface.php +++ b/src/Discord/DriverInterface.php @@ -12,7 +12,7 @@ namespace Discord\Http; use Psr\Http\Message\ResponseInterface; -use React\Promise\ExtendedPromiseInterface; +use React\Promise\PromiseInterface; /** * Interface for an HTTP driver. @@ -28,7 +28,7 @@ interface DriverInterface * * @param Request $request * - * @return ExtendedPromiseInterface + * @return \Discord\Http\PromiseHelpers\PolyFillPromiseInterface|\React\Promise\ExtendedPromiseInterface */ - public function runRequest(Request $request): ExtendedPromiseInterface; + public function runRequest(Request $request): PromiseInterface; } diff --git a/src/Discord/Drivers/Guzzle.php b/src/Discord/Drivers/Guzzle.php index 28af4f3..889844c 100644 --- a/src/Discord/Drivers/Guzzle.php +++ b/src/Discord/Drivers/Guzzle.php @@ -12,12 +12,12 @@ namespace Discord\Http\Drivers; use Discord\Http\DriverInterface; +use Discord\Http\PromiseHelpers\Deferred; use Discord\Http\Request; use GuzzleHttp\Client; use GuzzleHttp\RequestOptions; use React\EventLoop\LoopInterface; -use React\Promise\Deferred; -use React\Promise\ExtendedPromiseInterface; +use React\Promise\PromiseInterface; /** * guzzlehttp/guzzle driver for Discord HTTP client. (still with React Promise). @@ -55,7 +55,7 @@ public function __construct(?LoopInterface $loop = null, array $options = []) $this->client = new Client($options); } - public function runRequest(Request $request): ExtendedPromiseInterface + public function runRequest(Request $request): PromiseInterface { // Create a React promise $deferred = new Deferred(); diff --git a/src/Discord/Drivers/React.php b/src/Discord/Drivers/React.php index 5f58fc8..19a2560 100644 --- a/src/Discord/Drivers/React.php +++ b/src/Discord/Drivers/React.php @@ -12,10 +12,11 @@ namespace Discord\Http\Drivers; use Discord\Http\DriverInterface; +use Discord\Http\PromiseHelpers\PolyFillPromiseInterface; use Discord\Http\Request; use React\EventLoop\LoopInterface; use React\Http\Browser; -use React\Promise\ExtendedPromiseInterface; +use React\Promise\PromiseInterface; use React\Socket\Connector; /** @@ -54,12 +55,12 @@ public function __construct(LoopInterface $loop, array $options = []) $this->browser = $browser->withRejectErrorResponse(false); } - public function runRequest(Request $request): ExtendedPromiseInterface + public function runRequest(Request $request): PromiseInterface { - return $this->browser->{$request->getMethod()}( + return new PolyFillPromiseInterface($this->browser->{$request->getMethod()}( $request->getUrl(), $request->getHeaders(), $request->getContent() - ); + )); } } diff --git a/src/Discord/Http.php b/src/Discord/Http.php index 7a87e05..dc6d26f 100644 --- a/src/Discord/Http.php +++ b/src/Discord/Http.php @@ -19,12 +19,12 @@ use Discord\Http\Exceptions\NotFoundException; use Discord\Http\Exceptions\RateLimitException; use Discord\Http\Exceptions\RequestFailedException; +use Discord\Http\PromiseHelpers\Deferred; use Discord\Http\Multipart\MultipartBody; use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; use React\EventLoop\LoopInterface; -use React\Promise\Deferred; -use React\Promise\ExtendedPromiseInterface; +use React\Promise\PromiseInterface; use SplQueue; /** @@ -39,7 +39,7 @@ class Http * * @var string */ - public const VERSION = 'v10.3.0'; + public const VERSION = 'v10.4.0'; /** * Current Discord HTTP API version. @@ -160,9 +160,9 @@ public function setDriver(DriverInterface $driver): void * @param mixed $content * @param array $headers * - * @return ExtendedPromiseInterface + * @return \Discord\Http\PromiseHelpers\PolyFillPromiseInterface|\React\Promise\ExtendedPromiseInterface */ - public function get($url, $content = null, array $headers = []): ExtendedPromiseInterface + public function get($url, $content = null, array $headers = []): PromiseInterface { if (! ($url instanceof Endpoint)) { $url = Endpoint::bind($url); @@ -178,9 +178,9 @@ public function get($url, $content = null, array $headers = []): ExtendedPromise * @param mixed $content * @param array $headers * - * @return ExtendedPromiseInterface + * @return \Discord\Http\PromiseHelpers\PolyFillPromiseInterface|\React\Promise\ExtendedPromiseInterface */ - public function post($url, $content = null, array $headers = []): ExtendedPromiseInterface + public function post($url, $content = null, array $headers = []): PromiseInterface { if (! ($url instanceof Endpoint)) { $url = Endpoint::bind($url); @@ -196,9 +196,9 @@ public function post($url, $content = null, array $headers = []): ExtendedPromis * @param mixed $content * @param array $headers * - * @return ExtendedPromiseInterface + * @return \Discord\Http\PromiseHelpers\PolyFillPromiseInterface|\React\Promise\ExtendedPromiseInterface */ - public function put($url, $content = null, array $headers = []): ExtendedPromiseInterface + public function put($url, $content = null, array $headers = []): PromiseInterface { if (! ($url instanceof Endpoint)) { $url = Endpoint::bind($url); @@ -214,9 +214,9 @@ public function put($url, $content = null, array $headers = []): ExtendedPromise * @param mixed $content * @param array $headers * - * @return ExtendedPromiseInterface + * @return \Discord\Http\PromiseHelpers\PolyFillPromiseInterface|\React\Promise\ExtendedPromiseInterface */ - public function patch($url, $content = null, array $headers = []): ExtendedPromiseInterface + public function patch($url, $content = null, array $headers = []): PromiseInterface { if (! ($url instanceof Endpoint)) { $url = Endpoint::bind($url); @@ -232,9 +232,9 @@ public function patch($url, $content = null, array $headers = []): ExtendedPromi * @param mixed $content * @param array $headers * - * @return ExtendedPromiseInterface + * @return \Discord\Http\PromiseHelpers\PolyFillPromiseInterface|\React\Promise\ExtendedPromiseInterface */ - public function delete($url, $content = null, array $headers = []): ExtendedPromiseInterface + public function delete($url, $content = null, array $headers = []): PromiseInterface { if (! ($url instanceof Endpoint)) { $url = Endpoint::bind($url); @@ -251,9 +251,9 @@ public function delete($url, $content = null, array $headers = []): ExtendedProm * @param mixed $content * @param array $headers * - * @return ExtendedPromiseInterface + * @return \Discord\Http\PromiseHelpers\PolyFillPromiseInterface|\React\Promise\ExtendedPromiseInterface */ - public function queueRequest(string $method, Endpoint $url, $content, array $headers = []): ExtendedPromiseInterface + public function queueRequest(string $method, Endpoint $url, $content, array $headers = []): PromiseInterface { $deferred = new Deferred(); @@ -318,16 +318,16 @@ protected function guessContent(&$content) * @param Request $request * @param Deferred $deferred * - * @return ExtendedPromiseInterface + * @return \Discord\Http\PromiseHelpers\PolyFillPromiseInterface|\React\Promise\ExtendedPromiseInterface */ - protected function executeRequest(Request $request, Deferred $deferred = null): ExtendedPromiseInterface + protected function executeRequest(Request $request, Deferred $deferred = null): PromiseInterface { if ($deferred === null) { $deferred = new Deferred(); } if ($this->rateLimit) { - $deferred->reject($this->rateLimit); + $deferred->reject($this->rateLimit); // TODO handle resolve ratelimit return $deferred->promise(); } @@ -383,7 +383,7 @@ protected function executeRequest(Request $request, Deferred $deferred = null): }); } - $deferred->reject($rateLimit->isGlobal() ? $this->rateLimit : $rateLimit); + $deferred->reject($rateLimit->isGlobal() ? $this->rateLimit : $rateLimit); // TODO handle resolve ratelimit } // Bad Gateway // Cloudflare SSL Handshake error @@ -476,7 +476,7 @@ protected function checkQueue(): void --$this->waiting; $this->checkQueue(); $deferred->resolve($result); - }, function ($e) use ($deferred) { + }, function ($e) use ($deferred) { // TODO handle resolve reject --$this->waiting; $this->checkQueue(); $deferred->reject($e); diff --git a/src/Discord/PromiseHelpers/CancellablePromiseInterface.php b/src/Discord/PromiseHelpers/CancellablePromiseInterface.php new file mode 100644 index 0000000..62bd41b --- /dev/null +++ b/src/Discord/PromiseHelpers/CancellablePromiseInterface.php @@ -0,0 +1,26 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE file. + */ + + namespace Discord\Http\PromiseHelpers; + + /** + * A transition helper from react/promise v2 to react/promise v3. + * Please do not use this polyfill class in place of real CancellablePromiseInterface. + * + * @see \React\Promise\CancellablePromiseInterface + * + * @internal Used internally for DiscordPHP v10 + * + * @since 10.4.0 + */ +interface CancellablePromiseInterface +{ + public function cancel(); +} diff --git a/src/Discord/PromiseHelpers/Deferred.php b/src/Discord/PromiseHelpers/Deferred.php new file mode 100644 index 0000000..a28b846 --- /dev/null +++ b/src/Discord/PromiseHelpers/Deferred.php @@ -0,0 +1,139 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE file. + */ + +namespace Discord\Http\PromiseHelpers; + +use React\Promise\Deferred as ReactDeferred; +use React\Promise\PromiseInterface; + +/** + * A transition helper from react/promise v2 to react/promise v3. + * Please do not use this polyfill class in place of real Deferred. + * + * @see \React\Promise\Deferred + * + * @internal Used internally for DiscordPHP v10 + * + * @since 10.4.0 + */ +final class Deferred implements PromisorInterface +{ + /** + * The actual Promisor + */ + public ReactDeferred $deferred; + + /** + * Determine the installed package is Promise-v3 + */ + public static bool $isPromiseV3 = false; + + /** + * @var PolyFillPromiseInterface|PolyFillPromiseInterface + */ + private $promise; + + /** + * @param callable|ReactDeferred $canceller Canceller callback or a Deferred to use + * + * @throws \InvalidArgumentException $canceller is not null or callable or a Deferred + */ + public function __construct($canceller = null) + { + if ($canceller instanceof ReactDeferred) { + $this->deferred = $canceller; + } elseif (null === $canceller || is_callable($canceller)) { + $this->deferred = new ReactDeferred($canceller); + } else { + throw new \InvalidArgumentException('$canceller must be either null or callable or Deferred'); + } + } + + /** + * @return PolyFillPromiseInterface|PromiseInterface|\React\Promise\ExtendedPromiseInterface + */ + public function promise() + { + if (!static::$isPromiseV3) { + // Just use the same react/promise v2 promise + return $this->deferred->promise(); + } + + if (null === $this->promise) { + // Wrap with the polyfill if user installed react/promise v3 + $this->promise = new PolyFillPromiseInterface($this->deferred->promise()); + } + + return $this->promise; + } + + /** + * @see React\Promise\Deferred::resolve() + * + * @return void + */ + public function resolve($value = null) + { + $this->deferred->resolve($value); + } + + /** + * @see React\Promise\Deferred::reject() + * + * @param \Throwable $reason required in Promise-v3, will be resolved if not a throwable + * + * @throws \InvalidArgumentException $reason is null & react/promise is v3 + * + * @return void + */ + public function reject($reason = null) + { + if (static::$isPromiseV3) { + if (null === $reason) { + $reason = new \InvalidArgumentException('reject($reason) must not be null'); + } elseif (!($reason instanceof \Throwable)) { + return $this->deferred->resolve($reason); + } + } + + $this->deferred->reject($reason); + } + + /** + * Not supported + * + * @deprecated + */ + public function notify($update = null) + { + if (method_exists($this->deferred, 'notify')) { + $this->deferred->notify($update); + return; + } + + throw new \BadMethodCallException('notify() is not supported with this polyfill and react/promise v3'); + } + + /** + * Not supported + * + * @deprecated + */ + public function progress($update = null) + { + if (method_exists($this->deferred, 'progress')) { + $this->deferred->progress($update); + return; + } + + throw new \BadMethodCallException('progress() is not supported with this polyfill and react/promise v3'); + } +} diff --git a/src/Discord/PromiseHelpers/ExtendedPromiseInterface.php b/src/Discord/PromiseHelpers/ExtendedPromiseInterface.php new file mode 100644 index 0000000..bd4a86b --- /dev/null +++ b/src/Discord/PromiseHelpers/ExtendedPromiseInterface.php @@ -0,0 +1,33 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE file. + */ + +namespace Discord\Http\PromiseHelpers; + +/** + * A transition helper from react/promise v2 to react/promise v3. + * Please do not use this polyfill class in place of real ExtendedPromiseInterface. + * + * @see \React\Promise\ExtendedPromiseInterface + * + * @internal Used internally for DiscordPHP v10 + * + * @since 10.4.0 + */ +interface ExtendedPromiseInterface +{ + public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null); + + public function otherwise(callable $onRejected); + + public function always(callable $onFulfilledOrRejected); + + public function progress(callable $onProgress); +} diff --git a/src/Discord/PromiseHelpers/PolyfillPromiseInterface.php b/src/Discord/PromiseHelpers/PolyfillPromiseInterface.php new file mode 100644 index 0000000..ad7d438 --- /dev/null +++ b/src/Discord/PromiseHelpers/PolyfillPromiseInterface.php @@ -0,0 +1,175 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE file. + */ + +namespace Discord\Http\PromiseHelpers; + +use React\Promise\Promise; +use React\Promise\PromiseInterface; +use function React\Promise\reject; + +/** + * A transition helper from react/promise v2 to react/promise v3. + * Please do not use this polyfill class in place of real PromiseInterface. + * + * @see \React\Promise\PromiseInterface + * @see ExtendedPromiseInterface + * @see CancellablePromiseInterface + * + * @internal Used internally for DiscordPHP v10 + * + * @since 10.4.0 + */ +final class PolyFillPromiseInterface implements PromiseInterface, ExtendedPromiseInterface, CancellablePromiseInterface +{ + /** + * The actual Promise + */ + public Promise $promise; + + public function __construct(Promise $promise) + { + $this->promise = $promise; + } + + /** + * Converts then() and wrap it with this polyfill class. + * + * @see React\Promise\PromiseInterface::then() + * + * @param callable|null $onFulfilled + * @param callable|null $onRejected + * @param callable|null $onProgress This argument must not be used anymore. + * + * @throws \InvalidArgumentException $onProgress is not null & react/promise is v3 + * + * @return self|PromiseInterface + */ + public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null): PromiseInterface + { + if (method_exists($this->promise, 'progress')) { + return new self($this->promise->then($onFulfilled, $onRejected, $onProgress)); + } + + if (null !== $onProgress) { + return reject(new \InvalidArgumentException('$onProgress is not supported with this polyfill')); + } + + return new self($this->promise->then($onFulfilled, $onRejected)); + } + + /** + * Do not use this in v3, use then(). + * + * @see React\Promise\ExtendedPromiseInterface::done() v2 + * + * @param callable|null $onFulfilled + * @param callable|null $onRejected + * @param callable|null $onProgress This argument must not be used anymore. + * + * @throws \InvalidArgumentException $onProgress is not null & react/promise is v3 + * + * @deprecated 10.4.0 If you see this, please change done() to then() + * + * @return void + */ + public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) + { + if (method_exists($this->promise, 'done')) { + $this->promise->done($onFulfilled, $onRejected, $onProgress); + return; + } + + if (null !== $onProgress) { + throw new \InvalidArgumentException('$onProgress is not supported with this polyfill'); + } + + $this->promise->then($onFulfilled, $onRejected); + return; + } + + /** + * @see React\Promise\ExtendedPromiseInterface::otherwise() v2 + * + * @deprecated Promise-v3 Use catch() instead. + */ + public function otherwise(callable $onRejected): PromiseInterface + { + return $this->catch($onRejected); + } + + /** + * @see React\Promise\ExtendedPromiseInterface::always() v2 + * + * @deprecated Promise-v3 Use finally() instead. + */ + public function always(callable $onFulfilledOrRejected): PromiseInterface + { + return $this->finally($onFulfilledOrRejected); + } + + /** + * @see React\Promise\CancellablePromiseInterface::cancel() v2 + * @see React\Promise\PromiseInterface::cancel() v3 + * + * @return void + */ + public function cancel(): void + { + $this->promise->cancel(); + return; + } + + /** + * Not supported + * + * @deprecated + */ + public function progress(callable $onProgress) + { + if (method_exists($this->promise, 'progress')) { + return $this->promise->progress($onProgress); + } + + return reject(new \BadMethodCallException('progress() is not supported with this polyfill and react/promise v3')); + } + + /** + * @see React\Promise\PromiseInterface::catch() v3 + * @see React\Promise\ExtendedPromiseInterface::otherwise() v2 + * + * @param callable $onRejected + * @return PromiseInterface + */ + public function catch(callable $onRejected): PromiseInterface + { + if (method_exists($this->promise, 'catch')) { + return $this->promise->catch($onRejected); + } + + return $this->promise->then(null, $onRejected); + } + + /** + * @see React\Promise\PromiseInterface::finally() v3 + * @see React\Promise\ExtendedPromiseInterface::always() v2 + * + * @param callable $onFulfilledOrRejected + * @return PromiseInterface + */ + public function finally (callable $onFulfilledOrRejected): PromiseInterface + { + if (method_exists($this->promise, 'finally')) { + return $this->promise->finally($onFulfilledOrRejected); + } + + return $this->promise->always($onFulfilledOrRejected); + } +} diff --git a/src/Discord/PromiseHelpers/PromisorInterface.php b/src/Discord/PromiseHelpers/PromisorInterface.php new file mode 100644 index 0000000..2f8879e --- /dev/null +++ b/src/Discord/PromiseHelpers/PromisorInterface.php @@ -0,0 +1,26 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE file. + */ + + namespace Discord\Http\PromiseHelpers; + + /** + * A transition helper from react/promise v2 to react/promise v3. + * Please do not use this polyfill class in place of real PromisorInterface. + * + * @see \React\Promise\PromisorInterface + * + * @internal Used internally for DiscordPHP v10 + * + * @since 10.4.0 + */ +interface PromisorInterface +{ + public function promise(); +} diff --git a/src/Discord/PromiseHelpers/bootstrap.php b/src/Discord/PromiseHelpers/bootstrap.php new file mode 100644 index 0000000..fc98cf3 --- /dev/null +++ b/src/Discord/PromiseHelpers/bootstrap.php @@ -0,0 +1,28 @@ + + * + * This file is subject to the MIT license that is bundled + * with this source code in the LICENSE file. + */ + +namespace Discord\Http\PromiseHelpers; + +/** + * Interface aliases for Promise-v2 to Promise-v3 backward compatibility. + * + * @since 10.4.0 + */ +if (!interface_exists('\React\Promise\ExtendedPromiseInterface')) { + class_alias(ExtendedPromiseInterface::class, '\React\Promise\ExtendedPromiseInterface'); +} +if (!interface_exists('\React\Promise\CancellablePromiseInterface')) { + class_alias(CancellablePromiseInterface::class, '\React\Promise\CancellablePromiseInterface'); +} +if (!interface_exists('\React\Promise\PromisorInterface')) { + Deferred::$isPromiseV3 = true; + class_alias(PromisorInterface::class, '\React\Promise\PromisorInterface'); +} diff --git a/src/Discord/Request.php b/src/Discord/Request.php index 190bad5..24bbf7c 100644 --- a/src/Discord/Request.php +++ b/src/Discord/Request.php @@ -11,7 +11,7 @@ namespace Discord\Http; -use React\Promise\Deferred; +use Discord\Http\PromiseHelpers\Deferred; /** * Represents an HTTP request.