Passed
Push — master ( 8390ba...8d7113 )
by Asmir
12:45
created

getPropertyDocblockTypeHint()   B

Complexity

Conditions 10
Paths 18

Size

Total Lines 45
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 27
c 1
b 0
f 0
nc 18
nop 1
dl 0
loc 45
rs 7.6666

How to fix   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
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
40
        $typeHint = implode('|', $unionTypeHint);
41
        if (count($unionTypeHint) > 1) {
42
            throw new \InvalidArgumentException(sprintf("Can't use union type %s for collection in %s:%s", $typeHint, $reflectionProperty->getDeclaringClass()->getName(), $reflectionProperty->getName()));
43
        }
44
45
        if (false !== strpos($typeHint, 'array<')) {
46
            $resolvedTypes = [];
47
            preg_match_all('#array<(.*)>#', $typeHint, $genericTypesToResolve);
48
            $genericTypesToResolve = $genericTypesToResolve[1][0];
49
            foreach (explode(',', $genericTypesToResolve) as $genericTypeToResolve) {
50
                $resolvedTypes[] = $this->resolveType(trim($genericTypeToResolve), $reflectionProperty);
51
            }
52
53
            return 'array<' . implode(',', $resolvedTypes) . '>';
54
        } elseif (false !== strpos($typeHint, '[]')) {
55
            $typeHint = rtrim($typeHint, '[]');
56
            $typeHint = $this->resolveType($typeHint, $reflectionProperty);
57
58
            return 'array<' . $typeHint . '>';
59
        }
60
61
        return $this->resolveType($typeHint, $reflectionProperty);
62
    }
63
64
    private function expandClassNameUsingUseStatements(string $typeHint, \ReflectionClass $declaringClass, \ReflectionProperty $reflectionProperty): string
65
    {
66
        if (class_exists($typeHint)) {
67
            return $typeHint;
68
        }
69
70
        $expandedClassName = $declaringClass->getNamespaceName() . '\\' . $typeHint;
71
        if (class_exists($expandedClassName)) {
72
            return $expandedClassName;
73
        }
74
75
        $classContents = file_get_contents($declaringClass->getFileName());
76
        $foundUseStatements = $this->gatherGroupUseStatements($classContents);
77
        $foundUseStatements = array_merge($this->gatherSingleUseStatements($classContents), $foundUseStatements);
78
79
        foreach ($foundUseStatements as $statementClassName) {
80
            if ($alias = explode('as', $statementClassName)) {
81
                if (array_key_exists(1, $alias) && trim($alias[1]) === $typeHint) {
82
                    return trim($alias[0]);
83
                }
84
            }
85
86
            if ($this->endsWith($statementClassName, $typeHint)) {
87
                return $statementClassName;
88
            }
89
        }
90
91
        throw new \InvalidArgumentException(sprintf("Can't use incorrect type %s for collection in %s:%s", $typeHint, $declaringClass->getName(), $reflectionProperty->getName()));
92
    }
93
94
    private function endsWith(string $statementClassToCheck, string $typeHintToSearchFor): bool
95
    {
96
        $typeHintToSearchFor = '\\' . $typeHintToSearchFor;
97
98
        return substr($statementClassToCheck, -strlen($typeHintToSearchFor)) === $typeHintToSearchFor;
99
    }
100
101
    private function isPrimitiveType(string $type): bool
102
    {
103
        return in_array($type, ['int', 'float', 'bool', 'string']);
104
    }
105
106
    private function hasGlobalNamespacePrefix(string $typeHint): bool
107
    {
108
        return self::GLOBAL_NAMESPACE_PREFIX === $typeHint[0];
109
    }
110
111
    private function gatherGroupUseStatements(string $classContents): array
112
    {
113
        $foundUseStatements = [];
114
        preg_match_all(self::GROUP_USE_STATEMENTS_REGEX, $classContents, $foundGroupUseStatements);
115
        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...
116
            foreach (explode(',', $foundGroupUseStatements[2][$useStatementIndex]) as $singleUseStatement) {
117
                $foundUseStatements[] = trim($foundGroupUseStatements[1][$useStatementIndex]) . trim($singleUseStatement);
118
            }
119
        }
120
121
        return $foundUseStatements;
122
    }
123
124
    private function gatherSingleUseStatements(string $classContents): array
125
    {
126
        $foundUseStatements = [];
127
        preg_match_all(self::SINGLE_USE_STATEMENTS_REGEX, $classContents, $foundSingleUseStatements);
128
        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...
129
            $foundUseStatements[] = trim($foundSingleUseStatements[1][$useStatementIndex]);
130
        }
131
132
        return $foundUseStatements;
133
    }
134
135
    private function getDeclaringClassOrTrait(\ReflectionProperty $reflectionProperty): \ReflectionClass
136
    {
137
        foreach ($reflectionProperty->getDeclaringClass()->getTraits() as $trait) {
138
            foreach ($trait->getProperties() as $traitProperty) {
139
                if ($traitProperty->getName() === $reflectionProperty->getName()) {
140
                    return $this->getDeclaringClassOrTrait($traitProperty);
141
                }
142
            }
143
        }
144
145
        return $reflectionProperty->getDeclaringClass();
146
    }
147
148
    private function resolveType(string $typeHint, \ReflectionProperty $reflectionProperty): string
149
    {
150
        if (!$this->hasGlobalNamespacePrefix($typeHint) && !$this->isPrimitiveType($typeHint)) {
151
            $typeHint = $this->expandClassNameUsingUseStatements($typeHint, $this->getDeclaringClassOrTrait($reflectionProperty), $reflectionProperty);
152
        }
153
154
        return ltrim($typeHint, '\\');
155
    }
156
157
    private function isArrayWithoutAnyType(string $typeHint): bool
158
    {
159
        return 'array' === $typeHint;
160
    }
161
}
162