Completed
Push — master ( bf3aca...17c735 )
by JM
02:31
created

Form::getFormErrors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 1
eloc 5
nc 1
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
    public function getFormErrors ()
263
    {
264
        return array_map(function (FormValidator $formValidator) {
265
            return $formValidator->getErrorMsg();
266
        }, array_filter($this->validators, function (FormValidator $validator) {
267
            return !$validator->isValid();
268
        }));
269
    }
270
271
    /**
272
     * Gets the markup that is to be outputted before the actual contents of the form. This method could be used for
273
     * even more manual control over outputting with a custom markup.
274
     * @return string
275
     */
276
    public function getWrapBefore ()
277
    {
278
        $dataFields = $this->getDataFieldsHTML();
279
        $errors     = join(array_map(function ($errorMsg) {
280
            return Form::renderFormError($errorMsg);
281
        }, $this->getFormErrors()));
282
        return "<form " . $this->getAttributesString() . ">" . $errors . $dataFields;
283
    }
284
285
    /**
286
     * Gets the markup that is to be outputted after the actual contents of the form. This method could be used for
287
     * even more manual control over outputting with a custom markup.
288
     *
289
     * @param bool $includeScripts Whether to include the scripts
290
     *
291
     * @return string
292
     */
293
    public function getWrapAfter ($includeScripts = true)
294
    {
295
        return "</form>" . ($includeScripts ? $this->getScriptHTML() : '');
296
    }
297
298
    /**
299
     * Wraps the <pre>$content</pre> inside a form tag, declaring error messages, datafields and the script contents.
300
     * This method could be useful for using a form with custom custom markup.
301
     *
302
     * @param string $content        The form "content" to be wrapped. Should declare all the required input fields.
303
     * @param bool   $includeScripts Whether to include the scripts
304
     *
305
     * @return string The HTML content
306
     */
307
    public function wrap ($content, $includeScripts = true)
308
    {
309
        return $this->getWrapBefore() . $content . $this->getWrapAfter($includeScripts);
310
    }
311
312
    /**
313
     * Renders and returns complete form markup as HTML. Use this to echo the form using default markup to the webpage.
314
     *
315
     * @param bool $showLabel
316
     *
317
     * @return string
318
     */
319
    public function getHTML ($showLabel = true)
320
    {
321
        return $this->wrap($this->getInnerHTML());
322
    }
323
324
    public function getID ()
325
    {
326
        return "form-" . $this->getGlobalSlug();
327
    }
328
329
    /**
330
     * Retrieves an array of this form's fields
331
     *
332
     * @param boolean $includeInactiveFields whether to include inactive fields as well
333
     *
334
     * @return Field[]
335
     */
336
    public function getFields ($includeInactiveFields = false)
337
    {
338
        $fields = array();
339
        foreach ($this->getChildren(true, $includeInactiveFields) as $component)
340
            if ($component instanceof Field)
341
                array_push($fields, $component);
342
        return $fields;
343
    }
344
345
    public function getJsonData ()
346
    {
347
        $fields = array();
348
        foreach ($this->getFields() as $field)
349
            /* @var $field Field */
350
            $fields[$field->getID()] = $field->getJsonData();
351
        $liveValidators = array();
352
        foreach ($this->validators as $validator)
353
            /* @var $validator SynchronizableFormValidator */
354
            if ($validator->isLive())
355
                $liveValidators[] = $validator->getJsonData();
356
        return array(
357
            "slug"           => $this->getLocalSlug(),
358
            "fields"         => $fields,
359
            "liveValidators" => $liveValidators,
360
            "dataFields"     => $this->dataFields,
361
            "submitCallback" => $this->javascriptCallback,
362
            "isProcessed"    => $this->isProcessed,
363
            "isActivated"    => $this->isUserSubmitted,
364
            "isSubmitted"    => $this->isCallbacksubmitted
365
        );
366
    }
367
368
    public function activateHandlers ()
369
    {
370
        foreach ($this->activateCallbacks as $psCallback)
371
            if (is_callable($psCallback))
372
                call_user_func($psCallback, $this);
373
    }
374
375
    /**
376
     * Will attempt to submit the form upon calling the process function, even if the user has not activated it
377
     */
378
    public function forceSubmit ()
379
    {
380
        $this->forceSubmit = true;
381
    }
382
383
    /**
384
     * Submits the form internally. You're not usually supposed to call this function directly.
385
     */
386
    private function submit ()
387
    {
388
        foreach ($this->getFields() as $field)
389
            /* @var $field Field */
390
            $field->submit();
391
        $this->isCallbacksubmitted = true;
392
        foreach ($this->callback as $callback)
393
            if (is_callable($callback))
394
                call_user_func($callback, $this);
395
    }
396
397
    public function isValid ()
398
    {
399
        if (!parent::isValid())
400
            return false;
401
        foreach ($this->validators as $validator)
402
            /* @var $validator FormValidator */
403
            if (!$validator->process($this))
404
                return false;
405
        return true;
406
    }
407
408
    public function getErrorMessages ()
409
    {
410
        $e = array();
411
        foreach ($this->validators as $validator)
412
            /* @var $validator FormValidator */
413
            if (!$validator->isValid())
414
                $e[] = $validator->getErrorMsg();
415
        return $e;
416
    }
417
418
    /**
419
     * Check if form was activated, then validates, calls handlers, then submits. Call this function before displaying
420
     * the form.
421
     */
422
    public function process ()
423
    {
424
        if ($this->isProcessed)
425
            return;
426
        $this->isProcessed = true;
427
        foreach ($this->getFields() as $field)
428
            /* @var $field Field */
429
            $field->preprocess();
430
        $this->isUserSubmitted = $this->getValue($this->getSubmitConfirmFieldName(), 'false') == 'true' || $this->forceSubmit;
431
        if ($this->isUserSubmitted) {
432
            foreach ($this->getFields() as $field)
433
                $field->restoreValue($this->method);
434
435
            $this->activateHandlers();
436
437
            if ($this->isValid()) {
438
                $this->submit();
439
            }
440
        }
441
    }
442
443
    /**
444
     * Checks whether the form has been processed
445
     * @return boolean
446
     */
447
    public function isProcessed ()
448
    {
449
        return $this->isProcessed;
450
    }
451
452
    /**
453
     * Indicates whether the user has tried to submit the form
454
     * @return boolean
455
     */
456
    public function isRequested ()
457
    {
458
        return $this->isUserSubmitted;
459
    }
460
461
    /**
462
     * Indicates whether the form was validated correctly
463
     * @return boolean
464
     */
465
    public function isSubmitted ()
466
    {
467
        return $this->isCallbacksubmitted;
468
    }
469
470
    /**
471
     * Returns a short human-readable slug/string describing the object
472
     * @return string
473
     */
474
    function describeObject ()
475
    {
476
        return "form";
477
    }
478
479
    /**
480
     * Gets a complete associated array containing all the data that needs to be stored
481
     *
482
     * @param bool $useName           Whether to use the field name (if not, the fields shorter local slug is used)
483
     * @param bool $includeDataFields Whether to include the data fields
484
     *
485
     * @return array
486
     */
487
    function getValues ($useName = true, $includeDataFields = true)
488
    {
489
        $values = array();
490
        foreach ($this->getFields() as $field)
491
            /* @var $field Field */
492
            if ($field->getCollectData()) {
493
                $key          = $useName ? $field->getName() : $field->getLocalSlug();
494
                $values[$key] = $field->getValue();
495
            }
496
        if ($includeDataFields)
497
            $values = array_merge($values, $this->dataFields);
498
        return $values;
499
    }
500
501
    /**
502
     * Restores the values from an associated array. Only defined properties will be overwritten
503
     *
504
     * @param array $values
505
     */
506
    function setValues (array $values = array())
507
    {
508
        foreach ($this->getFields() as $field)
509
            if (array_key_exists($field->getName(), $values))
510
                $field->setValue($values[$field->getName()]);
511
        foreach ($this->dataFields as $dataKey => $dataVal)
512
            if (array_key_exists($dataKey, $values))
513
                $this->dataFields[$dataKey] = $values[$dataKey];
514
    }
515
516
    /**
517
     * @return string
518
     */
519
    protected function getDataFieldsHTML ()
520
    {
521
        $dataFields = '';
522
        foreach ($this->dataFields as $key => $value)
523
            $dataFields .= "<input type=\"hidden\" name=\"$key\" value=\"$value\">";
524
        return $dataFields;
525
    }
526
527
    /**
528
     * @return string
529
     */
530
    public function getInnerHTML ()
531
    {
532
        return parent::getHTML();
533
    }
534
}
535