AnnotationsDriver::getMetadataForClass()   C
last analyzed

Complexity

Conditions 13
Paths 51

Size

Total Lines 53
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 13.0096

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 27
c 1
b 0
f 0
nc 51
nop 1
dl 0
loc 53
ccs 25
cts 26
cp 0.9615
crap 13.0096
rs 6.6166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Bdf\Serializer\Metadata\Driver;
4
5
use Bdf\Serializer\Metadata\Builder\ClassMetadataBuilder;
6
use Bdf\Serializer\Metadata\ClassMetadata;
7
use Bdf\Serializer\Type\Type;
8
use phpDocumentor\Reflection\DocBlock;
9
use phpDocumentor\Reflection\DocBlock\Tag;
10
use phpDocumentor\Reflection\DocBlockFactory;
11
use phpDocumentor\Reflection\Types\ContextFactory;
12
use ReflectionClass;
13
use ReflectionProperty;
14
15
/**
16
 * AnnotationsDriver
17
 *
18
 * based on doctrine annotations
19
 *
20
 * @author  Seb
21
 */
22
class AnnotationsDriver implements DriverInterface
23
{
24
    /**
25
     * @var DocBlockFactory
26
     */
27
    private $docBlockFactory;
28
29
    /**
30
     * @var ContextFactory
31
     */
32
    private $contextFactory;
33
34
    /**
35
     * AnnotationsDriver constructor.
36
     */
37 178
    public function __construct()
38
    {
39 178
        $this->docBlockFactory = DocBlockFactory::createInstance();
40 178
        $this->contextFactory = new ContextFactory();
41
    }
42
43
    /**
44
     * {@inheritdoc}
45
     */
46 76
    public function getMetadataForClass(ReflectionClass $class): ?ClassMetadata
47
    {
48 76
        if ($class->isInterface() || $class->isAbstract()) {
49
            return null;
50
        }
51
52 76
        $annotations = [];
53 76
        $reflection = $class;
54
55
        // Get all properties annotations from the hierarchy
56
        do {
57 76
            foreach ($this->getClassProperties($reflection) as $property) {
58
                // PHP serialize behavior: we skip the static properties.
59 74
                if ($property->isStatic()) {
60 2
                    continue;
61
                }
62
63 74
                $annotation = $this->getPropertyAnnotations($property);
64
65 74
                if (isset($annotation['SerializeIgnore'])) {
66 2
                    continue;
67
                }
68
69 74
                if (isset($annotations[$property->name])) {
70 2
                    $annotations[$property->name] = array_merge($annotation, $annotations[$property->name]);
71
                } else {
72 74
                    $annotations[$property->name] = $annotation;
73
                }
74
            }
75
76 76
            $reflection = $reflection->getParentClass();
77 76
        } while ($reflection);
78
79
        // Parse annotations
80 76
        $builder = new ClassMetadataBuilder($class);
81
82 76
        if ($class->hasMethod('__wakeup')) {
83 4
            $builder->postDenormalization('__wakeup');
84
        }
85
86 76
        foreach ($annotations as $name => $annotation) {
87 74
            $property = $builder->add($name, isset($annotation['type']) ? $annotation['type'] : Type::MIXED);
88
89 74
            if (isset($annotation['since'])) {
90 2
                $property->since($annotation['since']);
91
            }
92
93 74
            if (isset($annotation['until'])) {
94 2
                $property->until($annotation['until']);
95
            }
96
        }
97
98 76
        return $builder->build();
99
    }
100
101
    /**
102
     * Gets the class properties
103
     *
104
     * @param ReflectionClass $reflection
105
     *
106
     * @return ReflectionProperty[]
107
     */
108 76
    private function getClassProperties(ReflectionClass $reflection): array
109
    {
110 76
        if (!$reflection->hasMethod('__sleep')) {
111
            // The class has no magic method __sleep, we return all the properties.
112 72
            return $reflection->getProperties();
113
        }
114
115 4
        $properties = [];
116 4
        $instance = $reflection->newInstanceWithoutConstructor();
117
118 4
        foreach ($reflection->getMethod('__sleep')->invoke($instance) as $name) {
119 4
            $properties[] = $reflection->getProperty($name);
120
        }
121
122 4
        return $properties;
123
    }
124
125
    /**
126
     * Get annotations from the property
127
     *
128
     * @param ReflectionProperty $property
129
     *
130
     * @return array
131
     */
132 74
    private function getPropertyAnnotations(ReflectionProperty $property): array
133
    {
134
        try {
135 74
            $tags = $this->docBlockFactory->create($property, $this->contextFactory->createFromReflector($property))->getTags();
136 62
        } catch (\InvalidArgumentException $e) {
137 62
            $tags = [];
138
        }
139
140 74
        $annotations = [];
141
142
        // Tags mapping
143 74
        foreach ($tags as $tag) {
144 24
            list($option, $value) = $this->createSerializationTag($tag, $property);
145
146 24
            if ($option !== null && !isset($annotations[$option])) {
147 24
                $annotations[$option] = $value;
148
            }
149
        }
150
151
        // Adding php type if no precision has been added with annotation
152 74
        if (PHP_VERSION_ID >= 70400 && $property->hasType() && !isset($annotations['type'])) {
153
            /** @psalm-suppress UndefinedMethod */
154 24
            $annotations['type'] = $this->findType($property->getType()->getName(), $property);
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

154
            $annotations['type'] = $this->findType($property->getType()->/** @scrutinizer ignore-call */ getName(), $property);
Loading history...
155
        }
156
157 74
        return $annotations;
158
    }
159
160
    /**
161
     * Create the serialization info
162
     *
163
     * @param Tag $tag
164
     * @param ReflectionProperty $property
165
     *
166
     * @return array
167
     */
168 24
    private function createSerializationTag($tag, $property): array
169
    {
170 24
        switch ($tag->getName()) {
171 24
            case 'var':
172 24
                if ($tag instanceof DocBlock\Tags\InvalidTag) {
173 4
                    return ['type', $this->findType((string) $tag, $property)];
174
                }
175
176
                /** @var DocBlock\Tags\Var_ $tag */
177 24
                return ['type', $this->findType((string)$tag->getType(), $property)];
178
179 4
            case 'since':
180
                /** @var DocBlock\Tags\Since $tag */
181 2
                return ['since', (string)$tag->getVersion()];
182
183 4
            case 'until':
184
                /** @var DocBlock\Tags\Generic $tag */
185 2
                return ['until', (string)$tag->getDescription()];
186
187 2
            case 'SerializeIgnore':
188 2
                return ['SerializeIgnore', true];
189
        }
190
191
        return [null, null];
192
    }
193
194
    /**
195
     * Filter the var tag
196
     *
197
     * @param string $var
198
     * @param ReflectionProperty $property
199
     *
200
     * @return string
201
     */
202 44
    private function findType($var, $property): ?string
203
    {
204
        // Clear psalm structure notation and generics
205 44
        $var = preg_replace('/(.*)\{.*\}/u', '$1', $var);
206 44
        $var = preg_replace('/(.*)<.*>/u', '$1', $var);
207
208
        // All known alias from phpdoc that should be mapped to a serializer type
209 44
        $alias = [
210 44
            'bool' => Type::BOOLEAN,
211 44
            'false' => Type::BOOLEAN,
212 44
            'true' => Type::BOOLEAN,
213 44
            'int' => Type::INTEGER,
214 44
            'void' => Type::TNULL,
215 44
            'scalar' => Type::STRING,
216 44
            'iterable' => Type::TARRAY,
217 44
            'object' => \stdClass::class,
218 44
            'callback' => 'callable',
219 44
            'self' => $property->class,
220 44
            '$this' => $property->class,
221 44
            'static' => $property->class,
222 44
        ];
223
224 44
        if (strpos($var, '|') === false) {
225 42
            $var = ltrim($var, '\\');
226
227 42
            return isset($alias[$var]) ? $alias[$var] : $var;
228
        }
229
230 8
        foreach (explode('|', $var) as $candidate) {
231 8
            $candidate = ltrim($candidate, '\\');
232
233 8
            if (isset($alias[$candidate])) {
234 2
                $candidate = $alias[$candidate];
235
            }
236
237 8
            if ($candidate !== '' && $candidate !== Type::TNULL) {
238 8
                return $candidate;
239
            }
240
        }
241
242
        // We let here the getMetadataForClass add the default type
243
        return null;
244
    }
245
}
246