Passed
Push — master ( e4da81...49dc91 )
by Vincent
08:33
created

AnnotationsDriver::getMetadataForClass()   C

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 170
    public function __construct()
38
    {
39 170
        $this->docBlockFactory = DocBlockFactory::createInstance();
40 170
        $this->contextFactory = new ContextFactory();
41
    }
42
43
    /**
44
     * {@inheritdoc}
45
     */
46 72
    public function getMetadataForClass(ReflectionClass $class): ?ClassMetadata
47
    {
48 72
        if ($class->isInterface() || $class->isAbstract()) {
49
            return null;
50
        }
51
52 72
        $annotations = [];
53 72
        $reflection = $class;
54
55
        // Get all properties annotations from the hierarchy
56
        do {
57 72
            foreach ($this->getClassProperties($reflection) as $property) {
58
                // PHP serialize behavior: we skip the static properties.
59 70
                if ($property->isStatic()) {
60 2
                    continue;
61
                }
62
63 70
                $annotation = $this->getPropertyAnnotations($property);
64
65 70
                if (isset($annotation['SerializeIgnore'])) {
66 2
                    continue;
67
                }
68
69 70
                if (isset($annotations[$property->name])) {
70 2
                    $annotations[$property->name] = array_merge($annotation, $annotations[$property->name]);
71
                } else {
72 70
                    $annotations[$property->name] = $annotation;
73
                }
74
            }
75
76 72
            $reflection = $reflection->getParentClass();
77 72
        } while ($reflection);
78
79
        // Parse annotations
80 72
        $builder = new ClassMetadataBuilder($class);
81
82 72
        if ($class->hasMethod('__wakeup')) {
83 4
            $builder->postDenormalization('__wakeup');
84
        }
85
86 72
        foreach ($annotations as $name => $annotation) {
87 70
            $property = $builder->add($name, isset($annotation['type']) ? $annotation['type'] : Type::MIXED);
88
89 70
            if (isset($annotation['since'])) {
90 2
                $property->since($annotation['since']);
91
            }
92
93 70
            if (isset($annotation['until'])) {
94 2
                $property->until($annotation['until']);
95
            }
96
        }
97
98 72
        return $builder->build();
99
    }
100
101
    /**
102
     * Gets the class properties
103
     *
104
     * @param ReflectionClass $reflection
105
     *
106
     * @return ReflectionProperty[]
107
     */
108 72
    private function getClassProperties(ReflectionClass $reflection): array
109
    {
110 72
        if (!$reflection->hasMethod('__sleep')) {
111
            // The class has no magic method __sleep, we return all the properties.
112 68
            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 70
    private function getPropertyAnnotations(ReflectionProperty $property): array
133
    {
134
        try {
135 70
            $tags = $this->docBlockFactory->create($property, $this->contextFactory->createFromReflector($property))->getTags();
136 62
        } catch (\InvalidArgumentException $e) {
137 62
            $tags = [];
138
        }
139
140 70
        $annotations = [];
141
142
        // Tags mapping
143 70
        foreach ($tags as $tag) {
144 20
            list($option, $value) = $this->createSerializationTag($tag, $property);
145
146 20
            if ($option !== null && !isset($annotations[$option])) {
147 20
                $annotations[$option] = $value;
148
            }
149
        }
150
151
        // Adding php type if no precision has been added with annotation
152 70
        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 70
        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 20
    private function createSerializationTag($tag, $property): array
169
    {
170 20
        switch ($tag->getName()) {
171 20
            case 'var':
172 20
                if ($tag instanceof DocBlock\Tags\InvalidTag) {
173 4
                    return ['type', $this->findType((string) $tag, $property)];
174
                }
175
176
                /** @var DocBlock\Tags\Var_ $tag */
177 20
                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 40
    private function findType($var, $property): ?string
203
    {
204
        // Clear psalm structure notation and generics
205 40
        $var = preg_replace('/(.*)\{.*\}/u', '$1', $var);
206 40
        $var = preg_replace('/(.*)<.*>/u', '$1', $var);
207
208
        // All known alias from phpdoc that should be mapped to a serializer type
209 40
        $alias = [
210 40
            'bool' => Type::BOOLEAN,
211 40
            'false' => Type::BOOLEAN,
212 40
            'true' => Type::BOOLEAN,
213 40
            'int' => Type::INTEGER,
214 40
            'void' => Type::TNULL,
215 40
            'scalar' => Type::STRING,
216 40
            'iterable' => Type::TARRAY,
217 40
            'object' => \stdClass::class,
218 40
            'callback' => 'callable',
219 40
            'self' => $property->class,
220 40
            '$this' => $property->class,
221 40
            'static' => $property->class,
222 40
        ];
223
224 40
        if (strpos($var, '|') === false) {
225 38
            $var = ltrim($var, '\\');
226
227 38
            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