Form   F
last analyzed

Complexity

Total Complexity 74

Size/Duplication

Total Lines 448
Duplicated Lines 0 %

Test Coverage

Coverage 91.13%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 74
eloc 208
dl 0
loc 448
ccs 226
cts 248
cp 0.9113
rs 2.48
c 2
b 1
f 0

34 Methods

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