Form::isValid()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 10
rs 9.2
cc 4
eloc 7
nc 4
nop 0
1
<?php
2
3
namespace fieldwork;
4
5
6
use fieldwork\components\Field;
7
use fieldwork\components\GroupComponent;
8
use fieldwork\methods\Method;
9
use fieldwork\methods\POST;
10
use fieldwork\validators\FormValidator;
11
use fieldwork\validators\SynchronizableFormValidator;
12
13
class Form extends GroupComponent implements FormData, Synchronizable
14
{
15
16
    const TARGET_SELF  = "_self";
17
    const TARGET_BLANK = "_blank";
18
19
    private
20
        $action,
21
        $method,
22
        $target,
23
        $validators = array(),
24
        $callback = array(),
25
        $activateCallbacks = array(),
26
        $javascriptCallback = array(),
27
28
        $forceSubmit = false,
29
        $dataFields,
30
31
        $isUserSubmitted = false,
32
        $isProcessed = false,
33
        $isCallbacksubmitted = false;
34
35
    /**
36
     * Instantiates a new Form
37
     *
38
     * @param string $slug
39
     * @param string $action
40
     * @param Method $method
41
     * @param string $target
42
     */
43
    public function __construct ($slug, $action = '', $method = null, $target = self::TARGET_SELF)
44
    {
45
        parent::__construct($slug);
46
        $this->action                                         = $action;
47
        $this->method                                         = ($method === null ? new POST() : $method);
48
        $this->target                                         = $target;
49
        $this->dataFields[$this->getSubmitConfirmFieldName()] = 'true';
50
        $this->register();
51
    }
52
53
    /**
54
     * Resets state and field values. DOES NOT remove validators and callbacks
55
     */
56
    public function reset ()
57
    {
58
        $this->isUserSubmitted     = false;
59
        $this->isProcessed         = false;
60
        $this->isCallbacksubmitted = false;
61
        parent::reset();
62
    }
63
64
    /**
65
     * Registers the form globally
66
     * @return Form this
67
     */
68
    private function register ()
69
    {
70
        FW::registerForm($this);
71
        return $this;
72
    }
73
74
    /**
75
     * Searches form fields
76
     *
77
     * @param string $query                 ID to search for
78
     * @param bool   $includeInactiveFields Whether to include inactive fields in the search
79
     *
80
     * @return null|Field closest match or null if not found
81
     */
82
    public function f ($query, $includeInactiveFields = false)
83
    {
84
        $minLength = -1;
85
        $match     = null;
86
        foreach ($this->getFields($includeInactiveFields) as $field)
87
            /* @var $field Field */
88
            if (preg_match("/^(.*)" . preg_quote($query) . "$/", $field->getGlobalSlug(), $matches)) {
89
                $l = strlen($matches[1]);
90
                if ($l < $minLength || $minLength == -1) {
91
                    $minLength = $l;
92
                    $match     = $field;
93
                }
94
            }
95
        return $match;
96
    }
97
98
    /**
99
     * Searches form fields and returns its value
100
     *
101
     * @param string $query   id to search for
102
     * @param string $default default value
103
     *
104
     * @return string value of closest match or default value if field not found
105
     */
106
    public function v ($query, $default = '')
107
    {
108
        $f = $this->f($query);
109
        if ($f !== null)
110
            return $f->getValue();
111
        else
112
            return $default;
113
    }
114
115
    /**
116
     * Returns an array of fields of which data is to be collected
117
     * @return Field[]
118
     */
119
    public function c ()
120
    {
121
        $fields = array();
122
        foreach ($this->getFields() as $field)
123
            /* @var $field Field */
124
            if ($field->getCollectData())
125
                $fields[] = $field;
126
        return $fields;
127
    }
128
129
    /**
130
     * Adds a new form-level validator
131
     *
132
     * @param FormValidator $validator validator
133
     * @param boolean       $unshift   Whether to add the validator to the front of the array
134
     *
135
     * @return static
136
     */
137
    public function addValidator (FormValidator $validator, $unshift = false)
138
    {
139
        if ($unshift)
140
            array_unshift($this->validators, $validator);
141
        else
142
            $this->validators[] = $validator;
143
        return $this;
144
    }
145
146
    public function getDataFieldName ($slug)
147
    {
148
        return $this->getGlobalSlug() . '-data-' . $slug;
149
    }
150
151
    public function getSubmitConfirmFieldName ()
152
    {
153
        return $this->getDataFieldName('submit');
154
    }
155
156
    /**
157
     * Finds a field by its localslug value
158
     *
159
     * @param $localSlug
160
     *
161
     * @return Field|null
162
     */
163
    public function getField ($localSlug)
164
    {
165
        foreach ($this->getFields() as $field)
166
            /* @var $field Field */
167
            if ($field->getLocalSlug() == $localSlug)
168
                return $field;
169
        return null;
170
    }
171
172
    public function getValue ($key, $default = '')
173
    {
174
        return $this->method->getValue($key, $default);
175
    }
176
177
    public function hasValue ($key)
178
    {
179
        return $this->method->hasValue($key);
180
    }
181
182
    /**
183
     * Attach a function that will be called upon submitting the form. The first argument passed to the function will
184
     * be an instance of FormData.
185
     *
186
     * @param callable $callback
187
     */
188
    public function attachCallback ($callback)
189
    {
190
        $this->callback[] = $callback;
191
    }
192
193
    public function attachActivateCallback ($callback)
194
    {
195
        $this->activateCallbacks[] = $callback;
196
    }
197
198
    public function attachJavascriptCallback ($callback)
199
    {
200
        $this->javascriptCallback[] = $callback;
201
    }
202
203
    public function getDataFields ()
204
    {
205
        return $this->dataFields;
206
    }
207
208
    public function getAction ()
209
    {
210
        return $this->action;
211
    }
212
213
    public function setAction ($action)
214
    {
215
        $this->action = $action;
216
    }
217
218
    public function getAttributes ()
219
    {
220
        return array_merge(parent::getAttributes(), $this->method->getFormAttributes(), array(
221
            'id'     => $this->getID(),
222
            'action' => $this->action,
223
            'target' => $this->target
224
        ));
225
    }
226
227
    public function getClasses ()
228
    {
229
        return array_merge(parent::getClasses(), array(
230
            'fieldwork-form'
231
        ));
232
    }
233
234
    /**
235
     * Gets the script that will instantiate the form
236
     * @return string
237
     */
238
    public function getScript ()
239
    {
240
        return "jQuery(function($){ $('#" . $this->getID() . "').fieldwork(" . json_encode($this->getJsonData(), JSON_PRETTY_PRINT) . "); });";
241
    }
242
243
    /**
244
     * Gets the script tag that will instantiate the form
245
     * @return string
246
     */
247
    public function getScriptHTML ()
248
    {
249
        $openingTag = "<script type='text/javascript'>";
250
        $closingTag = "</script>";
251
        return $openingTag . $this->getScript() . $closingTag;
252
    }
253
254
    static public function renderFormError ($errorMsg)
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
255
    {
256
        return sprintf("<div class=\"form-error card red white\"><div class=\"card-content red-text text-darken-4\">%s</div></div>", $errorMsg);
257
    }
258
259
    /**
260
     * Get all the error messages for form-wide validators that returned invalid
261
     *
262
     * @return string[]
263
     */
264
    public function getFormErrors ()
265
    {
266
        return array_map(function (FormValidator $formValidator) {
267
            return $formValidator->getErrorMsg();
268
        }, array_filter($this->validators, function (FormValidator $validator) {
269
            return !$validator->isValid();
270
        }));
271
    }
272
273
    /**
274
     * Gets the markup that is to be outputted before the actual contents of the form. This method could be used for
275
     * even more manual control over outputting with a custom markup.
276
     * @return string
277
     */
278
    public function getWrapBefore ()
279
    {
280
        $dataFields = $this->getDataFieldsHTML();
281
        $errors     = join(array_map(function ($errorMsg) {
282
            return Form::renderFormError($errorMsg);
283
        }, $this->getFormErrors()));
284
        return "<form " . $this->getAttributesString() . ">" . $errors . $dataFields;
285
    }
286
287
    /**
288
     * Gets the markup that is to be outputted after the actual contents of the form. This method could be used for
289
     * even more manual control over outputting with a custom markup.
290
     *
291
     * @param bool $includeScripts Whether to include the scripts
292
     *
293
     * @return string
294
     */
295
    public function getWrapAfter ($includeScripts = true)
296
    {
297
        return "</form>" . ($includeScripts ? $this->getScriptHTML() : '');
298
    }
299
300
    /**
301
     * Wraps the <pre>$content</pre> inside a form tag, declaring error messages, datafields and the script contents.
302
     * This method could be useful for using a form with custom custom markup.
303
     *
304
     * @param string $content        The form "content" to be wrapped. Should declare all the required input fields.
305
     * @param bool   $includeScripts Whether to include the scripts
306
     *
307
     * @return string The HTML content
308
     */
309
    public function wrap ($content, $includeScripts = true)
310
    {
311
        return $this->getWrapBefore() . $content . $this->getWrapAfter($includeScripts);
312
    }
313
314
    /**
315
     * Renders and returns complete form markup as HTML. Use this to echo the form using default markup to the webpage.
316
     *
317
     * @param bool $showLabel
318
     *
319
     * @return string
320
     */
321
    public function getHTML ($showLabel = true)
322
    {
323
        return $this->wrap($this->getInnerHTML());
324
    }
325
326
    public function getID ()
327
    {
328
        return "form-" . $this->getGlobalSlug();
329
    }
330
331
    /**
332
     * Retrieves an array of this form's fields
333
     *
334
     * @param boolean $includeInactiveFields whether to include inactive fields as well
335
     *
336
     * @return Field[]
337
     */
338
    public function getFields ($includeInactiveFields = false)
339
    {
340
        $fields = array();
341
        foreach ($this->getChildren(true, $includeInactiveFields) as $component)
342
            if ($component instanceof Field)
343
                array_push($fields, $component);
344
        return $fields;
345
    }
346
347
    public function getJsonData ()
348
    {
349
        $fields = array();
350
        foreach ($this->getFields() as $field)
351
            /* @var $field Field */
352
            $fields[$field->getID()] = $field->getJsonData();
353
        $liveValidators = array();
354
        foreach ($this->validators as $validator)
355
            /* @var $validator SynchronizableFormValidator */
356
            if ($validator->isLive())
357
                $liveValidators[] = $validator->getJsonData();
358
        return array(
359
            "slug"           => $this->getLocalSlug(),
360
            "fields"         => $fields,
361
            "liveValidators" => $liveValidators,
362
            "dataFields"     => $this->dataFields,
363
            "submitCallback" => $this->javascriptCallback,
364
            "isProcessed"    => $this->isProcessed,
365
            "isActivated"    => $this->isUserSubmitted,
366
            "isSubmitted"    => $this->isCallbacksubmitted
367
        );
368
    }
369
370
    public function activateHandlers ()
371
    {
372
        foreach ($this->activateCallbacks as $psCallback)
373
            if (is_callable($psCallback))
374
                call_user_func($psCallback, $this);
375
    }
376
377
    /**
378
     * Will attempt to submit the form upon calling the process function, even if the user has not activated it
379
     */
380
    public function forceSubmit ()
381
    {
382
        $this->forceSubmit = true;
383
    }
384
385
    /**
386
     * Submits the form internally. You're not usually supposed to call this function directly.
387
     */
388
    private function submit ()
389
    {
390
        foreach ($this->getFields() as $field)
391
            /* @var $field Field */
392
            $field->submit();
393
        $this->isCallbacksubmitted = true;
394
        foreach ($this->callback as $callback)
395
            if (is_callable($callback))
396
                call_user_func($callback, $this);
397
    }
398
399
    public function isValid ()
400
    {
401
        if (!parent::isValid())
402
            return false;
403
        foreach ($this->validators as $validator)
404
            /* @var $validator FormValidator */
405
            if (!$validator->process($this))
406
                return false;
407
        return true;
408
    }
409
410
    public function getErrorMessages ()
411
    {
412
        $e = array();
413
        foreach ($this->validators as $validator)
414
            /* @var $validator FormValidator */
415
            if (!$validator->isValid())
416
                $e[] = $validator->getErrorMsg();
417
        return $e;
418
    }
419
420
    /**
421
     * Check if form was activated, then validates, calls handlers, then submits. Call this function before displaying
422
     * the form.
423
     */
424
    public function process ()
425
    {
426
        if ($this->isProcessed)
427
            return;
428
        $this->isProcessed = true;
429
        foreach ($this->getFields() as $field)
430
            /* @var $field Field */
431
            $field->preprocess();
432
        $this->isUserSubmitted = $this->getValue($this->getSubmitConfirmFieldName(), 'false') == 'true' || $this->forceSubmit;
433
        if ($this->isUserSubmitted) {
434
            foreach ($this->getFields() as $field)
435
                $field->restoreValue($this->method);
436
437
            $this->activateHandlers();
438
439
            if ($this->isValid()) {
440
                $this->submit();
441
            }
442
        }
443
    }
444
445
    /**
446
     * Checks whether the form has been processed
447
     * @return boolean
448
     */
449
    public function isProcessed ()
450
    {
451
        return $this->isProcessed;
452
    }
453
454
    /**
455
     * Indicates whether the user has tried to submit the form
456
     * @return boolean
457
     */
458
    public function isRequested ()
459
    {
460
        return $this->isUserSubmitted;
461
    }
462
463
    /**
464
     * Indicates whether the form was validated correctly
465
     * @return boolean
466
     */
467
    public function isSubmitted ()
468
    {
469
        return $this->isCallbacksubmitted;
470
    }
471
472
    /**
473
     * Returns a short human-readable slug/string describing the object
474
     * @return string
475
     */
476
    function describeObject ()
477
    {
478
        return "form";
479
    }
480
481
    /**
482
     * Gets a complete associated array containing all the data that needs to be stored
483
     *
484
     * @param bool $useName           Whether to use the field name (if not, the fields shorter local slug is used)
485
     * @param bool $includeDataFields Whether to include the data fields
486
     *
487
     * @return array
488
     */
489
    function getValues ($useName = true, $includeDataFields = true)
490
    {
491
        $values = array();
492
        foreach ($this->getFields() as $field)
493
            /* @var $field Field */
494
            if ($field->getCollectData()) {
495
                $key          = $useName ? $field->getName() : $field->getLocalSlug();
496
                $values[$key] = $field->getValue();
497
            }
498
        if ($includeDataFields)
499
            $values = array_merge($values, $this->dataFields);
500
        return $values;
501
    }
502
503
    /**
504
     * Restores the values from an associated array. Only defined properties will be overwritten
505
     *
506
     * @param array $values
507
     */
508
    function setValues (array $values = array())
509
    {
510
        foreach ($this->getFields() as $field)
511
            if (array_key_exists($field->getName(), $values))
512
                $field->setValue($values[$field->getName()]);
513
        foreach ($this->dataFields as $dataKey => $dataVal)
514
            if (array_key_exists($dataKey, $values))
515
                $this->dataFields[$dataKey] = $values[$dataKey];
516
    }
517
518
    /**
519
     * @return string
520
     */
521
    protected function getDataFieldsHTML ()
522
    {
523
        $dataFields = '';
524
        foreach ($this->dataFields as $key => $value)
525
            $dataFields .= "<input type=\"hidden\" name=\"$key\" value=\"$value\">";
526
        return $dataFields;
527
    }
528
529
    /**
530
     * @return string
531
     */
532
    public function getInnerHTML ()
533
    {
534
        return parent::getHTML();
535
    }
536
}
537