Passed
Pull Request — master (#190)
by Sebastian
03:29
created

TypeHelper::compareTypes()   D

Complexity

Conditions 9
Paths 8

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 4.909
c 0
b 0
f 0
cc 9
eloc 15
nc 8
nop 2
1
<?php
2
3
namespace Digia\GraphQL\Util;
4
5
use Digia\GraphQL\Error\InvalidTypeException;
6
use Digia\GraphQL\Language\Node\ListTypeNode;
7
use Digia\GraphQL\Language\Node\NamedTypeNode;
8
use Digia\GraphQL\Language\Node\NonNullTypeNode;
9
use Digia\GraphQL\Language\Node\TypeNodeInterface;
10
use Digia\GraphQL\Schema\SchemaInterface;
11
use Digia\GraphQL\Type\Definition\AbstractTypeInterface;
12
use Digia\GraphQL\Type\Definition\LeafTypeInterface;
13
use Digia\GraphQL\Type\Definition\ListType;
14
use Digia\GraphQL\Type\Definition\NonNullType;
15
use Digia\GraphQL\Type\Definition\ObjectType;
16
use Digia\GraphQL\Type\Definition\TypeInterface;
17
use function Digia\GraphQL\Type\newList;
18
use function Digia\GraphQL\Type\newNonNull;
19
20
class TypeHelper
21
{
22
23
    /**
24
     * Provided two types, return true if the types are equal (invariant).
25
     *
26
     * @param TypeInterface $typeA
27
     * @param TypeInterface $typeB
28
     *
29
     * @return bool
30
     */
31
    public function isEqualType(
32
        TypeInterface $typeA,
33
        TypeInterface $typeB
34
    ): bool {
35
        // Equivalent types are equal.
36
        if ($typeA === $typeB) {
37
            return true;
38
        }
39
40
        // If either type is non-null, the other must also be non-null.
41
        if ($typeA instanceof NonNullType && $typeB instanceof NonNullType) {
42
            return $this->isEqualType($typeA->getOfType(), $typeB->getOfType());
43
        }
44
45
        // If either type is a list, the other must also be a list.
46
        if ($typeA instanceof ListType && $typeB instanceof ListType) {
47
            return $this->isEqualType($typeA->getOfType(), $typeB->getOfType());
48
        }
49
50
        // Otherwise the types are not equal.
51
        return false;
52
    }
53
54
    /**
55
     * Provided a type and a super type, return true if the first type is either
56
     * equal or a subset of the second super type (covariant).
57
     *
58
     * @param SchemaInterface $schema
59
     * @param TypeInterface $maybeSubtype
60
     * @param TypeInterface $superType
61
     *
62
     * @return bool
63
     */
64
    public function isTypeSubtypeOf(
65
        SchemaInterface $schema,
66
        TypeInterface $maybeSubtype,
67
        TypeInterface $superType
68
    ): bool {
69
        // Equivalent type is a valid subtype.
70
        if ($maybeSubtype === $superType) {
71
            return true;
72
        }
73
74
        // If superType is non-null, maybeSubType must also be non-null.
75
        if ($superType instanceof NonNullType) {
76
            if ($maybeSubtype instanceof NonNullType) {
77
                return $this->isTypeSubtypeOf($schema,
78
                    $maybeSubtype->getOfType(), $superType->getOfType());
79
            }
80
81
            return false;
82
        }
83
84
        if ($maybeSubtype instanceof NonNullType) {
85
            // If superType is nullable, maybeSubType may be non-null or nullable.
86
            return $this->isTypeSubtypeOf($schema, $maybeSubtype->getOfType(),
87
                $superType);
88
        }
89
90
        // If superType type is a list, maybeSubType type must also be a list.
91
        if ($superType instanceof ListType) {
92
            if ($maybeSubtype instanceof ListType) {
93
                return $this->isTypeSubtypeOf($schema,
94
                    $maybeSubtype->getOfType(), $superType->getOfType());
95
            }
96
97
            return false;
98
        }
99
100
        if ($maybeSubtype instanceof ListType) {
101
            // If superType is not a list, maybeSubType must also be not a list.
102
            return false;
103
        }
104
105
        // If superType type is an abstract type, maybeSubType type may be a currently
106
        // possible object type.
107
        if ($superType instanceof AbstractTypeInterface &&
108
            $maybeSubtype instanceof ObjectType &&
109
            $schema->isPossibleType($superType, $maybeSubtype)) {
110
            return true;
111
        }
112
113
        // Otherwise, the child type is not a valid subtype of the parent type.
114
        return false;
115
    }
116
117
    /**
118
     * Provided two composite types, determine if they "overlap". Two composite
119
     * types overlap when the Sets of possible concrete types for each
120
     * intersect.
121
     *
122
     * This is often used to determine if a fragment of a given type could
123
     * possibly be visited in a context of another type.
124
     *
125
     * @param SchemaInterface $schema
126
     * @param TypeInterface $typeA
127
     * @param TypeInterface $typeB
128
     *
129
     * @return bool
130
     */
131
    public function doTypesOverlap(
132
        SchemaInterface $schema,
133
        TypeInterface $typeA,
134
        TypeInterface $typeB
135
    ): bool {
136
        // Equivalent types overlap
137
        if ($typeA === $typeB) {
138
            return true;
139
        }
140
141
        if ($typeA instanceof AbstractTypeInterface) {
142
            if ($typeB instanceof AbstractTypeInterface) {
143
                // If both types are abstract, then determine if there is any intersection
144
                // between possible concrete types of each.
145
                return arraySome($schema->getPossibleTypes($typeA),
146
                    function (TypeInterface $type) use ($schema, $typeB) {
147
                        return $schema->isPossibleType($typeB, $type);
148
                    });
149
            }
150
151
            // Determine if the latter type is a possible concrete type of the former.
152
153
            /** @noinspection PhpParamsInspection */
154
            return $schema->isPossibleType($typeA, $typeB);
155
        }
156
157
        if ($typeB instanceof AbstractTypeInterface) {
158
            // Determine if the former type is a possible concrete type of the latter.
159
            /** @noinspection PhpParamsInspection */
160
            return $schema->isPossibleType($typeB, $typeA);
161
        }
162
163
        // Otherwise the types do not overlap.
164
        return false;
165
    }
166
167
    /**
168
     * Two types conflict if both types could not apply to a value
169
     * simultaneously. Composite types are ignored as their individual field
170
     * types will be compared later recursively. However List and Non-Null
171
     * types must match.
172
     *
173
     * @param TypeInterface $typeA
174
     * @param TypeInterface $typeB
175
     *
176
     * @return bool
177
     */
178
    public function compareTypes(
179
        TypeInterface $typeA,
180
        TypeInterface $typeB
181
    ): bool {
182
        if ($typeA instanceof ListType) {
183
            return $typeB instanceof ListType
184
                ? $this->compareTypes($typeA->getOfType(), $typeB->getOfType())
185
                : true;
186
        }
187
        if ($typeB instanceof ListType) {
188
            return true;
189
        }
190
        if ($typeA instanceof NonNullType) {
191
            return $typeB instanceof NonNullType
192
                ? $this->compareTypes($typeA->getOfType(), $typeB->getOfType())
193
                : true;
194
        }
195
        if ($typeB instanceof NonNullType) {
196
            return true;
197
        }
198
        if ($typeA instanceof LeafTypeInterface || $typeB instanceof LeafTypeInterface) {
199
            return $typeA !== $typeB;
200
        }
201
202
        return false;
203
    }
204
205
    /**
206
     * Given a Schema and an AST node describing a type, return a GraphQLType
207
     * definition which applies to that type. For example, if provided the
208
     * parsed AST node for `[User]`, a GraphQLList instance will be returned,
209
     * containing the type called "User" found in the schema. If a type called
210
     * "User" is not found in the schema, then undefined will be returned.
211
     *
212
     * @param SchemaInterface $schema
213
     * @param TypeNodeInterface $typeNode
214
     *
215
     * @return TypeInterface|null
216
     * @throws InvalidTypeException
217
     */
218
    public function fromAST(
219
        SchemaInterface $schema,
220
        TypeNodeInterface $typeNode
221
    ): ?TypeInterface
222
    {
223
        $innerType = null;
224
225
        if ($typeNode instanceof ListTypeNode) {
226
            $innerType = $this->fromAST($schema, $typeNode->getType());
227
228
            return null !== $innerType ? newList($innerType) : null;
229
        }
230
231
        if ($typeNode instanceof NonNullTypeNode) {
232
            $innerType = $this->fromAST($schema, $typeNode->getType());
233
234
            return null !== $innerType ? newNonNull($innerType) : null;
235
        }
236
237
        if ($typeNode instanceof NamedTypeNode) {
238
            return $schema->getType($typeNode->getNameValue());
239
        }
240
241
        throw new InvalidTypeException(sprintf('Unexpected type kind: %s',
242
            $typeNode->getKind()));
243
    }
244
}
245