Completed
Pull Request — master (#1214)
by
unknown
10:34
created

DocBlockTypeResolver   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 151
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 71
c 3
b 0
f 0
dl 0
loc 151
rs 9.68
wmc 34

10 Methods

Rating   Name   Duplication   Size   Complexity  
A resolveType() 0 7 3
B getPropertyDocblockTypeHint() 0 44 10
A getDeclaringClassOrTrait() 0 11 4
A hasGlobalNamespacePrefix() 0 3 1
A isArrayWithoutAnyType() 0 3 1
A gatherSingleUseStatements() 0 9 2
A gatherGroupUseStatements() 0 11 3
A isPrimitiveType() 0 3 1
B expandClassNameUsingUseStatements() 0 27 8
A endsWith() 0 5 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace JMS\Serializer\Metadata\Driver;
6
7
class DocBlockTypeResolver
8
{
9
    /** resolve type hints from property */
10
    private const CLASS_PROPERTY_TYPE_HINT_REGEX = '#@var[\s]*([^\n\$]*)#';
11
    /** resolve single use statements */
12
    private const SINGLE_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[\s]*([^;\n]*)[\s]*;$/m';
13
    /** resolve group use statements */
14
    private const GROUP_USE_STATEMENTS_REGEX = '/^[^\S\r\n]*use[[\s]*([^;\n]*)[\s]*{([a-zA-Z0-9\s\n\r,]*)};$/m';
15
    private const GLOBAL_NAMESPACE_PREFIX = '\\';
16
17
    public function getPropertyDocblockTypeHint(\ReflectionProperty $reflectionProperty): ?string
18
    {
19
        if (!$reflectionProperty->getDocComment()) {
20
            return null;
21
        }
22
23
        preg_match_all(self::CLASS_PROPERTY_TYPE_HINT_REGEX, $reflectionProperty->getDocComment(), $matchedDocBlockParameterTypes);
24
        if (!isset($matchedDocBlockParameterTypes[1][0])) {
25
            return null;
26
        }
27
28
        $typeHint = trim($matchedDocBlockParameterTypes[1][0]);
29
        if ($this->isArrayWithoutAnyType($typeHint)) {
30
            return null;
31
        }
32
33
        $unionTypeHint = [];
34
        foreach (explode('|', $typeHint) as $singleTypeHint) {
35
            if ('null' !== $singleTypeHint) {
36
                $unionTypeHint[] = $singleTypeHint;
37
            }
38
        }
39
        $typeHint = implode('|', $unionTypeHint);
40
        if (count($unionTypeHint) > 1) {
41
            throw new \InvalidArgumentException(sprintf("Can't use union type %s for collection in %s:%s", $typeHint, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
42
        }
43
44
        if (false !== strpos($typeHint, 'array<')) {
45
            $resolvedTypes = [];
46
            preg_match_all("#array<(.*)>#", $typeHint, $genericTypesToResolve);
47
            $genericTypesToResolve = $genericTypesToResolve[1][0];
48
            foreach (explode(",", $genericTypesToResolve) as $genericTypeToResolve) {
49
                $resolvedTypes[] = $this->resolveType(trim($genericTypeToResolve), $reflectionProperty);
50
            }
51
52
            return "array<" . implode(",", $resolvedTypes) . ">";
53
        }elseif (false !== strpos($typeHint, '[]')) {
54
            $typeHint = rtrim($typeHint, '[]');
55
            $typeHint = $this->resolveType($typeHint, $reflectionProperty);
56
57
            return 'array<' . $typeHint . '>';
58
        }
59
60
        return $this->resolveType($typeHint, $reflectionProperty);
61
    }
62
63
    private function expandClassNameUsingUseStatements(string $typeHint, \ReflectionClass $declaringClass, \ReflectionProperty $reflectionProperty): string
64
    {
65
        if (class_exists($typeHint)) {
66
            return $typeHint;
67
        }
68
        $expandedClassName = $declaringClass->getNamespaceName() . '\\' . $typeHint;
69
        if (class_exists($expandedClassName)) {
70
            return $expandedClassName;
71
        }
72
73
        $classContents = file_get_contents($declaringClass->getFileName());
74
        $foundUseStatements = $this->gatherGroupUseStatements($classContents);
75
        $foundUseStatements = array_merge($this->gatherSingleUseStatements($classContents), $foundUseStatements);
76
77
        foreach ($foundUseStatements as $statementClassName) {
78
            if ($alias = explode('as', $statementClassName)) {
79
                if (array_key_exists(1, $alias) && trim($alias[1]) === $typeHint) {
80
                    return trim($alias[0]);
81
                }
82
            }
83
84
            if ($this->endsWith($statementClassName, $typeHint)) {
85
                return $statementClassName;
86
            }
87
        }
88
89
        throw new \InvalidArgumentException(sprintf("Can't use incorrect type %s for collection in %s:%s", $typeHint, $declaringClass->getName(), $reflectionProperty->getName()));
90
    }
91
92
    private function endsWith(string $statementClassToCheck, string $typeHintToSearchFor): bool
93
    {
94
        $typeHintToSearchFor = '\\' . $typeHintToSearchFor;
95
96
        return substr($statementClassToCheck, -strlen($typeHintToSearchFor)) === $typeHintToSearchFor;
97
    }
98
99
    private function isPrimitiveType(string $type): bool
100
    {
101
        return in_array($type, ['int', 'float', 'bool', 'string']);
102
    }
103
104
    private function hasGlobalNamespacePrefix(string $typeHint): bool
105
    {
106
        return self::GLOBAL_NAMESPACE_PREFIX === $typeHint[0];
107
    }
108
109
    private function gatherGroupUseStatements(string $classContents): array
110
    {
111
        $foundUseStatements = [];
112
        preg_match_all(self::GROUP_USE_STATEMENTS_REGEX, $classContents, $foundGroupUseStatements);
113
        for ($useStatementIndex = 0; $useStatementIndex < count($foundGroupUseStatements[0]); $useStatementIndex++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
114
            foreach (explode(',', $foundGroupUseStatements[2][$useStatementIndex]) as $singleUseStatement) {
115
                $foundUseStatements[] = trim($foundGroupUseStatements[1][$useStatementIndex]) . trim($singleUseStatement);
116
            }
117
        }
118
119
        return $foundUseStatements;
120
    }
121
122
    private function gatherSingleUseStatements(string $classContents): array
123
    {
124
        $foundUseStatements = [];
125
        preg_match_all(self::SINGLE_USE_STATEMENTS_REGEX, $classContents, $foundSingleUseStatements);
126
        for ($useStatementIndex = 0; $useStatementIndex < count($foundSingleUseStatements[0]); $useStatementIndex++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
127
            $foundUseStatements[] = trim($foundSingleUseStatements[1][$useStatementIndex]);
128
        }
129
130
        return $foundUseStatements;
131
    }
132
133
    private function getDeclaringClassOrTrait(\ReflectionProperty $reflectionProperty): \ReflectionClass
134
    {
135
        foreach ($reflectionProperty->getDeclaringClass()->getTraits() as $trait) {
136
            foreach ($trait->getProperties() as $traitProperty) {
137
                if ($traitProperty->getName() === $reflectionProperty->getName()) {
138
                    return $this->getDeclaringClassOrTrait($traitProperty);
139
                }
140
            }
141
        }
142
143
        return $reflectionProperty->getDeclaringClass();
144
    }
145
146
    private function resolveType(string $typeHint, \ReflectionProperty $reflectionProperty): string
147
    {
148
        if (!$this->hasGlobalNamespacePrefix($typeHint) && !$this->isPrimitiveType($typeHint)) {
149
            $typeHint = $this->expandClassNameUsingUseStatements($typeHint, $this->getDeclaringClassOrTrait($reflectionProperty), $reflectionProperty);
150
        }
151
152
        return ltrim($typeHint, '\\');
153
    }
154
155
    private function isArrayWithoutAnyType(string $typeHint): bool
156
    {
157
        return $typeHint === "array";
158
    }
159
}
160