AnnotationsDriver::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
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