Completed
Push — master ( c61969...df1bf5 )
by Marcus
02:35
created

Validator::assert()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 13
c 0
b 0
f 0
ccs 0
cts 8
cp 0
rs 9.4285
cc 2
eloc 8
nc 2
nop 1
crap 6
1
<?php
2
3
namespace Mbright\Validation;
4
5
use Mbright\Validation\Exception\ValidationException;
6
use Mbright\Validation\Exception\ValidationFailureException;
7
use Mbright\Validation\Spec\AbstractSpec;
8
use Mbright\Validation\Spec\SanitizeSpec;
9
use Mbright\Validation\Spec\ValidateSpec;
10
use Mbright\Validation\Failure\FailureCollection;
11
12
class Validator
13
{
14
    /** @var FailureCollection */
15
    protected $failures;
16
17
    /** @var ValidateSpec[] */
18
    protected $validateSpecs = [];
19
20
    /** @var SanitizeSpec[] */
21
    protected $sanitizeSpecs = [];
22
23
    /**
24
     * Fields to skip during validation.
25
     *
26
     * @var array
27
     */
28
    protected $skip = [];
29
30
    /**
31
     * Messages to use for a field.
32
     *
33
     * Index is the fieldName, value is the message.
34
     *
35
     * @var array
36
     */
37
    protected $filedMessages = [];
38
39
    /**
40
     * Validator constructor.
41
     *
42
     * @param FailureCollection $failureCollection
43
     */
44 54
    public function __construct(
45
        FailureCollection $failureCollection
46
    ) {
47 54
        $this->failures = $failureCollection;
48 54
        $this->init();
49 54
    }
50
51
    /**
52
     * Asserts the validator, throws a ValidationFailureException if anything fails
53
     *
54
     * @param $subject
55
     *
56
     * @throws ValidationFailureException
57
     *
58
     * @return bool
59
     */
60
    public function __invoke(&$subject): bool
61
    {
62
        return $this->assert($subject);
63
    }
64
65
    /**
66
     * Hook function that can be implemented in an extended custom validator class.
67
     *
68
     * If a custom class extends Validator, this method is the hook to provide some default rule configuration.
69
     */
70 54
    protected function init(): void
71
    {
72
        //do nothing
73 54
    }
74
75
    /**
76
     * Returns the collection of validation failures.
77
     *
78
     * @return FailureCollection
79
     */
80 21
    public function getFailures(): FailureCollection
81
    {
82 21
        return $this->failures;
83
    }
84
85
    /**
86
     * @param string $fieldName
87
     * @param string $message
88
     *
89
     * @return Validator
90
     */
91 3
    public function setFieldMessage(string $fieldName, string $message): self
92
    {
93 3
        $this->filedMessages[$fieldName] = $message;
94
95 3
        return $this;
96
    }
97
98
    /**
99
     * Configure the validator to validate the given $field, with the given $rule.
100
     *
101
     * @param string $field
102
     *
103
     * @return ValidateSpec
104
     */
105 39
    public function validate(string $field): ValidateSpec
106
    {
107 39
        $spec = new ValidateSpec($field);
108 39
        $this->validateSpecs[] = $spec;
109
110 39
        return $spec;
111
    }
112
113
    /**
114
     * Configure the validator to sanitize the given $field, with the given $rule.
115
     *
116
     * @param string $field
117
     * @param string $ruleName
0 ignored issues
show
Bug introduced by
There is no parameter named $ruleName. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
118
     * @param array $args
0 ignored issues
show
Bug introduced by
There is no parameter named $args. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
119
     *
120
     * @return SanitizeSpec
121
     */
122 18
    public function sanitize(string $field): SanitizeSpec
123
    {
124 18
        $spec = new SanitizeSpec($field);
125 18
        $this->sanitizeSpecs[] = $spec;
126
127 18
        return $spec;
128
    }
129
130
    /**
131
     * Applies the validator to the subject and throws an exception upon failure
132
     *
133
     * @param $subject
134
     *
135
     * @throws ValidationFailureException
136
     *
137
     * @return bool
138
     */
139
    public function assert(&$subject)
140
    {
141
        if ($this->apply($subject)) {
142
            return true;
143
        }
144
145
        $message = $this->failures->getMessagesAsString();
146
        $e = new ValidationFailureException($message);
147
        $e->setFailures($this->failures);
148
        $e->setSubject($subject);
149
150
        throw $e;
151
    }
152
153
    /**
154
     * Applies the configured validate and sanitize rules to the given $subject.
155
     *
156
     * @param array|object $subject
157
     *
158
     * @return bool
159
     */
160 51
    public function apply(&$subject): bool
161
    {
162 51
        if (is_array($subject)) {
163 6
            return $this->applyToArray($subject);
164
        }
165
166 45
        return $this->applyToObject($subject);
167
    }
168
169
    /**
170
     * Handles applying all rules to an array.
171
     *
172
     * The array gets type casted to an object then passed to `$this->applyToObject`. because this is all done by
173
     * reference, we can type cast it back to an array _after_ the `applyToObject()` call and return the $subject with
174
     * any values that many have been alerted by sanitize rules.
175
     *
176
     * @param array $subject
177
     *
178
     * @return bool
179
     */
180 6
    protected function applyToArray(array &$subject): bool
181
    {
182 6
        $object = (object) $subject;
183 6
        $result = $this->applyToObject($object);
184 6
        $subject = (array) $object;
185
186 6
        return $result;
187
    }
188
189
    /**
190
     * Applies all sanitize and validate specs to a given object.
191
     *
192
     * Sanitize specs run first
193
     *
194
     * @param $subject
195
     *
196
     * @return bool
197
     */
198 51
    protected function applyToObject($subject): bool
199
    {
200 51
        $continue = true;
201 51
        foreach ($this->sanitizeSpecs as $sanitizeSpec) {
202 18
            $continue = $this->applySpec($subject, $sanitizeSpec);
203 18
            if (!$continue) {
204 18
                break;
205
            }
206
        }
207
208 51
        if ($continue) {
209 48
            foreach ($this->validateSpecs as $validateSpec) {
210 36
                $continue = $this->applySpec($subject, $validateSpec);
211 36
                if (!$continue) {
212 36
                    break;
213
                }
214
            }
215
        }
216
217 51
        return $this->failures->isEmpty();
218
    }
219
220
    /**
221
     * Applies a given spec to the subject.
222
     *
223
     * If the spec returns false, this will log the error.
224
     *
225
     * @param object $subject
226
     * @param AbstractSpec $spec
227
     *
228
     * @return bool
229
     */
230 51
    protected function applySpec($subject, AbstractSpec $spec): bool
231
    {
232 51
        if (in_array($spec->getField(), $this->skip)) {
233 3
            return true;
234
        }
235
236 51
        if ($spec($subject)) {
237 27
            return true;
238
        }
239
240 24
        $this->failSpec($spec);
241
242 24
        if ($spec->getFailureMode() === $spec::HALTING_FAILURE) {
243 3
            return false;
244
        }
245
246 21
        return true;
247
    }
248
249
    /**
250
     * Handles failing a spec.
251
     *
252
     * If the spec is set to a HardFailure, add its field to the skip list.
253
     *
254
     * @param AbstractSpec $spec
255
     */
256 24
    protected function failSpec(AbstractSpec $spec): void
257
    {
258 24
        $field = $spec->getField();
259
260 24
        if ($spec->getFailureMode() === $spec::HARD_FAILURE) {
261 6
            $this->skip[] = $field;
262
        }
263
264 24
        if (isset($this->filedMessages[$spec->getField()])) {
265 3
            $this->failures->set($field, $this->filedMessages[$field]);
266
        } else {
267 21
            $this->failures->add($field, $spec->getMessage(), $spec->getArgs());
268
        }
269 24
    }
270
}
271