Completed
Push — master ( 3c40fd...fd4f3a )
by Petr
06:28
created

Editable   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 289
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 36.63%

Importance

Changes 9
Bugs 2 Features 1
Metric Value
wmc 49
c 9
b 2
f 1
lcom 1
cbo 8
dl 0
loc 289
rs 8.5454
ccs 37
cts 101
cp 0.3663

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getEditableCallback() 0 4 1
A getEditableValueCallback() 0 4 1
A getEditableRowCallback() 0 4 1
A isEditable() 0 4 1
A isEditableDisabled() 0 4 1
A setEditable() 0 10 3
A setEditableControl() 0 7 2
A setEditableCallback() 0 7 2
A setEditableValueCallback() 0 7 2
A setEditableRowCallback() 0 7 2
A disableEditable() 0 7 1
A getEditableControl() 0 9 2
B handleEditable() 0 25 6
A handleEditableControl() 0 16 3
C setClientSideOptions() 0 35 14
A getHeaderPrototype() 0 11 2
B getCellPrototype() 0 16 5

How to fix   Complexity   

Complex Class

Complex classes like Editable 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 Editable, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file is part of the Grido (http://grido.bugyik.cz)
5
 *
6
 * Copyright (c) 2014 Petr Bugyík (http://petr.bugyik.cz)
7
 *
8
 * For the full copyright and license information, please view
9
 * the file LICENSE.md that was distributed with this source code.
10
 */
11
12
namespace Grido\Components\Columns;
13
14
use Grido\Exception;
15
16
/**
17
 * An inline editable column.
18
 *
19
 * @package     Grido
20
 * @subpackage  Components\Columns
21
 * @author      Jakub Kopřiva <[email protected]>
22
 * @author      Petr Bugyík
23
 *
24
 * @property \Nette\Forms\IControl $editableControl
25
 * @property callback $editableCallback
26
 * @property callback $editableValueCallback
27 1
 * @property callback $editableRowCallback
28 1
 */
29
abstract class Editable extends Column
30 1
{
31
    /** @var bool */
32
    protected $editable = FALSE;
33
34
    /** @var bool */
35
    protected $editableDisabled = FALSE;
36
37
    /** @var \Nette\Forms\IControl Custom control for inline editing */
38
    protected $editableControl;
39
40
    /** @var callback for custom handling with edited data; function($id, $newValue, $oldValue, Editable $column) {} */
41
    protected $editableCallback;
42
43
    /** @var callback for custom value; function($row, Columns\Editable $column) {} */
44
    protected $editableValueCallback;
45
46
    /** @var callback for getting row; function($row, Columns\Editable $column) {} */
47
    protected $editableRowCallback;
48
49
    /**
50
     * Sets column as editable.
51
     * @param callback $callback function($id, $newValue, $oldValue, Columns\Editable $column) {}
52
     * @param \Nette\Forms\IControl $control
53
     * @return Editable
54
     */
55
    public function setEditable($callback = NULL, \Nette\Forms\IControl $control = NULL)
56
    {
57 1
        $this->editable = TRUE;
58 1
        $this->setClientSideOptions();
59
60 1
        $callback && $this->setEditableCallback($callback);
61 1
        $control && $this->setEditableControl($control);
62
63 1
        return $this;
64
    }
65
66
    /**
67
     * Sets control for inline editation.
68
     * @param \Nette\Forms\IControl $control
69
     * @return Editable
70
     */
71
    public function setEditableControl(\Nette\Forms\IControl $control)
72
    {
73 1
        $this->isEditable() ?: $this->setEditable();
74 1
        $this->editableControl = $control;
75
76 1
        return $this;
77
    }
78
79
    /**
80
     * Sets editable callback.
81
     * @param callback $callback function($id, $newValue, $oldValue, Columns\Editable $column) {}
82
     * @return Editable
83
     */
84
    public function setEditableCallback($callback)
85
    {
86 1
        $this->isEditable() ?: $this->setEditable();
87 1
        $this->editableCallback = $callback;
88
89 1
        return $this;
90
    }
91
92
    /**
93
     * Sets editable value callback.
94
     * @param callback $callback for custom value; function($row, Columns\Editable $column) {}
95
     * @return Editable
96
     */
97
    public function setEditableValueCallback($callback)
98
    {
99 1
        $this->isEditable() ?: $this->setEditable();
100 1
        $this->editableValueCallback = $callback;
101
102 1
        return $this;
103
    }
104
105
    /**
106
     * Sets editable row callback - it's required when used editable collumn with customRenderCallback
107
     * @param callback $callback for getting row; function($id, Columns\Editable $column) {}
108
     * @return Editable
109
     */
110
    public function setEditableRowCallback($callback)
111
    {
112 1
        $this->isEditable() ?: $this->setEditable();
113 1
        $this->editableRowCallback = $callback;
114
115 1
        return $this;
116
    }
117
118
    /**
119
     * @return Editable
120
     */
121
    public function disableEditable()
122
    {
123 1
        $this->editable = FALSE;
124 1
        $this->editableDisabled = TRUE;
125
126 1
        return $this;
127
    }
128
129
    /**
130
     * @throws Exception
131
     */
132
    protected function setClientSideOptions()
133
    {
134 1
        $options = $this->grid->getClientSideOptions();
135 1
        if (!isset($options['editable'])) { //only once
136 1
            $this->grid->setClientSideOptions(['editable' => TRUE]);
137 1
            $this->grid->onRender[] = function(\Grido\Grid $grid)
138
            {
139
                foreach ($grid->getComponent(Column::ID)->getComponents() as $column) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Nette\ComponentModel\IComponent as the method getComponents() does only exist in the following implementations of said interface: Grido\Components\Actions\Action, Grido\Components\Actions\Event, Grido\Components\Actions\Href, Grido\Components\Button, Grido\Components\Columns\Column, Grido\Components\Columns\Date, Grido\Components\Columns\Editable, Grido\Components\Columns\Email, Grido\Components\Columns\Link, Grido\Components\Columns\Number, Grido\Components\Columns\Text, Grido\Components\Component, Grido\Components\Container, Grido\Components\Export, Grido\Components\Filters\Check, Grido\Components\Filters\Custom, Grido\Components\Filters\Date, Grido\Components\Filters\DateRange, Grido\Components\Filters\Filter, Grido\Components\Filters\Number, Grido\Components\Filters\Select, Grido\Components\Filters\Text, Grido\Components\Operation, Grido\Grid, KdybyModule\CliPresenter, Nette\Application\UI\Control, Nette\Application\UI\Form, Nette\Application\UI\Multiplier, Nette\Application\UI\Presenter, Nette\Application\UI\PresenterComponent, Nette\ComponentModel\Container, Nette\Forms\Container, Nette\Forms\Form.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
140
                    if (!$column instanceof Editable || !$column->isEditable()) {
141
                        continue;
142
                    }
143
144
                    $colDb = $column->getColumn();
145
                    $colName = $column->getName();
146
                    $isMissing = function ($method) use ($grid) {
147
                        return $grid->model instanceof \Grido\DataSources\Model
148
                            ? !method_exists($grid->model->dataSource, $method)
149
                            : TRUE;
150
                    };
151
152
                    if (($column->editableCallback === NULL && (!is_string($colDb) || strpos($colDb, '.'))) ||
153
                        ($column->editableCallback === NULL && $isMissing('update'))
154
                    ) {
155
                        $msg = "Column '$colName' has error: You must define callback via setEditableCallback().";
156
                        throw new Exception($msg);
157
                    }
158
159
                    if ($column->editableRowCallback === NULL && $column->customRender && $isMissing('getRow')) {
160
                        $msg = "Column '$colName' has error: You must define callback via setEditableRowCallback().";
161
                        throw new Exception($msg);
162
                    }
163
                }
164
            };
165 1
        }
166 1
    }
167
168
    /**********************************************************************************************/
169
170
    /**
171
     * Returns header cell prototype (<th> html tag).
172
     * @return \Nette\Utils\Html
173
     */
174
    public function getHeaderPrototype()
175
    {
176
        $th = parent::getHeaderPrototype();
177
178
        if ($this->isEditable()) {
179
            $th->data['grido-editable-handler'] = $this->link('editable!');
180
            $th->data['grido-editableControl-handler'] = $this->link('editableControl!');
181
        }
182
183
        return $th;
184
    }
185
186
    /**
187
     * Returns cell prototype (<td> html tag).
188
     * @param mixed $row
189
     * @return \Nette\Utils\Html
190
     */
191
    public function getCellPrototype($row = NULL)
192
    {
193
        $td = parent::getCellPrototype($row);
194
195
        if ($this->isEditable() && $row !== NULL) {
196
            if (!in_array('editable', $td->class)) {
197
                $td->class[] = 'editable';
198
            }
199
200
            $td->data['grido-editable-value'] = $this->editableValueCallback === NULL
201
                ? $this->getValue($row)
202
                : call_user_func_array($this->editableValueCallback, [$row, $this]);
203
        }
204
205
        return $td;
206
    }
207
208
    /**
209
     * Returns control for editation.
210
     * @returns \Nette\Forms\Controls\TextInput
211
     */
212
    public function getEditableControl()
213
    {
214 1
        if ($this->editableControl === NULL) {
215 1
            $this->editableControl = new \Nette\Forms\Controls\TextInput;
216 1
            $this->editableControl->controlPrototype->class[] = 'form-control';
217 1
        }
218
219 1
        return $this->editableControl;
220
    }
221
222
    /**
223
     * @return callback
224
     * @internal
225
     */
226
    public function getEditableCallback()
227
    {
228 1
        return $this->editableCallback;
229
    }
230
231
    /**
232
     * @return callback
233
     * @internal
234
     */
235
    public function getEditableValueCallback()
236
    {
237 1
        return $this->editableValueCallback;
238
    }
239
240
    /**
241
     * @return callback
242
     * @internal
243
     */
244
    public function getEditableRowCallback()
245
    {
246 1
        return $this->editableRowCallback;
247
    }
248
249
    /**
250
     * @return bool
251
     * @internal
252
     */
253
    public function isEditable()
254
    {
255 1
        return $this->editable;
256
    }
257
258
    /**
259
     * @return bool
260
     * @internal
261
     */
262
    public function isEditableDisabled()
263
    {
264 1
        return $this->editableDisabled;
265
    }
266
267
    /**********************************************************************************************/
268
269
    /**
270
     * @internal
271
     */
272
    public function handleEditable($id, $newValue, $oldValue)
273
    {
274
        $this->grid->onRender($this->grid);
275
276
        if (!$this->presenter->isAjax() || !$this->isEditable()) {
277
            $this->presenter->terminate();
278
        }
279
280
        $success = $this->editableCallback
281
            ? call_user_func_array($this->editableCallback, [$id, $newValue, $oldValue, $this])
282
            : $this->grid->model->update($id, [$this->getColumn() => $newValue], $this->grid->primaryKey);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Grido\DataSources\IDataSource as the method update() does only exist in the following implementations of said interface: Grido\DataSources\NetteDatabase.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
283
284
        if (is_callable($this->customRender)) {
285
            $row = $this->editableRowCallback
286
                ? call_user_func_array($this->editableRowCallback, [$id, $this])
287
                : $this->grid->model->getRow($id, $this->grid->primaryKey);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Grido\DataSources\IDataSource as the method getRow() does only exist in the following implementations of said interface: Grido\DataSources\DibiFluent, Grido\DataSources\NetteDatabase.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
288
            $html = call_user_func_array($this->customRender, [$row]);
289
        } else {
290
            $html = $this->formatValue($newValue);
291
        }
292
293
        $payload = ['updated' => (bool) $success, 'html' => (string) $html];
294
        $response = new \Nette\Application\Responses\JsonResponse($payload);
295
        $this->presenter->sendResponse($response);
296
    }
297
298
    /**
299
     * @internal
300
     */
301
    public function handleEditableControl($value)
302
    {
303
        $this->grid->onRender($this->grid);
304
305
        if (!$this->presenter->isAjax() || !$this->isEditable()) {
306
            $this->presenter->terminate();
307
        }
308
309
        $control = $this->getEditableControl();
310
        $control->setValue($value);
311
312
        $this->getForm()->addComponent($control, 'edit' . $this->getName());
0 ignored issues
show
Documentation introduced by
$control is of type object<Nette\Forms\IControl>, but the function expects a object<Nette\ComponentModel\IComponent>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
313
314
        $response = new \Nette\Application\Responses\TextResponse($control->getControl()->render());
315
        $this->presenter->sendResponse($response);
316
    }
317
}
318