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

use GraphQL\Error\Error;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\AbstractType;
use GraphQL\Type\Definition\CompositeType;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\UnionType;
use GraphQL\Validator\ValidationContext;
use GraphQL\Utils\TypeInfo;

class PossibleFragmentSpreads extends AbstractValidationRule
{
    static function typeIncompatibleSpreadMessage($fragName, $parentType, $fragType)
    {
        return "Fragment \"$fragName\" cannot be spread here as objects of type \"$parentType\" can never be of type \"$fragType\".";
    }

    static function typeIncompatibleAnonSpreadMessage($parentType, $fragType)
    {
        return "Fragment cannot be spread here as objects of type \"$parentType\" can never be of type \"$fragType\".";
    }

    public function getVisitor(ValidationContext $context)
    {
        return [
            NodeKind::INLINE_FRAGMENT => function(InlineFragmentNode $node) use ($context) {
                $fragType = $context->getType();
                $parentType = $context->getParentType();

                if ($fragType instanceof CompositeType &&
                    $parentType instanceof CompositeType &&
                    !$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) {
                    $context->reportError(new Error(
                        self::typeIncompatibleAnonSpreadMessage($parentType, $fragType),
                        [$node]
                    ));
                }
            },
            NodeKind::FRAGMENT_SPREAD => function(FragmentSpreadNode $node) use ($context) {
                $fragName = $node->name->value;
                $fragType = $this->getFragmentType($context, $fragName);
                $parentType = $context->getParentType();

                if ($fragType && $parentType && !$this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) {
                    $context->reportError(new Error(
                        self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType),
                        [$node]
                    ));
                }
            }
        ];
    }

    private function getFragmentType(ValidationContext $context, $name)
    {
        $frag = $context->getFragment($name);
        if ($frag) {
            $type = TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition);
            if ($type instanceof CompositeType) {
                return $type;
            }
        }
        return null;
    }

    private function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType)
    {
        // Checking in the order of the most frequently used scenarios:
        // Parent type === fragment type
        if ($parentType === $fragType) {
            return true;
        }

        // Parent type is interface or union, fragment type is object type
        if ($parentType instanceof AbstractType && $fragType instanceof ObjectType) {
            return $schema->isPossibleType($parentType, $fragType);
        }

        // Parent type is object type, fragment type is interface (or rather rare - union)
        if ($parentType instanceof ObjectType && $fragType instanceof AbstractType) {
            return $schema->isPossibleType($fragType, $parentType);
        }

        // Both are object types:
        if ($parentType instanceof ObjectType && $fragType instanceof ObjectType) {
            return $parentType === $fragType;
        }

        // Both are interfaces
        // This case may be assumed valid only when implementations of two interfaces intersect
        // But we don't have information about all implementations at runtime
        // (getting this information via $schema->getPossibleTypes() requires scanning through whole schema
        // which is very costly to do at each request due to PHP "shared nothing" architecture)
        //
        // So in this case we just make it pass - invalid fragment spreads will be simply ignored during execution
        // See also https://github.com/webonyx/graphql-php/issues/69#issuecomment-283954602
        if ($parentType instanceof InterfaceType && $fragType instanceof InterfaceType) {
            return true;

            // Note that there is one case when we do have information about all implementations:
            // When schema descriptor is defined ($schema->hasDescriptor())
            // BUT we must avoid situation when some query that worked in development had suddenly stopped
            // working in production. So staying consistent and always validate.
        }

        // Interface within union
        if ($parentType instanceof UnionType && $fragType instanceof InterfaceType) {
            foreach ($parentType->getTypes() as $type) {
                if ($type->implementsInterface($fragType)) {
                    return true;
                }
            }
        }

        if ($parentType instanceof InterfaceType && $fragType instanceof UnionType) {
            foreach ($fragType->getTypes() as $type) {
                if ($type->implementsInterface($parentType)) {
                    return true;
                }
            }
        }

        if ($parentType instanceof UnionType && $fragType instanceof UnionType) {
            foreach ($fragType->getTypes() as $type) {
                if ($parentType->isPossibleType($type)) {
                    return true;
                }
            }
        }

        return false;
    }
}