Passed
Pull Request — master (#464)
by Sergei
02:35
created

Callback::__construct()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 13
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 4
c 1
b 0
f 0
nc 3
nop 5
dl 0
loc 13
ccs 5
cts 5
cp 1
crap 5
rs 9.6111
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Rule;
6
7
use Attribute;
8
use Closure;
9
use InvalidArgumentException;
10
use ReflectionObject;
11
use Yiisoft\Validator\AfterInitAttributeEventInterface;
12
use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait;
13
use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait;
14
use Yiisoft\Validator\Rule\Trait\WhenTrait;
15
use Yiisoft\Validator\RuleWithOptionsInterface;
16
use Yiisoft\Validator\SkipOnEmptyInterface;
17
use Yiisoft\Validator\SkipOnErrorInterface;
18
use Yiisoft\Validator\WhenInterface;
19
20
/**
21
 * Defines validation options to validating the value using a callback.
22
 *
23
 * @see CallbackHandler
24
 *
25
 * @psalm-import-type WhenType from WhenInterface
26
 */
27
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
28
final class Callback implements
29
    RuleWithOptionsInterface,
30
    SkipOnErrorInterface,
31
    WhenInterface,
32
    SkipOnEmptyInterface,
33 16
    AfterInitAttributeEventInterface
34
{
35
    use SkipOnEmptyTrait;
36
    use SkipOnErrorTrait;
37
    use WhenTrait;
38
39
    /**
40
     * @param callable|null $callback Callable with the `function ($value, $rule, $context): Result` signature that
41
     * performs the validation. Mutually exclusive with {@see $method}.
42
     * @param string|null $method Name of a validated object method with the `function ($value, $rule, $context): Result`
43
     * signature that performs the validation. Mutually exclusive with {@see $callback}.
44
     * @param bool|callable|null $skipOnEmpty Whether to skip this rule if the value validated is empty.
45
     * See {@see SkipOnEmptyInterface}.
46
     * @param bool $skipOnError Whether to skip this rule if any of the previous rules gave an error.
47
     * See {@see SkipOnErrorInterface}.
48
     * @param Closure|null $when A callable to define a condition for applying the rule.
49
     * See {@see WhenInterface}.
50 16
     * @psalm-param WhenType $when
51 1
     *
52
     * @throws InvalidArgumentException When neither {@see $callback} nor {@see $method} is specified or
53
     * both are specified at the same time.
54 15
     */
55 1
    public function __construct(
56
        private mixed $callback = null,
57
        private string|null $method = null,
58
        private mixed $skipOnEmpty = null,
59 1
        private bool $skipOnError = false,
60
        private Closure|null $when = null,
61 1
    ) {
62
        if ($this->callback === null && $this->method === null) {
63
            throw new InvalidArgumentException('Either "$callback" or "$method" must be specified.');
64 17
        }
65
66 17
        if ($this->callback !== null && $this->method !== null) {
67
            throw new InvalidArgumentException('"$callback" and "$method" are mutually exclusive.');
68
        }
69 2
    }
70
71 2
    public function getName(): string
72
    {
73
        return 'callback';
74 4
    }
75
76 4
    /**
77 1
     * Get the callable that performs validation.
78
     *
79
     * @return callable|null The callable that performs validation.
80 3
     *
81
     * @see $callback
82 3
     */
83 3
    public function getCallback(): callable|null
84 1
    {
85
        return $this->callback;
86
    }
87
88
    /**
89
     * Get a name of a validated object method that performs the validation.
90
     *
91
     * @return string|null Name of a method that performs the validation.
92 2
     *
93
     * @see $method
94
     */
95 3
    public function getMethod(): string|null
96
    {
97
        return $this->method;
98 3
    }
99 3
100 3
    public function afterInitAttribute(object $object, int $target): void
101
    {
102
        if ($this->method === null) {
103
            return;
104 16
        }
105
106 16
        $method = $this->method;
107
108
        $reflection = new ReflectionObject($object);
109
        if (!$reflection->hasMethod($method)) {
110
            throw new InvalidArgumentException(
111
                sprintf(
112
                    'Method "%s" does not exist in class "%s".',
113
                    $method,
114
                    $object::class,
115
                )
116
            );
117
        }
118
119
        /** @psalm-suppress MixedMethodCall */
120
        $this->callback = Closure::bind(fn (mixed ...$args): mixed => $object->{$method}(...$args), $object, $object);
121
    }
122
123
    public function getOptions(): array
124
    {
125
        return [
126
            'method' => $this->method,
127
            'skipOnEmpty' => $this->getSkipOnEmptyOption(),
128
            'skipOnError' => $this->skipOnError,
129
        ];
130
    }
131
132
    public function getHandler(): string
133
    {
134
        return CallbackHandler::class;
135
    }
136
}
137