Argument   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 198
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 70
c 1
b 0
f 0
dl 0
loc 198
rs 9.36
wmc 38

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A type() 0 3 2
A isUnionType() 0 3 1
A types() 0 15 3
C supports() 0 49 17
A isOptional() 0 3 1
A hasType() 0 3 1
A defaultValue() 0 3 1
B allows() 0 22 8
A reflectionTypes() 0 12 3
1
<?php
2
3
namespace Zenstruck\Callback;
4
5
/**
6
 * @author Kevin Bond <[email protected]>
7
 */
8
final class Argument
9
{
10
    /**
11
     * Allow exact type (always enabled).
12
     */
13
    public const EXACT = 0;
14
15
    /**
16
     * If type is class, parent classes are supported.
17
     */
18
    public const COVARIANCE = 2;
19
20
    /**
21
     * If type is class, child classes are supported.
22
     */
23
    public const CONTRAVARIANCE = 4;
24
25
    /**
26
     * If type is string, do not support other scalar types. Follows
27
     * same logic as "declare(strict_types=1)".
28
     */
29
    public const STRICT = 8;
30
31
    /**
32
     * If type is float, do not support int (implies {@see STRICT).
33
     */
34
    public const VERY_STRICT = 16;
35
36
    private const TYPE_NORMALIZE_MAP = [
37
        'boolean' => 'bool',
38
        'integer' => 'int',
39
        'double' => 'float',
40
        'resource (closed)' => 'resource',
41
    ];
42
43
    private const ALLOWED_TYPE_MAP = [
44
        'string' => ['bool', 'int', 'float'],
45
        'bool' => ['string', 'int', 'float'],
46
        'float' => ['string', 'int', 'bool'],
47
        'int' => ['string', 'float', 'bool'],
48
    ];
49
50
    /** @var \ReflectionParameter */
51
    private $parameter;
52
53
    public function __construct(\ReflectionParameter $parameter)
54
    {
55
        $this->parameter = $parameter;
56
    }
57
58
    public function type(): ?string
59
    {
60
        return $this->hasType() ? \implode('|', $this->types()) : null;
61
    }
62
63
    /**
64
     * @return string[]
65
     */
66
    public function types(): array
67
    {
68
        return \array_map(
69
            function(\ReflectionNamedType $type) {
70
                if ('self' !== $name = $type->getName()) {
71
                    return $name;
72
                }
73
74
                if (!$class = $this->parameter->getDeclaringClass()) {
75
                    throw new \LogicException('Unable to parse context of "self" typehint.');
76
                }
77
78
                return $class->name;
79
            },
80
            $this->reflectionTypes()
81
        );
82
    }
83
84
    public function hasType(): bool
85
    {
86
        return !empty($this->types());
87
    }
88
89
    public function isUnionType(): bool
90
    {
91
        return \count($this->types()) > 1;
92
    }
93
94
    public function isOptional(): bool
95
    {
96
        return $this->parameter->isOptional();
97
    }
98
99
    /**
100
     * @return mixed
101
     */
102
    public function defaultValue()
103
    {
104
        return $this->parameter->getDefaultValue();
105
    }
106
107
    /**
108
     * @param string $type    The type to check if this argument supports
109
     * @param int    $options {@see EXACT}, {@see COVARIANCE}, {@see CONTRAVARIANCE}
110
     *                        Bitwise disjunction of above is allowed
111
     */
112
    public function supports(string $type, int $options = self::EXACT|self::COVARIANCE): bool
113
    {
114
        if (!$this->hasType()) {
115
            // no type-hint so any type is supported
116
            return true;
117
        }
118
119
        if ('null' === \mb_strtolower($type) && $this->parameter->allowsNull()) {
120
            return true;
121
        }
122
123
        $type = self::TYPE_NORMALIZE_MAP[$type] ?? $type;
124
125
        foreach ($this->types() as $supportedType) {
126
            if ($supportedType === $type) {
127
                return true;
128
            }
129
130
            if ($options & self::COVARIANCE && \is_a($type, $supportedType, true)) {
131
                return true;
132
            }
133
134
            if ($options & self::CONTRAVARIANCE && \is_a($supportedType, $type, true)) {
135
                return true;
136
            }
137
138
            if ($options & self::VERY_STRICT) {
139
                continue;
140
            }
141
142
            if ('float' === $supportedType && 'int' === $type) {
143
                // strict typing allows int to pass a float validation
144
                return true;
145
            }
146
147
            if ($options & self::STRICT) {
148
                continue;
149
            }
150
151
            if (\in_array($type, self::ALLOWED_TYPE_MAP[$supportedType] ?? [], true)) {
152
                return true;
153
            }
154
155
            if ('string' === $supportedType && \method_exists($type, '__toString')) {
156
                return true;
157
            }
158
        }
159
160
        return false;
161
    }
162
163
    /**
164
     * @param mixed $value
165
     * @param bool  $strict {@see STRICT}
166
     */
167
    public function allows($value, bool $strict = false): bool
168
    {
169
        if (!$this->hasType()) {
170
            // no type-hint so any type is supported
171
            return true;
172
        }
173
174
        $type = \is_object($value) ? \get_class($value) : \gettype($value);
175
        $type = self::TYPE_NORMALIZE_MAP[$type] ?? $type;
176
        $options = $strict ? self::EXACT|self::COVARIANCE|self::STRICT : self::EXACT|self::COVARIANCE;
177
        $supports = $this->supports($type, $options);
178
179
        if (!$supports) {
180
            return false;
181
        }
182
183
        if ('string' === $type && !\is_numeric($value) && !\in_array('string', $this->types(), true)) {
184
            // non-numeric strings cannot be used for float/int
185
            return false;
186
        }
187
188
        return true;
189
    }
190
191
    /**
192
     * @return \ReflectionNamedType[]
193
     */
194
    private function reflectionTypes(): array
195
    {
196
        if (!$type = $this->parameter->getType()) {
197
            return [];
198
        }
199
200
        if ($type instanceof \ReflectionNamedType) {
201
            return [$type];
202
        }
203
204
        /** @var \ReflectionUnionType $type */
205
        return $type->getTypes();
206
    }
207
}
208