Completed
Pull Request — master (#44)
by David
02:24
created

RecursiveTypeMapper::canMapClassToInputType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
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>
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
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
                return $this->interfaces[$closestClassName];
110
            } else {
111
                $this->interfaces[$closestClassName] = $objectType;
112
            }
113
        }
114
        return $this->interfaces[$closestClassName];
115
    }
116
117
    /**
118
     * @return array<string,MappedClass>
119
     */
120
    private function getClassTree(): array
121
    {
122
        if ($this->mappedClasses === null) {
123
            $supportedClasses = array_flip($this->typeMapper->getSupportedClasses());
124
            foreach ($supportedClasses as $supportedClass => $foo) {
125
                $this->getMappedClass($supportedClass, $supportedClasses);
126
            }
127
        }
128
        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...
129
    }
130
131
    /**
132
     * @param string $className
133
     * @param array<string,int> $supportedClasses
134
     * @return MappedClass
135
     */
136
    private function getMappedClass(string $className, array $supportedClasses): MappedClass
137
    {
138
        if (!isset($this->mappedClasses[$className])) {
139
            $mappedClass = new MappedClass($className);
140
            $this->mappedClasses[$className] = $mappedClass;
141
            $parentClassName = $className;
142
            while ($parentClassName = get_parent_class($parentClassName)) {
143
                if (isset($supportedClasses[$parentClassName])) {
144
                    $parentMappedClass = $this->getMappedClass($parentClassName, $supportedClasses);
145
                    $mappedClass->setParent($parentMappedClass);
146
                    $parentMappedClass->addChild($mappedClass);
147
                    break;
148
                }
149
            }
150
        }
151
        return $this->mappedClasses[$className];
152
    }
153
154
    /**
155
     * Returns true if this type mapper can map the $className FQCN to a GraphQL input type.
156
     *
157
     * @param string $className
158
     * @return bool
159
     */
160
    public function canMapClassToInputType(string $className): bool
161
    {
162
        return $this->typeMapper->canMapClassToInputType($className);
163
    }
164
165
    /**
166
     * Maps a PHP fully qualified class name to a GraphQL input type.
167
     *
168
     * @param string $className
169
     * @return InputType&Type
170
     * @throws CannotMapTypeException
171
     */
172
    public function mapClassToInputType(string $className): InputType
173
    {
174
        return $this->typeMapper->mapClassToInputType($className);
175
    }
176
}
177