Test Failed
Push — develop ( 962ae6...081043 )
by Paul
09:56
created

Form   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 445
Duplicated Lines 0 %

Test Coverage

Coverage 44.38%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 73
eloc 206
c 2
b 1
f 0
dl 0
loc 445
ccs 79
cts 178
cp 0.4438
rs 2.56

34 Methods

Rating   Name   Duplication   Size   Complexity  
A normalizeFieldId() 0 14 4
A hidden() 0 10 3
A configHidden() 0 3 1
A config() 0 3 1
A fieldClass() 0 3 1
A session() 0 3 1
B mergeConfig() 0 31 6
A buildSubmitButton() 0 17 2
A fieldsFor() 0 3 1
A fieldsAll() 0 5 1
A allowedHiddenFieldOverrides() 0 3 1
A fields() 0 3 1
A fieldsVisible() 0 14 4
A normalizeField() 0 6 1
A normalizeFieldValue() 0 11 3
A classAttrResponse() 0 11 2
A args() 0 3 1
A visible() 0 9 3
A formName() 0 5 1
A fieldsHidden() 0 14 4
A buildResponse() 0 11 1
A loadSession() 0 7 1
A classAttrSubmitButton() 0 5 1
A buildFields() 0 13 3
A normalizeFieldChecked() 0 13 4
A build() 0 11 1
A __construct() 0 16 2
A normalizeFieldErrors() 0 4 1
B normalizeConditions() 0 21 7
A app() 0 3 1
A signForm() 0 14 2
A classAttrForm() 0 12 2
A field() 0 6 1
A offsetGet() 0 13 4

How to fix   Complexity   

Complex Class

Complex classes like Form often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Form, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace GeminiLabs\SiteReviews\Modules\Html;
4
5
use GeminiLabs\SiteReviews\Arguments;
6
use GeminiLabs\SiteReviews\Contracts\FieldContract;
7
use GeminiLabs\SiteReviews\Contracts\FormContract;
8
use GeminiLabs\SiteReviews\Contracts\PluginContract;
9
use GeminiLabs\SiteReviews\Helpers\Arr;
10
use GeminiLabs\SiteReviews\Helpers\Cast;
11
use GeminiLabs\SiteReviews\Helpers\Str;
12
use GeminiLabs\SiteReviews\Modules\Captcha;
13
use GeminiLabs\SiteReviews\Modules\Encryption;
14
use GeminiLabs\SiteReviews\Modules\Honeypot;
15
use GeminiLabs\SiteReviews\Modules\Sanitizer;
16
use GeminiLabs\SiteReviews\Modules\Style;
17
18
class Form extends \ArrayObject implements FormContract
19
{
20
    public Arguments $args;
21 16
    public array $config;
22
    protected Arguments $session;
23 16
24 16
    /**
25 16
     * The $args are expected to be sanitized before being passed to the form.
26 16
     */
27 16
    public function __construct(array $args = [], array $values = [])
28 16
    {
29 16
        $args = wp_parse_args($args, [
30 16
            'button_text' => __('Submit Form', 'site-reviews'),
31 16
            'button_text_loading' => __('Submitting, please wait...', 'site-reviews'),
32
        ]);
33
        if (empty($args['id'])) {
34
            $args['id'] = glsr(Sanitizer::class, ['values' => $args])->sanitizeIdHash('');
35
        }
36
        $this->args = glsr()->args($args);
37
        $this->config = $this->mergeConfig();
38
        $this->loadSession($values);
39
        parent::__construct($this->fieldsAll(), \ArrayObject::STD_PROP_LIST | \ArrayObject::ARRAY_AS_PROPS);
40
        array_map([$this, 'normalizeConditions'], $this->fields());
41
        $this->app()->action("{$this->formName()}/form", $this);
42
        $this->signForm();
43
    }
44
45
    public function app(): PluginContract
46
    {
47
        return glsr();
48
    }
49
50
    public function args(): Arguments
51
    {
52
        return $this->args;
53
    }
54
55
    public function build(): string
56
    {
57
        return glsr(Template::class)->build('templates/reviews-form', [
58
            'args' => $this->args,
59
            'context' => [
60
                'class' => $this->classAttrForm(),
61
                'fields' => $this->buildFields(),
62
                'response' => $this->buildResponse(),
63
                'submit_button' => $this->buildSubmitButton(),
64
            ],
65
            'form' => $this,
66
        ]);
67
    }
68 16
69
    public function config(): array
70 16
    {
71
        return [];
72
    }
73
74
    public function configHidden(): array
75
    {
76
        return [];
77
    }
78
79
    public function field(string $name, array $args): FieldContract
80
    {
81
        $className = $this->fieldClass();
82
        $field = new $className(wp_parse_args($args, compact('name')));
83
        $this->normalizeField($field);
84
        return $field;
85
    }
86
87
    public function fieldClass(): string
88
    {
89
        return Field::class;
90
    }
91
92
    /**
93
     * @return FieldContract[]
94
     */
95
    public function fields(): array
96
    {
97
        return $this->getArrayCopy();
98
    }
99
100
    /**
101
     * @return FieldContract[]
102
     */
103
    public function fieldsFor(string $group): array
104
    {
105 2
        return array_filter($this->fields(), fn ($field) => $group === $field->group);
106
    }
107 2
108 2
    public function formName(): string
109
    {
110
        $formName = (new \ReflectionClass($this))->getShortName();
111 2
        $formName = Str::dashCase($formName);
112 2
        return $formName;
113 2
    }
114
115
    /**
116
     * @return FieldContract[]
117
     */
118
    public function hidden(): array
119
    {
120
        $fields = [];
121
        foreach ($this->fields() as $field) {
122
            if ('hidden' === $field->original_type) {
123
                $fields[] = $field;
124
            }
125
        }
126
        usort($fields, fn ($a, $b) => $a->original_name <=> $b->original_name);
127 8
        return $fields;
128
    }
129 8
130 8
    public function loadSession(array $values): void
131 8
    {
132 8
        $this->session = glsr()->args([
133
            'errors' => [],
134
            'failed' => false,
135 8
            'message' => '',
136
            'values' => $values,
137
        ]);
138
    }
139
140
    #[\ReturnTypeWillChange]
141
    public function offsetGet($key)
142
    {
143
        $iterator = $this->getIterator();
144
        if (is_numeric($key)) {
145
            return $iterator[$key] ?? null;
146
        }
147
        foreach ($iterator as $field) {
148
            if ($key === $field->original_name) {
149
                return $field;
150
            }
151
        }
152
        return null;
153
    }
154
155
    public function session(): Arguments
156
    {
157
        return $this->session;
158
    }
159
160
    /**
161
     * @return FieldContract[]
162
     */
163
    public function visible(): array
164
    {
165
        $fields = [];
166
        foreach ($this->fields() as $field) {
167
            if ('hidden' !== $field->original_type) {
168
                $fields[] = $field;
169
            }
170
        }
171
        return $fields;
172
    }
173
174
    /**
175
     * An array of field names that can be overridden in the hiddenConfig array
176
     * by fields with the same name in the config array.
177
     * 
178
     * @return string[]
179
     */
180
    protected function allowedHiddenFieldOverrides(): array
181
    {
182
        return [];
183
    }
184
185
    protected function buildFields(): string
186
    {
187
        $fields = [];
188
        foreach ($this->hidden() as $field) {
189
            $fields[] = $field->build();
190
        }
191
        $fields[] = glsr(Honeypot::class)->build($this->args->id);
192
        foreach ($this->visible() as $field) {
193
            $fields[] = $field->build();
194
        }
195
        $rendered = implode("\n", $fields);
196
        $rendered = $this->app()->filterString("{$this->formName()}/build/fields", $rendered, $this);
197
        return $rendered;
198
    }
199
200
    protected function buildResponse(): string
201
    {
202
        $rendered = glsr(Template::class)->build('templates/form/response', [
203
            'context' => [
204
                'class' => $this->classAttrResponse(),
205
                'message' => wpautop($this->session->message),
206
            ],
207
            'has_errors' => !empty($this->session->errors),
208
        ]);
209
        $rendered = $this->app()->filterString("{$this->formName()}/build/response", $rendered, $this);
210
        return $rendered;
211
    }
212
213
    protected function buildSubmitButton(): string
214
    {
215
        $captcha = glsr(Captcha::class)->container();
216
        $rendered = glsr(Template::class)->build('templates/form/submit-button', [
217
            'context' => [
218 16
                'class' => $this->classAttrSubmitButton(),
219
                'loading_text' => $this->args->button_text_loading,
220 16
                'text' => $this->args->button_text,
221 16
            ],
222 16
        ]);
223
        $rendered = $this->app()->filterString("{$this->formName()}/build/submit_button", $rendered, $this);
224
        if ('above' === glsr_get_option('forms.captcha.placement')) {
225
            $rendered = $captcha.$rendered;
226
        } else {
227
            $rendered = $rendered.$captcha;
228
        }
229
        return $rendered;
230
    }
231
232
    protected function classAttrForm(): string
233
    {
234
        $classes = [
235
            $this->args->class,
236 16
            glsr(Style::class)->classes('form'),
237
        ];
238 16
        if (!empty($this->session->errors)) {
239 16
            $classes[] = glsr(Style::class)->validation('form_error');
240 16
        }
241 16
        $classes = implode(' ', $classes);
242 16
        $classes = glsr(Sanitizer::class)->sanitizeAttrClass($classes);
243
        return $classes;
244
    }
245 16
246
    protected function classAttrResponse(): string
247
    {
248
        $classes = [
249
            glsr(Style::class)->validation('form_message'),
250
        ];
251 16
        if (!empty($this->session->errors)) {
252
            $classes[] = glsr(Style::class)->validation('form_message_failed');
253 16
        }
254
        $classes = implode(' ', $classes);
255
        $classes = glsr(Sanitizer::class)->sanitizeAttrClass($classes);
256 16
        return $classes;
257 16
    }
258 2
259
    protected function classAttrSubmitButton(): string
260
    {
261
        $classes = glsr(Style::class)->classes('button');
262 2
        $classes = glsr(Sanitizer::class)->sanitizeAttrClass($classes);
263
        return $classes;
264 16
    }
265 16
266 16
    /**
267
     * @return FieldContract[]
268 2
     */
269 1
    protected function fieldsAll(): array
270
    {
271 2
        $fields = array_values(array_merge($this->fieldsHidden(), $this->fieldsVisible()));
272
        $fields = $this->app()->filterArray("{$this->formName()}/fields/all", $fields, $this);
273
        return $fields;
274
    }
275
276
    /**
277
     * @return FieldContract[]
278
     */
279
    protected function fieldsHidden(): array
280 16
    {
281
        $fields = [];
282 16
        foreach ($this->config as $name => $args) {
283 16
            if ('hidden' !== $args['type']) {
284 16
                continue;
285 16
            }
286
            $field = $this->field($name, $args);
287
            if ($field->isValid()) {
288
                $fields[$name] = $field;
289
            }
290
        }
291 16
        $fields = $this->app()->filterArray("{$this->formName()}/fields/hidden", $fields, $this);
292
        return $fields;
293 16
    }
294 16
295
    /**
296 15
     * @return FieldContract[]
297 1
     */
298
    protected function fieldsVisible(): array
299 14
    {
300 14
        $fields = [];
301 14
        foreach ($this->config as $name => $args) {
302
            if ('hidden' === $args['type']) {
303 1
                continue;
304
            }
305
            $field = $this->field($name, $args);
306
            if ($field->isValid()) {
307
                $fields[$name] = $field;
308
            }
309 16
        }
310
        $fields = $this->app()->filterArray("{$this->formName()}/fields/visible", $fields, $this);
311 16
        return $fields;
312 16
    }
313
314
    protected function mergeConfig(): array
315
    {
316
        $config = $this->config();
317
        $config = $this->app()->filterArray("{$this->formName()}/config", $config, $this);
318 16
        if (!wp_is_numeric_array($config)) { // allow custom filtered field order
319
            $order = array_keys($config);
320 16
            $order = $this->app()->filterArray("{$this->formName()}/fields/order", $order, $this);
321
            $ordered = array_intersect_key(array_merge(array_flip($order), $config), $config);
322
            $config = $ordered;
323 16
        }
324 16
        array_walk($config, function (&$field, $key) {
325
            if (!is_numeric($key)) {
326 16
                // ensure all fields include a name value to allow the field check below.
327
                $field['name'] = $key;
328
            }
329 16
        });
330 16
        foreach ($this->configHidden() as $name => $value) {
331 16
            if (in_array($name, $this->allowedHiddenFieldOverrides())) {
332
                $search = Arr::searchByKey($name, $config, 'name');
333
                if (false !== $search) {
334
                    continue; // skip this hidden field
335
                }
336
            }
337 16
            $config[$name] = [
338
                'name' => $name,
339 16
                'type' => 'hidden',
340 15
                'value' => $value,
341
            ];
342 16
        }
343
        $config = array_filter($config, fn ($args) => !empty($args['type']));
344 16
        return $config;
345 16
    }
346
347 8
    /**
348
     * @todo Should this be done manually? This shouldn't be run when getting field rules for validation
349
     */
350
    protected function normalizeConditions(FieldContract $field): void
351
    {
352
        if (!$conditions = $field->conditions()) {
353
            return;
354
        }
355
        $results = [];
356
        foreach ($conditions['conditions'] as $args) {
357
            if (!$triggerField = $this->offsetGet($args['name'])) {
358
                $results[] = true; // ignore if the condition's field does not exist
359
                continue;
360
            }
361
            $results[] = (new FieldCondition($args, $triggerField))->isValid();
362
        }
363
        $results = array_filter($results);
364
        if (count($results) === count($conditions['conditions'])) {
365
            return; // all conditions valid
366
        }
367
        if ('any' === $conditions['criteria'] && !empty($results)) {
368
            return; // some conditions valid
369
        }
370
        $field->is_hidden = true;
371
        // reset value
372
    }
373
374
    /**
375
     * Normalize the field with the form's session data.
376
     * Any normalization that is not specific to the form or session data
377
     * should be done in the field itself.
378
     */
379
    protected function normalizeField(FieldContract $field): void
380
    {
381
        $this->normalizeFieldChecked($field);
382
        $this->normalizeFieldErrors($field);
383
        $this->normalizeFieldId($field);
384
        $this->normalizeFieldValue($field);
385
    }
386
387
    /**
388
     * Set the checked attribute of the field from the session.
389
     */
390
    protected function normalizeFieldChecked(FieldContract $field): void
391
    {
392
        if (!$field->isChoiceField()) {
393
            return;
394
        }
395
        if (!is_scalar($field->value)) {
396
            return;
397
        }
398
        $value = Cast::toString($this->session->values[$field->original_name] ?? '');
399
        if (empty($value)) {
400
            return;
401
        }
402
        $field->checked = (string) $field->value === $value;
403
    }
404
405
    /**
406
     * Set the field errors from the session.
407
     */
408
    protected function normalizeFieldErrors(FieldContract $field): void
409
    {
410
        $errors = $this->session->errors[$field->original_name] ?? [];
411
        $field->errors = Arr::consolidate($errors);
412
    }
413
414
    /**
415
     * Prefix the field id with the form id.
416
     */
417
    protected function normalizeFieldId(FieldContract $field): void
418
    {
419
        if (empty($this->args->id)) {
420
            return;
421
        }
422
        if (empty($field->id)) {
423
            return;
424
        }
425
        if ($field->is_raw) {
426
            return;
427
        }
428
        $fieldId = Str::removePrefix($field->id, $field->namePrefix());
429
        $fieldId = Str::prefix($fieldId, $this->args->id);
430
        $field->id = $fieldId;
431
    }
432
433
    /**
434
     * Set the value attribute of the field from the session.
435
     */
436
    protected function normalizeFieldValue(FieldContract $field): void
437
    {
438
        if ($field->isChoiceField()) {
439
            $value = Cast::toArray($this->session->values[$field->original_name] ?? []);
440
        } else {
441
            $value = Cast::toString($this->session->values[$field->original_name] ?? '');
442
        }
443
        if (empty($value)) {
444
            return;
445
        }
446
        $field->value = $value;
447
    }
448
449
    protected function signForm(): void
450
    {
451
        $hidden = $this->hidden();
452
        $values = [];
453
        foreach ($hidden as $field) {
454
            $values[$field->original_name] = $field->value;
455
        }
456
        $values = $this->app()->filterArray("{$this->formName()}/signature/values", $values, $this);
457
        $signatureField = $this->field('form_signature', [
458
            'type' => 'hidden',
459
            'value' => glsr(Encryption::class)->encrypt(maybe_serialize($values)),
460
        ]);
461
        $fields = array_values(array_merge($hidden, [$signatureField], $this->visible()));
462
        $this->exchangeArray($fields);
463
    }
464
}
465