Passed
Pull Request — 4.x (#156)
by Hari
01:55
created

SubjectFilter   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 74
c 3
b 0
f 0
dl 0
loc 369
rs 10
wmc 27

15 Methods

Rating   Name   Duplication   Size   Complexity  
A sanitize() 0 3 1
A applyToArray() 0 6 1
A __invoke() 0 3 1
A apply() 0 13 3
A applySpec() 0 24 5
A addSpec() 0 5 1
A assert() 0 17 2
A __construct() 0 11 1
A init() 0 2 1
A applyToObject() 0 11 3
A validate() 0 3 1
A useFieldMessage() 0 3 1
A failed() 0 21 4
A getFailures() 0 3 1
A subFilter() 0 4 1
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 *
6
 * This file is part of Aura for PHP.
7
 *
8
 * @license http://opensource.org/licenses/mit-license.php MIT
9
 *
10
 */
11
namespace Aura\Filter;
12
13
use Aura\Filter\Exception\FilterFailed;
14
use Aura\Filter\Exception;
15
use Aura\Filter\Failure\Failure;
16
use Aura\Filter\Failure\FailureCollection;
17
use Aura\Filter\Spec\SanitizeSpec;
18
use Aura\Filter\Spec\Spec;
19
use Aura\Filter\Spec\ValidateSpec;
20
use Aura\Filter\Spec\SubSpecFactory;
21
use Aura\Filter\Spec\SubSpec;
22
use InvalidArgumentException;
23
24
/**
25
 *
26
 * A filter for an entire "subject" (i.e., an array or object).
27
 *
28
 * @package Aura.Filter
29
 *
30
 */
31
class SubjectFilter
32
{
33
    /**
34
     *
35
     * An array of specifications for the filter subject.
36
     *
37
     * @var array
38
     *
39
     */
40
    protected $specs = array();
41
42
    /**
43
     *
44
     * Skip these fields on the filter subject.
45
     *
46
     * @var array
47
     *
48
     */
49
    protected $skip = array();
50
51
    /**
52
     *
53
     * A collection of failure objects.
54
     *
55
     * @var FailureCollection
56
     *
57
     */
58
    protected $failures;
59
60
    /**
61
     *
62
     * Use these field-specific messages when a subject field fails.
63
     *
64
     * @var array
65
     *
66
     */
67
    protected $field_messages = array();
68
69
    /**
70
     *
71
     * A prototype ValidateSpec.
72
     *
73
     * @var ValidateSpec
74
     *
75
     */
76
    protected $validate_spec;
77
78
    /**
79
     *
80
     * A prototype SanitizeSpec.
81
     *
82
     * @var SanitizeSpec
83
     *
84
     */
85
    protected $sanitize_spec;
86
87
88
    /**
89
     * Factory for Sub subject specifications
90
     *
91
     * @var SubSpecFactory
92
     *
93
     * @access protected
94
     */
95
    protected $sub_spec_factory;
96
97
    /**
98
     *
99
     * A prototype FailureCollection.
100
     *
101
     * @var FailureCollection
102
     *
103
     */
104
    protected $proto_failures;
105
106
    /**
107
     *
108
     * Constructor.
109
     *
110
     * @param ValidateSpec $validate_spec A prototype ValidateSpec.
111
     *
112
     * @param ValidateSpec $sanitize_spec A prototype SanitizeSpec.
113
     *
114
     * @param SubSpecFactory $sub_spec_factory A factory for SubSpec
115
     *
116
     * @param FailureCollection $failures A prototype FailureCollection.
117
     */
118
    public function __construct(
119
        ValidateSpec $validate_spec,
120
        SanitizeSpec $sanitize_spec,
121
        SubSpecFactory $sub_spec_factory,
122
        FailureCollection $failures
123
    ) {
124
        $this->validate_spec = $validate_spec;
125
        $this->sanitize_spec = $sanitize_spec;
126
        $this->sub_spec_factory = $sub_spec_factory;
127
        $this->proto_failures = $failures;
128
        $this->init();
129
    }
130
131
    /**
132
     *
133
     * Initialization logic for this filter.
134
     *
135
     * @return null
136
     *
137
     */
138
    protected function init(): void
139
    {
140
        // do nothing
141
    }
142
143
    /**
144
     *
145
     * Asserts that the subject passes the filter.
146
     *
147
     * @param array|object $subject The subject to be filtered.
148
     *
149
     * @return null
150
     *
151
     * @throws Exception\FilterFailed when the assertion fails.
152
     *
153
     */
154
    public function __invoke(&$subject)
155
    {
156
        return $this->assert($subject);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->assert($subject) targeting Aura\Filter\SubjectFilter::assert() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug Best Practice introduced by
The expression return $this->assert($subject) returns the type void which is incompatible with the documented return type null.
Loading history...
157
    }
158
159
    /**
160
     *
161
     * Asserts that the subject passes the filter.
162
     *
163
     * @param array|object $subject The subject to be filtered.
164
     *
165
     * @return null
166
     *
167
     * @throws Exception\FilterFailed when the assertion fails.
168
     *
169
     */
170
    public function assert(&$subject): void
171
    {
172
        if ($this->apply($subject)) {
173
            return;
174
        }
175
176
        $class = get_class($this);
177
        $message = PHP_EOL
178
                 . "  Filter: {$class}" . PHP_EOL
179
                 . "  Fields:" . PHP_EOL
180
                 . $this->failures->getMessagesAsString('    ');
181
182
        $e = new FilterFailed($message);
183
        $e->setFilterClass($class);
184
        $e->setFailures($this->failures);
185
        $e->setSubject($subject);
186
        throw $e;
187
    }
188
189
    /**
190
     *
191
     * Adds a "validate" specification for a subject field.
192
     *
193
     * @param string $field The subject field name.
194
     *
195
     *
196
     */
197
    public function validate(string $field): Spec
198
    {
199
        return $this->addSpec(clone $this->validate_spec, $field);
200
    }
201
202
    /**
203
     *
204
     * Adds a "sanitize" specification for a subject field.
205
     *
206
     * @param string $field The subject field name.
207
     *
208
     *
209
     */
210
    public function sanitize(string $field): Spec
211
    {
212
        return $this->addSpec(clone $this->sanitize_spec, $field);
213
    }
214
215
    /**
216
     *
217
     * Adds a "subfilter" specification for a subject field.
218
     *
219
     * @param string $field The subject field name.
220
     *
221
     *
222
     */
223
    public function subFilter(string $field, $class = \Aura\Filter\SubjectFilter::class): Spec
224
    {
225
        $spec = $this->sub_spec_factory->newSubSpec($class);
226
        return $this->addSpec($spec, $field);
227
    }
228
229
    /**
230
     *
231
     * Adds a specification for a subject field.
232
     *
233
     * @param Spec $spec The specification object.
234
     *
235
     * @param string $field The subject field name.
236
     *
237
     *
238
     */
239
    protected function addSpec(Spec $spec, string $field): Spec
240
    {
241
        $this->specs[] = $spec;
242
        $spec->field($field);
243
        return $spec;
244
    }
245
246
    /**
247
     *
248
     * Specifies a custom message to use when a subject field fails.
249
     *
250
     * @param string $field The subject field name.
251
     *
252
     * @param string $message The failure message to use.
253
     *
254
     * @return null
255
     *
256
     */
257
    public function useFieldMessage(string $field, string $message): void
258
    {
259
        $this->field_messages[$field] = $message;
260
    }
261
262
    /**
263
     *
264
     * Applies the filter to a subject.
265
     *
266
     * @param array|object $subject The subject to be filtered.
267
     *
268
     * @return bool True on success, false on failure.
269
     *
270
     */
271
    public function apply(&$subject): bool
272
    {
273
        if (is_array($subject)) {
274
            return $this->applyToArray($subject);
275
        }
276
277
        if (! is_object($subject)) {
278
            $type = gettype($subject);
279
            $message = "Apply the filter to an array or object, not a {$type}.";
280
            throw new InvalidArgumentException($message);
281
        }
282
283
        return $this->applyToObject($subject);
284
    }
285
286
    /**
287
     *
288
     * Applies the rule specifications to an array.
289
     *
290
     * @param array $array The filter subject.
291
     *
292
     * @return bool True if all rules passed, false if not.
293
     *
294
     */
295
    protected function applyToArray(array &$array): bool
296
    {
297
        $object = (object) $array;
298
        $result = $this->applyToObject($object);
299
        $array = (array) $object;
300
        return $result;
301
    }
302
303
    /**
304
     *
305
     * Applies the rule specifications to an object.
306
     *
307
     *
308
     * @return bool True if all rules passed, false if not.
309
     *
310
     */
311
    protected function applyToObject(object $object): bool
312
    {
313
        $this->skip = array();
314
        $this->failures = clone $this->proto_failures;
315
        foreach ($this->specs as $spec) {
316
            $continue = $this->applySpec($spec, $object);
317
            if (! $continue) {
318
                break;
319
            }
320
        }
321
        return $this->failures->isEmpty();
322
    }
323
324
    /**
325
     *
326
     * Apply a rule specification to the subject.
327
     *
328
     * @param Spec $spec The rule specification.
329
     *
330
     *
331
     * @return bool True to continue, false to stop.
332
     *
333
     */
334
    protected function applySpec(Spec $spec, object $subject): bool
335
    {
336
        if (isset($this->skip[$spec->getField()])) {
337
338
            // Issue 140 . Some rule already failed for the field.
339
            // Check the current one have a stop rule or not.
340
            if ($spec->isStopRule()) {
341
                return false;
342
            }
343
344
            return true;
345
        }
346
347
        if (call_user_func($spec, $subject)) {
348
            return true;
349
        }
350
351
        $this->failed($spec);
352
353
        if ($spec->isStopRule()) {
354
            return false;
355
        }
356
357
        return true;
358
    }
359
360
    /**
361
     *
362
     * Adds a failure.
363
     *
364
     * @param Spec $spec The failed rule specification.
365
     *
366
     * @return void
367
     */
368
    protected function failed(Spec $spec)
369
    {
370
        $field = $spec->getField();
371
372
        if ($spec->isHardRule()) {
373
            $this->skip[$field] = true;
374
        }
375
376
        if ($spec instanceof SubSpec) {
377
            $this->failures->addSubfieldFailures($field, $spec);
378
379
            return;
380
        }
381
382
        if (isset($this->field_messages[$field])) {
383
            $this->failures->set($field, $this->field_messages[$field]);
384
385
            return;
386
        }
387
388
        $this->failures->add($field, $spec->getMessage(), $spec->getArgs());
389
    }
390
391
    /**
392
     *
393
     * Returns the failures.
394
     *
395
     *
396
     */
397
    public function getFailures(): FailureCollection
398
    {
399
        return $this->failures;
400
    }
401
}
402