Completed
Pull Request — develop (#720)
by Imants
03:58
created

Form_Basic::addField()   F

Complexity

Conditions 15
Paths 1728

Size

Total Lines 84
Code Lines 55

Duplication

Lines 10
Ratio 11.9 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 15
c 4
b 1
f 0
dl 10
loc 84
rs 2
eloc 55
nc 1728
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This class implements generic form, which you can actually use without
4
 * redeclaring it. Just add fields, buttons and use execute method.
5
 *
6
 * @author      Romans <[email protected]>
7
 * @copyright   See file COPYING
8
 *
9
 * @version     $Id$
10
 */
11
class Form_Basic extends View implements ArrayAccess
12
{
13
    /**
14
     * Template of layout if form has one.
15
     *
16
     * @var View
17
     */
18
    public $layout = null;
19
20
    /**
21
     * Here we will have a list of errors occured in the form, when we tried to
22
     * submit it. field_name => error
23
     *
24
     * @var array
25
     */
26
    public $errors = array();
27
28
    /**
29
     * Those templates will be used when rendering form and fields.
30
     *
31
     * @var array
32
     */
33
    public $template_chunks = array();
34
35
    /**
36
     * This array holds list of values prepared for fields before their
37
     * initialization. When fields are initialized they will look into this
38
     * array to see if there are default value for them.
39
     * Afterwards fields will link to $this->data, so changing
40
     * $this->data['fld_name'] would actually affect field's value.
41
     * You should use $this->set() and $this->get() to read/write individual
42
     * field values. You should use $this->setStaticSource() to load values from
43
     * hash, BUT - AAAAAAAAAA: this array is no more!!!
44
     *
45
     * @var array
46
     */
47
    public $data = array();
48
49
    /**
50
     * If this is true, we won't load data or submit or validate anything.
51
     *
52
     * @var null|bool
53
     */
54
    public $bail_out = null;
55
56
    /**
57
     * true - update() will try updating existing row. false - it would insert new.
58
     *
59
     * @var bool
60
     */
61
    protected $loaded_from_db = false;
62
63
    /**
64
     * Contains AJAX instances assigned to buttons.
65
     *
66
     * @var array
67
     */
68
    protected $ajax_submits = array();
69
70
    /**
71
     * If condition was passed to a form through GET, contains a GET field name.
72
     *
73
     * @var null|string
74
     */
75
    protected $get_field = null;
76
77
    /** @var array */
78
    protected $conditions = array();
79
80
    /** @var string JS widget name */
81
    public $js_widget = 'ui.atk4_form';
82
83
    /** @var array JS widget options */
84
    public $js_widget_arguments = array();
85
86
    /** @var string Class name of default exception */
87
    public $default_exception = 'Exception_ValidityCheck';
88
89
    /** @var string Class name of default controller */
90
    public $default_controller = 'Controller_MVCForm';
91
92
    /** @var Controller_Validator Validator object */
93
    public $validator = null;
94
95
    /**
96
     * Normally form fields are inserted using a form template. If you.
97
     * @todo check this - looks unused
98
     */
99
    public $search_for_field_spots;
100
101
    // {{{ Inherited properties
102
103
    /** @var App_Web */
104
    public $app;
105
106
    /** @var View */
107
    public $owner;
108
109
    // }}}
110
111
    public function init()
112
    {
113
        /*
114
         * During form initialization it will go through it's own template and
115
         * search for lots of small template chunks it will be using. If those
116
         * chunk won't be in template, it will fall back to default values.
117
         * This way you can re-define how form will look, but only what you need
118
         * in particular case. If you don't specify template at all, form will
119
         * work with default look.
120
         */
121
        parent::init();
122
123
        $this->getChunks();
124
125
        // After init method have been executed, it's safe for you to add
126
        // controls on the form. BTW, if you want to have default values such as
127
        // loaded from the table, then intialize $this->data array to default
128
        // values of those fields.
129
        $this->app->addHook('pre-exec', array($this, 'loadData'));
130
        $this->app->addHook('pre-render-output', array($this, 'lateSubmit'));
131
        $this->app->addHook('submitted', array($this, 'submitted'));
132
133
        $this->addHook('afterAdd', array($this, 'afterAdd'));
134
    }
135
    public function afterAdd($p, $c)
136
    {
137
        if ($c instanceof AbstractView) {
138
            $c->addHook('afterAdd', array($this, 'afterAdd'));
139
            $c->addMethod('addField', array($this, 'addField'));
140
            $c->addMethod('addSubmit', array($this, 'addSubmit'));
141
        }
142
    }
143
144
    protected function getChunks()
145
    {
146
        // commonly replaceable chunks
147
        $this->grabTemplateChunk('form_comment');
148
        $this->grabTemplateChunk('form_separator');
149
        $this->grabTemplateChunk('form_line'); // form line template,must contain field_caption,field_input,field_error
150
        if ($this->template->is_set('hidden_form_line')) {
151
            $this->grabTemplateChunk('hidden_form_line');
152
        }
153
        $this->grabTemplateChunk('field_error');    // template for error code, must contain field_error_str
154
        $this->grabTemplateChunk('field_mandatory');// template for marking mandatory fields
155
156
        // other grabbing will be done by field themselves as you will add them
157
        // to the form. They will try to look into this template, and if you
158
        // don't have apropriate templates for them, they will use default ones.
159
        $this->template_chunks['form'] = $this->template;
160
        $this->template_chunks['form']->del('Content');
161
        $this->template_chunks['form']->del('form_buttons');
162
        $this->template_chunks['form']->trySet('form_name', $this->name.'_form');
163
        $this->template_chunks['form']
164
            ->set('form_action', $this->app->url(null, array('submit' => $this->name)));
165
166
        return $this;
167
    }
168
169
    public function defaultTemplate($template = null, $tag = null)
170
    {
171
        return array('form');
172
    }
173
    public function grabTemplateChunk($name)
174
    {
175
        if ($this->template->is_set($name)) {
176
            $this->template_chunks[$name] = $this->template->cloneRegion($name);
177
        } else {
178
            unset($this->template_chunks[$name]);
179
            //return $this->fatal('missing form tag: '.$name);
180
            // hmm.. i wonder what ? :)
181
        }
182
    }
183
    /**
184
     * Should show error in field. Override this method to change form default alert.
185
     *
186
     * @param object $field Field instance that caused error
187
     * @param string $msg   message to show
188
     */
189
    public function showAjaxError($field, $msg)
190
    {
191
        // Avoid deprecated function use in reference field, line 246
192
        return $this->displayError($field, $msg);
193
    }
194
195
    public function displayError($field = null, $msg = null)
196
    {
197
        if (!$field) {
198
            // Field is not defined
199
            // TODO: add support for error in template
200
            $this->js()->univ()->alert($msg ?: 'Error in form')->execute();
201
        }
202
        if (!is_object($field)) {
203
            $field = $this->getElement($field);
204
        }
205
206
        $fn = $this->js_widget ? str_replace('ui.', '', $this->js_widget) : 'atk4_form';
207
        $this->js()->$fn('fieldError', $field->short_name, $msg)->execute();
208
    }
209
210
    /**
211
     * Adds error message to form field.
212
     *
213
     * @param string $field
214
     * @param string $text
215
     */
216
    public function error($field, $text = null)
217
    {
218
        /** @type Form_Field $form_field */
219
        $form_field = $this->getElement($field);
220
        $form_field->displayFieldError($text);
221
    }
222
223
    /**
224
     * Adds field in form.
225
     *
226
     * @param AbstractView|array|string $type
227
     * @param AbstractView|array|string $options
228
     * @param string $caption
229
     * @param string $attr Deprecated argument
230
     *
231
     * @return Form_Field
232
     */
233
    public function addField($type, $options = null, $caption = null, $attr = null)
234
    {
235
        $insert_into = $this->layout ?: $this;
236
237
        if (is_object($type) && $type instanceof AbstractView && !($type instanceof Form_Field)) {
238
239
            // using callback on a sub-view
240
            $insert_into = $type;
241
            list(, $type, $options, $caption, $attr) = func_get_args();
242
        }
243
244
        if ($options === null) {
245
            $options = $type;
246
            $type = 'Line';
247
        }
248
249 View Code Duplication
        if (is_array($options)) {
250
            $name = isset($options['name']) ? $options['name'] : null;
251
        } else {
252
            $name = $options; // backward compatibility
253
        }
254
        $name = preg_replace('|[^a-z0-9-_]|i', '_', $name);
255
256
        if ($caption === null) {
257
            $caption = ucwords(str_replace('_', ' ', $name));
258
        }
259
260
        /* normalzie name and put name back in options array */
261
        $name = $this->app->normalizeName($name);
262 View Code Duplication
        if (is_array($options)) {
263
            $options['name'] = $name;
264
        } else {
265
            $options = array('name' => $name);
266
        }
267
268
        $map = array(
269
            'dropdown' => 'DropDown',
270
            'checkboxlist' => 'CheckboxList',
271
            'hidden' => 'Hidden',
272
            'text' => 'Text',
273
            'line' => 'Line',
274
            'upload' => 'Upload',
275
            'radio' => 'Radio',
276
            'checkbox' => 'Checkbox',
277
            'password' => 'Password',
278
            'timepicker' => 'TimePicker',
279
            );
280
        $key = strtolower($type);
281
        $class = array_key_exists($key, $map) ? $map[$key] : $type;
282
283
        $class = $this->app->normalizeClassName($class, 'Form_Field');
284
285
        if ($insert_into === $this) {
286
            $template = $this->template->cloneRegion('form_line');
287
            $field = $this->add($class, $options, null, $template);
288
        } else {
289
            if ($insert_into->template->hasTag($name)) {
290
                $this->template->cloneRegion('field_input');
291
                $options['show_input_only'] = true;
292
                $field = $insert_into->add($class, $options, $name);
293
            } else {
294
                $template = $this->template->cloneRegion('form_line');
295
                $field = $insert_into->add($class, $options, null, $template);
296
            }
297
298
            // Keep Reference, for $form->getElement().
299
            $this->elements[$options['name']] = $field;
300
        }
301
        /** @type Form_Field $field */
302
303
        $field->setCaption($caption);
304
        $field->setForm($this);
305
        $field->template->trySet('field_type', strtolower($type));
306
307
        if ($attr) {
308
            if ($this->app->compat_42) {
309
                $field->setAttr($attr);
310
            } else {
311
                throw $this->exception('4th argument to addField is obsolete');
312
            }
313
        }
314
315
        return $field;
316
    }
317
318
    /**
319
     * Imports model fields in form by using form controller.
320
     *
321
     * @param Model $model
322
     * @param array|string|bool $fields
323
     */
324
    public function importFields($model, $fields = UNDEFINED)
325
    {
326
        /** @type Controller_MVCForm $c */
327
        $c = $this->add($this->default_controller);
328
        $c->importFields($model, $fields);
329
    }
330
331
    public function addSeparator($class = '', $attr = array())
332
    {
333
        if (!isset($this->template_chunks['form_separator'])) {
334
            /** @type View $v */
335
            $v = $this->add('View');
336
            return $v->addClass($class);
337
        }
338
        $c = clone $this->template_chunks['form_separator'];
339
        $c->trySet('fieldset_class', 'atk-cell '.$class);
340
        $this->template->trySet('fieldset_class', 'atk-cell');
341
        $this->template->trySet('form_class', 'atk-cells atk-cells-gutter-large');
342
343
        if (is_array($attr) && !empty($attr)) {
344
            foreach ($attr as $k => $v) {
345
                $c->appendHTML('fieldset_attributes', ' '.$k.'="'.$v.'"');
346
            }
347
        }
348
349
        /** @type Html $h */
350
        $h = $this->add('Html');
351
        return $h->set($c->render());
352
    }
353
354
    // Operating with field values
355
    //
356
    // {{{ ArrayAccess support
357
    public function offsetExists($name)
358
    {
359
        $f = $this->hasElement($name);
360
        return  $f && $f instanceof Form_Field;
361
    }
362
    public function offsetGet($name)
363
    {
364
        return $this->get($name);
365
    }
366
    public function offsetSet($name, $val)
367
    {
368
        $this->set($name, $val);
369
    }
370
    public function offsetUnset($name)
371
    {
372
        $this->set($name, null);
373
    }
374
    // }}}
375
    //
376
    public function get($field = null)
377
    {
378
        if (!$field) {
379
            return $this->data;
380
        }
381
382
        return $this->data[$field];
383
    }
384
    /*
385
     * temporarily disabled. TODO: will be implemented with abstract datatype
386
    function setSource($table,$db_fields=null){
387
        if(is_null($db_fields)){
388
            $db_fields=array();
389
            foreach($this->elements as $key=>$el){
390
                if(!($el instanceof Form_Field))continue;
391
                if($el->no_save)continue;
392
                $db_fields[]=$key;
393
            }
394
        }
395
        $this->dq = $this->app->db->dsql()
396
            ->table($table)
397
            ->field('*',$table)
398
            ->limit(1);
399
        return $this;
400
    }
401
     */
402
    public function set($field_or_array, $value = UNDEFINED)
403
    {
404
        // We use UNDEFINED, because 2nd argument of "null" is meaningfull
405
        if (is_array($field_or_array)) {
406
            foreach ($field_or_array as $key => $val) {
407
                if (isset($this->elements[$key]) && ($this->elements[$key] instanceof Form_Field)) {
408
                    $this->set($key, $val);
409
                }
410
            }
411
412
            return $this;
413
        }
414
415
        if (!isset($this->elements[$field_or_array])) {
416
            foreach ($this->elements as $key => $val) {
417
                echo "$key<br />";
418
            }
419
            throw new BaseException("Trying to set value for non-existant field $field_or_array");
420
        }
421
        if ($this->elements[$field_or_array] instanceof Form_Field) {
422
            $this->elements[$field_or_array]->set($value);
423
        } else {
424
            //throw new BaseException("Form fields must inherit from Form_Field ($field_or_array)");
425
            null;
426
        }
427
428
        return $this;
429
    }
430
    public function getAllFields()
431
    {
432
        return $this->get();
433
    }
434
    public function addSubmit($label = 'Save', $name = null)
435
    {
436
        if (is_object($label) && $label instanceof AbstractView && !($label instanceof Form_Field)) {
437
            // using callback on a sub-view
438
            $insert_into = $label;
439
            list(, $label, $name) = func_get_args();
440
            $submit = $insert_into->add('Form_Submit', array('name' => $name, 'form' => $this));
441
        } else {
442
            if ($this->layout && $this->layout->template->hasTag('FormButtons')) {
443
                $submit = $this->layout->add('Form_Submit', array('name' => $name, 'form' => $this), 'FormButtons');
444
            } else {
445
                $submit = $this->add('Form_Submit', array('name' => $name, 'form' => $this), 'form_buttons');
446
            }
447
        }
448
449
        $submit
450
            //->setIcon('ok') - removed as per dmity's request
451
            ->set($label)
452
            ->setNoSave();
453
454
        return $submit;
455
    }
456
    public function addButton($label = 'Button', $name = null)
457
    {
458
        if ($this->layout && $this->layout->template->hasTag('FormButtons')) {
459
            $button = $this->layout->add('Button', $name, 'FormButtons');
460
        } else {
461
            $button = $this->add('Button', $name, 'form_buttons');
462
        }
463
        /** @type Button $button */
464
        $button->setLabel($label);
465
466
        return $button;
467
    }
468
469
    public function loadData()
470
    {
471
        /**
472
         * This call will be sent to fields, and they will initialize their values from $this->data.
473
         */
474
        if (!is_null($this->bail_out)) {
475
            return;
476
        }
477
        $this->hook('post-loadData');
478
    }
479
480
    public function isLoadedFromDB()
481
    {
482
        return $this->loaded_from_db;
483
    }
484
    /* obsolete in 4.3 - use save() */
485
    public function update()
486
    {
487
        return $this->save();
488
    }
489
    public function save()
490
    {
491
        // TODO: start transaction here
492
        try {
493
            if ($this->hook('update')) {
494
                return $this;
495
            }
496
497
            if (!($m = $this->getModel())) {
498
                throw new BaseException("Can't save, model not specified");
499
            }
500
            if (!is_null($this->get_field)) {
501
                $this->app->stickyForget($this->get_field);
502
            }
503
            foreach ($this->elements as $short_name => $element) {
504
                if ($element instanceof Form_Field) {
505
                    if (!$element->no_save) {
506
                        //if(is_null($element->get()))
507
                        $m->set($short_name, $element->get());
508
                    }
509
                }
510
            }
511
            $m->save();
512
        } catch (BaseException $e) {
513 View Code Duplication
            if ($e instanceof Exception_ValidityCheck) {
514
                $f = $e->getField();
515
                if ($f && is_string($f) && $fld = $this->hasElement($f)) {
516
                    /** @type Form_Field $fld */
517
                    $fld->displayFieldError($e->getMessage());
518
                } else {
519
                    $this->js()->univ()->alert($e->getMessage())->execute();
520
                }
521
            }
522
            if ($e instanceof Exception_ForUser) {
523
                $this->js()->univ()->alert($e->getMessage())->execute();
524
            }
525
            throw $e;
526
        }
527
    }
528
    public function submitted()
529
    {
530
        /*
531
         * Default down-call submitted will automatically call this method if form was submitted
532
         */
533
        // We want to give flexibility to our controls and grant them a chance
534
        // to hook to those spots here.
535
        // On Windows platform mod_rewrite is lowercasing all the urls.
536
        if ($_GET['submit'] != $this->name) {
537
            return;
538
        }
539
        if (!is_null($this->bail_out)) {
540
            return $this->bail_out;
541
        }
542
543
        $this->hook('loadPOST');
544
        try {
545
            $this->hook('validate');
546
            $this->hook('post-validate');
547
548
            if (!empty($this->errors)) {
549
                return false;
550
            }
551
552
            if (($output = $this->hook('submit', array($this)))) {
553
                $has_output = false; // @todo all this [if] block logic should be re-checked, looks suspicious
554
                /* checking if anything usefull in output */
555
                if (is_array($output)) {
556
                    $has_output = false;
557
                    foreach ($output as $row) {
558
                        if ($row) {
559
                            $has_output = true;
560
                            $output = $row;
561
                            break;
562
                        }
563
                    }
564
                    if (!$has_output) {
565
                        return true;
566
                    }
567
                }
568
                /* TODO: need logic re-check here + test scripts */
569
                //if(!is_array($output))$output=array($output);
570
                // already array
571
                if ($has_output) {
572
                    if ($output instanceof jQuery_Chain) {
573
                        $this->js(null, $output)->execute();
574
                    } elseif (is_string($output)) {
575
                        $this->js(null, $this->js()->reload())->univ()->successMessage($output)->execute();
576
                    }
577
                }
578
            }
579
        } catch (BaseException $e) {
580 View Code Duplication
            if ($e instanceof Exception_ValidityCheck) {
581
                $f = $e->getField();
582
                if ($f && is_string($f) && $fld = $this->hasElement($f)) {
583
                    /** @type Form_Field $fld */
584
                    $fld->displayFieldError($e->getMessage());
585
                } else {
586
                    $this->js()->univ()->alert($e->getMessage())->execute();
587
                }
588
            }
589
            if ($e instanceof Exception_ForUser) {
590
                $this->js()->univ()->alert($e->getMessage())->execute();
591
            }
592
            throw $e;
593
        }
594
595
        return true;
596
    }
597
    public function lateSubmit()
598
    {
599
        if (@$_GET['submit'] != $this->name) {
600
            return;
601
        }
602
603
        if ($this->bail_out === null || $this->isSubmitted()) {
604
            $this->js()->univ()
605
                ->consoleError('Form '.$this->name.' submission is not handled.'.
606
                    ' See: http://agiletoolkit.org/doc/form/submit')
607
                ->execute();
608
        }
609
    }
610
    public function isSubmitted()
611
    {
612
        // This is alternative way for form submission. After form is initialized
613
        // you can call this method. It will hurry up all the steps, but you will
614
        // have ready-to-use form right away and can make submission handlers
615
        // easier
616
        if ($this->bail_out !== null) {
617
            return $this->bail_out;
618
        }
619
620
        $this->loadData();
621
        $result = $_POST && $this->submitted();
622
        $this->bail_out = $result;
623
624
        return $result;
625
    }
626
    public function onSubmit($callback)
627
    {
628
        $this->addHook('submit', $callback);
629
        $this->isSubmitted();
630
    }
631
    public function setLayout($template)
632
    {
633
        if (!$template instanceof AbstractView) {
634
            if (is_string($template)) {
635
                $template = $this->add('View', null, null, array($template));
636
            } else {
637
                $template = $this->add('View', null, null, $template);
638
            }
639
        }
640
641
        $this->layout = $template;
642
643
        return $this;
644
    }
645 View Code Duplication
    public function render()
646
    {
647
        // Assuming, that child fields already inserted their HTML code into 'form'/Content using 'form_line'
648
        // Assuming, that child buttons already inserted their HTML code into 'form'/form_buttons
649
650
        if ($this->js_widget) {
651
            $fn = str_replace('ui.', '', $this->js_widget);
652
            $this->js(true)->_load($this->js_widget)->$fn($this->js_widget_arguments);
653
        }
654
655
        return parent::render();
656
    }
657
    /**
658
     * @deprecated 4.3.2 use getElement() instead
659
     */
660
    public function hasField($name)
661
    {
662
        if (!@$this->app->compat_42) {
663
            throw $this->exception('Use $form->hasElement instead', '_Obsolete');
664
        }
665
666
        return isset($this->elements[$name]) ? $this->elements[$name] : false;
667
    }
668
    public function isClicked($name)
669
    {
670
        if (is_object($name)) {
671
            $name = $name->short_name;
672
        }
673
674
        return $_POST['ajax_submit'] == $name || isset($_POST[$this->name.'_'.$name]);
675
    }
676
    /* external error management */
677
    public function setFieldError($field, $name)
678
    {
679
        if (!$this->app->compat_42) {
680
            throw $this->exception('4.3', '_Obsolete');
681
        }
682
        $this->errors[$field] = (isset($this->errors[$field]) ? $this->errors[$field] : '').$name;
683
    }
684
685
    public function validate($rule)
686
    {
687
        if (!$this->validator) {
688
            $this->validator = $this->add('Controller_Validator');
689
            /** @type Controller_Validator $this->validator */
690
            $this->validator->on('post-validate');
691
        }
692
        $this->validator->is($rule);
693
    }
694
695
    /**
696
     * @deprecated 4.3.2 Will be removed in 4.4
697
     */
698
    public function addClass($class)
699
    {
700
        if ($class == 'stacked' || $class == 'atk-form-stacked') {
701
            // there are no longer stacked forms, instead a separate template must be used
702
            $this->template->loadTemplate('form/stacked');
703
            $this->getChunks();
704
            $this->template->trySet('_name', $this->getJSID());
705
706
            return $this;
707
        } else {
708
            return parent::addClass($class);
709
        }
710
    }
711
}
712