Passed
Pull Request — 1.x (#2)
by Kevin
01:41
created

invoke_with_not_enough_required_arguments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 10
nc 1
nop 0
dl 0
loc 14
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
namespace Zenstruck\Callback\Tests;
4
5
use PHPUnit\Framework\TestCase;
6
use Zenstruck\Callback;
7
use Zenstruck\Callback\Argument;
8
use Zenstruck\Callback\Exception\UnresolveableArgument;
9
use Zenstruck\Callback\Parameter;
10
11
/**
12
 * @author Kevin Bond <[email protected]>
13
 */
14
final class CallbackTest extends TestCase
15
{
16
    /**
17
     * @test
18
     */
19
    public function create_must_be_callable(): void
20
    {
21
        $this->expectException(\InvalidArgumentException::class);
22
23
        Callback::createFor('not a callable');
24
    }
25
26
    /**
27
     * @test
28
     */
29
    public function invoke_all_can_enforce_min_arguments(): void
30
    {
31
        $callback = Callback::createFor(function() { return 'ret'; });
32
33
        $this->expectException(\ArgumentCountError::class);
34
35
        $callback->invokeAll(Parameter::untyped('foo'), 1);
36
    }
37
38
    /**
39
     * @test
40
     */
41
    public function invoke_all_with_no_arguments(): void
42
    {
43
        $actual = Callback::createFor(function() { return 'ret'; })
44
            ->invokeAll(Parameter::untyped('foo'))
45
        ;
46
47
        $this->assertSame('ret', $actual);
48
    }
49
50
    /**
51
     * @test
52
     */
53
    public function invoke_all_with_string_callable(): void
54
    {
55
        $actual = Callback::createFor('strtoupper')
56
            ->invokeAll(Parameter::union(
57
                Parameter::untyped('foobar'),
58
                Parameter::typed('string', 'foobar')
59
            )
60
        )
61
        ;
62
63
        $this->assertSame('FOOBAR', $actual);
64
    }
65
66
    /**
67
     * @test
68
     */
69
    public function invoke_all_untyped_argument(): void
70
    {
71
        $actual = Callback::createFor(function($string) { return \mb_strtoupper($string); })
72
            ->invokeAll(Parameter::untyped('foobar'))
73
        ;
74
75
        $this->assertSame('FOOBAR', $actual);
76
    }
77
78
    /**
79
     * @test
80
     */
81
    public function invoke_all_primitive_typed_argument(): void
82
    {
83
        $actual = Callback::createFor(function(string $string) { return \mb_strtoupper($string); })
84
            ->invokeAll(Parameter::typed('string', 'foobar'))
85
        ;
86
87
        $this->assertSame('FOOBAR', $actual);
88
    }
89
90
    /**
91
     * @test
92
     */
93
    public function invoke_all_class_arguments(): void
94
    {
95
        $object = new Object2();
96
        $function = static function(Object1 $object1, Object2 $object2, $object3) {
97
            return [
98
                'object1' => $object1,
99
                'object2' => $object2,
100
                'object3' => $object3,
101
            ];
102
        };
103
104
        $actual = Callback::createFor($function)
105
            ->invokeAll(Parameter::union(
106
                Parameter::untyped($object),
107
                Parameter::typed(Object1::class, $object)
108
            ))
109
        ;
110
111
        $this->assertSame(
112
            [
113
                'object1' => $object,
114
                'object2' => $object,
115
                'object3' => $object,
116
            ],
117
            $actual
118
        );
119
    }
120
121
    /**
122
     * @test
123
     */
124
    public function invoke_all_class_arguments_value_factories(): void
125
    {
126
        $function = static function(Object1 $object1, Object2 $object2, $object3) {
127
            return [
128
                'object1' => $object1,
129
                'object2' => $object2,
130
                'object3' => $object3,
131
            ];
132
        };
133
        $factoryArgs = [];
134
        $factory = Parameter::factory(static function($arg) use (&$factoryArgs) {
135
            $factoryArgs[] = $arg;
136
137
            if ($arg) {
138
                return new $arg();
139
            }
140
141
            return new Object1();
142
        });
143
144
        $ret = Callback::createFor($function)
145
            ->invokeAll(Parameter::union(
146
                Parameter::untyped($factory),
147
                Parameter::typed(Object1::class, $factory)
148
            ))
149
        ;
150
151
        $this->assertSame(['object1', 'object2', 'object3'], \array_keys($ret));
152
        $this->assertInstanceOf(Object1::class, $ret['object1']);
153
        $this->assertInstanceOf(Object2::class, $ret['object2']);
154
        $this->assertInstanceOf(Object1::class, $ret['object3']);
155
        $this->assertSame(
156
            [Object1::class, Object2::class, null],
157
            $factoryArgs
158
        );
159
    }
160
161
    /**
162
     * @test
163
     */
164
    public function invoke_all_unresolvable_parameter(): void
165
    {
166
        $callback = Callback::createFor(static function(Object1 $object1, Object2 $object2, Object3 $object3) {});
0 ignored issues
show
Unused Code introduced by
The parameter $object1 is not used and could be removed. ( Ignorable by Annotation )

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

166
        $callback = Callback::createFor(static function(/** @scrutinizer ignore-unused */ Object1 $object1, Object2 $object2, Object3 $object3) {});

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $object2 is not used and could be removed. ( Ignorable by Annotation )

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

166
        $callback = Callback::createFor(static function(Object1 $object1, /** @scrutinizer ignore-unused */ Object2 $object2, Object3 $object3) {});

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $object3 is not used and could be removed. ( Ignorable by Annotation )

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

166
        $callback = Callback::createFor(static function(Object1 $object1, Object2 $object2, /** @scrutinizer ignore-unused */ Object3 $object3) {});

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
167
168
        $this->expectException(UnresolveableArgument::class);
169
        $this->expectExceptionMessage('Unable to resolve argument 3 for callback. Expected type: "mixed|Zenstruck\Callback\Tests\Object1"');
170
171
        $callback->invokeAll(Parameter::union(
172
            Parameter::untyped(new Object1()),
173
            Parameter::typed(Object1::class, new Object1())
174
        ));
175
    }
176
177
    /**
178
     * @test
179
     */
180
    public function invoke_with_no_args(): void
181
    {
182
        $actual = Callback::createFor(function() { return 'ret'; })
183
            ->invoke()
184
        ;
185
186
        $this->assertSame('ret', $actual);
187
    }
188
189
    /**
190
     * @test
191
     */
192
    public function invoke_with_resolvable_args(): void
193
    {
194
        $object = new Object2();
195
        $function = static function(Object1 $object1, Object2 $object2, $object3, $extra) {
196
            return [
197
                'object1' => $object1,
198
                'object2' => $object2,
199
                'object3' => $object3,
200
                'extra' => $extra,
201
            ];
202
        };
203
204
        $actual = Callback::createFor($function)
205
            ->invoke(
206
                Parameter::typed(Object1::class, $object),
207
                Parameter::typed(Object2::class, $object),
208
                Parameter::untyped($object),
209
                'value'
210
            )
211
        ;
212
213
        $this->assertSame(
214
            [
215
                'object1' => $object,
216
                'object2' => $object,
217
                'object3' => $object,
218
                'extra' => 'value',
219
            ],
220
            $actual
221
        );
222
    }
223
224
    /**
225
     * @test
226
     */
227
    public function invoke_with_unresolvable_argument(): void
228
    {
229
        $object = new Object2();
230
        $function = static function(Object1 $object1, $object2, $object3, $extra) {};
0 ignored issues
show
Unused Code introduced by
The parameter $object3 is not used and could be removed. ( Ignorable by Annotation )

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

230
        $function = static function(Object1 $object1, $object2, /** @scrutinizer ignore-unused */ $object3, $extra) {};

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $object1 is not used and could be removed. ( Ignorable by Annotation )

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

230
        $function = static function(/** @scrutinizer ignore-unused */ Object1 $object1, $object2, $object3, $extra) {};

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $extra is not used and could be removed. ( Ignorable by Annotation )

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

230
        $function = static function(Object1 $object1, $object2, $object3, /** @scrutinizer ignore-unused */ $extra) {};

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $object2 is not used and could be removed. ( Ignorable by Annotation )

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

230
        $function = static function(Object1 $object1, /** @scrutinizer ignore-unused */ $object2, $object3, $extra) {};

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
231
232
        $this->expectException(UnresolveableArgument::class);
233
        $this->expectExceptionMessage('Unable to resolve argument 2 for callback. Expected type: "Zenstruck\Callback\Tests\Object2"');
234
235
        Callback::createFor($function)
236
            ->invoke(
237
                Parameter::typed(Object1::class, $object),
238
                Parameter::typed(Object2::class, $object),
239
                Parameter::untyped($object),
240
                'value'
241
            )
242
        ;
243
    }
244
245
    /**
246
     * @test
247
     */
248
    public function invoke_with_not_enough_required_arguments(): void
249
    {
250
        $object = new Object2();
251
        $function = static function(Object1 $object1) {};
0 ignored issues
show
Unused Code introduced by
The parameter $object1 is not used and could be removed. ( Ignorable by Annotation )

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

251
        $function = static function(/** @scrutinizer ignore-unused */ Object1 $object1) {};

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
252
253
        $this->expectException(\ArgumentCountError::class);
254
        $this->expectExceptionMessage('No argument 2 for callable. Expected type: "Zenstruck\Callback\Tests\Object2"');
255
256
        Callback::createFor($function)
257
            ->invoke(
258
                Parameter::typed(Object1::class, $object),
259
                Parameter::typed(Object2::class, $object),
260
                Parameter::untyped($object),
261
                'value'
262
            )
263
        ;
264
    }
265
266
    /**
267
     * @test
268
     */
269
    public function can_mark_invoke_parameter_arguments_as_optional(): void
270
    {
271
        $actual = Callback::createFor(static function() { return 'ret'; })
272
            ->invoke(Parameter::typed('string', 'foobar')->optional())
273
        ;
274
275
        $this->assertSame('ret', $actual);
276
277
        $actual = Callback::createFor(static function(string $v) { return $v; })
278
            ->invoke(Parameter::typed('string', 'foobar')->optional())
279
        ;
280
281
        $this->assertSame('foobar', $actual);
282
    }
283
284
    /**
285
     * @test
286
     */
287
    public function is_stringable(): void
288
    {
289
        $this->assertStringMatchesFormat(__CLASS__.':%d', (string) Callback::createFor(function() {}));
290
        $this->assertStringMatchesFormat(__CLASS__.':%d', (string) Callback::createFor([$this, __METHOD__]));
291
        $this->assertStringMatchesFormat(Object4::class.':%d', (string) Callback::createFor(new Object4()));
292
        $this->assertStringMatchesFormat(Object4::class.':%d', (string) Callback::createFor([Object4::class, 'staticMethod']));
293
        $this->assertSame(__NAMESPACE__.'\test_function', (string) Callback::createFor(__NAMESPACE__.'\test_function'));
294
    }
295
296
    /**
297
     * @test
298
     * @requires PHP >= 8.0
299
     */
300
    public function invoke_can_support_union_typehints(): void
301
    {
302
        $callback = fn(Object1|string $arg) => 'ret';
0 ignored issues
show
Unused Code introduced by
The parameter $arg is not used and could be removed. ( Ignorable by Annotation )

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

302
        $callback = fn(/** @scrutinizer ignore-unused */ Object1|string $arg) => 'ret';

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
303
304
        $this->assertSame('ret', Callback::createFor($callback)->invokeAll(Parameter::typed(Object1::class, new Object1())));
305
        $this->assertSame('ret', Callback::createFor($callback)->invokeAll(Parameter::typed('string', 'value')));
306
        $this->assertSame('ret', Callback::createFor($callback)->invoke(Parameter::typed(Object1::class, new Object1())));
307
        $this->assertSame('ret', Callback::createFor($callback)->invoke(Parameter::typed('string', 'value')));
308
    }
309
310
    /**
311
     * @test
312
     */
313
    public function can_get_callback_arguments(): void
314
    {
315
        $callback = Callback::createFor(function(Object1 $a, $b, string $c) {});
0 ignored issues
show
Unused Code introduced by
The parameter $b is not used and could be removed. ( Ignorable by Annotation )

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

315
        $callback = Callback::createFor(function(Object1 $a, /** @scrutinizer ignore-unused */ $b, string $c) {});

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $c is not used and could be removed. ( Ignorable by Annotation )

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

315
        $callback = Callback::createFor(function(Object1 $a, $b, /** @scrutinizer ignore-unused */ string $c) {});

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $a is not used and could be removed. ( Ignorable by Annotation )

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

315
        $callback = Callback::createFor(function(/** @scrutinizer ignore-unused */ Object1 $a, $b, string $c) {});

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
316
317
        $this->assertSame(Object1::class, $callback->argument(0)->type());
318
        $this->assertNull($callback->argument(1)->type());
319
        $this->assertSame('string', $callback->argument(2)->type());
320
        $this->assertSame(
321
            [
322
                Object1::class,
323
                null,
324
                'string',
325
            ],
326
            \array_map(function(Argument $a) { return $a->type(); }, $callback->arguments())
327
        );
328
    }
329
330
    /**
331
     * @test
332
     * @requires PHP >= 8.0
333
     */
334
    public function can_get_union_callback_arguments(): void
335
    {
336
        $callback = Callback::createFor(fn(Object1|string $a, $b, string $c) => null);
0 ignored issues
show
Unused Code introduced by
The parameter $c is not used and could be removed. ( Ignorable by Annotation )

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

336
        $callback = Callback::createFor(fn(Object1|string $a, $b, /** @scrutinizer ignore-unused */ string $c) => null);

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $b is not used and could be removed. ( Ignorable by Annotation )

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

336
        $callback = Callback::createFor(fn(Object1|string $a, /** @scrutinizer ignore-unused */ $b, string $c) => null);

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $a is not used and could be removed. ( Ignorable by Annotation )

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

336
        $callback = Callback::createFor(fn(/** @scrutinizer ignore-unused */ Object1|string $a, $b, string $c) => null);

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
337
338
        $this->assertSame(Object1::class.'|string', $callback->argument(0)->type());
339
        $this->assertNull($callback->argument(1)->type());
340
        $this->assertSame('string', $callback->argument(2)->type());
341
        $this->assertSame(
342
            [
343
                Object1::class.'|string',
344
                null,
345
                'string',
346
            ],
347
            \array_map(function(Argument $a) { return $a->type(); }, $callback->arguments())
348
        );
349
    }
350
351
    /**
352
     * @test
353
     */
354
    public function exception_thrown_when_trying_to_access_invalid_argument(): void
355
    {
356
        $this->expectException(\OutOfRangeException::class);
357
358
        Callback::createFor(function() {})->argument(0);
359
    }
360
361
    /**
362
     * @test
363
     */
364
    public function value_factory_injects_argument_if_type_hinted(): void
365
    {
366
        $callback = Callback::createFor(function(string $a, int $b, $c) { return [$a, $b, $c]; });
367
        $factory = Parameter::factory(function(Argument $argument) {
368
            if ($argument->supports('string')) {
369
                return 'string';
370
            }
371
372
            if ($argument->supports('int')) {
373
                return 17;
374
            }
375
376
            return 'invalid';
377
        });
378
379
        $ret = $callback->invokeAll(
380
            Parameter::union(
381
                Parameter::typed('string', $factory),
382
                Parameter::typed('int', $factory),
383
                Parameter::untyped($factory)
384
            )
385
        );
386
387
        $this->assertSame(['string', 17, 'string'], $ret);
388
    }
389
390
    /**
391
     * @test
392
     */
393
    public function can_use_value_factory_with_no_argument(): void
394
    {
395
        $ret = Callback::createFor(function($value) { return $value; })
396
            ->invoke(Parameter::untyped(Parameter::factory(function() { return 'value'; })))
397
        ;
398
399
        $this->assertSame('value', $ret);
400
    }
401
402
    /**
403
     * @test
404
     * @requires PHP >= 8.0
405
     */
406
    public function value_factory_can_be_used_with_union_arguments_if_no_value_factory_argument(): void
407
    {
408
        $ret = Callback::createFor(fn(Object1|string $a) => $a)
409
            ->invoke(Parameter::typed('string', Parameter::factory(fn() => 'value')))
410
        ;
411
412
        $this->assertSame('value', $ret);
413
    }
414
415
    /**
416
     * @test
417
     * @requires PHP >= 8.0
418
     */
419
    public function value_factory_can_be_used_with_union_arguments_as_array(): void
420
    {
421
        $array = [];
422
        $factory = Parameter::factory(function(array $types) use (&$array) {
423
            $array = $types;
424
425
            return 'value';
426
        });
427
428
        $ret = Callback::createFor(fn(Object1|string $a) => $a)
429
            ->invoke(Parameter::typed('string', $factory))
430
        ;
431
432
        $this->assertSame('value', $ret);
433
        $this->assertSame([Object1::class, 'string'], $array);
434
    }
435
436
    /**
437
     * @test
438
     * @requires PHP >= 8.0
439
     */
440
    public function value_factory_cannot_accept_union_argument(): void
441
    {
442
        $this->expectException(\LogicException::class);
443
444
        Callback::createFor(fn(Object1|string $a) => $a)
445
            ->invoke(Parameter::typed('string', Parameter::factory(fn(string $type) => $type)))
446
        ;
447
    }
448
}
449
450
class Object1
451
{
452
}
453
454
class Object2 extends Object1
455
{
456
}
457
458
class Object3
459
{
460
}
461
462
class Object4
463
{
464
    public function __invoke()
465
    {
466
    }
467
468
    public static function staticMethod()
469
    {
470
    }
471
}
472
473
function test_function()
474
{
475
}
476