Completed
Push — master ( 1e6eb5...15e5c5 )
by David
18s queued 11s
created

RecursiveTypeMapper::findClosestMatchingParent()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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