Completed
Push — master ( 3e9600...6e89de )
by Mihail
05:41
created

Form::submitButton()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 2 Features 0
Metric Value
c 4
b 2
f 0
dl 0
loc 7
rs 9.4286
cc 1
eloc 5
nc 1
nop 2
1
<?php
2
3
namespace Ffcms\Core\Helper\HTML;
4
5
use Ffcms\Core\App;
6
use Ffcms\Core\Exception\SyntaxException;
7
use Ffcms\Core\Helper\HTML\System\NativeGenerator;
8
use Ffcms\Core\Helper\Type\Arr;
9
use Ffcms\Core\Helper\FileSystem\File;
10
use Ffcms\Core\Helper\Type\Obj;
11
use Ffcms\Core\Helper\Type\Str;
12
use Ffcms\Core\Arch\Model;
13
14
class Form extends NativeGenerator
15
{
16
    protected $structure = '<div class="form-group"><label for="%name%" class="col-md-3 control-label">%label%</label><div class="col-md-9">%item% <p class="help-block">%help%</p></div></div>';
17
    protected $structureCheckbox = '<div class="form-group"><div class="col-md-9 col-md-offset-3"><div class="checkbox"><label>%item% %label%</label></div><p class="help-block">%help%</p></div></div>';
18
    protected $structureCheckboxes = '<div class="checkbox"><label>%item%</label></div>';
19
    protected $structureJSError = '$("#%itemId%").parent().parent(".form-group").addClass("has-error")';
20
    protected $name;
21
    protected $formProperty = [];
22
    /** @var Model */
23
    protected $model;
24
25
26
    /**
27
     * Build form based on model properties
28
     * @param Model $model
29
     * @param array $property
30
     * @param array $structure
31
     * @throws SyntaxException
32
     */
33
    public function __construct($model, array $property = null, array $structure = null)
34
    {
35
        // prevent white-screen locks when model is not passed or passed wrong
36
        if (!$model instanceof Model) {
37
            throw new SyntaxException('Bad model type passed in form builder. Check for init: new Form()');
38
        }
39
40
        $this->model = $model;
41
        $this->name = $model->getFormName();
42
43
        // set custom html build structure form fields
44
        if (Obj::isArray($structure)) {
45
            if (isset($structure['base']) && !Str::likeEmpty($structure['base'])) {
46
                $this->structure = $structure['base'];
47
            }
48
            if (isset($structure['checkbox']) && !Str::likeEmpty($structure['checkbox'])) {
49
                $this->structureCheckbox = $structure['checkbox'];
50
            }
51
            if (isset($structure['checkboxes']) && !Str::likeEmpty($structure['checkboxes'])) {
52
                $this->structureCheckboxes = $structure['checkboxes'];
53
            }
54
            if (isset($structure['jserror']) && !Str::likeEmpty($structure['jserror'])) {
55
                $this->structureJSError = $structure['jserror'];
56
            }
57
        }
58
59
        $property['method'] = $this->model->getSubmitMethod();
60
61
        $property['id'] = $this->name; // define form id
62
        // if action is not defined - define it
63
        if (Str::likeEmpty($property['action'])) {
64
            $property['action'] = App::$Request->getFullUrl();
65
        }
66
67
        // set property global for this form
68
        $this->formProperty = $property;
69
    }
70
71
    /**
72
     * Open form tag with prepared properties
73
     * @return string
74
     */
75
    public function start()
76
    {
77
        return '<form' . self::applyProperty($this->formProperty) . '>';
78
    }
79
80
    /**
81
     * Display form field. Allowed type: text, password, textarea, checkbox, select, checkboxes, file, captcha, email, hidden
82
     * @param $object
83
     * @param $type
84
     * @param null|array $property
85
     * @param null|string $helper
86
     * @param null|string $structure
87
     * @return null|string
88
     */
89
    public function field($object, $type, $property = null, $helper = null, $structure = null)
90
    {
91
        if ($this->model === null) {
92
            if (App::$Debug !== null) {
93
                App::$Debug->addMessage('Form model is not defined for field name: ' . strip_tags($object));
94
            }
95
            return null;
96
        }
97
98
        // can be dots separated object
99
        $propertyName = $object;
100
        if (Str::contains('.', $propertyName)) {
101
            $propertyName = strstr($propertyName, '.', true);
102
        }
103
104
        if (!property_exists($this->model, $propertyName)) {
105
            if (App::$Debug !== null) {
106
                App::$Debug->addMessage('Form field "' . $object . '" is not defined in model: ' . get_class($this->model), 'error');
107
            }
108
            return null;
109
        }
110
111
        if (null === $structure) {
112
            if ($type === 'checkbox') {
113
                $structure = $this->structureCheckbox;
114
            } else {
115
                $structure = $this->structure;
116
            }
117
            // structureCheckboxes is apply'd to each items in builder later
118
        }
119
120
        $labelFor = $this->name . '-' . $propertyName;
121
        $labelText = $this->model->getLabel($object);
122
        $itemValue = $this->model->{$propertyName};
123
        // sounds like a dot-separated $object
124
        if ($propertyName !== $object) {
125
            $nesting = trim(strstr($object, '.'), '.');
126
            $labelFor .= '-' . Str::replace('.', '-', $nesting);
127
            $itemValue = Arr::getByPath($nesting, $itemValue);
128
        }
129
        $itemBody = $this->dataTypeTag($type, $object, $itemValue, $property);
130
        // only item if hidden type
131
        if ($type === 'hidden') {
132
            return $itemBody;
133
        }
134
135
        return Str::replace(
136
            ['%name%', '%label%', '%item%', '%help%'],
137
            [$labelFor, $labelText, $itemBody, self::nohtml($helper)],
138
            $structure
139
        );
140
    }
141
142
    protected function dataTypeTag($type, $name, $value = null, $property = null)
143
    {
144
        if (!Obj::isArray($property) && $property !== null) {
145
            throw new SyntaxException('Property must be passed as array or null! Field: ' . $name);
146
        }
147
148
        $propertyString = null;
0 ignored issues
show
Unused Code introduced by
$propertyString is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
149
        $selectOptions = [];
150
        if (Obj::isArray($property['options'])) {
151
            $selectOptions = $property['options'];
152
            unset($property['options']);
153
        }
154
155
        // jquery validation quick-build some rules
156
        $rules = $this->model->getValidationRule($name);
157
        if (count($rules) > 0) {
158
            foreach ($rules as $rule_name => $rule_value) {
159
                switch ($rule_name) {
160
                    case 'required':
161
                        $property['required'] = null;
162
                        break;
163
                    case 'length_min':
164
                        $property['minlength'] = $rule_value;
165
                        break;
166
                    case 'length_max':
167
                        $property['maxlength'] = $rule_value;
168
                        break;
169
                }
170
            }
171
        }
172
173
        $response = null;
174
175
        // get html allow rule from field init
176
        $html = false;
177
        if (isset($property['html']) && $property['html'] === true) {
178
            $html = true;
179
        }
180
        if (Obj::isArray($property) && array_key_exists('html', $property)) {
181
            unset($property['html']);
182
        }
183
184
        // standard property data definition
185
        $property['name'] = $property['id'] = $this->name; // form global name
186
        if ($value !== null) {
187
            $property['value'] = $value;
188
        }
189
190
        // sounds like a array-path based
191
        if (Str::contains('.', $name)) {
192
            $splitedName = explode('.', $name);
193
            foreach ($splitedName as $nameKey) {
194
                $property['name'] .= '[' . $nameKey . ']';
195
                $property['id'] .= '-' . $nameKey;
196
            }
197
        } else { // standard property's definition - add field name
198
            $property['name'] .= '[' . $name . ']';
199
            $property['id'] .= '-' . $name;
200
        }
201
202
        switch ($type) {
203 View Code Duplication
            case 'password':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
204
                $property['type'] = 'password';
205
                unset($property['value']);
206
                $response = self::buildSingleTag('input', $property);
207
                break;
208
            case 'textarea':
209
                unset($property['value']);
210
                $response = self::buildContainerTag('textarea', $property, $value, $html);
211
                break;
212
            case 'checkbox': // single checkbox for ON or OFF" value
213
                // hook DOM model
214
                $response = self::buildSingleTag('input', ['type' => 'hidden', 'value' => '0', 'name' => $property['name']]); // hidden 0 elem
215
                $property['type'] = 'checkbox';
216
                if ($value === 1 || $value === true || $value === '1') {
217
                    $property['checked'] = null; // set boolean attribute, maybe = "checked" is better
218
                }
219
                unset($property['required']);
220
                $property['value'] = '1';
221
                $response .= self::buildSingleTag('input', $property);
222
                break;
223
            case 'checkboxes':
224
                if (!Obj::isArray($selectOptions)) {
225
                    if (App::$Debug !== null) {
226
                        App::$Debug->addMessage('Checkboxes field ' . $name . ' field have no options', 'warning');
227
                    }
228
                    break;
229
                }
230
231
                $property['type'] = 'checkbox';
232
                $property['name'] .= '[]';
233
                unset($property['value'], $property['id']);
234
235
                $buildCheckboxes = null;
236
237
                foreach ($selectOptions as $opt) {
238
                    if (Obj::isArray($value) && Arr::in($opt, $value)) {
239
                        $property['checked'] = null;
240
                    } else {
241
                        unset($property['checked']); // remove checked if it setted before
242
                    }
243
                    $property['value'] = $opt;
244
                    // apply structured checkboxes style for each item
245
                    $buildCheckboxes .= Str::replace('%item%', self::buildSingleTag('input', $property) . self::nohtml($opt), $this->structureCheckboxes);
246
                }
247
248
                $response = $buildCheckboxes;
249
                break;
250
            case 'select':
251
                if (count($selectOptions) < 1) {
252
                    $response = 'Form select ' . self::nohtml($name) . ' have no options';
253
                } else {
254
                    unset($property['value']);
255
                    $optionsKey = $property['optionsKey'] === true;
256
                    unset($property['optionsKey']);
257
                    $buildOpt = null;
258
                    foreach ($selectOptions as $optIdx => $opt) {
259
                        $optionProperty = [];
260
                        if (true === $optionsKey) { // options with value => text
261
                            $optionProperty['value'] = $optIdx;
262
                            if ($optIdx == $value) {
263
                                $optionProperty['selected'] = null; // def boolean attribute html5
264
                            }
265
                        } else { // only value option
266
                            if ($opt == $value) {
267
                                $optionProperty['selected'] = null; // def boolean attribute html5
268
                            }
269
                        }
270
                        $buildOpt .= self::buildContainerTag('option', $optionProperty, $opt);
271
                    }
272
273
                    $response = self::buildContainerTag('select', $property, $buildOpt, true);
274
                }
275
                break;
276
            case 'captcha':
277
                if (App::$Captcha->isFull()) {
278
                    $response = App::$Captcha->get();
279
                } else {
280
                    $image = App::$Captcha->get();
281
                    $response = self::buildSingleTag('img', ['id' => 'src-secure-image', 'src' => $image, 'alt' => 'secure image', 'onClick' => 'this.src=\'' . $image . '&rnd=\'+Math.random()']);
282
                    $property['type'] = 'text';
283
                    $response .= self::buildSingleTag('input', $property);
284
                }
285
                break;
286
            case 'email':
287
                $property['type'] = 'email';
288
                $response = self::buildSingleTag('input', $property);
289
                break;
290 View Code Duplication
            case 'file':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
291
                $property['type'] = 'file';
292
                unset($property['value']);
293
                $response = self::buildSingleTag('input', $property);
294
                break;
295
            case 'hidden':
296
                $property['type'] = 'hidden';
297
                $response = self::buildSingleTag('input', $property);
298
                break;
299
            case 'div':
300
                unset($property['value']);
301
                $response = self::buildContainerTag('div', $property, $value, $html);
302
                break;
303
            default:
304
                $property['type'] = 'text';
305
                $response = self::buildSingleTag('input', $property);
306
                break;
307
        }
308
        return $response;
309
    }
310
311
    /**
312
     * Display submit button for current form
313
     * @param string $title
314
     * @param array $property
315
     * @return string
316
     */
317
    public function submitButton($title, array $property = [])
318
    {
319
        $property['type'] = 'submit';
320
        $property['name'] = $this->name . '[submit]';
321
        $property['value'] = $title;
322
        return self::buildSingleTag('input', $property);
323
    }
324
325
    /**
326
     * Finish current form.
327
     * @param bool $validate
328
     * @return string
329
     */
330
    public function finish($validate = true)
331
    {
332
        // pre-validate form fields based on model rules and jquery.validation
333
        if ($validate) {
334
            App::$Alias->addPlainCode('js', '$().ready(function() { $("#' . $this->name . '").validate(); });');
335
            App::$Alias->setCustomLibrary('js', '/vendor/bower/jquery-validation/dist/jquery.validate.min.js');
336
            if (App::$Request->getLanguage() !== 'en') {
337
                $localeFile = '/vendor/bower/jquery-validation/src/localization/messages_' . App::$Request->getLanguage() . '.js';
338
                if (File::exist($localeFile)) {
339
                    App::$Alias->setCustomLibrary('js', $localeFile);
340
                }
341
            }
342
            // if model is not empty - add js error color notification
343
            if ($this->model !== null) {
344
                $badAttr = $this->model->getBadAttribute();
345
                $formName = $this->model->getFormName();
346
                if (Obj::isArray($badAttr) && count($badAttr) > 0) {
347
                    $jsError = $this->structureJSError;
348
                    foreach ($badAttr as $attr) {
0 ignored issues
show
Bug introduced by
The expression $badAttr of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
349
                        $itemId = $formName . '-' . $attr;
350
                        App::$Alias->addPlainCode('js', Str::replace('%itemId%', $itemId, $jsError));
351
                    }
352
                }
353
            }
354
        }
355
        return '</form>';
356
    }
357
}