Passed
Push — master ( 4af1af...24af2b )
by SignpostMarv
02:21
created

ValidateTypeExpectNonNullableString()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 22
nc 4
nop 5
dl 0
loc 35
ccs 22
cts 22
cp 1
crap 3
rs 9.568
c 0
b 0
f 0
1
<?php
2
/**
3
* @author SignpostMarv
4
*/
5
declare(strict_types=1);
6
7
namespace SignpostMarv\DaftMagicPropertyAnalysis;
8
9
use Closure;
10
use InvalidArgumentException;
11
use ReflectionFunction;
12
use ReflectionNamedType;
13
use ReflectionType;
14
15
class DefinitionAssistant
16
{
17
    const ARG_INDEX_CLOSURE_GETTER = 2;
18
19
    const ARG_INDEX_CLOSURE_SETTER = 3;
20
21
    const IN_ARRAY_STRICT_MODE = true;
22
23
    const COUNT_EXPECT_AT_LEAST_ONE_PROPERTY = 1;
24
25
    const COUNT_EXPECTED_REQUIRED_PARAMETERS = 1;
26
27
    const PARAM_INDEX_FIRST = 0;
28
29
    const BOOL_IS_PARAM = true;
30
31
    const BOOL_IS_RETURN = false;
32
33
    /**
34
    * @var array<string, array<int, string>>
35
    */
36
    protected static $properties = [];
37
38
    /**
39
    * @var array<string, Closure>
40
    */
41
    protected static $getters = [];
42
43
    /**
44
    * @var array<string, Closure>
45
    */
46
    protected static $setters = [];
47
48 16
    public static function IsTypeUnregistered(string $type) : bool
49
    {
50 16
        if ( ! interface_exists($type) && ! class_exists($type)) {
51 2
            throw new InvalidArgumentException(
52
                'Argument 1 passed to ' .
53
                __METHOD__ .
54 2
                '() must be a class or interface!'
55
            );
56
        }
57
58 14
        return ! isset(static::$properties[$type]);
59
    }
60
61 16
    public static function RegisterType(
62
        string $type,
63
        ? Closure $getter,
64
        ? Closure $setter,
65
        string ...$properties
66
    ) : void {
67 16
        if ( ! static::IsTypeUnregistered($type)) {
68 2
            throw new InvalidArgumentException(
69
                'Argument 1 passed to ' .
70
                __METHOD__ .
71 2
                '() has already been registered!'
72
            );
73 14
        } elseif (is_null($getter) && is_null($setter)) {
74 2
            throw new InvalidArgumentException(
75 2
                'One or both of arguments 2 and 3 must be specified!'
76
            );
77 12
        } elseif (count($properties) < self::COUNT_EXPECT_AT_LEAST_ONE_PROPERTY) {
78 2
            throw new InvalidArgumentException(
79 2
                'Argument 4 must be specified!'
80
            );
81
        }
82
83
        /**
84
        * @var string
85
        */
86 10
        $type = $type;
87
88 10
        static::MaybeRegisterTypeGetter($type, $getter);
89 8
        static::MaybeRegisterTypeSetter($type, $setter);
0 ignored issues
show
Bug introduced by
$type of type object is incompatible with the type string expected by parameter $type of SignpostMarv\DaftMagicPr...ybeRegisterTypeSetter(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

89
        static::MaybeRegisterTypeSetter(/** @scrutinizer ignore-type */ $type, $setter);
Loading history...
90
91 6
        static::$properties[$type] = $properties;
92 6
    }
93
94 4
    public static function GetterMethodName(string $type, string $property) : ? string
95
    {
96
        if (
97 4
            in_array($property, static::$properties[$type] ?? [], self::IN_ARRAY_STRICT_MODE) &&
98 4
            isset(static::$getters[$type])
99
        ) {
100
            /**
101
            * @var string|null
102
            */
103 4
            $out = static::$getters[$type]($property);
104
105 4
            return $out;
106
        }
107
108 2
        return null;
109
    }
110
111 4
    public static function SetterMethodName(string $type, string $property) : ? string
112
    {
113
        if (
114 4
            in_array($property, static::$properties[$type] ?? [], self::IN_ARRAY_STRICT_MODE) &&
115 4
            isset(static::$setters[$type])
116
        ) {
117
            /**
118
            * @var string|null
119
            */
120 4
            $out = static::$setters[$type]($property);
121
122 4
            return $out;
123
        }
124
125 2
        return null;
126
    }
127
128
    /**
129
    * @param mixed $maybe
130
    *
131
    * @return array<int, string>
132
    */
133 12
    public static function ObtainExpectedProperties($maybe) : array
134
    {
135 12
        if ( ! is_string($maybe) && ! is_object($maybe)) {
136 8
            throw new InvalidArgumentException(
137
                'Argument 1 passed to ' .
138
                __METHOD__ .
139
                '() must be either a string or an object, ' .
140 8
                gettype($maybe) .
141 8
                ' given!'
142
            );
143
        }
144
145
        /**
146
        * @var array<int, string>
147
        */
148 4
        $out = array_values(array_unique(array_reduce(
149 4
            array_filter(
150 4
                static::$properties,
151
                function (string $type) use ($maybe) : bool {
152 4
                    return is_a($maybe, $type, is_string($maybe));
153 4
                },
154 4
                ARRAY_FILTER_USE_KEY
155
            ),
156 4
            'array_merge',
157 4
            []
158
        )));
159
160 4
        return $out;
161
    }
162
163 22
    protected static function ValidateClosure(
164
        Closure $closure,
165
        int $argument,
166
        string $method
167
    ) : Closure {
168 22
        $ref = new ReflectionFunction($closure);
169
170 22
        if (self::COUNT_EXPECTED_REQUIRED_PARAMETERS !== $ref->getNumberOfRequiredParameters()) {
171 4
            throw new InvalidArgumentException(
172
                'Argument ' .
173 4
                $argument .
174 4
                ' passed to ' .
175 4
                $method .
176 4
                '() must be a closure with 1 required parameter!'
177
            );
178
        }
179
180 18
        $ref_param = $ref->getParameters()[self::PARAM_INDEX_FIRST];
181
182 18
        if ( ! $ref_param->hasType()) {
183 2
            throw new InvalidArgumentException(
184
                'Argument ' .
185 2
                $argument .
186 2
                ' passed to ' .
187 2
                $method .
188 2
                '() must be a closure with a typed first argument!'
189
            );
190
        }
191
192 16
        $closure = static::ValidateTypeExpectNonNullableString(
193 16
            $closure,
194 16
            $ref_param->getType(),
195 16
            $argument,
196 16
            $method,
197 16
            self::BOOL_IS_PARAM
198
        );
199
200 12
        if ( ! $ref->hasReturnType()) {
201 2
            throw new InvalidArgumentException(
202
                'Argument ' .
203 2
                $argument .
204 2
                ' passed to ' .
205 2
                $method .
206 2
                '() must have a return type!'
207
            );
208
        }
209
210 10
        $ref_return = $ref->getReturnType();
211
212 10
        return static::ValidateTypeExpectNonNullableString(
213 10
            $closure,
214 10
            $ref_return,
215 10
            $argument,
216 10
            $method,
217 10
            self::BOOL_IS_RETURN
218
        );
219
    }
220
221 24
    protected static function ValidateTypeExpectNonNullableString(
222
        Closure $closure,
223
        ? ReflectionType $ref,
224
        int $argument,
225
        string $method,
226
        bool $isParam
227
    ) : Closure {
228 24
        $not_named_type = ' named return type';
229 24
        $nullable = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $nullable is dead and can be removed.
Loading history...
230 24
        $return_type = 'return type';
0 ignored issues
show
Unused Code introduced by
The assignment to $return_type is dead and can be removed.
Loading history...
231
232 24
        if ($isParam) {
233 20
            $not_named_type = ' strongly-typed first argument';
234 20
            $nullable = 'non-';
235 20
            $return_type = 'first argument';
236
        }
237
238 24
        if ( ! ($ref instanceof ReflectionNamedType)) {
239 8
            throw new InvalidArgumentException(
240
                'Argument ' .
241 8
                $argument .
242 8
                ' passed to ' .
243 8
                $method .
244 8
                '() must be a closure with a' .
245 8
                $not_named_type .
246 8
                '!'
247
            );
248
        }
249
250 16
        return static::ValidateTypeExpectNonNullableStringWithNamedType(
251 16
            $closure,
252 16
            $ref,
253 16
            $argument,
254 16
            $method,
255 16
            $isParam
256
        );
257
    }
258
259 16
    protected static function ValidateTypeExpectNonNullableStringWithNamedType(
260
        Closure $closure,
261
        ReflectionNamedType $ref,
262
        int $argument,
263
        string $method,
264
        bool $isParam
265
    ) : Closure {
266 16
        $nullable = '';
267 16
        $return_type = 'return type';
268
269 16
        if ($isParam) {
270 16
            $nullable = 'non-';
271 16
            $return_type = 'first argument';
272
        }
273
274 16
        if ($isParam ? $ref->allowsNull() : ( ! $ref->allowsNull())) {
275 4
            throw new InvalidArgumentException(
276
                'Argument ' .
277 4
                $argument .
278 4
                ' passed to ' .
279 4
                $method .
280 4
                '() must be a closure with a ' .
281 4
                $nullable .
282 4
                'nullable ' .
283 4
                $return_type .
284 4
                '!'
285
            );
286 14
        } elseif ('string' !== $ref->getName()) {
287 4
            throw new InvalidArgumentException(
288
                'Argument ' .
289 4
                $argument .
290 4
                ' passed to ' .
291 4
                $method .
292 4
                '() must be a closure with a string ' .
293 4
                $return_type .
294 4
                ', ' .
295 4
                $ref->getName() .
296 4
                ' given!'
297
            );
298
        }
299
300 12
        return $closure;
301
    }
302
303 10
    private static function MaybeRegisterTypeGetter(string $type, ? Closure $getter) : void
304
    {
305 10
        if ( ! is_null($getter)) {
306 8
            if ( ! method_exists($type, '__get')) {
307 2
                throw new InvalidArgumentException(
308
                    'Argument 1 passed to ' .
309
                    __CLASS__ .
310 2
                    '::RegisterType() must declare __get() !'
311
                );
312
            }
313
314
            /**
315
            * @var string
316
            */
317 6
            $type = $type;
318
319
            /**
320
            * @var Closure
321
            */
322 6
            $getter = static::ValidateClosure(
323 6
                $getter,
324 6
                self::ARG_INDEX_CLOSURE_GETTER,
325 6
                (__CLASS__ . '::RegisterType')
326
            );
327
328 6
            self::$getters[$type] = $getter;
329
        }
330 8
    }
331
332 8
    private static function MaybeRegisterTypeSetter(string $type, ? Closure $setter) : void
333
    {
334 8
        if ( ! is_null($setter)) {
335 8
            if ( ! method_exists($type, '__set')) {
336 2
                throw new InvalidArgumentException(
337
                    'Argument 1 passed to ' .
338
                    __CLASS__ .
339 2
                    '::RegisterType() must declare __set() !'
340
                );
341
            }
342
343
            /**
344
            * @var string
345
            */
346 6
            $type = $type;
347
348
            /**
349
            * @var Closure
350
            */
351 6
            $setter = static::ValidateClosure(
352 6
                $setter,
353 6
                self::ARG_INDEX_CLOSURE_SETTER,
354 6
                (__CLASS__ . '::RegisterType')
355
            );
356
357 6
            self::$setters[$type] = $setter;
358
        }
359 6
    }
360
}
361