Form   F
last analyzed

Complexity

Total Complexity 74

Size/Duplication

Total Lines 448
Duplicated Lines 0 %

Test Coverage

Coverage 44.38%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 74
eloc 208
c 2
b 1
f 0
dl 0
loc 448
ccs 79
cts 178
cp 0.4438
rs 2.48

34 Methods

Rating   Name   Duplication   Size   Complexity  
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
A buildSubmitButton() 0 17 2
A fieldsFor() 0 3 1
A allowedHiddenFieldOverrides() 0 3 1
A fields() 0 3 1
A args() 0 3 1
A visible() 0 9 3
A formName() 0 5 1
A buildResponse() 0 11 1
A loadSession() 0 7 1
A buildFields() 0 13 3
A build() 0 11 1
A __construct() 0 16 2
A app() 0 3 1
A field() 0 6 1
A offsetGet() 0 13 4
A normalizeFieldId() 0 14 4
B mergeConfig() 0 31 6
A fieldsAll() 0 5 1
A fieldsVisible() 0 14 4
A normalizeField() 0 6 1
A normalizeFieldValue() 0 11 3
A classAttrResponse() 0 11 2
A fieldsHidden() 0 14 4
A classAttrSubmitButton() 0 5 1
A normalizeFieldChecked() 0 13 4
A normalizeFieldErrors() 0 4 1
B normalizeConditions() 0 21 7
A signForm() 0 14 2
A classAttrForm() 0 15 3

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
        if (empty($args['id'])) {
30 16
            $args['id'] = glsr(Sanitizer::class)->sanitizeIdUnique('');
31 16
        }
32
        $args = wp_parse_args($args, [
33
            'button_text' => __('Submit Form', 'site-reviews'),
34
            'button_text_loading' => __('Submitting, please wait...', 'site-reviews'),
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
        if (glsr_get_option('settings.forms.session_storage', false, 'bool')) {
242 16
            $classes[] = 'glsr-persist-data';
243
        }
244
        $classes = implode(' ', $classes);
245 16
        $classes = glsr(Sanitizer::class)->sanitizeAttrClass($classes);
246
        return $classes;
247
    }
248
249
    protected function classAttrResponse(): string
250
    {
251 16
        $classes = [
252
            glsr(Style::class)->validation('form_message'),
253 16
        ];
254
        if (!empty($this->session->errors)) {
255
            $classes[] = glsr(Style::class)->validation('form_message_failed');
256 16
        }
257 16
        $classes = implode(' ', $classes);
258 2
        $classes = glsr(Sanitizer::class)->sanitizeAttrClass($classes);
259
        return $classes;
260
    }
261
262 2
    protected function classAttrSubmitButton(): string
263
    {
264 16
        $classes = glsr(Style::class)->classes('button');
265 16
        $classes = glsr(Sanitizer::class)->sanitizeAttrClass($classes);
266 16
        return $classes;
267
    }
268 2
269 1
    /**
270
     * @return FieldContract[]
271 2
     */
272
    protected function fieldsAll(): array
273
    {
274
        $fields = array_values(array_merge($this->fieldsHidden(), $this->fieldsVisible()));
275
        $fields = $this->app()->filterArray("{$this->formName()}/fields/all", $fields, $this);
276
        return $fields;
277
    }
278
279
    /**
280 16
     * @return FieldContract[]
281
     */
282 16
    protected function fieldsHidden(): array
283 16
    {
284 16
        $fields = [];
285 16
        foreach ($this->config as $name => $args) {
286
            if ('hidden' !== $args['type']) {
287
                continue;
288
            }
289
            $field = $this->field($name, $args);
290
            if ($field->isValid()) {
291 16
                $fields[$name] = $field;
292
            }
293 16
        }
294 16
        $fields = $this->app()->filterArray("{$this->formName()}/fields/hidden", $fields, $this);
295
        return $fields;
296 15
    }
297 1
298
    /**
299 14
     * @return FieldContract[]
300 14
     */
301 14
    protected function fieldsVisible(): array
302
    {
303 1
        $fields = [];
304
        foreach ($this->config as $name => $args) {
305
            if ('hidden' === $args['type']) {
306
                continue;
307
            }
308
            $field = $this->field($name, $args);
309 16
            if ($field->isValid()) {
310
                $fields[$name] = $field;
311 16
            }
312 16
        }
313
        $fields = $this->app()->filterArray("{$this->formName()}/fields/visible", $fields, $this);
314
        return $fields;
315
    }
316
317
    protected function mergeConfig(): array
318 16
    {
319
        $config = $this->config();
320 16
        $config = $this->app()->filterArray("{$this->formName()}/config", $config, $this);
321
        if (!wp_is_numeric_array($config)) { // allow custom filtered field order
322
            $order = array_keys($config);
323 16
            $order = $this->app()->filterArray("{$this->formName()}/fields/order", $order, $this);
324 16
            $ordered = array_intersect_key(array_merge(array_flip($order), $config), $config);
325
            $config = $ordered;
326 16
        }
327
        array_walk($config, function (&$field, $key) {
328
            if (!is_numeric($key)) {
329 16
                // ensure all fields include a name value to allow the field check below.
330 16
                $field['name'] = $key;
331 16
            }
332
        });
333
        foreach ($this->configHidden() as $name => $value) {
334
            if (in_array($name, $this->allowedHiddenFieldOverrides())) {
335
                $search = Arr::searchByKey($name, $config, 'name');
336
                if (false !== $search) {
337 16
                    continue; // skip this hidden field
338
                }
339 16
            }
340 15
            $config[$name] = [
341
                'name' => $name,
342 16
                'type' => 'hidden',
343
                'value' => $value,
344 16
            ];
345 16
        }
346
        $config = array_filter($config, fn ($args) => !empty($args['type']));
347 8
        return $config;
348
    }
349
350
    /**
351
     * @todo Should this be done manually? This shouldn't be run when getting field rules for validation
352
     */
353
    protected function normalizeConditions(FieldContract $field): void
354
    {
355
        if (!$conditions = $field->conditions()) {
356
            return;
357
        }
358
        $results = [];
359
        foreach ($conditions['conditions'] as $args) {
360
            if (!$triggerField = $this->offsetGet($args['name'])) {
361
                $results[] = true; // ignore if the condition's field does not exist
362
                continue;
363
            }
364
            $results[] = (new FieldCondition($args, $triggerField))->isValid();
365
        }
366
        $results = array_filter($results);
367
        if (count($results) === count($conditions['conditions'])) {
368
            return; // all conditions valid
369
        }
370
        if ('any' === $conditions['criteria'] && !empty($results)) {
371
            return; // some conditions valid
372
        }
373
        $field->is_hidden = true;
374
        // reset value
375
    }
376
377
    /**
378
     * Normalize the field with the form's session data.
379
     * Any normalization that is not specific to the form or session data
380
     * should be done in the field itself.
381
     */
382
    protected function normalizeField(FieldContract $field): void
383
    {
384
        $this->normalizeFieldChecked($field);
385
        $this->normalizeFieldErrors($field);
386
        $this->normalizeFieldId($field);
387
        $this->normalizeFieldValue($field);
388
    }
389
390
    /**
391
     * Set the checked attribute of the field from the session.
392
     */
393
    protected function normalizeFieldChecked(FieldContract $field): void
394
    {
395
        if (!$field->isChoiceField()) {
396
            return;
397
        }
398
        if (!is_scalar($field->value)) {
399
            return;
400
        }
401
        $value = Cast::toString($this->session->values[$field->original_name] ?? '');
402
        if (empty($value)) {
403
            return;
404
        }
405
        $field->checked = (string) $field->value === $value;
406
    }
407
408
    /**
409
     * Set the field errors from the session.
410
     */
411
    protected function normalizeFieldErrors(FieldContract $field): void
412
    {
413
        $errors = $this->session->errors[$field->original_name] ?? [];
414
        $field->errors = Arr::consolidate($errors);
415
    }
416
417
    /**
418
     * Prefix the field id with the form id.
419
     */
420
    protected function normalizeFieldId(FieldContract $field): void
421
    {
422
        if (empty($this->args->id)) {
423
            return;
424
        }
425
        if (empty($field->id)) {
426
            return;
427
        }
428
        if ($field->is_raw) {
429
            return;
430
        }
431
        $fieldId = Str::removePrefix($field->id, $field->namePrefix());
432
        $fieldId = Str::prefix($fieldId, $this->args->id);
433
        $field->id = $fieldId;
434
    }
435
436
    /**
437
     * Set the value attribute of the field from the session.
438
     */
439
    protected function normalizeFieldValue(FieldContract $field): void
440
    {
441
        if ($field->isChoiceField()) {
442
            $value = Cast::toArray($this->session->values[$field->original_name] ?? []);
443
        } else {
444
            $value = Cast::toString($this->session->values[$field->original_name] ?? '');
445
        }
446
        if (empty($value)) {
447
            return;
448
        }
449
        $field->value = $value;
450
    }
451
452
    protected function signForm(): void
453
    {
454
        $hidden = $this->hidden();
455
        $values = [];
456
        foreach ($hidden as $field) {
457
            $values[$field->original_name] = $field->value;
458
        }
459
        $values = $this->app()->filterArray("{$this->formName()}/signature/values", $values, $this);
460
        $signatureField = $this->field('form_signature', [
461
            'type' => 'hidden',
462
            'value' => glsr(Encryption::class)->encrypt(maybe_serialize($values)),
463
        ]);
464
        $fields = array_values(array_merge($hidden, [$signatureField], $this->visible()));
465
        $this->exchangeArray($fields);
466
    }
467
}
468