ClassInfoReflector::getDefaultValueResolver()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 11
ccs 6
cts 6
cp 1
crap 2
rs 10
c 1
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Diezz\YamlToObjectMapper;
4
5
use Diezz\YamlToObjectMapper\Attributes\Collection;
6
use Diezz\YamlToObjectMapper\Attributes\DefaultValueResolver;
7
use Diezz\YamlToObjectMapper\Attributes\HasNotDefaultValue;
8
use Diezz\YamlToObjectMapper\Attributes\IgnoreUnknown;
9
use Diezz\YamlToObjectMapper\Attributes\Required;
10
use Diezz\YamlToObjectMapper\Attributes\Setter;
11
use ReflectionClass;
12
use ReflectionException;
13
use ReflectionProperty;
14
15
class ClassInfoReflector
16
{
17
    /**
18
     * @throws ReflectionException
19
     */
20 7
    public function introspect(string $targetClass): ClassInfo
21
    {
22 7
        $instance = new ClassInfo();
23 7
        $instance->setClassName($targetClass);
24
25 7
        $reflection = new ReflectionClass($targetClass);
26 7
        $properties = $reflection->getProperties();
27 7
        $instance->setIgnoreUnknown($this->isIgnoreUnknown($reflection));
28
29 7
        foreach ($properties as $property) {
30 7
            $classField = new ClassField();
31 7
            $propertyName = $property->getName();
32
33 7
            $classField->setName($propertyName);
34 7
            $classField->setRequired($this->isRequired($property));
35 7
            $classField->setHasDefaultValue($this->resolveHasDefaultValue($property));
36 7
            $classField->setDefaultValueResolver($this->getDefaultValueResolver($property));
37 7
            $this->resolveType($property, $classField);
38 7
            $this->resolveSetter($reflection, $property, $classField);
39
40 7
            $instance->addClassField($propertyName, $classField);
41
        }
42
43 7
        $this->fixCircularReferences($instance);
44
45 7
        return $instance;
46
    }
47
48 7
    private function isIgnoreUnknown(ReflectionClass $reflectionClass): bool
49
    {
50 7
        $attributes = $reflectionClass->getAttributes(IgnoreUnknown::class);
51
52 7
        return !empty($attributes);
53
    }
54
55 7
    private function getDefaultValueResolver(ReflectionProperty $reflectionProperty): ?string
56
    {
57 7
        $attributes = $reflectionProperty->getAttributes(DefaultValueResolver::class);
58 7
        if (!empty($attributes)) {
59
            /** @var DefaultValueResolver $attribute */
60 1
            $attribute = $attributes[0]->newInstance();
61
62 1
            return $attribute->getResolver();
63
        }
64
65 6
        return null;
66
    }
67
68 7
    private function getNestedClassInfos(ClassInfo $classInfo): array
69
    {
70 7
        $map = [];
71
72 7
        foreach ($classInfo->getFields() as $field) {
73 7
            if ($field->getClassInfo() !== null) {
74 1
                $map[$field->getType()] = $field->getClassInfo();
75 1
                foreach ($this->getNestedClassInfos($field->getClassInfo()) as $k => $f) {
76
                    $map[$k] = $f;
77
                }
78
            }
79
        }
80
81 7
        return $map;
82
    }
83
84 7
    private function resolveSetter(ReflectionClass $reflectionClass, ReflectionProperty $reflectionProperty, ClassField $classField): void
85
    {
86 7
        $isPublic = $reflectionProperty->isPublic();
87 7
        $classField->setIsPublic($isPublic);
88
89 7
        if (true === $isPublic) {
90 5
            return;
91
        }
92
93 2
        $setterAttributeOptional = $reflectionProperty->getAttributes(Setter::class);
94 2
        if (!empty($setterAttributeOptional)) {
95
            /**@var  Setter $setterAttribute */
96 1
            $setterAttribute = $setterAttributeOptional[0]->newInstance();
97 1
            $classField->setSetter($setterAttribute->getSetterName());
98
99 1
            return;
100
        }
101
102 2
        $possibleSetter = "set" . ucfirst($reflectionProperty->getName());
103 2
        if (false === $reflectionClass->hasMethod($possibleSetter)) {
104
            throw new \RuntimeException(sprintf(
105
                "Unable to find suitable setter for property %s for class %s",
106
                $reflectionProperty->getName(),
107
                $reflectionClass->getName()
108
            ));
109
        }
110
111 2
        $classField->setSetter($possibleSetter);
112 2
    }
113
114
115 7
    private function resolveHasDefaultValue(ReflectionProperty $property): bool
116
    {
117 7
        $attributes = $property->getAttributes(HasNotDefaultValue::class);
118 7
        if (empty($attributes)) {
119 7
            return $property->hasDefaultValue();
120
        }
121
122 1
        return false;
123
    }
124
125
    /**
126
     * @throws ReflectionException
127
     */
128 7
    private function resolveType(ReflectionProperty $reflectionProperty, ClassField $classField): void
129
    {
130 7
        $type = $reflectionProperty->getType();
131 7
        if (null === $type) {
132 1
            $classField->setType('mixed');
133
134 1
            return;
135
        }
136 7
        $typeName = $type->getName();
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

136
        /** @scrutinizer ignore-call */ 
137
        $typeName = $type->getName();
Loading history...
137 7
        $isNested = false;
138 7
        $isList = false;
139 7
        $isCollection = false;
140 7
        if (!$type->isBuiltin()) {
141
            $isNested = true;
142 7
        } else if ('array' === $type->getName()) {
143 5
            $attributes = $reflectionProperty->getAttributes(Collection::class);
144 5
            $isList = true;
145 5
            if (!empty($attributes)) {
146 1
                $isNested = true;
147 1
                $isCollection = true;
148
                /** @var Collection $attribute */
149 1
                $attribute = $attributes[0]->newInstance();
150 1
                $typeName = $attribute->getClass();
151
            }
152
        }
153
154 7
        $classField->setType($typeName);
155 7
        $classField->setIsList($isList);
156 7
        $classField->setIsTypedCollection($isCollection);
157 7
        if ($isNested) {
158 1
            if ($typeName === $reflectionProperty->class) {
159
                //prevent loop on nested elements of the same type
160
                return;
161
            }
162 1
            $classField->setClassInfo($this->introspect($typeName));
163
        }
164 7
    }
165
166
    /**
167
     * There are should be 3 ways how to figure out whether the field is required or not.
168
     * 1. Use attribute #Required. The most preferable way
169
     * 2. If value has default value it means that fields isn't required
170
     * 3. Use property typehint e.g
171
     * ```
172
     * private int $value
173
     * ```
174
     * would be treated as required and
175
     * ```
176
     * private ?int $value
177
     * ```
178
     * would br treated as optional(non required) field
179
     * 4. phpDoc comment. If doc comment contains `@var` type and the type contains `null|...` or `...|null' that would
180
     * be treated as optional, otherwise as required.
181
     *
182
     * If all three ways doesn't give a result the field would be treated as optional.
183
     *
184
     * @return bool
185
     */
186 7
    private function isRequired(ReflectionProperty $reflectionProperty): bool
187
    {
188 7
        $attributes = $reflectionProperty->getAttributes(Required::class);
189 7
        if (!empty($attributes)) {
190 1
            return true;
191
        }
192
193 7
        $propertyType = $reflectionProperty->getType();
194
        //if property hasn't type hint by default it's equal to null
195 7
        if (null !== $propertyType && $reflectionProperty->hasDefaultValue()) {
196 1
            return false;
197
        }
198
199 7
        if (null !== $propertyType) {
200 7
            return !$propertyType->allowsNull();
201
        }
202
203 1
        $doc = $reflectionProperty->getDocComment();
204 1
        if (false === $doc) {
205
            return false;
206
        }
207
208 1
        if (preg_match('/@var\s+(\S+)/', $doc, $matches)) {
209 1
            [, $type] = $matches;
210
211 1
            return !(str_contains($type, 'null|') or str_contains($type, '|null'));
212
        }
213
214
        return false;
215
    }
216
217 7
    private function findUnresolved(ClassInfo $classInfo, array $map): void
218
    {
219 7
        foreach ($classInfo->getFields() as $field) {
220 7
            if ($field->getClassInfo() === null) {
221
222 7
                if (!array_key_exists($field->getType(), $map)) {
223 7
                    continue;
224
                }
225
226
                $field->setClassInfo($map[$field->getType()]);
227 1
            } else if ($field->getClassInfo() !== null) {
228 1
                $this->findUnresolved($field->getClassInfo(), $map);
229
            }
230
        }
231 7
    }
232
233 7
    public function fixCircularReferences(ClassInfo $classInfo): void
234
    {
235 7
        $map = $this->getNestedClassInfos($classInfo);
236 7
        $this->findUnresolved($classInfo, $map);
237 7
    }
238
}
239