SyncPromise.php 4.82 KB
Newer Older
Ketan's avatar
Ketan committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
<?php
namespace GraphQL\Executor\Promise\Adapter;

use GraphQL\Utils\Utils;

/**
 * Class SyncPromise
 *
 * Simplistic (yet full-featured) implementation of Promises A+ spec for regular PHP `sync` mode
 * (using queue to defer promises execution)
 *
 * @package GraphQL\Executor\Promise\Adapter
 */
class SyncPromise
{
    const PENDING = 'pending';
    const FULFILLED = 'fulfilled';
    const REJECTED = 'rejected';

    /**
     * @var \SplQueue
     */
    public static $queue;

    public static function getQueue()
    {
        return self::$queue ?: self::$queue = new \SplQueue();
    }

    public static function runQueue()
    {
        $q = self::$queue;
        while ($q && !$q->isEmpty()) {
            $task = $q->dequeue();
            $task();
        }
    }

    public $state = self::PENDING;

    public $result;

    /**
     * Promises created in `then` method of this promise and awaiting for resolution of this promise
     * @var array
     */
    private $waiting = [];

    public function reject($reason)
    {
        if (!$reason instanceof \Exception && !$reason instanceof \Throwable) {
            throw new \Exception('SyncPromise::reject() has to be called with an instance of \Throwable');
        }

        switch ($this->state) {
            case self::PENDING:
                $this->state = self::REJECTED;
                $this->result = $reason;
                $this->enqueueWaitingPromises();
                break;
            case self::REJECTED:
                if ($reason !== $this->result) {
                    throw new \Exception("Cannot change rejection reason");
                }
                break;
            case self::FULFILLED:
                throw new \Exception("Cannot reject fulfilled promise");
        }
        return $this;
    }

    public function resolve($value)
    {
        switch ($this->state) {
            case self::PENDING:
                if ($value === $this) {
                    throw new \Exception("Cannot resolve promise with self");
                }
                if (is_object($value) && method_exists($value, 'then')) {
                    $value->then(
                        function($resolvedValue) {
                            $this->resolve($resolvedValue);
                        },
                        function($reason) {
                            $this->reject($reason);
                        }
                    );
                    return $this;
                }

                $this->state = self::FULFILLED;
                $this->result = $value;
                $this->enqueueWaitingPromises();
                break;
            case self::FULFILLED:
                if ($this->result !== $value) {
                    throw new \Exception("Cannot change value of fulfilled promise");
                }
                break;
            case self::REJECTED:
                throw new \Exception("Cannot resolve rejected promise");
        }
        return $this;
    }

    public function then(callable $onFulfilled = null, callable $onRejected = null)
    {
        if ($this->state === self::REJECTED && !$onRejected) {
            return $this;
        }
        if ($this->state === self::FULFILLED && !$onFulfilled) {
            return $this;
        }
        $tmp = new self();
        $this->waiting[] = [$tmp, $onFulfilled, $onRejected];

        if ($this->state !== self::PENDING) {
            $this->enqueueWaitingPromises();
        }

        return $tmp;
    }

    private function enqueueWaitingPromises()
    {
        Utils::invariant($this->state !== self::PENDING, 'Cannot enqueue derived promises when parent is still pending');

        foreach ($this->waiting as $descriptor) {
            self::getQueue()->enqueue(function () use ($descriptor) {
                /** @var $promise self */
                list($promise, $onFulfilled, $onRejected) = $descriptor;

                if ($this->state === self::FULFILLED) {
                    try {
                        $promise->resolve($onFulfilled ? $onFulfilled($this->result) : $this->result);
                    } catch (\Exception $e) {
                        $promise->reject($e);
                    } catch (\Throwable $e) {
                        $promise->reject($e);
                    }
                } else if ($this->state === self::REJECTED) {
                    try {
                        if ($onRejected) {
                            $promise->resolve($onRejected($this->result));
                        } else {
                            $promise->reject($this->result);
                        }
                    } catch (\Exception $e) {
                        $promise->reject($e);
                    } catch (\Throwable $e) {
                        $promise->reject($e);
                    }
                }
            });
        }
        $this->waiting = [];
    }
}