Bootstrap3Form   C
last analyzed

Complexity

Total Complexity 77

Size/Duplication

Total Lines 399
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 77
c 1
b 0
f 0
lcom 1
cbo 5
dl 0
loc 399
ccs 205
cts 205
cp 1
rs 5.4715

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
B __get() 0 12 6
A prompt() 0 12 4
A size() 0 4 2
B align() 0 12 7
A message() 0 7 1
B header() 0 27 5
C label() 0 51 13
F field() 0 48 18
A close() 0 8 2
A checkbox() 0 11 3
A radio() 0 11 3
B group() 0 21 8
A submit() 0 20 4

How to fix   Complexity   

Complex Class

Complex classes like Bootstrap3Form 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Bootstrap3Form, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace BootPress\Bootstrap;
4
5
use BootPress\Form\Component as Form;
6
7
class Bootstrap3Form extends Form
8
{
9
    use Base;
10
    
11
    private $bp;
12
    private $prompt = array(
13
        'info' => 'glyphicon glyphicon-question-sign',
14
    );
15
    private $input = '';
16
    private $align = 'form-horizontal';
17
    private $collapse = 'sm';
18
    private $indent = 2;
19
    
20
    /**
21
     * {@inheritdoc}
22
     */
23 1
    public function __construct($name = 'form', $method = 'post', Bootstrap3 $bp)
24
    {
25 1
        parent::__construct($name, $method = 'post');
26 1
        $this->bp = $bp;
27 1
    }
28
    
29
    /**
30
     * A private property getter.
31
     * 
32
     * @param string $name 
33
     * 
34
     * @return null|string
35
     */
36 1
    public function __get($name)
37
    {
38
        switch ($name) {
39 1
            case 'prompt':
40 1
            case 'input':
41 1
            case 'align':
42 1
            case 'collapse':
43 1
            case 'indent':
44 1
                return $this->$name;
45
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
46
        }
47 1
    }
48
    /**
49
     * This is to add html tags, or semicolons, or asterisks, or whatever you would like to all of the form's prompts.
50
     * 
51
     * @param string      $place    Either '**info**', '**append**', or '**prepend**' to the prompt.  You only have one shot at each.
52
     * @param string      $html     Whatever you would like to add.  For 'info', this will be the icon class you want to use.
53
     * @param false|mixed $required If this is anything but (bool) false and $place == 'prepend', the the $html will only be prepended if the field is required per the ``$form->validator``.
54
     * 
55
     * ```php
56
$form->prompt('prepend', '<font color="red">*</font> ', 'required'); // If the field is required it will add a red asterisk to the front.
57
58
$form->prompt('append', ':'); // Adds a semicolon to all of the prompts.
59
     * ```
60
     */
61 1
    public function prompt($place, $html, $required = false)
62
    {
63
        switch ($place) {
64 1
            case 'info':
65 1
            case 'append':
66 1
                $this->prompt[$place] = $html;
67 1
                break;
68 1
            case 'prepend':
69 1
                $this->prompt['prepend'] = array('html'=>$html, 'required'=>(bool) $required);
70 1
                break;
71
        }
72 1
    }
73
    
74
    /**
75
     * Supersize or undersize your input fields.
76
     * 
77
     * @param string $input Either '**lg**' (large), '**md**' (medium - the default), or '**sm**' (small).
78
     * 
79
     * ```php
80
     * $form->size('lg');
81
     * ```
82
     */
83 1
    public function size($input)
84
    {
85 1
        $this->input = (in_array($input, array('lg', 'sm'))) ? 'input-'.$input : '';
86 1
    }
87
88
    /**
89
     * Utilize any Bootstrap form style
90
     * 
91
     * @param string  $direction The options are:
92
     *
93
     * - '**collapse**' - This will display the form prompt immediately above the field.
94
     * - '**inline**' - All of the fields will be inline with each other, and the form prompts will be removed.
95
     * - '**horizontal**' - Vertically aligns all of the fields with the prompt immediately preceding, and right aligned.
96
     * 
97
     * @param string  $collapse  Either '**xs**', '**sm**', '**md**', or '**lg**'.  This is the breaking point so to speak for a '**horizontal**' form.  It is the device size on which the form will '**collapse**'.
98
     * @param integer $indent    The number of columns (up to 12) that you would like to indent the field in a '**horizontal**' form.
99
     * 
100
     * ```php
101
     * $form->align('collapse');
102
     * ```
103
     */
104 1
    public function align($direction = 'horizontal', $collapse = 'sm', $indent = 2)
105
    {
106 1
        if ($direction == 'collapse') {
107 1
            $this->align = '';
108 1
        } elseif ($direction == 'inline') {
109 1
            $this->align = 'form-inline';
110 1
        } else {
111 1
            $this->align = 'form-horizontal';
112 1
            $this->collapse = (in_array($collapse, array('xs', 'sm', 'md', 'lg'))) ? $collapse : 'sm';
113 1
            $this->indent = (is_numeric($indent) && $indent > 0 && $indent < 12) ? $indent : 2;
0 ignored issues
show
Documentation Bug introduced by
It seems like is_numeric($indent) && $...dent < 12 ? $indent : 2 can also be of type double or string. However, the property $indent is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
114
        }
115 1
    }
116
    
117
    /**
118
     * Use to display a message on the retrun trip after you have ``$page->eject()``ed them.  The Bootstrap alert status message will be displayed at the top of the form when you return ``$form->header()``.
119
     * 
120
     * @param string $status   Either '**success**', '**info**', '**warning**', or '**danger**'.  If this is '**html**', then the $message will be delivered as is.
121
     * @param string $message  The message you would like to get across to your user.  ``<h1-6>`` headers and ``<a>`` links may be used.
122
     * 
123
     * ```php
124
     * if ($vars = $form->validator->certified()) {
125
     *     $form->message('success', 'Good job, you are doing great!');
126
     *     $page->eject($form->eject);
127
     * }
128
     * ```
129
     */
130 1
    public function message($status, $message)
131
    {
132 1
        $this->page->session->getFlashBag()->add($this->header['name'], array(
0 ignored issues
show
Documentation introduced by
The property $session is declared private in BootPress\Page\Component. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
133 1
            'status' => $status,
134 1
            'msg' => $message,
135 1
        ));
136 1
    }
137
    
138
    /**
139
     * {@inheritdoc}
140
     */
141 1
    public function header(array $validate = array())
142
    {
143 1
        $this->validator->jquery('form[name='.$this->header['name'].']', array_merge(array(
144 1
            'ignore' => '[]',
145 1
            'errorClass' => '"has-error"',
146 1
            'validClass' => '""',
147 1
            'errorElement' => '"span"',
148 1
            'highlight' => 'function(element, errorClass, validClass){ $(element).closest("div.form-group").addClass(errorClass).removeClass(validClass).find("p.validation").show(); }',
149 1
            'unhighlight' => 'function(element, errorClass, validClass){ $(element).closest("div.form-group").removeClass(errorClass).addClass(validClass).find("p.validation").text("").hide(); }',
150 1
            'errorPlacement' => 'function(error, element){ $(element).closest("div.form-group").find("p.validation").html(error); }',
151 1
            'submitHandler' => 'function(form, event){ event.preventDefault(); $(form).find("button[type=submit]").button("loading"); form.submit(); }',
152 1
            'onkeyup' => 'false',
153 1
        ), $validate));
154 1
        $html = "\n";
155 1
        if ($this->align == 'form-horizontal') {
156 1
            $html .= '<div class="row"><div class="col-'.$this->collapse.'-12">';
157 1
        }
158 1
        if ($flash = $this->page->session->getFlashBag()->get($this->header['name'])) {
0 ignored issues
show
Documentation introduced by
The property $session is declared private in BootPress\Page\Component. Since you implemented __get(), maybe consider adding a @property or @property-read annotation. This makes it easier for IDEs to provide auto-completion.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
159 1
            $html .= ($flash[0]['status'] == 'html') ? $flash[0]['msg'] : $this->bp->alert($flash[0]['status'], $flash[0]['msg']);
160 1
        }
161 1
        $html .= trim(parent::header());
162 1
        if (!empty($this->align)) {
163 1
            $html = $this->addClass($html, array('form' => $this->align));
164 1
        }
165
166 1
        return $html;
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     * 
172
     * @param false|mixed $inline This tells us if you want the checkboxes to be inline (any value but false), or not (false).
173
     */
174 1
    public function checkbox($field, array $attributes = array(), $inline = false)
175
    {
176 1
        $disabled = in_array('disabled', $attributes) ? 'disabled' : '';
177 1
        if ($inline !== false) {
178 1
            $wrap = $this->page->tag('label', array('class' => array('checkbox-inline', $this->input, $disabled)), '%s');
179 1
        } else {
180 1
            $wrap = $this->page->tag('div', array('class' => array('checkbox', $this->input, $disabled)), '<label>%s</label>');
181
        }
182
183 1
        return parent::checkbox($field, $attributes, $wrap);
184
    }
185
186
    /**
187
     * {@inheritdoc}
188
     * 
189
     * @param false|mixed $inline This tells us if you want the radio buttons to be inline (any value but false), or not (false).
190
     */
191 1
    public function radio($field, array $attributes = array(), $inline = false)
192
    {
193 1
        $disabled = in_array('disabled', $attributes) ? 'disabled' : '';
194 1
        if ($inline !== false) {
195 1
            $wrap = $this->page->tag('label', array('class' => array('radio-inline', $this->input, $disabled)), '%s');
196 1
        } else {
197 1
            $wrap = $this->page->tag('div', array('class' => array('radio', $this->input, $disabled)), '<label>%s</label>');
198
        }
199
200 1
        return parent::radio($field, $attributes, $wrap);
201
    }
202
203
    /**
204
     * Group an input field with addons.  You can prepend and/or append a ``$bp->button(...)``, ``$bp->icon('...')``, or just a string of text.  To prepend or append multiple elements, then make it an ``array($html, ...)`` of addons.
205
     * 
206
     * @param string|array $prepend An element to place before the $input.
207
     * @param string|array $append  An element to place after the $input.
208
     * @param string $input   The form field to wrap.    
209
     * 
210
     * @return string A ``<div class="input-group">...</div>`` html string.
211
     * 
212
     * ```php
213
     * echo $form->group('$', '.00', $form->text('amount'));
214
     * ```
215
     */
216 1
    public function group($prepend, $append, $input)
217
    {
218 1
        if (!empty($prepend)) {
219 1
            foreach ((array) $prepend as $html) {
220 1
                $class = (strpos($html, 'btn') !== false) ? 'input-group-btn' : 'input-group-addon';
221 1
                $input = $this->page->tag('div', array('class' => $class), $html).$input;
222 1
            }
223 1
        }
224 1
        if (!empty($append)) {
225 1
            foreach ((array) $append as $html) {
226 1
                $class = (strpos($html, 'btn') !== false) ? 'input-group-btn' : 'input-group-addon';
227 1
                $input = $input.$this->page->tag('div', array('class' => $class), $html);
228 1
            }
229 1
        }
230 1
        $group = array('input-group');
231 1
        if (!empty($this->input)) {
232 1
            $group[] = str_replace('-', '-group-', $this->input);
233 1
        }
234
235 1
        return $this->page->tag('div', array('class' => $group), $input);
236
    }
237
    
238
    /**
239
     * This is to add html tags, or semicolons, or asterisks, or whatever you would like include with all of the form's prompts.
240
     * 
241
     * @param string|array $prompt  The form label reference.  If you want to include additional information relative to the field, then you can make this an ``array($prompt => $info)``, or an ``array($prompt, $info)`` that will appear when cliked or hovered over.  To customize the icon set ``$form->prompt('info', 'fa fa-info-circle')``.
242
     * @param string       $name    The name of the associated input field.
243
     * @param string       $id      The id of the associated input field.
244
     * 
245
     * @return string  The generated HTML ``<label>``.
246
     */
247 1
    public function label($prompt, $name, $id)
248
    {
249 1
        if (empty($prompt)) {
250 1
            return '';
251
        }
252 1
        if (is_array($prompt)) {
253 1
            list($prompt, $info) = (count($prompt) > 1) ? array_values($prompt) : each($prompt);
254 1
        }
255 1
        if (empty($prompt) || strpos($prompt, '<label') !== false) {
256 1
            return $prompt;
257
        }
258 1
        if (isset($this->prompt['prepend'])) {
259 1
            if (!$this->prompt['prepend']['required'] || $this->validator->required($name)) {
260 1
                $prompt = $this->prompt['prepend']['html'] . $prompt;
261 1
            }
262 1
        }
263 1
        if (isset($this->prompt['append'])) {
264 1
            $prompt .= $this->prompt['append'];
265 1
        }
266 1
        if (isset($info)) {
267 1
            $prompt .= ' '.$this->page->tag('i', array(
268 1
                'title' => htmlspecialchars($info),
269 1
                'class' => $this->prompt['info'],
270 1
                'style' => 'cursor:pointer;',
271 1
                'data-html' => 'true',
272 1
                'data-toggle' => 'tooltip',
273 1
                'data-placement' => 'bottom',
274 1
                'data-container' => 'form[name='.$this->header['name'].']',
275 1
            ), '');
276 1
            $this->page->jquery('$(\'[data-toggle="tooltip"]\').tooltip();');
277 1
        }
278 1
        switch ($this->align) {
279 1
            case 'form-inline':
280 1
                $class = 'sr-only';
281 1
                break;
282 1
            case 'form-horizontal':
283
                $class = array(
284 1
                    "col-{$this->collapse}-{$this->indent}",
285 1
                    'control-label',
286 1
                    $this->input,
287 1
                );
288 1
                break;
289 1
            default:
290 1
                $class = $this->input;
291 1
                break;
292 1
        }
293 1
        return $this->page->tag('label', array(
294 1
            'class' => $class,
295 1
            'for' => $id,
296 1
        ), $prompt);
297
    }
298
299
    /**
300
     * Adds a (properly formatted) $prompt to your $input field, and manages any error messages.
301
     * 
302
     * @param string $prompt The $input's name.
303
     * @param string $input  A form field.
304
     * @param string $error  An optional error to override, or include with the field.
305
     * 
306
     * @return string A ``<div class="form-group">...</div>`` html string.
307
     * 
308
     * ```php
309
     * echo $form->field('Amount', $form->group('$', '.00', $form->text('amount')));
310
     * ```
311
     */
312 1
    public function field($prompt, $input, $error = null)
313
    {
314 1
        foreach (array('input', 'select', 'textarea', 'button', 'p') as $tag) {
315 1
            if ($this->firstTagAttributes($input, $matches, '<'.$tag)) {
316 1
                break;
317
            }
318 1
        }
319 1
        list($first, $tag, $attributes) = $matches;
0 ignored issues
show
Bug introduced by
The variable $matches does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Unused Code introduced by
The assignment to $first is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
320 1
        $type = (isset($attributes['type'])) ? $attributes['type'] : '';
321 1
        $name = (isset($attributes['name'])) ? $attributes['name'] : '';
322 1
        $id = (isset($attributes['id'])) ? $attributes['id'] : '';
323 1
        $prompt = $this->label($prompt, $name, $id);
324
        switch ($tag) {
325 1
            case 'input':
326 1
            case 'select':
327 1
            case 'textarea':
328 1
                $input = $this->addClass($input, array('p'=>'help-block'));
329 1
                if ($tag != 'input' || !in_array($type, array('checkbox', 'radio', 'file', 'submit', 'reset', 'button'))) {
330 1
                    $input = $this->addClass($input, array($tag=>'form-control '.$this->input));
331 1
                }
332 1
                break;
333 1
            case 'p':
334 1
                $input = $this->addClass($input, array('p'=>'form-control-static'));
335 1
                break;
336
        }
337 1
        $group = array('form-group');
338 1
        $msg = (empty($name)) ? null : $this->validator->error($name);
339 1
        if (!is_null($error)) {
340 1
            $msg = $error; // override all
341 1
        }
342 1
        if (!empty($msg)) {
343 1
            $group[] = 'has-error';
344 1
            $error = '<p class="validation help-block">'.$msg.'</p>';
345 1
        } elseif (!empty($name)) { // only include this when needed for validation
346 1
            $error = '<p class="validation help-block" style="display:none;"></p>';
347 1
        }
348 1
        if ($this->align == 'form-horizontal') {
349 1
            $class = array('col-'.$this->collapse.'-'.(12 - $this->indent));
350 1
            if (empty($prompt)) {
351 1
                $class[] = 'col-'.$this->collapse.'-offset-'.$this->indent;
352 1
            }
353 1
            $html = $prompt.$this->page->tag('div', array('class'=>$class), $error.$input);
354 1
        } else {
355 1
            $html = $prompt.$error.$input;
356
        }
357
358 1
        return "\n\t".$this->page->tag('div', array('class'=>$group), $html);
359
    }
360
361
    /**
362
     * Quickly adds a submit button to your form.
363
     * 
364
     * @param string $submit What you would like the submit button to say.  If it starts with a '**<**', then we assume you have spelled it all out for us.
365
     * @param string $reset  This will add a reset button if you give it a value, and if it starts with a '**<**' then it can be whatever you want it to be.  You can keep adding args until you run out of ideas for buttons to include.
366
     * 
367
     * @return string A ``<div class="form-group">...</div>`` html string with buttons.
368
     * 
369
     * ```php
370
     * echo $form->submit();
371
     * ```
372
     */
373 1
    public function submit($submit = 'Submit', $reset = '')
374
    {
375
        // never use name="submit" per: http://jqueryvalidation.org/reference/#developing-and-debugging-a-form
376 1
        $buttons = func_get_args();
377 1
        if (substr($submit, 0, 1) != '<') {
378 1
            $buttons[0] = $this->page->tag('button', array(
379 1
                'type' => 'submit',
380 1
                'class' => array('btn', 'btn-primary', str_replace('input', 'btn', $this->input)),
381 1
                'data-loading-text' => 'Submitting...',
382 1
            ), $submit);
383 1
        }
384 1
        if (isset($buttons[1]) && substr($reset, 0, 1) != '<') {
385 1
            $buttons[1] = $this->page->tag('button', array(
386 1
                'type' => 'reset',
387 1
                'class' => array('btn', 'btn-default', str_replace('input', 'btn', $this->input)),
388 1
            ), $reset);
389 1
        }
390
391 1
        return $this->field('', implode(' ', $buttons));
392
    }
393
394
    /**
395
     * {@inheritdoc}
396
     */
397 1
    public function close()
398
    {
399 1
        $html = parent::close();
400 1
        if ($this->align == 'form-horizontal') {
401 1
            $html .= '</div></div>';
402 1
        }
403 1
        return $html;
404
    }
405
}
406