• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JsonMapper / JsonMapper / 14739223412

29 Apr 2025 07:01PM UTC coverage: 98.719% (+2.9%) from 95.789%
14739223412

push

github

web-flow
feat: replace docblock type parsing with reflection docblock library (#199)

* feat: replace docblcok type parsing with reflection docblock library

* fix: add support for array shape

* test: add lazy annotation map test

* style: correct code style violations

* test: add additional test cases

* test: add test for newly introduced method

* docs: add changelog entry

101 of 111 new or added lines in 4 files covered. (90.99%)

1002 of 1015 relevant lines covered (98.72%)

68.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.18
/src/ValueObjects/LazyAnnotationMap.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace JsonMapper\ValueObjects;
6

7
use JsonMapper\Builders\PropertyBuilder;
8
use JsonMapper\Parser\Import;
9
use phpDocumentor\Reflection\DocBlock;
10
use phpDocumentor\Reflection\DocBlockFactory;
11
use phpDocumentor\Reflection\PseudoType;
12
use phpDocumentor\Reflection\PseudoTypes;
13
use phpDocumentor\Reflection\Type;
14
use phpDocumentor\Reflection\Types;
15
use phpDocumentor\Reflection\Types\Context;
16

17
class LazyAnnotationMap
18
{
19
    private string $input;
20
    /** @var Import[] */
21
    private array $imports;
22
    private ?DocBlock $docBlock = null;
23
    private string $namespace;
24

25
    /**
26
     * @param Import[] $imports
27
     */
28
    public function __construct(
29
        string $input,
30
        string $namespace = '',
31
        array $imports = []
32
    ) {
33
        $this->input = $input;
180✔
34
        $this->namespace = $namespace;
180✔
35
        $this->imports = $imports;
180✔
36
    }
37

38
    public function hasVar(): bool
39
    {
40
        $this->initialize();
24✔
41

42
        return !is_null($this->getFirstTagByNameAndVariableName('var'));
24✔
43
    }
44

45
    public function hasParam(string $variableName): bool
46
    {
47
        $this->initialize();
24✔
48

49
        return !is_null($this->getFirstTagByNameAndVariableName('param', $variableName));
24✔
50
    }
51

52
    public function tagToPropertyBuilder(string $tagName, ?string $variableName = null): PropertyBuilder
53
    {
54
        $this->initialize();
132✔
55
        $tag = $this->getFirstTagByNameAndVariableName($tagName, $variableName);
132✔
56

57
        if (\is_null($tag)) {
132✔
58
            throw new \RuntimeException('Missing tag with name ' . $tagName . ' for variable ' . $variableName);
12✔
59
        }
60

61
        $type = $tag->getType();
120✔
62
        if (is_null($type)) {
120✔
NEW
63
            throw new \RuntimeException('Tag has no type');
×
64
        }
65

66
        $builder = PropertyBuilder::new()
120✔
67
            ->setIsNullable($type instanceof Types\Nullable);
120✔
68

69
        // Unpack nullable type (non-compound type prefixed with question mark)
70
        if ($type instanceof Types\Nullable) {
120✔
71
            $type = $type->getActualType();
24✔
72
        }
73
        // Unpack pseudo type
74
        if ($type instanceof PseudoType && ! $type instanceof PseudoTypes\List_) {
120✔
75
            $type = $type->underlyingType();
12✔
76
        }
77
        if ($type instanceof Types\Compound) {
120✔
78
            $types = $type->getIterator()->getArrayCopy();
24✔
79
        } else {
80
            $types = [$type];
96✔
81
        }
82

83
        foreach ($types as $type) {
120✔
84
            switch (get_class($type)) {
120✔
85
                case Types\Null_::class:
86
                    $builder->setIsNullable(true);
12✔
87
                    break;
12✔
88
                case Types\String_::class:
89
                case Types\Boolean::class:
90
                case Types\Float_::class:
91
                case Types\Integer::class:
92
                case Types\Object_::class:
93
                case Types\Mixed_::class:
94
                case Types\Array_::class:
95
                case PseudoTypes\List_::class:
96
                case PseudoTypes\ArrayShape::class:
97
                    $builder->addType($this->mapDocBlockTypeClassToPropertyType($type), $this->mapDoCBlockTypeToArrayDimension($type));
120✔
98
                    break;
120✔
99
                default:
NEW
100
                    throw new \RuntimeException('Unexpected type ' . get_class($type));
×
101
            }
102
        }
103

104
        return $builder;
120✔
105
    }
106

107
    private function initialize(): void
108
    {
109
        if (!\is_null($this->docBlock)) {
180✔
NEW
110
            return;
×
111
        }
112

113
        $factory = DocBlockFactory::createInstance();
180✔
114

115
        $namespaceAliases = [];
180✔
116
        foreach ($this->imports as $import) {
180✔
117
            if ($import->hasAlias()) {
12✔
NEW
118
                $namespaceAliases[$import->getAlias()] = $import->getImport();
×
NEW
119
                continue;
×
120
            }
121

122
            $farMostRightNamespaceSeparator = strrpos($import->getImport(), '\\');
12✔
123
            if ($farMostRightNamespaceSeparator === false) {
12✔
NEW
124
                $namespaceAliases[$import->getImport()] = $import->getImport();
×
NEW
125
                continue;
×
126
            }
127

128
            $key = substr($import->getImport(), $farMostRightNamespaceSeparator + 1);
12✔
129
            $namespaceAliases[$key] = $import->getImport();
12✔
130
        }
131
        $this->docBlock = $factory->create(
180✔
132
            $this->input,
180✔
133
            new Context(
180✔
134
                $this->namespace,
180✔
135
                $namespaceAliases,
90✔
136
            )
90✔
137
        );
90✔
138
    }
139

140
    private function getFirstTagByNameAndVariableName(string $name, ?string $variableName = null): ?DocBlock\Tags\TagWithType
141
    {
142
        $tags = $this->docBlock->getTagsWithTypeByName($name);
180✔
143

144
        if (\is_null($variableName)) {
180✔
145
            return array_shift($tags);
156✔
146
        }
147

148
        $matches = array_filter($tags, static function (DocBlock\Tags\TagWithType $tag) use ($variableName): bool {
24✔
149
            if (!$tag instanceof DocBlock\Tags\Param && !$tag instanceof DocBlock\Tags\Var_) {
12✔
NEW
150
                return false;
×
151
            }
152
            return $tag->getVariableName() === $variableName;
12✔
153
        });
12✔
154

155
        return array_shift($matches);
24✔
156
    }
157

158
    private function mapDocBlockTypeClassToPropertyType(Type $type): string
159
    {
160
        switch (get_class($type)) {
120✔
161
            case Types\String_::class:
162
            case Types\ClassString::class:
163
                return 'string';
48✔
164
            case Types\Boolean::class:
165
                return 'bool';
12✔
166
            case Types\Float_::class:
167
                return 'float';
12✔
168
            case Types\Integer::class:
169
                return 'int';
36✔
170
            case Types\Mixed_::class:
171
                return 'mixed';
12✔
172
            case Types\Object_::class:
173
                return ltrim($type->__toString(), '\\');
12✔
174
            case Types\Array_::class:
175
            case PseudoTypes\List_::class:
176
                return $this->mapDocBlockTypeClassToPropertyType($type->getValueType());
12✔
177
            case PseudoTypes\ArrayShape::class:
NEW
178
                return $this->mapDocBlockTypeClassToPropertyType($type->underlyingType());
×
179
            default:
NEW
180
                throw new \RuntimeException('Unexpected type ' . get_class($type));
×
181
        }
182
    }
183

184
    private function mapDoCBlockTypeToArrayDimension(Type $type): ArrayInformation
185
    {
186
        $dimensions = 0;
120✔
187
        while ($type instanceof Types\Array_) {
120✔
188
            $dimensions++;
12✔
189
            $type = $type->getValueType();
12✔
190
        }
191

192
        if ($dimensions === 0) {
120✔
193
            return ArrayInformation::notAnArray();
108✔
194
        }
195

196
        return ArrayInformation::multiDimension($dimensions);
12✔
197
    }
198
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc