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