<?php

namespace OhMyBrew\ShopifyApp\Traits;

use Illuminate\Database\Eloquent\SoftDeletes;
use OhMyBrew\ShopifyApp\Facades\ShopifyApp;
use OhMyBrew\ShopifyApp\Models\Charge;
use OhMyBrew\ShopifyApp\Models\Plan;
use OhMyBrew\ShopifyApp\Scopes\NamespaceScope;
use OhMyBrew\ShopifyApp\Services\ShopSession;

/**
 * Responsible for reprecenting a shop record.
 */
trait ShopModelTrait
{
    use SoftDeletes;

    /**
     * The API instance.
     *
     * @var \OhMyBrew\BasicShopifyAPI
     */
    protected $api;

    /**
     * The session instance.
     *
     * @var \OhMyBrew\ShopifyApp\Services\ShopSession
     */
    protected $session;

    /**
     * Constructor for the model.
     *
     * @param array $attributes The model attribues to pass in.
     *
     * @return self
     */
    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
    }

    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(new NamespaceScope());
    }

    /**
     * Creates or returns an instance of session for the shop.
     *
     * @return \OhMyBrew\ShopifyApp\Services\ShopSession
     */
    public function session()
    {
        if (!$this->session) {
            // Create new session instance
            $this->session = new ShopSession($this);
        }

        // Return existing instance
        return $this->session;
    }

    /**
     * Creates or returns an instance of API for the shop.
     *
     * @return \OhMyBrew\BasicShopifyAPI
     */
    public function api()
    {
        if (!$this->api) {
            // Get the domain and token
            $shopDomain = $this->shopify_domain;
            $token = $this->session()->getToken();

            // Create new API instance
            $this->api = ShopifyApp::api();
            $this->api->setSession($shopDomain, $token);
        }

        // Return existing instance
        return $this->api;
    }

    /**
     * Checks is shop is grandfathered in.
     *
     * @return bool
     */
    public function isGrandfathered()
    {
        return ((bool) $this->grandfathered) === true;
    }

    /**
     * Get charges.
     *
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function charges()
    {
        return $this->hasMany(Charge::class);
    }

    /**
     * Checks if charges have been applied to the shop.
     *
     * @return bool
     */
    public function hasCharges()
    {
        return $this->charges->isNotEmpty();
    }

    /**
     * Gets the plan.
     *
     * @return \OhMyBrew\ShopifyApp\Models\Plan
     */
    public function plan()
    {
        return $this->belongsTo(Plan::class);
    }

    /**
     * Checks if the shop is freemium.
     *
     * @return bool
     */
    public function isFreemium()
    {
        return ((bool) $this->freemium) === true;
    }

    /**
     * Gets the last single or recurring charge for the shop.
     *
     * @return null|\OhMyBrew\ShopifyApp\Models\Charge
     */
    public function planCharge()
    {
        return $this->charges()
            ->whereIn('type', [Charge::CHARGE_RECURRING, Charge::CHARGE_ONETIME])
            ->where('plan_id', $this->plan_id)
            ->orderBy('created_at', 'desc')
            ->first();
    }

    /**
     * Checks if the access token is filled.
     *
     * @return bool
     */
    public function hasOfflineAccess()
    {
        return !empty($this->shopify_token);
    }
}


<?php

namespace OhMyBrew\ShopifyApp\Traits;

use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\View;
use OhMyBrew\ShopifyApp\Facades\ShopifyApp;
use OhMyBrew\ShopifyApp\Models\Charge;
use OhMyBrew\ShopifyApp\Models\Plan;
use OhMyBrew\ShopifyApp\Models\Shop;
use OhMyBrew\ShopifyApp\Requests\StoreUsageCharge;
use OhMyBrew\ShopifyApp\Services\BillingPlan;
use OhMyBrew\ShopifyApp\Services\UsageCharge;

/**
 * Responsible for billing a shop for plans and usage charges.
 */
trait BillingControllerTrait
{
    /**
     * Redirects to billing screen for Shopify.
     *
     * @param \OhMyBrew\ShopifyApp\Models\Plan $plan The plan.
     *
     * @return \Illuminate\View\View
     */
    public function index(Plan $plan)
    {
        // If the plan is null, get a plan
        if (is_null($plan) || ($plan && !$plan->exists)) {
            $plan = Plan::where('on_install', true)->first();
        }

        // Get the confirmation URL
        $bp = new BillingPlan(ShopifyApp::shop(), $plan);
        $url = $bp->confirmationUrl();

        // Do a fullpage redirect
        return View::make('shopify-app::billing.fullpage_redirect', compact('url'));
    }

    /**
     * Processes the response from the customer.
     *
     * @param \OhMyBrew\ShopifyApp\Models\Plan $plan The plan.
     *
     * @return \Illuminate\Http\RedirectResponse
     */
    public function process(Plan $plan)
    {
        // Activate the plan and save
        $shop = ShopifyApp::shop();
        $bp = new BillingPlan($shop, $plan);
        $bp->setChargeId(Request::query('charge_id'));
        $bp->activate();
        $bp->save();

        // All good, update the shop's plan and take them off freemium (if applicable)
        $shop->update([
            'freemium' => false,
            'plan_id'  => $plan->id,
        ]);

        // Go to homepage of app
        return Redirect::route('home')->with('success', 'billing');
    }

    /**
     * Allows for setting a usage charge.
     *
     * @param \OhMyBrew\ShopifyApp\Requests\StoreUsageCharge $request
     *
     * @return \Illuminate\Http\RedirectResponse
     */
    public function usageCharge(StoreUsageCharge $request)
    {
        // Activate and save the usage charge
        $validated = $request->validated();
        $uc = new UsageCharge(ShopifyApp::shop(), $validated);
        $uc->activate();
        $uc->save();

        // All done, return with success
        return isset($validated['redirect']) ?
            Redirect::to($validated['redirect'])->with('success', 'usage_charge') :
            Redirect::back()->with('success', 'usage_charge');
    }
}


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddBillingToShopsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('shops', function (Blueprint $table) {
            $table->bigInteger('charge_id')->nullable(true)->default(null);
            $table->boolean('grandfathered')->default(false);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('shops', function (Blueprint $table) {
            // Laravel doesn't seem to support multiple dropColumn commands
            // See: (https://github.com/laravel/framework/issues/2979#issuecomment-227468621)
            $table->dropColumn(['charge_id', 'grandfathered']);
        });
    }
}


<?php

namespace OhMyBrew\ShopifyApp\Services;

use Exception;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;
use OhMyBrew\ShopifyApp\Models\Charge;
use OhMyBrew\ShopifyApp\Models\Plan;
use OhMyBrew\ShopifyApp\Models\Shop;

/**
 * Responsible for creating a confirmation URL for a billing plan,
 * activation of a billing plan, and getting the charge details.
 */
class BillingPlan
{
    /**
     * The shop.
     *
     * @var \OhMyBrew\ShopifyApp\Models\Shop
     */
    protected $shop;

    /**
     * The shop API.
     *
     * @var \OhMyBrew\BasicShopifyAPI
     */
    protected $api;

    /**
     * The plan to use.
     *
     * @var \OhMyBrew\ShopifyApp\Models\Plan
     */
    protected $plan;

    /**
     * The charge ID.
     *
     * @var int|string
     */
    protected $chargeId;

    /**
     * Response to the charge activation.
     *
     * @var object
     */
    protected $response;

    /**
     * Constructor for billing plan class.
     *
     * @param \OhMyBrew\ShopifyApp\Models\Shop $shop The shop to target for billing.
     * @param \OhMyBrew\ShopifyApp\Models\Plan $plan The plan from the database.
     *
     * @return self
     */
    public function __construct(Shop $shop, Plan $plan)
    {
        $this->shop = $shop;
        $this->api = $this->shop->api();
        $this->plan = $plan;

        return $this;
    }

    /**
     * Sets the charge ID.
     *
     * @param int|string $chargeId The charge ID to use
     *
     * @return $this
     */
    public function setChargeId($chargeId)
    {
        $this->chargeId = $chargeId;

        return $this;
    }

    /**
     * Gets the charge information for a previously inited charge.
     *
     * @return object
     */
    public function getCharge()
    {
        // Check if we have a charge ID to use
        if (!$this->chargeId) {
            throw new Exception('Can not get charge information without charge ID.');
        }

        // Run API to grab details
        return $this->api->rest(
            'GET',
            "/admin/{$this->plan->typeAsString(true)}/{$this->chargeId}.json"
        )->body->{$this->plan->typeAsString()};
    }

    /**
     * Gets the confirmation URL to redirect the customer to.
     * This URL sends them to Shopify's billing page.
     *
     * Example usage:
     * (new BillingPlan([shop], [plan]))->setDetails($plan)->confirmationUrl();
     *
     * @return string
     */
    public function confirmationUrl()
    {
        // Begin the charge request
        $charge = $this->api->rest(
            'POST',
            "/admin/{$this->plan->typeAsString(true)}.json",
            ["{$this->plan->typeAsString()}" => $this->chargeParams()]
        )->body->{$this->plan->typeAsString()};

        return $charge->confirmation_url;
    }

    /**
     * Returns the charge params sent with the post request.
     *
     * @return array
     */
    public function chargeParams()
    {
        // Build the charge array
        $chargeDetails = [
            'name'          => $this->plan->name,
            'price'         => $this->plan->price,
            'test'          => $this->plan->isTest(),
            'trial_days'    => $this->plan->hasTrial() ? $this->plan->trial_days : 0,
            'return_url'    => URL::secure(Config::get('shopify-app.billing_redirect'), ['plan_id' => $this->plan->id]),
        ];

        // Handle capped amounts for UsageCharge API
        if (isset($this->plan->capped_amount) && $this->plan->capped_amount > 0) {
            $chargeDetails['capped_amount'] = $this->plan->capped_amount;
            $chargeDetails['terms'] = $this->plan->terms;
        }

        return $chargeDetails;
    }

    /**
     * Activates a plan to the shop.
     *
     * Example usage:
     * (new BillingPlan([shop], [plan]))->setChargeId((int) request('charge_id'))->activate();
     *
     * @return object
     */
    public function activate()
    {
        // Check if we have a charge ID to use
        if (!$this->chargeId) {
            throw new Exception('Can not activate plan without a charge ID.');
        }

        // Activate and return the API response
        $this->response = $this->api->rest(
            'POST',
            "/admin/{$this->plan->typeAsString(true)}/{$this->chargeId}/activate.json"
        )->body->{$this->plan->typeAsString()};

        return $this->response;
    }

    /**
     * Cancels the current charge for the shop, saves the new charge.
     *
     * @return bool
     */
    public function save()
    {
        if (!$this->response) {
            throw new Exception('No activation response was recieved.');
        }

        // Cancel the last charge
        $planCharge = $this->shop->planCharge();
        if ($planCharge && !$planCharge->isDeclined() && !$planCharge->isCancelled()) {
            $planCharge->cancel();
        }

        // Create a charge
        $charge = Charge::firstOrNew([
            'charge_id' => $this->chargeId,
            'shop_id'   => $this->shop->id,
        ]);

        $charge->plan_id = $this->plan->id;
        $charge->type = $this->plan->type;
        $charge->status = $this->response->status;

        if ($this->plan->isType(Plan::PLAN_RECURRING)) {
            // Recurring plan specifics
            $charge->billing_on = $this->response->billing_on;
            $charge->trial_ends_on = $this->response->trial_ends_on;
        }

        // Set the activated on, try for the API, fallback to today
        $charge->activated_on = $this->response->activated_on ?? Carbon::today()->format('Y-m-d');

        // Merge in the plan details since the fields match the database columns
        $planDetails = $this->chargeParams();
        unset($planDetails['return_url']);
        foreach ($planDetails as $key => $value) {
            $charge->{$key} = $value;
        }

        // Finally, save the charge
        return $charge->save();
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Validator;

use Exception;
use GraphQL\Error\Error;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\Rules\ExecutableDefinitions;
use GraphQL\Validator\Rules\FieldsOnCorrectType;
use GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
use GraphQL\Validator\Rules\KnownArgumentNames;
use GraphQL\Validator\Rules\KnownArgumentNamesOnDirectives;
use GraphQL\Validator\Rules\KnownDirectives;
use GraphQL\Validator\Rules\KnownFragmentNames;
use GraphQL\Validator\Rules\KnownTypeNames;
use GraphQL\Validator\Rules\LoneAnonymousOperation;
use GraphQL\Validator\Rules\LoneSchemaDefinition;
use GraphQL\Validator\Rules\NoFragmentCycles;
use GraphQL\Validator\Rules\NoUndefinedVariables;
use GraphQL\Validator\Rules\NoUnusedFragments;
use GraphQL\Validator\Rules\NoUnusedVariables;
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
use GraphQL\Validator\Rules\ProvidedRequiredArgumentsOnDirectives;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\QuerySecurityRule;
use GraphQL\Validator\Rules\ScalarLeafs;
use GraphQL\Validator\Rules\UniqueArgumentNames;
use GraphQL\Validator\Rules\UniqueDirectivesPerLocation;
use GraphQL\Validator\Rules\UniqueFragmentNames;
use GraphQL\Validator\Rules\UniqueInputFieldNames;
use GraphQL\Validator\Rules\UniqueOperationNames;
use GraphQL\Validator\Rules\UniqueVariableNames;
use GraphQL\Validator\Rules\ValidationRule;
use GraphQL\Validator\Rules\ValuesOfCorrectType;
use GraphQL\Validator\Rules\VariablesAreInputTypes;
use GraphQL\Validator\Rules\VariablesDefaultValueAllowed;
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
use Throwable;
use function array_filter;
use function array_map;
use function array_merge;
use function count;
use function implode;
use function is_array;
use function sprintf;

/**
 * Implements the "Validation" section of the spec.
 *
 * Validation runs synchronously, returning an array of encountered errors, or
 * an empty array if no errors were encountered and the document is valid.
 *
 * A list of specific validation rules may be provided. If not provided, the
 * default list of rules defined by the GraphQL specification will be used.
 *
 * Each validation rule is an instance of GraphQL\Validator\Rules\ValidationRule
 * which returns a visitor (see the [GraphQL\Language\Visitor API](reference.md#graphqllanguagevisitor)).
 *
 * Visitor methods are expected to return an instance of [GraphQL\Error\Error](reference.md#graphqlerrorerror),
 * or array of such instances when invalid.
 *
 * Optionally a custom TypeInfo instance may be provided. If not provided, one
 * will be created from the provided schema.
 */
class DocumentValidator
{
    /** @var ValidationRule[] */
    private static $rules = [];

    /** @var ValidationRule[]|null */
    private static $defaultRules;

    /** @var QuerySecurityRule[]|null */
    private static $securityRules;

    /** @var ValidationRule[]|null */
    private static $sdlRules;

    /** @var bool */
    private static $initRules = false;

    /**
     * Primary method for query validation. See class description for details.
     *
     * @param ValidationRule[]|null $rules
     *
     * @return Error[]
     *
     * @api
     */
    public static function validate(
        Schema $schema,
        DocumentNode $ast,
        ?array $rules = null,
        ?TypeInfo $typeInfo = null
    ) {
        if ($rules === null) {
            $rules = static::allRules();
        }

        if (is_array($rules) === true && count($rules) === 0) {
            // Skip validation if there are no rules
            return [];
        }

        $typeInfo = $typeInfo ?: new TypeInfo($schema);

        return static::visitUsingRules($schema, $typeInfo, $ast, $rules);
    }

    /**
     * Returns all global validation rules.
     *
     * @return ValidationRule[]
     *
     * @api
     */
    public static function allRules()
    {
        if (! self::$initRules) {
            static::$rules     = array_merge(static::defaultRules(), self::securityRules(), self::$rules);
            static::$initRules = true;
        }

        return self::$rules;
    }

    public static function defaultRules()
    {
        if (self::$defaultRules === null) {
            self::$defaultRules = [
                ExecutableDefinitions::class        => new ExecutableDefinitions(),
                UniqueOperationNames::class         => new UniqueOperationNames(),
                LoneAnonymousOperation::class       => new LoneAnonymousOperation(),
                KnownTypeNames::class               => new KnownTypeNames(),
                FragmentsOnCompositeTypes::class    => new FragmentsOnCompositeTypes(),
                VariablesAreInputTypes::class       => new VariablesAreInputTypes(),
                ScalarLeafs::class                  => new ScalarLeafs(),
                FieldsOnCorrectType::class          => new FieldsOnCorrectType(),
                UniqueFragmentNames::class          => new UniqueFragmentNames(),
                KnownFragmentNames::class           => new KnownFragmentNames(),
                NoUnusedFragments::class            => new NoUnusedFragments(),
                PossibleFragmentSpreads::class      => new PossibleFragmentSpreads(),
                NoFragmentCycles::class             => new NoFragmentCycles(),
                UniqueVariableNames::class          => new UniqueVariableNames(),
                NoUndefinedVariables::class         => new NoUndefinedVariables(),
                NoUnusedVariables::class            => new NoUnusedVariables(),
                KnownDirectives::class              => new KnownDirectives(),
                UniqueDirectivesPerLocation::class  => new UniqueDirectivesPerLocation(),
                KnownArgumentNames::class           => new KnownArgumentNames(),
                UniqueArgumentNames::class          => new UniqueArgumentNames(),
                ValuesOfCorrectType::class          => new ValuesOfCorrectType(),
                ProvidedNonNullArguments::class     => new ProvidedNonNullArguments(),
                VariablesDefaultValueAllowed::class => new VariablesDefaultValueAllowed(),
                VariablesInAllowedPosition::class   => new VariablesInAllowedPosition(),
                OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(),
                UniqueInputFieldNames::class        => new UniqueInputFieldNames(),
            ];
        }

        return self::$defaultRules;
    }

    /**
     * @return QuerySecurityRule[]
     */
    public static function securityRules()
    {
        // This way of defining rules is deprecated
        // When custom security rule is required - it should be just added via DocumentValidator::addRule();
        // TODO: deprecate this

        if (self::$securityRules === null) {
            self::$securityRules = [
                DisableIntrospection::class => new DisableIntrospection(DisableIntrospection::DISABLED), // DEFAULT DISABLED
                QueryDepth::class           => new QueryDepth(QueryDepth::DISABLED), // default disabled
                QueryComplexity::class      => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
            ];
        }

        return self::$securityRules;
    }

    public static function sdlRules()
    {
        if (self::$sdlRules === null) {
            self::$sdlRules = [
                LoneSchemaDefinition::class                  => new LoneSchemaDefinition(),
                KnownDirectives::class                       => new KnownDirectives(),
                KnownArgumentNamesOnDirectives::class        => new KnownArgumentNamesOnDirectives(),
                UniqueDirectivesPerLocation::class           => new UniqueDirectivesPerLocation(),
                UniqueArgumentNames::class                   => new UniqueArgumentNames(),
                UniqueInputFieldNames::class                 => new UniqueInputFieldNames(),
                ProvidedRequiredArgumentsOnDirectives::class => new ProvidedRequiredArgumentsOnDirectives(),
            ];
        }

        return self::$sdlRules;
    }

    /**
     * This uses a specialized visitor which runs multiple visitors in parallel,
     * while maintaining the visitor skip and break API.
     *
     * @param ValidationRule[] $rules
     *
     * @return Error[]
     */
    public static function visitUsingRules(Schema $schema, TypeInfo $typeInfo, DocumentNode $documentNode, array $rules)
    {
        $context  = new ValidationContext($schema, $documentNode, $typeInfo);
        $visitors = [];
        foreach ($rules as $rule) {
            $visitors[] = $rule->getVisitor($context);
        }
        Visitor::visit($documentNode, Visitor::visitWithTypeInfo($typeInfo, Visitor::visitInParallel($visitors)));

        return $context->getErrors();
    }

    /**
     * Returns global validation rule by name. Standard rules are named by class name, so
     * example usage for such rules:
     *
     * $rule = DocumentValidator::getRule(GraphQL\Validator\Rules\QueryComplexity::class);
     *
     * @param string $name
     *
     * @return ValidationRule
     *
     * @api
     */
    public static function getRule($name)
    {
        $rules = static::allRules();

        if (isset($rules[$name])) {
            return $rules[$name];
        }

        $name = sprintf('GraphQL\\Validator\\Rules\\%s', $name);

        return $rules[$name] ?? null;
    }

    /**
     * Add rule to list of global validation rules
     *
     * @api
     */
    public static function addRule(ValidationRule $rule)
    {
        self::$rules[$rule->getName()] = $rule;
    }

    public static function isError($value)
    {
        return is_array($value)
            ? count(array_filter(
                $value,
                static function ($item) {
                    return $item instanceof Exception || $item instanceof Throwable;
                }
            )) === count($value)
            : ($value instanceof Exception || $value instanceof Throwable);
    }

    public static function append(&$arr, $items)
    {
        if (is_array($items)) {
            $arr = array_merge($arr, $items);
        } else {
            $arr[] = $items;
        }

        return $arr;
    }

    /**
     * Utility which determines if a value literal node is valid for an input type.
     *
     * Deprecated. Rely on validation for documents co
     * ntaining literal values.
     *
     * @deprecated
     *
     * @return Error[]
     */
    public static function isValidLiteralValue(Type $type, $valueNode)
    {
        $emptySchema = new Schema([]);
        $emptyDoc    = new DocumentNode(['definitions' => []]);
        $typeInfo    = new TypeInfo($emptySchema, $type);
        $context     = new ValidationContext($emptySchema, $emptyDoc, $typeInfo);
        $validator   = new ValuesOfCorrectType();
        $visitor     = $validator->getVisitor($context);
        Visitor::visit($valueNode, Visitor::visitWithTypeInfo($typeInfo, $visitor));

        return $context->getErrors();
    }

    public static function assertValidSDLExtension(DocumentNode $documentAST, Schema $schema)
    {
        $errors = self::visitUsingRules($schema, new TypeInfo($schema), $documentAST, self::sdlRules());
        if (count($errors) !== 0) {
            throw new Error(
                implode(
                    "\n\n",
                    array_map(static function (Error $error) : string {
                        return $error->message;
                    }, $errors)
                )
            );
        }
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Type\Definition;

use GraphQL\Utils\Utils;

class NonNull extends Type implements WrappingType, OutputType, InputType
{
    /** @var NullableType */
    private $ofType;

    /**
     * @param NullableType $type
     */
    public function __construct($type)
    {
        $this->ofType = self::assertNullableType($type);
    }

    /**
     * @param mixed $type
     *
     * @return NullableType
     */
    public static function assertNullableType($type)
    {
        Utils::invariant(
            Type::isType($type) && ! $type instanceof self,
            'Expected ' . Utils::printSafe($type) . ' to be a GraphQL nullable type.'
        );

        return $type;
    }

    /**
     * @param mixed $type
     *
     * @return self
     */
    public static function assertNullType($type)
    {
        Utils::invariant(
            $type instanceof self,
            'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Non-Null type.'
        );

        return $type;
    }

    /**
     * @return string
     */
    public function toString()
    {
        return $this->getWrappedType()->toString() . '!';
    }

    /**
     * @param bool $recurse
     *
     * @return Type
     */
    public function getWrappedType($recurse = false)
    {
        $type = $this->ofType;

        return $recurse && $type instanceof WrappingType ? $type->getWrappedType($recurse) : $type;
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Experimental\Executor;

use Generator;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\Error\Warning;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\ExecutorImplementation;
use GraphQL\Executor\Promise\Promise;
use GraphQL\Executor\Promise\PromiseAdapter;
use GraphQL\Executor\Values;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\ValueNode;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\LeafType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Type\Introspection;
use GraphQL\Type\Schema;
use GraphQL\Utils\AST;
use GraphQL\Utils\Utils;
use SplQueue;
use stdClass;
use Throwable;
use function is_array;
use function is_string;
use function sprintf;

class CoroutineExecutor implements Runtime, ExecutorImplementation
{
    /** @var object */
    private static $undefined;

    /** @var Schema */
    private $schema;

    /** @var callable */
    private $fieldResolver;

    /** @var PromiseAdapter */
    private $promiseAdapter;

    /** @var mixed|null */
    private $rootValue;

    /** @var mixed|null */
    private $contextValue;

    /** @var mixed|null */
    private $rawVariableValues;

    /** @var mixed|null */
    private $variableValues;

    /** @var DocumentNode */
    private $documentNode;

    /** @var string|null */
    private $operationName;

    /** @var Collector */
    private $collector;

    /** @var Error[] */
    private $errors;

    /** @var SplQueue */
    private $queue;

    /** @var SplQueue */
    private $schedule;

    /** @var stdClass */
    private $rootResult;

    /** @var int */
    private $pending;

    /** @var callable */
    private $doResolve;

    public function __construct(
        PromiseAdapter $promiseAdapter,
        Schema $schema,
        DocumentNode $documentNode,
        $rootValue,
        $contextValue,
        $rawVariableValues,
        ?string $operationName,
        callable $fieldResolver
    ) {
        if (self::$undefined === null) {
            self::$undefined = Utils::undefined();
        }

        $this->schema            = $schema;
        $this->fieldResolver     = $fieldResolver;
        $this->promiseAdapter    = $promiseAdapter;
        $this->rootValue         = $rootValue;
        $this->contextValue      = $contextValue;
        $this->rawVariableValues = $rawVariableValues;
        $this->documentNode      = $documentNode;
        $this->operationName     = $operationName;
    }

    public static function create(
        PromiseAdapter $promiseAdapter,
        Schema $schema,
        DocumentNode $documentNode,
        $rootValue,
        $contextValue,
        $variableValues,
        ?string $operationName,
        callable $fieldResolver
    ) {
        return new static(
            $promiseAdapter,
            $schema,
            $documentNode,
            $rootValue,
            $contextValue,
            $variableValues,
            $operationName,
            $fieldResolver
        );
    }

    private static function resultToArray($value, $emptyObjectAsStdClass = true)
    {
        if ($value instanceof stdClass) {
            $array = [];
            foreach ($value as $propertyName => $propertyValue) {
                $array[$propertyName] = self::resultToArray($propertyValue);
            }
            if ($emptyObjectAsStdClass && empty($array)) {
                return new stdClass();
            }

            return $array;
        }

        if (is_array($value)) {
            $array = [];
            foreach ($value as $key => $item) {
                $array[$key] = self::resultToArray($item);
            }

            return $array;
        }

        return $value;
    }

    public function doExecute() : Promise
    {
        $this->rootResult = new stdClass();
        $this->errors     = [];
        $this->queue      = new SplQueue();
        $this->schedule   = new SplQueue();
        $this->pending    = 0;

        $this->collector = new Collector($this->schema, $this);
        $this->collector->initialize($this->documentNode, $this->operationName);

        if (! empty($this->errors)) {
            return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $this->errors));
        }

        [$errors, $coercedVariableValues] = Values::getVariableValues(
            $this->schema,
            $this->collector->operation->variableDefinitions ?: [],
            $this->rawVariableValues ?: []
        );

        if (! empty($errors)) {
            return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $errors));
        }

        $this->variableValues = $coercedVariableValues;

        foreach ($this->collector->collectFields($this->collector->rootType, $this->collector->operation->selectionSet) as $shared) {
            /** @var CoroutineContextShared $shared */

            // !!! assign to keep object keys sorted
            $this->rootResult->{$shared->resultName} = null;

            $ctx = new CoroutineContext(
                $shared,
                $this->collector->rootType,
                $this->rootValue,
                $this->rootResult,
                [$shared->resultName]
            );

            $fieldDefinition = $this->findFieldDefinition($ctx);
            if (! $fieldDefinition->getType() instanceof NonNull) {
                $ctx->nullFence = [$shared->resultName];
            }

            if ($this->collector->operation->operation === 'mutation' && ! $this->queue->isEmpty()) {
                $this->schedule->enqueue($ctx);
            } else {
                $this->queue->enqueue(new Strand($this->spawn($ctx)));
            }
        }

        $this->run();

        if ($this->pending > 0) {
            return $this->promiseAdapter->create(function (callable $resolve) {
                $this->doResolve = $resolve;
            });
        }

        return $this->promiseAdapter->createFulfilled($this->finishExecute($this->rootResult, $this->errors));
    }

    /**
     * @param object|null $value
     * @param Error[]     $errors
     */
    private function finishExecute($value, array $errors) : ExecutionResult
    {
        $this->rootResult     = null;
        $this->errors         = null;
        $this->queue          = null;
        $this->schedule       = null;
        $this->pending        = null;
        $this->collector      = null;
        $this->variableValues = null;

        if ($value !== null) {
            $value = self::resultToArray($value, false);
        }

        return new ExecutionResult($value, $errors);
    }

    /**
     * @internal
     */
    public function evaluate(ValueNode $valueNode, InputType $type)
    {
        return AST::valueFromAST($valueNode, $type, $this->variableValues);
    }

    /**
     * @internal
     */
    public function addError($error)
    {
        $this->errors[] = $error;
    }

    private function run()
    {
        RUN:
        while (! $this->queue->isEmpty()) {
            /** @var Strand $strand */
            $strand = $this->queue->dequeue();

            try {
                if ($strand->success !== null) {
                    RESUME:

                    if ($strand->success) {
                        $strand->current->send($strand->value);
                    } else {
                        $strand->current->throw($strand->value);
                    }

                    $strand->success = null;
                    $strand->value   = null;
                }

                START:
                if ($strand->current->valid()) {
                    $value = $strand->current->current();

                    if ($value instanceof Generator) {
                        $strand->stack[$strand->depth++] = $strand->current;
                        $strand->current                 = $value;
                        goto START;
                    } elseif ($this->promiseAdapter->isThenable($value)) {
                        // !!! increment pending before calling ->then() as it may invoke the callback right away
                        ++$this->pending;

                        $this->promiseAdapter
                            ->convertThenable($value)
                            ->then(
                                function ($value) use ($strand) {
                                    $strand->success = true;
                                    $strand->value   = $value;
                                    $this->queue->enqueue($strand);
                                    $this->done();
                                },
                                function (Throwable $throwable) use ($strand) {
                                    $strand->success = false;
                                    $strand->value   = $throwable;
                                    $this->queue->enqueue($strand);
                                    $this->done();
                                }
                            );
                        continue;
                    } else {
                        $strand->success = true;
                        $strand->value   = $value;
                        goto RESUME;
                    }
                }

                $strand->success = true;
                $strand->value   = $strand->current->getReturn();
            } catch (Throwable $reason) {
                $strand->success = false;
                $strand->value   = $reason;
            }

            if ($strand->depth <= 0) {
                continue;
            }

            $current         = &$strand->stack[--$strand->depth];
            $strand->current = $current;
            $current         = null;
            goto RESUME;
        }

        if ($this->pending > 0 || $this->schedule->isEmpty()) {
            return;
        }

        /** @var CoroutineContext $ctx */
        $ctx = $this->schedule->dequeue();
        $this->queue->enqueue(new Strand($this->spawn($ctx)));
        goto RUN;
    }

    private function done()
    {
        --$this->pending;

        $this->run();

        if ($this->pending > 0) {
            return;
        }

        $doResolve = $this->doResolve;
        $doResolve($this->finishExecute($this->rootResult, $this->errors));
    }

    private function spawn(CoroutineContext $ctx)
    {
        // short-circuit evaluation for __typename
        if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
            $ctx->result->{$ctx->shared->resultName} = $ctx->type->name;

            return;
        }

        try {
            if ($ctx->shared->typeGuard1 === $ctx->type) {
                $resolve                = $ctx->shared->resolveIfType1;
                $ctx->resolveInfo       = clone $ctx->shared->resolveInfoIfType1;
                $ctx->resolveInfo->path = $ctx->path;
                $arguments              = $ctx->shared->argumentsIfType1;
                $returnType             = $ctx->resolveInfo->returnType;
            } else {
                $fieldDefinition = $this->findFieldDefinition($ctx);

                if ($fieldDefinition->resolveFn !== null) {
                    $resolve = $fieldDefinition->resolveFn;
                } elseif ($ctx->type->resolveFieldFn !== null) {
                    $resolve = $ctx->type->resolveFieldFn;
                } else {
                    $resolve = $this->fieldResolver;
                }

                $returnType = $fieldDefinition->getType();

                $ctx->resolveInfo = new ResolveInfo(
                    $ctx->shared->fieldName,
                    $ctx->shared->fieldNodes,
                    $returnType,
                    $ctx->type,
                    $ctx->path,
                    $this->schema,
                    $this->collector->fragments,
                    $this->rootValue,
                    $this->collector->operation,
                    $this->variableValues
                );

                $arguments = Values::getArgumentValuesForMap(
                    $fieldDefinition,
                    $ctx->shared->argumentValueMap,
                    $this->variableValues
                );

                // !!! assign only in batch when no exception can be thrown in-between
                $ctx->shared->typeGuard1         = $ctx->type;
                $ctx->shared->resolveIfType1     = $resolve;
                $ctx->shared->argumentsIfType1   = $arguments;
                $ctx->shared->resolveInfoIfType1 = $ctx->resolveInfo;
            }

            $value = $resolve($ctx->value, $arguments, $this->contextValue, $ctx->resolveInfo);

            if (! $this->completeValueFast($ctx, $returnType, $value, $ctx->path, $returnValue)) {
                $returnValue = yield $this->completeValue(
                    $ctx,
                    $returnType,
                    $value,
                    $ctx->path,
                    $ctx->nullFence
                );
            }
        } catch (Throwable $reason) {
            $this->addError(Error::createLocatedError(
                $reason,
                $ctx->shared->fieldNodes,
                $ctx->path
            ));

            $returnValue = self::$undefined;
        }

        if ($returnValue !== self::$undefined) {
            $ctx->result->{$ctx->shared->resultName} = $returnValue;
        } elseif ($ctx->resolveInfo !== null && $ctx->resolveInfo->returnType instanceof NonNull) { // !!! $ctx->resolveInfo might not have been initialized yet
            $result =& $this->rootResult;
            foreach ($ctx->nullFence ?? [] as $key) {
                if (is_string($key)) {
                    $result =& $result->{$key};
                } else {
                    $result =& $result[$key];
                }
            }
            $result = null;
        }
    }

    private function findFieldDefinition(CoroutineContext $ctx)
    {
        if ($ctx->shared->fieldName === Introspection::SCHEMA_FIELD_NAME && $ctx->type === $this->schema->getQueryType()) {
            return Introspection::schemaMetaFieldDef();
        }

        if ($ctx->shared->fieldName === Introspection::TYPE_FIELD_NAME && $ctx->type === $this->schema->getQueryType()) {
            return Introspection::typeMetaFieldDef();
        }

        if ($ctx->shared->fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
            return Introspection::typeNameMetaFieldDef();
        }

        return $ctx->type->getField($ctx->shared->fieldName);
    }

    /**
     * @param mixed    $value
     * @param string[] $path
     * @param mixed    $returnValue
     */
    private function completeValueFast(CoroutineContext $ctx, Type $type, $value, array $path, &$returnValue) : bool
    {
        // special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
        if ($this->promiseAdapter->isThenable($value) || $value instanceof Throwable) {
            return false;
        }

        $nonNull = false;
        if ($type instanceof NonNull) {
            $nonNull = true;
            $type    = $type->getWrappedType();
        }

        if (! $type instanceof LeafType) {
            return false;
        }

        if ($type !== $this->schema->getType($type->name)) {
            $hint = '';
            if ($this->schema->getConfig()->typeLoader) {
                $hint = sprintf(
                    'Make sure that type loader returns the same instance as defined in %s.%s',
                    $ctx->type,
                    $ctx->shared->fieldName
                );
            }
            $this->addError(Error::createLocatedError(
                new InvariantViolation(
                    sprintf(
                        'Schema must contain unique named types but contains multiple types named "%s". %s ' .
                        '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
                        $type->name,
                        $hint
                    )
                ),
                $ctx->shared->fieldNodes,
                $path
            ));

            $value = null;
        }

        if ($value === null) {
            $returnValue = null;
        } else {
            try {
                $returnValue = $type->serialize($value);
            } catch (Throwable $error) {
                $this->addError(Error::createLocatedError(
                    new InvariantViolation(
                        'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
                        0,
                        $error
                    ),
                    $ctx->shared->fieldNodes,
                    $path
                ));
                $returnValue = null;
            }
        }

        if ($nonNull && $returnValue === null) {
            $this->addError(Error::createLocatedError(
                new InvariantViolation(sprintf(
                    'Cannot return null for non-nullable field %s.%s.',
                    $ctx->type->name,
                    $ctx->shared->fieldName
                )),
                $ctx->shared->fieldNodes,
                $path
            ));

            $returnValue = self::$undefined;
        }

        return true;
    }

    /**
     * @param mixed         $value
     * @param string[]      $path
     * @param string[]|null $nullFence
     *
     * @return mixed
     */
    private function completeValue(CoroutineContext $ctx, Type $type, $value, array $path, ?array $nullFence)
    {
        $nonNull     = false;
        $returnValue = null;

        if ($type instanceof NonNull) {
            $nonNull = true;
            $type    = $type->getWrappedType();
        } else {
            $nullFence = $path;
        }

        // !!! $value might be promise, yield to resolve
        try {
            if ($this->promiseAdapter->isThenable($value)) {
                $value = yield $value;
            }
        } catch (Throwable $reason) {
            $this->addError(Error::createLocatedError(
                $reason,
                $ctx->shared->fieldNodes,
                $path
            ));
            if ($nonNull) {
                $returnValue = self::$undefined;
            } else {
                $returnValue = null;
            }
            goto CHECKED_RETURN;
        }

        if ($value === null) {
            $returnValue = $value;
            goto CHECKED_RETURN;
        } elseif ($value instanceof Throwable) {
            // special handling of Throwable inherited from JS reference implementation, but makes no sense in this PHP
            $this->addError(Error::createLocatedError(
                $value,
                $ctx->shared->fieldNodes,
                $path
            ));
            if ($nonNull) {
                $returnValue = self::$undefined;
            } else {
                $returnValue = null;
            }
            goto CHECKED_RETURN;
        }

        if ($type instanceof ListOfType) {
            $returnValue = [];
            $index       = -1;
            $itemType    = $type->getWrappedType();
            foreach ($value as $itemValue) {
                ++$index;

                $itemPath   = $path;
                $itemPath[] = $index; // !!! use arrays COW semantics

                try {
                    if (! $this->completeValueFast($ctx, $itemType, $itemValue, $itemPath, $itemReturnValue)) {
                        $itemReturnValue = yield $this->completeValue($ctx, $itemType, $itemValue, $itemPath, $nullFence);
                    }
                } catch (Throwable $reason) {
                    $this->addError(Error::createLocatedError(
                        $reason,
                        $ctx->shared->fieldNodes,
                        $itemPath
                    ));
                    $itemReturnValue = null;
                }
                if ($itemReturnValue === self::$undefined) {
                    $returnValue = self::$undefined;
                    goto CHECKED_RETURN;
                }
                $returnValue[$index] = $itemReturnValue;
            }

            goto CHECKED_RETURN;
        } else {
            if ($type !== $this->schema->getType($type->name)) {
                $hint = '';
                if ($this->schema->getConfig()->typeLoader) {
                    $hint = sprintf(
                        'Make sure that type loader returns the same instance as defined in %s.%s',
                        $ctx->type,
                        $ctx->shared->fieldName
                    );
                }
                $this->addError(Error::createLocatedError(
                    new InvariantViolation(
                        sprintf(
                            'Schema must contain unique named types but contains multiple types named "%s". %s ' .
                            '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
                            $type->name,
                            $hint
                        )
                    ),
                    $ctx->shared->fieldNodes,
                    $path
                ));

                $returnValue = null;
                goto CHECKED_RETURN;
            }

            if ($type instanceof LeafType) {
                try {
                    $returnValue = $type->serialize($value);
                } catch (Throwable $error) {
                    $this->addError(Error::createLocatedError(
                        new InvariantViolation(
                            'Expected a value of type "' . Utils::printSafe($type) . '" but received: ' . Utils::printSafe($value),
                            0,
                            $error
                        ),
                        $ctx->shared->fieldNodes,
                        $path
                    ));
                    $returnValue = null;
                }
                goto CHECKED_RETURN;
            } elseif ($type instanceof CompositeType) {
                /** @var ObjectType|null $objectType */
                $objectType = null;
                if ($type instanceof InterfaceType || $type instanceof UnionType) {
                    $objectType = $type->resolveType($value, $this->contextValue, $ctx->resolveInfo);

                    if ($objectType === null) {
                        $objectType = yield $this->resolveTypeSlow($ctx, $value, $type);
                    }

                    // !!! $objectType->resolveType() might return promise, yield to resolve
                    $objectType = yield $objectType;
                    if (is_string($objectType)) {
                        $objectType = $this->schema->getType($objectType);
                    }

                    if ($objectType === null) {
                        $this->addError(Error::createLocatedError(
                            sprintf(
                                'Composite type "%s" did not resolve concrete object type for value: %s.',
                                $type->name,
                                Utils::printSafe($value)
                            ),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = self::$undefined;
                        goto CHECKED_RETURN;
                    } elseif (! $objectType instanceof ObjectType) {
                        $this->addError(Error::createLocatedError(
                            new InvariantViolation(sprintf(
                                'Abstract type %s must resolve to an Object type at ' .
                                'runtime for field %s.%s with value "%s", received "%s". ' .
                                'Either the %s type should provide a "resolveType" ' .
                                'function or each possible type should provide an "isTypeOf" function.',
                                $type,
                                $ctx->resolveInfo->parentType,
                                $ctx->resolveInfo->fieldName,
                                Utils::printSafe($value),
                                Utils::printSafe($objectType),
                                $type
                            )),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = null;
                        goto CHECKED_RETURN;
                    } elseif (! $this->schema->isPossibleType($type, $objectType)) {
                        $this->addError(Error::createLocatedError(
                            new InvariantViolation(sprintf(
                                'Runtime Object type "%s" is not a possible type for "%s".',
                                $objectType,
                                $type
                            )),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = null;
                        goto CHECKED_RETURN;
                    } elseif ($objectType !== $this->schema->getType($objectType->name)) {
                        $this->addError(Error::createLocatedError(
                            new InvariantViolation(
                                sprintf(
                                    'Schema must contain unique named types but contains multiple types named "%s". ' .
                                    'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
                                    'type instance as referenced anywhere else within the schema ' .
                                    '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
                                    $objectType,
                                    $type
                                )
                            ),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = null;
                        goto CHECKED_RETURN;
                    }
                } elseif ($type instanceof ObjectType) {
                    $objectType = $type;
                } else {
                    $this->addError(Error::createLocatedError(
                        sprintf(
                            'Unexpected field type "%s".',
                            Utils::printSafe($type)
                        ),
                        $ctx->shared->fieldNodes,
                        $path
                    ));

                    $returnValue = self::$undefined;
                    goto CHECKED_RETURN;
                }

                $typeCheck = $objectType->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
                if ($typeCheck !== null) {
                    // !!! $objectType->isTypeOf() might return promise, yield to resolve
                    $typeCheck = yield $typeCheck;
                    if (! $typeCheck) {
                        $this->addError(Error::createLocatedError(
                            sprintf('Expected value of type "%s" but got: %s.', $type->name, Utils::printSafe($value)),
                            $ctx->shared->fieldNodes,
                            $path
                        ));

                        $returnValue = null;
                        goto CHECKED_RETURN;
                    }
                }

                $returnValue = new stdClass();

                if ($ctx->shared->typeGuard2 === $objectType) {
                    foreach ($ctx->shared->childContextsIfType2 as $childCtx) {
                        $childCtx              = clone $childCtx;
                        $childCtx->type        = $objectType;
                        $childCtx->value       = $value;
                        $childCtx->result      = $returnValue;
                        $childCtx->path        = $path;
                        $childCtx->path[]      = $childCtx->shared->resultName; // !!! uses array COW semantics
                        $childCtx->nullFence   = $nullFence;
                        $childCtx->resolveInfo = null;

                        $this->queue->enqueue(new Strand($this->spawn($childCtx)));

                        // !!! assign null to keep object keys sorted
                        $returnValue->{$childCtx->shared->resultName} = null;
                    }
                } else {
                    $childContexts = [];

                    foreach ($this->collector->collectFields($objectType, $ctx->shared->mergedSelectionSet ?? $this->mergeSelectionSets($ctx)) as $childShared) {
                        /** @var CoroutineContextShared $childShared */

                        $childPath   = $path;
                        $childPath[] = $childShared->resultName; // !!! uses array COW semantics
                        $childCtx    = new CoroutineContext(
                            $childShared,
                            $objectType,
                            $value,
                            $returnValue,
                            $childPath,
                            $nullFence
                        );

                        $childContexts[] = $childCtx;

                        $this->queue->enqueue(new Strand($this->spawn($childCtx)));

                        // !!! assign null to keep object keys sorted
                        $returnValue->{$childShared->resultName} = null;
                    }

                    $ctx->shared->typeGuard2           = $objectType;
                    $ctx->shared->childContextsIfType2 = $childContexts;
                }

                goto CHECKED_RETURN;
            } else {
                $this->addError(Error::createLocatedError(
                    sprintf('Unhandled type "%s".', Utils::printSafe($type)),
                    $ctx->shared->fieldNodes,
                    $path
                ));

                $returnValue = null;
                goto CHECKED_RETURN;
            }
        }

        CHECKED_RETURN:
        if ($nonNull && $returnValue === null) {
            $this->addError(Error::createLocatedError(
                new InvariantViolation(sprintf(
                    'Cannot return null for non-nullable field %s.%s.',
                    $ctx->type->name,
                    $ctx->shared->fieldName
                )),
                $ctx->shared->fieldNodes,
                $path
            ));

            return self::$undefined;
        }

        return $returnValue;
    }

    private function mergeSelectionSets(CoroutineContext $ctx)
    {
        $selections = [];

        foreach ($ctx->shared->fieldNodes as $fieldNode) {
            if ($fieldNode->selectionSet === null) {
                continue;
            }

            foreach ($fieldNode->selectionSet->selections as $selection) {
                $selections[] = $selection;
            }
        }

        return $ctx->shared->mergedSelectionSet = new SelectionSetNode(['selections' => $selections]);
    }

    private function resolveTypeSlow(CoroutineContext $ctx, $value, AbstractType $abstractType)
    {
        if ($value !== null &&
            is_array($value) &&
            isset($value['__typename']) &&
            is_string($value['__typename'])
        ) {
            return $this->schema->getType($value['__typename']);
        }

        if ($abstractType instanceof InterfaceType && $this->schema->getConfig()->typeLoader) {
            Warning::warnOnce(
                sprintf(
                    'GraphQL Interface Type `%s` returned `null` from its `resolveType` function ' .
                    'for value: %s. Switching to slow resolution method using `isTypeOf` ' .
                    'of all possible implementations. It requires full schema scan and degrades query performance significantly. ' .
                    ' Make sure your `resolveType` always returns valid implementation or throws.',
                    $abstractType->name,
                    Utils::printSafe($value)
                ),
                Warning::WARNING_FULL_SCHEMA_SCAN
            );
        }

        $possibleTypes = $this->schema->getPossibleTypes($abstractType);

        // to be backward-compatible with old executor, ->isTypeOf() is called for all possible types,
        // it cannot short-circuit when the match is found

        $selectedType = null;
        foreach ($possibleTypes as $type) {
            $typeCheck = yield $type->isTypeOf($value, $this->contextValue, $ctx->resolveInfo);
            if ($selectedType !== null || $typeCheck !== true) {
                continue;
            }

            $selectedType = $type;
        }

        return $selectedType;
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Utils;

use GraphQL\Error\Error;
use GraphQL\Executor\Values;
use GraphQL\Language\AST\DirectiveDefinitionNode;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\EnumTypeExtensionNode;
use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
use GraphQL\Language\AST\ListTypeNode;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\TypeNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\Token;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\InputType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use Throwable;
use function array_reverse;
use function implode;
use function is_array;
use function is_string;
use function sprintf;

class ASTDefinitionBuilder
{
    /** @var Node[] */
    private $typeDefinitionsMap;

    /** @var callable */
    private $typeConfigDecorator;

    /** @var bool[] */
    private $options;

    /** @var callable */
    private $resolveType;

    /** @var Type[] */
    private $cache;

    /**
     * @param Node[] $typeDefinitionsMap
     * @param bool[] $options
     */
    public function __construct(
        array $typeDefinitionsMap,
        $options,
        callable $resolveType,
        ?callable $typeConfigDecorator = null
    ) {
        $this->typeDefinitionsMap  = $typeDefinitionsMap;
        $this->typeConfigDecorator = $typeConfigDecorator;
        $this->options             = $options;
        $this->resolveType         = $resolveType;

        $this->cache = Type::getAllBuiltInTypes();
    }

    public function buildDirective(DirectiveDefinitionNode $directiveNode)
    {
        return new Directive([
            'name'        => $directiveNode->name->value,
            'description' => $this->getDescription($directiveNode),
            'locations'   => Utils::map(
                $directiveNode->locations,
                static function ($node) {
                    return $node->value;
                }
            ),
            'args'        => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null,
            'astNode'     => $directiveNode,
        ]);
    }

    /**
     * Given an ast node, returns its string description.
     */
    private function getDescription($node)
    {
        if ($node->description) {
            return $node->description->value;
        }
        if (isset($this->options['commentDescriptions'])) {
            $rawValue = $this->getLeadingCommentBlock($node);
            if ($rawValue !== null) {
                return BlockString::value("\n" . $rawValue);
            }
        }

        return null;
    }

    private function getLeadingCommentBlock($node)
    {
        $loc = $node->loc;
        if (! $loc || ! $loc->startToken) {
            return null;
        }
        $comments = [];
        $token    = $loc->startToken->prev;
        while ($token &&
            $token->kind === Token::COMMENT &&
            $token->next && $token->prev &&
            $token->line + 1 === $token->next->line &&
            $token->line !== $token->prev->line
        ) {
            $value      = $token->value;
            $comments[] = $value;
            $token      = $token->prev;
        }

        return implode("\n", array_reverse($comments));
    }

    private function makeInputValues($values)
    {
        return Utils::keyValMap(
            $values,
            static function ($value) {
                return $value->name->value;
            },
            function ($value) {
                // Note: While this could make assertions to get the correctly typed
                // value, that would throw immediately while type system validation
                // with validateSchema() will produce more actionable results.
                $type = $this->internalBuildWrappedType($value->type);

                $config = [
                    'name'        => $value->name->value,
                    'type'        => $type,
                    'description' => $this->getDescription($value),
                    'astNode'     => $value,
                ];
                if (isset($value->defaultValue)) {
                    $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
                }

                return $config;
            }
        );
    }

    /**
     * @return Type|InputType
     *
     * @throws Error
     */
    private function internalBuildWrappedType(TypeNode $typeNode)
    {
        $typeDef = $this->buildType($this->getNamedTypeNode($typeNode));

        return $this->buildWrappedType($typeDef, $typeNode);
    }

    /**
     * @param string|NamedTypeNode $ref
     *
     * @return Type
     *
     * @throws Error
     */
    public function buildType($ref)
    {
        if (is_string($ref)) {
            return $this->internalBuildType($ref);
        }

        return $this->internalBuildType($ref->name->value, $ref);
    }

    /**
     * @param string             $typeName
     * @param NamedTypeNode|null $typeNode
     *
     * @return Type
     *
     * @throws Error
     */
    private function internalBuildType($typeName, $typeNode = null)
    {
        if (! isset($this->cache[$typeName])) {
            if (isset($this->typeDefinitionsMap[$typeName])) {
                $type = $this->makeSchemaDef($this->typeDefinitionsMap[$typeName]);
                if ($this->typeConfigDecorator) {
                    $fn = $this->typeConfigDecorator;
                    try {
                        $config = $fn($type->config, $this->typeDefinitionsMap[$typeName], $this->typeDefinitionsMap);
                    } catch (Throwable $e) {
                        throw new Error(
                            sprintf('Type config decorator passed to %s threw an error ', static::class) .
                            sprintf('when building %s type: %s', $typeName, $e->getMessage()),
                            null,
                            null,
                            null,
                            null,
                            $e
                        );
                    }
                    if (! is_array($config) || isset($config[0])) {
                        throw new Error(
                            sprintf(
                                'Type config decorator passed to %s is expected to return an array, but got %s',
                                static::class,
                                Utils::getVariableType($config)
                            )
                        );
                    }
                    $type = $this->makeSchemaDefFromConfig($this->typeDefinitionsMap[$typeName], $config);
                }
                $this->cache[$typeName] = $type;
            } else {
                $fn                     = $this->resolveType;
                $this->cache[$typeName] = $fn($typeName, $typeNode);
            }
        }

        return $this->cache[$typeName];
    }

    /**
     * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeDefinitionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode|UnionTypeDefinitionNode $def
     *
     * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType
     *
     * @throws Error
     */
    private function makeSchemaDef($def)
    {
        if (! $def) {
            throw new Error('def must be defined.');
        }
        switch ($def->kind) {
            case NodeKind::OBJECT_TYPE_DEFINITION:
                return $this->makeTypeDef($def);
            case NodeKind::INTERFACE_TYPE_DEFINITION:
                return $this->makeInterfaceDef($def);
            case NodeKind::ENUM_TYPE_DEFINITION:
                return $this->makeEnumDef($def);
            case NodeKind::UNION_TYPE_DEFINITION:
                return $this->makeUnionDef($def);
            case NodeKind::SCALAR_TYPE_DEFINITION:
                return $this->makeScalarDef($def);
            case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
                return $this->makeInputObjectDef($def);
            default:
                throw new Error(sprintf('Type kind of %s not supported.', $def->kind));
        }
    }

    private function makeTypeDef(ObjectTypeDefinitionNode $def)
    {
        $typeName = $def->name->value;

        return new ObjectType([
            'name'        => $typeName,
            'description' => $this->getDescription($def),
            'fields'      => function () use ($def) {
                return $this->makeFieldDefMap($def);
            },
            'interfaces'  => function () use ($def) {
                return $this->makeImplementedInterfaces($def);
            },
            'astNode'     => $def,
        ]);
    }

    private function makeFieldDefMap($def)
    {
        return $def->fields
            ? Utils::keyValMap(
                $def->fields,
                static function ($field) {
                    return $field->name->value;
                },
                function ($field) {
                    return $this->buildField($field);
                }
            )
            : [];
    }

    public function buildField(FieldDefinitionNode $field)
    {
        return [
            // Note: While this could make assertions to get the correctly typed
            // value, that would throw immediately while type system validation
            // with validateSchema() will produce more actionable results.
            'type'              => $this->internalBuildWrappedType($field->type),
            'description'       => $this->getDescription($field),
            'args'              => $field->arguments ? $this->makeInputValues($field->arguments) : null,
            'deprecationReason' => $this->getDeprecationReason($field),
            'astNode'           => $field,
        ];
    }

    /**
     * Given a collection of directives, returns the string value for the
     * deprecation reason.
     *
     * @param EnumValueDefinitionNode | FieldDefinitionNode $node
     *
     * @return string
     */
    private function getDeprecationReason($node)
    {
        $deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node);

        return $deprecated['reason'] ?? null;
    }

    private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def)
    {
        if ($def->interfaces) {
            // Note: While this could make early assertions to get the correctly
            // typed values, that would throw immediately while type system
            // validation with validateSchema() will produce more actionable results.
            return Utils::map(
                $def->interfaces,
                function ($iface) {
                    return $this->buildType($iface);
                }
            );
        }

        return null;
    }

    private function makeInterfaceDef(InterfaceTypeDefinitionNode $def)
    {
        $typeName = $def->name->value;

        return new InterfaceType([
            'name'        => $typeName,
            'description' => $this->getDescription($def),
            'fields'      => function () use ($def) {
                return $this->makeFieldDefMap($def);
            },
            'astNode'     => $def,
        ]);
    }

    private function makeEnumDef(EnumTypeDefinitionNode $def)
    {
        return new EnumType([
            'name'        => $def->name->value,
            'description' => $this->getDescription($def),
            'values'      => $def->values
                ? Utils::keyValMap(
                    $def->values,
                    static function ($enumValue) {
                        return $enumValue->name->value;
                    },
                    function ($enumValue) {
                        return [
                            'description'       => $this->getDescription($enumValue),
                            'deprecationReason' => $this->getDeprecationReason($enumValue),
                            'astNode'           => $enumValue,
                        ];
                    }
                )
                : [],
            'astNode'     => $def,
        ]);
    }

    private function makeUnionDef(UnionTypeDefinitionNode $def)
    {
        return new UnionType([
            'name'        => $def->name->value,
            'description' => $this->getDescription($def),
            // Note: While this could make assertions to get the correctly typed
            // values below, that would throw immediately while type system
            // validation with validateSchema() will produce more actionable results.
            'types'       => $def->types
                ? Utils::map(
                    $def->types,
                    function ($typeNode) {
                        return $this->buildType($typeNode);
                    }
                ) :
                [],
            'astNode'     => $def,
        ]);
    }

    private function makeScalarDef(ScalarTypeDefinitionNode $def)
    {
        return new CustomScalarType([
            'name'        => $def->name->value,
            'description' => $this->getDescription($def),
            'astNode'     => $def,
            'serialize'   => static function ($value) {
                return $value;
            },
        ]);
    }

    private function makeInputObjectDef(InputObjectTypeDefinitionNode $def)
    {
        return new InputObjectType([
            'name'        => $def->name->value,
            'description' => $this->getDescription($def),
            'fields'      => function () use ($def) {
                return $def->fields
                    ? $this->makeInputValues($def->fields)
                    : [];
            },
            'astNode'     => $def,
        ]);
    }

    /**
     * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeExtensionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode $def
     * @param mixed[]                                                                                $config
     *
     * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType
     *
     * @throws Error
     */
    private function makeSchemaDefFromConfig($def, array $config)
    {
        if (! $def) {
            throw new Error('def must be defined.');
        }
        switch ($def->kind) {
            case NodeKind::OBJECT_TYPE_DEFINITION:
                return new ObjectType($config);
            case NodeKind::INTERFACE_TYPE_DEFINITION:
                return new InterfaceType($config);
            case NodeKind::ENUM_TYPE_DEFINITION:
                return new EnumType($config);
            case NodeKind::UNION_TYPE_DEFINITION:
                return new UnionType($config);
            case NodeKind::SCALAR_TYPE_DEFINITION:
                return new CustomScalarType($config);
            case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
                return new InputObjectType($config);
            default:
                throw new Error(sprintf('Type kind of %s not supported.', $def->kind));
        }
    }

    /**
     * @param TypeNode|ListTypeNode|NonNullTypeNode $typeNode
     *
     * @return TypeNode
     */
    private function getNamedTypeNode(TypeNode $typeNode)
    {
        $namedType = $typeNode;
        while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) {
            $namedType = $namedType->type;
        }

        return $namedType;
    }

    /**
     * @param TypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
     *
     * @return Type
     */
    private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode)
    {
        if ($inputTypeNode->kind === NodeKind::LIST_TYPE) {
            return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type));
        }
        if ($inputTypeNode->kind === NodeKind::NON_NULL_TYPE) {
            $wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type);

            return Type::nonNull(NonNull::assertNullableType($wrappedType));
        }

        return $innerType;
    }

    /**
     * @return mixed[]
     */
    public function buildInputField(InputValueDefinitionNode $value) : array
    {
        $type = $this->internalBuildWrappedType($value->type);

        $config = [
            'name' => $value->name->value,
            'type' => $type,
            'description' => $this->getDescription($value),
            'astNode' => $value,
        ];

        if ($value->defaultValue) {
            $config['defaultValue'] = $value->defaultValue;
        }

        return $config;
    }

    /**
     * @return mixed[]
     */
    public function buildEnumValue(EnumValueDefinitionNode $value) : array
    {
        return [
            'description' => $this->getDescription($value),
            'deprecationReason' => $this->getDeprecationReason($value),
            'astNode' => $value,
        ];
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Utils;

use ArrayAccess;
use GraphQL\Type\Definition\EnumValueDefinition;
use InvalidArgumentException;
use SplObjectStorage;
use function array_key_exists;
use function array_search;
use function array_splice;
use function is_array;
use function is_float;
use function is_int;
use function is_object;
use function is_string;

/**
 * Similar to PHP array, but allows any type of data to act as key (including arrays, objects, scalars)
 *
 * Note: unfortunately when storing array as key - access and modification is O(N)
 * (yet this should be really rare case and should be avoided when possible)
 */
class MixedStore implements ArrayAccess
{
    /** @var EnumValueDefinition[] */
    private $standardStore;

    /** @var mixed[] */
    private $floatStore;

    /** @var SplObjectStorage */
    private $objectStore;

    /** @var callable[] */
    private $arrayKeys;

    /** @var EnumValueDefinition[] */
    private $arrayValues;

    /** @var callable[] */
    private $lastArrayKey;

    /** @var mixed */
    private $lastArrayValue;

    /** @var mixed */
    private $nullValue;

    /** @var bool */
    private $nullValueIsSet;

    /** @var mixed */
    private $trueValue;

    /** @var bool */
    private $trueValueIsSet;

    /** @var mixed */
    private $falseValue;

    /** @var bool */
    private $falseValueIsSet;

    public function __construct()
    {
        $this->standardStore   = [];
        $this->floatStore      = [];
        $this->objectStore     = new SplObjectStorage();
        $this->arrayKeys       = [];
        $this->arrayValues     = [];
        $this->nullValueIsSet  = false;
        $this->trueValueIsSet  = false;
        $this->falseValueIsSet = false;
    }

    /**
     * Whether a offset exists
     *
     * @link http://php.net/manual/en/arrayaccess.offsetexists.php
     *
     * @param mixed $offset

     * An offset to check for.
     *

     *
     * @return bool true on success or false on failure.
     *

     *

     * The return value will be casted to boolean if non-boolean was returned.
     */
    public function offsetExists($offset)
    {
        if ($offset === false) {
            return $this->falseValueIsSet;
        }
        if ($offset === true) {
            return $this->trueValueIsSet;
        }
        if (is_int($offset) || is_string($offset)) {
            return array_key_exists($offset, $this->standardStore);
        }
        if (is_float($offset)) {
            return array_key_exists((string) $offset, $this->floatStore);
        }
        if (is_object($offset)) {
            return $this->objectStore->offsetExists($offset);
        }
        if (is_array($offset)) {
            foreach ($this->arrayKeys as $index => $entry) {
                if ($entry === $offset) {
                    $this->lastArrayKey   = $offset;
                    $this->lastArrayValue = $this->arrayValues[$index];

                    return true;
                }
            }
        }
        if ($offset === null) {
            return $this->nullValueIsSet;
        }

        return false;
    }

    /**
     * Offset to retrieve
     *
     * @link http://php.net/manual/en/arrayaccess.offsetget.php
     *
     * @param mixed $offset

     * The offset to retrieve.
     *

     *
     * @return mixed Can return all value types.
     */
    public function offsetGet($offset)
    {
        if ($offset === true) {
            return $this->trueValue;
        }
        if ($offset === false) {
            return $this->falseValue;
        }
        if (is_int($offset) || is_string($offset)) {
            return $this->standardStore[$offset];
        }
        if (is_float($offset)) {
            return $this->floatStore[(string) $offset];
        }
        if (is_object($offset)) {
            return $this->objectStore->offsetGet($offset);
        }
        if (is_array($offset)) {
            // offsetGet is often called directly after offsetExists, so optimize to avoid second loop:
            if ($this->lastArrayKey === $offset) {
                return $this->lastArrayValue;
            }
            foreach ($this->arrayKeys as $index => $entry) {
                if ($entry === $offset) {
                    return $this->arrayValues[$index];
                }
            }
        }
        if ($offset === null) {
            return $this->nullValue;
        }

        return null;
    }

    /**
     * Offset to set
     *
     * @link http://php.net/manual/en/arrayaccess.offsetset.php
     *
     * @param mixed $offset

     * The offset to assign the value to.
     *

     * @param mixed $value

     *  The value to set.
     *

     *
     * @return void
     */
    public function offsetSet($offset, $value)
    {
        if ($offset === false) {
            $this->falseValue      = $value;
            $this->falseValueIsSet = true;
        } elseif ($offset === true) {
            $this->trueValue      = $value;
            $this->trueValueIsSet = true;
        } elseif (is_int($offset) || is_string($offset)) {
            $this->standardStore[$offset] = $value;
        } elseif (is_float($offset)) {
            $this->floatStore[(string) $offset] = $value;
        } elseif (is_object($offset)) {
            $this->objectStore[$offset] = $value;
        } elseif (is_array($offset)) {
            $this->arrayKeys[]   = $offset;
            $this->arrayValues[] = $value;
        } elseif ($offset === null) {
            $this->nullValue      = $value;
            $this->nullValueIsSet = true;
        } else {
            throw new InvalidArgumentException('Unexpected offset type: ' . Utils::printSafe($offset));
        }
    }

    /**
     * Offset to unset
     *
     * @link http://php.net/manual/en/arrayaccess.offsetunset.php
     *
     * @param mixed $offset

     * The offset to unset.
     *

     *
     * @return void
     */
    public function offsetUnset($offset)
    {
        if ($offset === true) {
            $this->trueValue      = null;
            $this->trueValueIsSet = false;
        } elseif ($offset === false) {
            $this->falseValue      = null;
            $this->falseValueIsSet = false;
        } elseif (is_int($offset) || is_string($offset)) {
            unset($this->standardStore[$offset]);
        } elseif (is_float($offset)) {
            unset($this->floatStore[(string) $offset]);
        } elseif (is_object($offset)) {
            $this->objectStore->offsetUnset($offset);
        } elseif (is_array($offset)) {
            $index = array_search($offset, $this->arrayKeys, true);

            if ($index !== false) {
                array_splice($this->arrayKeys, $index, 1);
                array_splice($this->arrayValues, $index, 1);
            }
        } elseif ($offset === null) {
            $this->nullValue      = null;
            $this->nullValueIsSet = false;
        }
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Type;

use Generator;
use GraphQL\Error\Error;
use GraphQL\Error\InvariantViolation;
use GraphQL\GraphQL;
use GraphQL\Language\AST\SchemaDefinitionNode;
use GraphQL\Language\AST\SchemaTypeExtensionNode;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Utils\TypeInfo;
use GraphQL\Utils\Utils;
use Traversable;
use function array_values;
use function implode;
use function is_array;
use function is_callable;
use function sprintf;

/**
 * Schema Definition (see [related docs](type-system/schema.md))
 *
 * A Schema is created by supplying the root types of each type of operation:
 * query, mutation (optional) and subscription (optional). A schema definition is
 * then supplied to the validator and executor. Usage Example:
 *
 *     $schema = new GraphQL\Type\Schema([
 *       'query' => $MyAppQueryRootType,
 *       'mutation' => $MyAppMutationRootType,
 *     ]);
 *
 * Or using Schema Config instance:
 *
 *     $config = GraphQL\Type\SchemaConfig::create()
 *         ->setQuery($MyAppQueryRootType)
 *         ->setMutation($MyAppMutationRootType);
 *
 *     $schema = new GraphQL\Type\Schema($config);
 */
class Schema
{
    /** @var SchemaConfig */
    private $config;

    /**
     * Contains currently resolved schema types
     *
     * @var Type[]
     */
    private $resolvedTypes = [];

    /** @var Type[][]|null */
    private $possibleTypeMap;

    /**
     * True when $resolvedTypes contain all possible schema types
     *
     * @var bool
     */
    private $fullyLoaded = false;

    /** @var InvariantViolation[]|null */
    private $validationErrors;

    /** @var SchemaTypeExtensionNode[] */
    public $extensionASTNodes;

    /**
     * @param mixed[]|SchemaConfig $config
     *
     * @api
     */
    public function __construct($config)
    {
        if (is_array($config)) {
            $config = SchemaConfig::create($config);
        }

        // If this schema was built from a source known to be valid, then it may be
        // marked with assumeValid to avoid an additional type system validation.
        if ($config->getAssumeValid()) {
            $this->validationErrors = [];
        } else {
            // Otherwise check for common mistakes during construction to produce
            // clear and early error messages.
            Utils::invariant(
                $config instanceof SchemaConfig,
                'Schema constructor expects instance of GraphQL\Type\SchemaConfig or an array with keys: %s; but got: %s',
                implode(
                    ', ',
                    [
                        'query',
                        'mutation',
                        'subscription',
                        'types',
                        'directives',
                        'typeLoader',
                    ]
                ),
                Utils::getVariableType($config)
            );
            Utils::invariant(
                ! $config->types || is_array($config->types) || is_callable($config->types),
                '"types" must be array or callable if provided but got: ' . Utils::getVariableType($config->types)
            );
            Utils::invariant(
                ! $config->directives || is_array($config->directives),
                '"directives" must be Array if provided but got: ' . Utils::getVariableType($config->directives)
            );
        }

        $this->config            = $config;
        $this->extensionASTNodes = $config->extensionASTNodes;

        if ($config->query) {
            $this->resolvedTypes[$config->query->name] = $config->query;
        }
        if ($config->mutation) {
            $this->resolvedTypes[$config->mutation->name] = $config->mutation;
        }
        if ($config->subscription) {
            $this->resolvedTypes[$config->subscription->name] = $config->subscription;
        }
        if (is_array($this->config->types)) {
            foreach ($this->resolveAdditionalTypes() as $type) {
                if (isset($this->resolvedTypes[$type->name])) {
                    Utils::invariant(
                        $type === $this->resolvedTypes[$type->name],
                        sprintf(
                            'Schema must contain unique named types but contains multiple types named "%s" (see http://webonyx.github.io/graphql-php/type-system/#type-registry).',
                            $type
                        )
                    );
                }
                $this->resolvedTypes[$type->name] = $type;
            }
        }
        $this->resolvedTypes += Type::getStandardTypes() + Introspection::getTypes();

        if ($this->config->typeLoader) {
            return;
        }

        // Perform full scan of the schema
        $this->getTypeMap();
    }

    /**
     * @return Generator
     */
    private function resolveAdditionalTypes()
    {
        $types = $this->config->types ?: [];

        if (is_callable($types)) {
            $types = $types();
        }

        if (! is_array($types) && ! $types instanceof Traversable) {
            throw new InvariantViolation(sprintf(
                'Schema types callable must return array or instance of Traversable but got: %s',
                Utils::getVariableType($types)
            ));
        }

        foreach ($types as $index => $type) {
            if (! $type instanceof Type) {
                throw new InvariantViolation(sprintf(
                    'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but entry at %s is %s',
                    $index,
                    Utils::printSafe($type)
                ));
            }
            yield $type;
        }
    }

    /**
     * Returns array of all types in this schema. Keys of this array represent type names, values are instances
     * of corresponding type definitions
     *
     * This operation requires full schema scan. Do not use in production environment.
     *
     * @return Type[]
     *
     * @api
     */
    public function getTypeMap()
    {
        if (! $this->fullyLoaded) {
            $this->resolvedTypes = $this->collectAllTypes();
            $this->fullyLoaded   = true;
        }

        return $this->resolvedTypes;
    }

    /**
     * @return Type[]
     */
    private function collectAllTypes()
    {
        $typeMap = [];
        foreach ($this->resolvedTypes as $type) {
            $typeMap = TypeInfo::extractTypes($type, $typeMap);
        }
        foreach ($this->getDirectives() as $directive) {
            if (! ($directive instanceof Directive)) {
                continue;
            }

            $typeMap = TypeInfo::extractTypesFromDirectives($directive, $typeMap);
        }
        // When types are set as array they are resolved in constructor
        if (is_callable($this->config->types)) {
            foreach ($this->resolveAdditionalTypes() as $type) {
                $typeMap = TypeInfo::extractTypes($type, $typeMap);
            }
        }

        return $typeMap;
    }

    /**
     * Returns a list of directives supported by this schema
     *
     * @return Directive[]
     *
     * @api
     */
    public function getDirectives()
    {
        return $this->config->directives ?: GraphQL::getStandardDirectives();
    }

    /**
     * Returns schema query type
     *
     * @return ObjectType
     *
     * @api
     */
    public function getQueryType()
    {
        return $this->config->query;
    }

    /**
     * Returns schema mutation type
     *
     * @return ObjectType|null
     *
     * @api
     */
    public function getMutationType()
    {
        return $this->config->mutation;
    }

    /**
     * Returns schema subscription
     *
     * @return ObjectType|null
     *
     * @api
     */
    public function getSubscriptionType()
    {
        return $this->config->subscription;
    }

    /**
     * @return SchemaConfig
     *
     * @api
     */
    public function getConfig()
    {
        return $this->config;
    }

    /**
     * Returns type by it's name
     *
     * @param string $name
     *
     * @return Type|null
     *
     * @api
     */
    public function getType($name)
    {
        if (! isset($this->resolvedTypes[$name])) {
            $type = $this->loadType($name);
            if (! $type) {
                return null;
            }
            $this->resolvedTypes[$name] = $type;
        }

        return $this->resolvedTypes[$name];
    }

    /**
     * @param string $name
     *
     * @return bool
     */
    public function hasType($name)
    {
        return $this->getType($name) !== null;
    }

    /**
     * @param string $typeName
     *
     * @return Type
     */
    private function loadType($typeName)
    {
        $typeLoader = $this->config->typeLoader;

        if (! $typeLoader) {
            return $this->defaultTypeLoader($typeName);
        }

        $type = $typeLoader($typeName);

        if (! $type instanceof Type) {
            throw new InvariantViolation(
                sprintf(
                    'Type loader is expected to return valid type "%s", but it returned %s',
                    $typeName,
                    Utils::printSafe($type)
                )
            );
        }
        if ($type->name !== $typeName) {
            throw new InvariantViolation(
                sprintf('Type loader is expected to return type "%s", but it returned "%s"', $typeName, $type->name)
            );
        }

        return $type;
    }

    /**
     * @param string $typeName
     *
     * @return Type
     */
    private function defaultTypeLoader($typeName)
    {
        // Default type loader simply fallbacks to collecting all types
        $typeMap = $this->getTypeMap();

        return $typeMap[$typeName] ?? null;
    }

    /**
     * Returns all possible concrete types for given abstract type
     * (implementations for interfaces and members of union type for unions)
     *
     * This operation requires full schema scan. Do not use in production environment.
     *
     * @return ObjectType[]
     *
     * @api
     */
    public function getPossibleTypes(AbstractType $abstractType)
    {
        $possibleTypeMap = $this->getPossibleTypeMap();

        return isset($possibleTypeMap[$abstractType->name]) ? array_values($possibleTypeMap[$abstractType->name]) : [];
    }

    /**
     * @return Type[][]
     */
    private function getPossibleTypeMap()
    {
        if ($this->possibleTypeMap === null) {
            $this->possibleTypeMap = [];
            foreach ($this->getTypeMap() as $type) {
                if ($type instanceof ObjectType) {
                    foreach ($type->getInterfaces() as $interface) {
                        if (! ($interface instanceof InterfaceType)) {
                            continue;
                        }

                        $this->possibleTypeMap[$interface->name][$type->name] = $type;
                    }
                } elseif ($type instanceof UnionType) {
                    foreach ($type->getTypes() as $innerType) {
                        $this->possibleTypeMap[$type->name][$innerType->name] = $innerType;
                    }
                }
            }
        }

        return $this->possibleTypeMap;
    }

    /**
     * Returns true if object type is concrete type of given abstract type
     * (implementation for interfaces and members of union type for unions)
     *
     * @return bool
     *
     * @api
     */
    public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType)
    {
        if ($abstractType instanceof InterfaceType) {
            return $possibleType->implementsInterface($abstractType);
        }

        /** @var UnionType $abstractType */
        return $abstractType->isPossibleType($possibleType);
    }

    /**
     * Returns instance of directive by name
     *
     * @param string $name
     *
     * @return Directive
     *
     * @api
     */
    public function getDirective($name)
    {
        foreach ($this->getDirectives() as $directive) {
            if ($directive->name === $name) {
                return $directive;
            }
        }

        return null;
    }

    /**
     * @return SchemaDefinitionNode
     */
    public function getAstNode()
    {
        return $this->config->getAstNode();
    }

    /**
     * Validates schema.
     *
     * This operation requires full schema scan. Do not use in production environment.
     *
     * @throws InvariantViolation
     *
     * @api
     */
    public function assertValid()
    {
        $errors = $this->validate();

        if ($errors) {
            throw new InvariantViolation(implode("\n\n", $this->validationErrors));
        }

        $internalTypes = Type::getStandardTypes() + Introspection::getTypes();
        foreach ($this->getTypeMap() as $name => $type) {
            if (isset($internalTypes[$name])) {
                continue;
            }

            $type->assertValid();

            // Make sure type loader returns the same instance as registered in other places of schema
            if (! $this->config->typeLoader) {
                continue;
            }

            Utils::invariant(
                $this->loadType($name) === $type,
                sprintf(
                    'Type loader returns different instance for %s than field/argument definitions. Make sure you always return the same instance for the same type name.',
                    $name
                )
            );
        }
    }

    /**
     * Validates schema.
     *
     * This operation requires full schema scan. Do not use in production environment.
     *
     * @return InvariantViolation[]|Error[]
     *
     * @api
     */
    public function validate()
    {
        // If this Schema has already been validated, return the previous results.
        if ($this->validationErrors !== null) {
            return $this->validationErrors;
        }
        // Validate the schema, producing a list of errors.
        $context = new SchemaValidationContext($this);
        $context->validateRootTypes();
        $context->validateDirectives();
        $context->validateTypes();

        // Persist the results of validation before returning to ensure validation
        // does not run multiple times for this schema.
        $this->validationErrors = $context->getErrors();

        return $this->validationErrors;
    }
}