Validator::apply()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Mbright\Validation;
4
5
use Mbright\Validation\Exception\ValidationFailureException;
6
use Mbright\Validation\Spec\AbstractSpec;
7
use Mbright\Validation\Spec\SanitizeSpec;
8
use Mbright\Validation\Spec\ValidateSpec;
9
use Mbright\Validation\Failure\FailureCollection;
10
11
class Validator
12
{
13
    /** @var FailureCollection */
14
    protected $failures;
15
16
    /** @var ValidateSpec[] */
17
    protected $validateSpecs = [];
18
19
    /** @var SanitizeSpec[] */
20
    protected $sanitizeSpecs = [];
21
22
    /**
23
     * Fields to skip during validation.
24
     *
25
     * @var array
26
     */
27
    protected $skip = [];
28
29
    /**
30
     * Messages to use for a field.
31
     *
32
     * Index is the fieldName, value is the message.
33
     *
34
     * @var array
35
     */
36
    protected $filedMessages = [];
37
38
    /**
39
     * Validator constructor.
40
     *
41
     * @param FailureCollection $failureCollection
42
     */
43 54
    public function __construct(
44
        FailureCollection $failureCollection
45
    ) {
46 54
        $this->failures = $failureCollection;
47 54
        $this->init();
48 54
    }
49
50
    /**
51
     * Asserts the validator, throws a ValidationFailureException if anything fails
52
     *
53
     * @param $subject
54
     *
55
     * @throws ValidationFailureException
56
     *
57
     * @return bool
58
     */
59
    public function __invoke(&$subject): bool
60
    {
61
        return $this->assert($subject);
62
    }
63
64
    /**
65
     * Hook function that can be implemented in an extended custom validator class.
66
     *
67
     * If a custom class extends Validator, this method is the hook to provide some default rule configuration.
68
     */
69 54
    protected function init(): void
70
    {
71
        //do nothing
72 54
    }
73
74
    /**
75
     * Returns the collection of validation failures.
76
     *
77
     * @return FailureCollection
78
     */
79 21
    public function getFailures(): FailureCollection
80
    {
81 21
        return $this->failures;
82
    }
83
84
    /**
85
     * @param string $fieldName
86
     * @param string $message
87
     *
88
     * @return Validator
89
     */
90 3
    public function setFieldMessage(string $fieldName, string $message): self
91
    {
92 3
        $this->filedMessages[$fieldName] = $message;
93
94 3
        return $this;
95
    }
96
97
    /**
98
     * Configure the validator to validate the given $field, with the given $rule.
99
     *
100
     * @param string $field
101
     *
102
     * @return ValidateSpec
103
     */
104 39
    public function validate(string $field): ValidateSpec
105
    {
106 39
        $spec = new ValidateSpec($field);
107 39
        $this->validateSpecs[] = $spec;
108
109 39
        return $spec;
110
    }
111
112
    /**
113
     * Configure the validator to sanitize the given $field, with the given $rule.
114
     *
115
     * @param string $field
116
     * @param string $ruleName
117
     * @param array $args
118
     *
119
     * @return SanitizeSpec
120
     */
121 18
    public function sanitize(string $field): SanitizeSpec
122
    {
123 18
        $spec = new SanitizeSpec($field);
124 18
        $this->sanitizeSpecs[] = $spec;
125
126 18
        return $spec;
127
    }
128
129
    /**
130
     * Applies the validator to the subject and throws an exception upon failure
131
     *
132
     * @param $subject
133
     *
134
     * @throws ValidationFailureException
135
     *
136
     * @return bool
137
     */
138
    public function assert(&$subject)
139
    {
140
        if ($this->apply($subject)) {
141
            return true;
142
        }
143
144
        $message = $this->failures->getMessagesAsString();
145
        $e = new ValidationFailureException($message);
146
        $e->setFailures($this->failures);
147
        $e->setSubject($subject);
148
149
        throw $e;
150
    }
151
152
    /**
153
     * Applies the configured validate and sanitize rules to the given $subject.
154
     *
155
     * @param array|object $subject
156
     *
157
     * @return bool
158
     */
159 51
    public function apply(&$subject): bool
160
    {
161 51
        if (is_array($subject)) {
162 6
            return $this->applyToArray($subject);
163
        }
164
165 45
        return $this->applyToObject($subject);
166
    }
167
168
    /**
169
     * Handles applying all rules to an array.
170
     *
171
     * The array gets type casted to an object then passed to `$this->applyToObject`. because this is all done by
172
     * reference, we can type cast it back to an array _after_ the `applyToObject()` call and return the $subject with
173
     * any values that many have been alerted by sanitize rules.
174
     *
175
     * @param array $subject
176
     *
177
     * @return bool
178
     */
179 6
    protected function applyToArray(array &$subject): bool
180
    {
181 6
        $object = (object) $subject;
182 6
        $result = $this->applyToObject($object);
183 6
        $subject = (array) $object;
184
185 6
        return $result;
186
    }
187
188
    /**
189
     * Applies all sanitize and validate specs to a given object.
190
     *
191
     * Sanitize specs run first
192
     *
193
     * @param $subject
194
     *
195
     * @return bool
196
     */
197 51
    protected function applyToObject($subject): bool
198
    {
199 51
        $continue = true;
200 51
        foreach ($this->sanitizeSpecs as $sanitizeSpec) {
201 18
            $continue = $this->applySpec($subject, $sanitizeSpec);
202 18
            if (!$continue) {
203 18
                break;
204
            }
205
        }
206
207 51
        if ($continue) {
208 48
            foreach ($this->validateSpecs as $validateSpec) {
209 36
                $continue = $this->applySpec($subject, $validateSpec);
210 36
                if (!$continue) {
211 36
                    break;
212
                }
213
            }
214
        }
215
216 51
        return $this->failures->isEmpty();
217
    }
218
219
    /**
220
     * Applies a given spec to the subject.
221
     *
222
     * If the spec returns false, this will log the error.
223
     *
224
     * @param object $subject
225
     * @param AbstractSpec $spec
226
     *
227
     * @return bool
228
     */
229 51
    protected function applySpec($subject, AbstractSpec $spec): bool
230
    {
231 51
        if (in_array($spec->getField(), $this->skip)) {
232 3
            return true;
233
        }
234
235 51
        if ($spec($subject)) {
236 27
            return true;
237
        }
238
239 24
        $this->failSpec($spec);
240
241 24
        if ($spec->getFailureMode() === $spec::HALTING_FAILURE) {
242 3
            return false;
243
        }
244
245 21
        return true;
246
    }
247
248
    /**
249
     * Handles failing a spec.
250
     *
251
     * If the spec is set to a HardFailure, add its field to the skip list.
252
     *
253
     * @param AbstractSpec $spec
254
     */
255 24
    protected function failSpec(AbstractSpec $spec): void
256
    {
257 24
        $field = $spec->getField();
258
259 24
        if ($spec->getFailureMode() === $spec::HARD_FAILURE) {
260 6
            $this->skip[] = $field;
261
        }
262
263 24
        if (isset($this->filedMessages[$spec->getField()])) {
264 3
            $this->failures->set($field, $this->filedMessages[$field]);
265
        } else {
266 21
            $this->failures->add($field, $spec->getMessage(), $spec->getRuleClass(), $spec->getArgs());
267
        }
268 24
    }
269
}
270