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

DocBlockTypeResolver::hasGlobalNamespacePrefix()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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