Passed
Pull Request — master (#45)
by David
01:45
created

RecursiveTypeMapper::mapClassToInterfaceOrType()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 18
rs 9.9
c 0
b 0
f 0
cc 4
nc 4
nop 1
1
<?php
2
3
4
namespace TheCodingMachine\GraphQL\Controllers\Mappers;
5
6
7
use function array_flip;
8
use function get_parent_class;
9
use GraphQL\Type\Definition\InputType;
10
use GraphQL\Type\Definition\InterfaceType;
11
use GraphQL\Type\Definition\OutputType;
12
use GraphQL\Type\Definition\Type;
13
use TheCodingMachine\GraphQL\Controllers\Types\InterfaceFromObjectType;
14
15
/**
16
 * This class wraps a TypeMapperInterface into a RecursiveTypeMapperInterface.
17
 * While the wrapped class does only tests one given class, the recursive type mapper
18
 * tests the class and all its parents.
19
 */
20
class RecursiveTypeMapper implements RecursiveTypeMapperInterface
21
{
22
    /**
23
     * @var TypeMapperInterface
24
     */
25
    private $typeMapper;
26
27
    /**
28
     * An array mapping a class name to the MappedClass instance (useful to know if the class has children)
29
     *
30
     * @var array<string,MappedClass>|null
31
     */
32
    private $mappedClasses;
33
34
    /**
35
     * An array of interfaces OR object types if no interface matching.
36
     *
37
     * @var array<string,OutputType&Type>
38
     */
39
    private $interfaces = [];
40
41
    public function __construct(TypeMapperInterface $typeMapper)
42
    {
43
        $this->typeMapper = $typeMapper;
44
    }
45
46
    /**
47
     * Returns true if this type mapper can map the $className FQCN to a GraphQL type.
48
     *
49
     * @param string $className The class name to look for (this function looks into parent classes if the class does not match a type).
50
     * @return bool
51
     */
52
    public function canMapClassToType(string $className): bool
53
    {
54
        return $this->findClosestMatchingParent($className) !== null;
55
    }
56
57
    /**
58
     * Maps a PHP fully qualified class name to a GraphQL type.
59
     *
60
     * @param string $className The class name to look for (this function looks into parent classes if the class does not match a type).
61
     * @return OutputType&Type
62
     * @throws CannotMapTypeException
63
     */
64
    public function mapClassToType(string $className): OutputType
65
    {
66
        $closestClassName = $this->findClosestMatchingParent($className);
67
        if ($closestClassName === null) {
68
            throw CannotMapTypeException::createForType($className);
69
        }
70
        return $this->typeMapper->mapClassToType($closestClassName);
71
    }
72
73
    /**
74
     * Returns the closest parent that can be mapped, or null if nothing can be matched.
75
     *
76
     * @param string $className
77
     * @return string|null
78
     */
79
    private function findClosestMatchingParent(string $className): ?string
80
    {
81
        do {
82
            if ($this->typeMapper->canMapClassToType($className)) {
83
                return $className;
84
            }
85
        } while ($className = get_parent_class($className));
86
        return null;
87
    }
88
89
    /**
90
     * Maps a PHP fully qualified class name to a GraphQL interface (or returns null if no interface is found).
91
     *
92
     * @param string $className The exact class name to look for (this function does not look into parent classes).
93
     * @return OutputType&Type
94
     * @throws CannotMapTypeException
95
     */
96
    public function mapClassToInterfaceOrType(string $className): OutputType
97
    {
98
        $closestClassName = $this->findClosestMatchingParent($className);
99
        if ($closestClassName === null) {
100
            throw CannotMapTypeException::createForType($className);
101
        }
102
        if (!isset($this->interfaces[$closestClassName])) {
103
            $objectType = $this->typeMapper->mapClassToType($closestClassName);
104
105
            $supportedClasses = $this->getClassTree();
106
            if (!empty($supportedClasses[$closestClassName]->getChildren())) {
107
                // Cast as an interface
108
                $this->interfaces[$closestClassName] = new InterfaceFromObjectType($objectType, $this);
109
            } else {
110
                $this->interfaces[$closestClassName] = $objectType;
111
            }
112
        }
113
        return $this->interfaces[$closestClassName];
114
    }
115
116
    /**
117
     * Finds the list of interfaces returned by $className.
118
     *
119
     * @param string $className
120
     * @return InterfaceType[]
121
     */
122
    public function findInterfaces(string $className): array
123
    {
124
        $interfaces = [];
125
        while ($className = $this->findClosestMatchingParent($className)) {
126
            $type = $this->mapClassToInterfaceOrType($className);
127
            if ($type instanceof InterfaceType) {
128
                $interfaces[] = $type;
129
            }
130
            $className = get_parent_class($className);
131
            if ($className === false) {
132
                break;
133
            }
134
        }
135
        return $interfaces;
136
    }
137
138
    /**
139
     * @return array<string,MappedClass>
140
     */
141
    private function getClassTree(): array
142
    {
143
        if ($this->mappedClasses === null) {
144
            $supportedClasses = array_flip($this->typeMapper->getSupportedClasses());
145
            foreach ($supportedClasses as $supportedClass => $foo) {
146
                $this->getMappedClass($supportedClass, $supportedClasses);
147
            }
148
        }
149
        return $this->mappedClasses;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->mappedClasses could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
150
    }
151
152
    /**
153
     * @param string $className
154
     * @param array<string,int> $supportedClasses
155
     * @return MappedClass
156
     */
157
    private function getMappedClass(string $className, array $supportedClasses): MappedClass
158
    {
159
        if (!isset($this->mappedClasses[$className])) {
160
            $mappedClass = new MappedClass(/*$className*/);
161
            $this->mappedClasses[$className] = $mappedClass;
162
            $parentClassName = $className;
163
            while ($parentClassName = get_parent_class($parentClassName)) {
164
                if (isset($supportedClasses[$parentClassName])) {
165
                    $parentMappedClass = $this->getMappedClass($parentClassName, $supportedClasses);
166
                    //$mappedClass->setParent($parentMappedClass);
167
                    $parentMappedClass->addChild($mappedClass);
168
                    break;
169
                }
170
            }
171
        }
172
        return $this->mappedClasses[$className];
173
    }
174
175
    /**
176
     * Returns true if this type mapper can map the $className FQCN to a GraphQL input type.
177
     *
178
     * @param string $className
179
     * @return bool
180
     */
181
    public function canMapClassToInputType(string $className): bool
182
    {
183
        return $this->typeMapper->canMapClassToInputType($className);
184
    }
185
186
    /**
187
     * Maps a PHP fully qualified class name to a GraphQL input type.
188
     *
189
     * @param string $className
190
     * @return InputType&Type
191
     * @throws CannotMapTypeException
192
     */
193
    public function mapClassToInputType(string $className): InputType
194
    {
195
        return $this->typeMapper->mapClassToInputType($className);
196
    }
197
}
198