Completed
Push — master ( e1ce60...8f54cc )
by Jean-Christophe
03:31
created

DataTable   D

Complexity

Total Complexity 78

Size/Duplication

Total Lines 402
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 4
Bugs 2 Features 0
Metric Value
wmc 78
lcom 1
cbo 13
dl 0
loc 402
rs 4.2342
c 4
b 2
f 0

37 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
B run() 0 14 6
A _generateBehavior() 0 6 2
A getTable() 0 3 1
D compile() 0 43 13
A _hideColumns() 0 7 2
A _generateHeader() 0 6 2
A _generateContent() 0 15 3
A _generateRow() 0 22 3
A _generatePagination() 0 13 3
A _associatePaginationBehavior() 0 5 2
A _getFieldName() 0 3 1
A _getFieldCaption() 0 3 1
B _setToolbarPosition() 0 15 7
A afterCompile() 0 4 1
A addToolbarRow() 0 10 2
A getHtmlComponent() 0 3 1
A getUrls() 0 3 1
A setUrls() 0 10 2
A paginate() 0 4 1
A autoPaginate() 0 4 1
A refresh() 0 4 1
A addSearchInToolbar() 0 3 1
A getSearchField() 0 7 2
A onNewRow() 0 4 1
A asForm() 0 3 1
A getTargetSelector() 0 6 2
A setTargetSelector() 0 4 1
A getRefreshSelector() 0 5 2
A setRefreshSelector() 0 4 1
A show() 0 7 3
A getRowClass() 0 3 1
A setRowClass() 0 4 1
A setEmptyMessage() 0 4 1
A setSortable() 0 4 1
A setActiveRowSelector() 0 4 1
A hideColumn() 0 6 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace Ajax\semantic\widgets\datatable;
4
5
use Ajax\common\Widget;
6
use Ajax\JsUtils;
7
use Ajax\semantic\html\collections\table\HtmlTable;
8
use Ajax\semantic\html\elements\HtmlInput;
9
use Ajax\semantic\html\collections\menus\HtmlPaginationMenu;
10
use Ajax\semantic\html\modules\checkbox\HtmlCheckbox;
11
use Ajax\semantic\html\base\constants\Direction;
12
use Ajax\service\JArray;
13
use Ajax\semantic\widgets\base\InstanceViewer;
14
use Ajax\semantic\html\collections\table\traits\TableTrait;
15
use Ajax\semantic\html\collections\HtmlMessage;
16
use Ajax\semantic\html\collections\menus\HtmlMenu;
17
18
/**
19
 * DataTable widget for displaying list of objects
20
 * @version 1.0
21
 * @author jc
22
 * @since 2.2
23
 *
24
 */
25
class DataTable extends Widget {
26
	use TableTrait,DataTableFieldAsTrait,HasCheckboxesTrait;
27
	protected $_searchField;
28
	protected $_urls;
29
	protected $_pagination;
30
	protected $_compileParts;
31
	protected $_deleteBehavior;
32
	protected $_editBehavior;
33
	protected $_visibleHover=false;
34
	protected $_targetSelector;
35
	protected $_refreshSelector;
36
	protected $_emptyMessage;
37
	protected $_json;
38
	protected $_rowClass="";
39
	protected $_sortable;
40
	protected $_hiddenColumns;
41
42
43
	public function __construct($identifier,$model,$modelInstance=NULL) {
44
		parent::__construct($identifier, $model,$modelInstance);
45
		$this->_init(new InstanceViewer($identifier), "table", new HtmlTable($identifier, 0,0), false);
46
		$this->_urls=[];
47
		$this->_emptyMessage=new HtmlMessage("","nothing to display");
48
		$this->_emptyMessage->setIcon("info circle");
49
	}
50
51
	public function run(JsUtils $js){
52
		if($this->_hasCheckboxes && isset($js)){
53
			$this->_runCheckboxes($js);
54
		}
55
		if($this->_visibleHover){
56
			$js->execOn("mouseover", "#".$this->identifier." tr", "$(event.target).closest('tr').find('.visibleover').css('visibility', 'visible');",["preventDefault"=>false,"stopPropagation"=>true]);
57
			$js->execOn("mouseout", "#".$this->identifier." tr", "$(event.target).closest('tr').find('.visibleover').css('visibility', 'hidden');",["preventDefault"=>false,"stopPropagation"=>true]);
58
		}
59
		if(\is_array($this->_deleteBehavior))
60
			$this->_generateBehavior("delete",$this->_deleteBehavior, $js);
61
		if(\is_array($this->_editBehavior))
62
			$this->_generateBehavior("edit",$this->_editBehavior,$js);
63
		return parent::run($js);
64
	}
65
66
67
68
	protected function _generateBehavior($op,$params,JsUtils $js){
69
		if(isset($this->_urls[$op])){
70
			$params=\array_merge($params,["attr"=>"data-ajax"]);
71
			$js->getOnClick("#".$this->identifier." ._".$op, $this->_urls[$op],$this->getTargetSelector(),$params);
72
		}
73
	}
74
75
	/**
76
	 * {@inheritDoc}
77
	 * @see \Ajax\semantic\html\collections\table\TableTrait::getTable()
78
	 */
79
	protected function getTable() {
80
		return $this->content["table"];
81
	}
82
83
84
	public function compile(JsUtils $js=NULL,&$view=NULL){
85
		if(!$this->_generated){
86
			$this->_instanceViewer->setInstance($this->_model);
87
			$captions=$this->_instanceViewer->getCaptions();
88
89
			$table=$this->content["table"];
90
91
			if($this->_hasCheckboxes){
92
				$this->_generateMainCheckbox($captions);
93
			}
94
95
			$table->setRowCount(0, \sizeof($captions));
96
			$this->_generateHeader($table,$captions);
97
98
			if(isset($this->_compileParts))
99
				$table->setCompileParts($this->_compileParts);
100
101
			if(isset($this->_searchField) && isset($js)){
102
				if(isset($this->_urls["refresh"]))
103
					$this->_searchField->postOn("change", $this->_urls["refresh"],"{'s':$(this).val()}","#".$this->identifier." tbody",["preventDefault"=>false,"jqueryDone"=>"replaceWith"]);
104
			}
105
106
			$this->_generateContent($table);
107
108
			if($this->_hasCheckboxes && $table->hasPart("thead")){
109
					$table->getHeader()->getCell(0, 0)->addClass("no-sort");
110
			}
111
112
			if(isset($this->_toolbar)){
113
				$this->_setToolbarPosition($table, $captions);
114
			}
115
			if(isset($this->_pagination) && $this->_pagination->getVisible()){
116
				$this->_generatePagination($table,$js);
117
			}
118
119
			$this->content=JArray::sortAssociative($this->content, [PositionInTable::BEFORETABLE,"table",PositionInTable::AFTERTABLE]);
120
			$this->_compileForm();
121
			if(isset($this->_hiddenColumns))
122
				$this->_hideColumns();
123
			$this->_generated=true;
124
		}
125
		return parent::compile($js,$view);
126
	}
127
128
	protected function _hideColumns(){
129
		$table=$this->getTable();
130
		foreach ($this->_hiddenColumns as $colIndex){
131
			$table->hideColumn($colIndex);
132
		}
133
		return $this;
134
	}
135
136
	protected function _generateHeader(HtmlTable $table,$captions){
137
		$table->setHeaderValues($captions);
138
		if(isset($this->_sortable)){
139
			$table->setSortable($this->_sortable);
140
		}
141
	}
142
143
144
145
	protected function _generateContent($table){
146
		$objects=$this->_modelInstance;
147
		if(isset($this->_pagination)){
148
			$objects=$this->_pagination->getObjects($this->_modelInstance);
149
		}
150
			InstanceViewer::setIndex(0);
151
			$table->fromDatabaseObjects($objects, function($instance) use($table){
152
				return $this->_generateRow($instance, $table);
153
			});
154
		if($table->getRowCount()==0){
155
			$result=$table->addRow();
156
			$result->mergeRow();
157
			$result->setValues([$this->_emptyMessage]);
158
		}
159
	}
160
161
	protected function _generateRow($instance,&$table,$checkedClass=null){
162
		$this->_instanceViewer->setInstance($instance);
163
		InstanceViewer::$index++;
164
		$values= $this->_instanceViewer->getValues();
165
		$id=$this->_instanceViewer->getIdentifier();
166
		if($this->_hasCheckboxes){
167
			$ck=new HtmlCheckbox("ck-".$this->identifier."-".$id,"");
168
			$ck->setOnChange("event.stopPropagation();");
169
			$field=$ck->getField();
170
			$field->setProperty("value",$id);
171
			$field->setProperty("name", "selection[]");
172
			if(isset($checkedClass))
173
				$field->setClass($checkedClass);
174
			\array_unshift($values, $ck);
175
		}
176
		$result=$table->newRow();
177
		$result->setIdentifier($this->identifier."-tr-".$id);
178
		$result->setProperty("data-ajax",$id);
179
		$result->setValues($values);
180
		$result->addToProperty("class",$this->_rowClass);
181
		return $result;
182
	}
183
184
	protected function _generatePagination($table,$js=NULL){
185
		if(isset($this->_toolbar)){
186
			if($this->_toolbarPosition==PositionInTable::FOOTER)
187
				$this->_toolbar->setFloated("left");
188
		}
189
		$footer=$table->getFooter();
190
		$footer->mergeCol();
191
		$menu=new HtmlPaginationMenu("pagination-".$this->identifier,$this->_pagination->getPagesNumbers());
192
		$menu->floatRight();
193
		$menu->setActiveItem($this->_pagination->getPage()-1);
194
		$footer->addValues($menu);
195
		$this->_associatePaginationBehavior($menu,$js);
196
	}
197
198
	protected function _associatePaginationBehavior(HtmlMenu $menu,JsUtils $js=NULL){
199
		if(isset($this->_urls["refresh"])){
200
			$menu->postOnClick($this->_urls["refresh"],"{'p':$(this).attr('data-page')}",$this->getRefreshSelector(),["preventDefault"=>false,"jqueryDone"=>"replaceWith"]);
201
		}
202
	}
203
204
	protected function _getFieldName($index){
205
		return parent::_getFieldName($index)."[]";
206
	}
207
208
	protected function _getFieldCaption($index){
209
		return null;
210
	}
211
212
	protected function _setToolbarPosition($table,$captions=NULL){
213
		switch ($this->_toolbarPosition){
214
			case PositionInTable::BEFORETABLE:
215
			case PositionInTable::AFTERTABLE:
216
				if(isset($this->_compileParts)===false){
217
					$this->content[$this->_toolbarPosition]=$this->_toolbar;
218
				}
219
				break;
220
			case PositionInTable::HEADER:
221
			case PositionInTable::FOOTER:
222
			case PositionInTable::BODY:
223
				$this->addToolbarRow($this->_toolbarPosition,$table, $captions);
224
				break;
225
		}
226
	}
227
228
	/**
229
	 * Associates a $callback function after the compilation of the field at $index position
230
	 * The $callback function can take the following arguments : $field=>the compiled field, $instance : the active instance of the object, $index: the field position
231
	 * @param int $index postion of the compiled field
232
	 * @param callable $callback function called after the field compilation
233
	 * @return DataTable
234
	 */
235
	public function afterCompile($index,$callback){
236
		$this->_instanceViewer->afterCompile($index,$callback);
237
		return $this;
238
	}
239
240
	private function addToolbarRow($part,$table,$captions){
241
		$hasPart=$table->hasPart($part);
242
		if($hasPart){
243
			$row=$table->getPart($part)->addRow(\sizeof($captions));
244
		}else{
245
			$row=$table->getPart($part)->getRow(0);
246
		}
247
		$row->mergeCol();
248
		$row->setValues([$this->_toolbar]);
249
	}
250
251
	public function getHtmlComponent(){
252
		return $this->content["table"];
253
	}
254
255
	public function getUrls() {
256
		return $this->_urls;
257
	}
258
259
	/**
260
	 * Sets the associative array of urls for refreshing, updating or deleting
261
	 * @param string|array $urls associative array with keys refresh: for refreshing with search field or pagination, edit : for updating a row, delete: for deleting a row
262
	 * @return DataTable
263
	 */
264
	public function setUrls($urls) {
265
		if(\is_array($urls)){
266
			$this->_urls["refresh"]=JArray::getValue($urls, "refresh",0);
267
			$this->_urls["edit"]=JArray::getValue($urls, "edit",1);
268
			$this->_urls["delete"]=JArray::getValue($urls, "delete",2);
269
		}else{
270
			$this->_urls=["refresh"=>$urls,"edit"=>$urls,"delete"=>$urls];
271
		}
272
		return $this;
273
	}
274
275
	/**
276
	 * Paginates the DataTable element with a Semantic HtmlPaginationMenu component
277
	 * @param number $page the active page number
278
	 * @param number $total_rowcount the total number of items
279
	 * @param number $items_per_page The number of items per page
280
	 * @param number $pages_visibles The number of visible pages in the Pagination component
281
	 * @return DataTable
282
	 */
283
	public function paginate($page,$total_rowcount,$items_per_page=10,$pages_visibles=null){
284
		$this->_pagination=new Pagination($items_per_page,$pages_visibles,$page,$total_rowcount);
285
		return $this;
286
	}
287
288
	/**
289
	 * Auto Paginates the DataTable element with a Semantic HtmlPaginationMenu component
290
	 * @param number $page the active page number
291
	 * @param number $items_per_page The number of items per page
292
	 * @param number $pages_visibles The number of visible pages in the Pagination component
293
	 * @return DataTable
294
	 */
295
	public function autoPaginate($page=1,$items_per_page=10,$pages_visibles=4){
296
		$this->_pagination=new Pagination($items_per_page,$pages_visibles,$page);
297
		return $this;
298
	}
299
300
301
302
	/**
303
	 * @param array $compileParts
304
	 * @return DataTable
305
	 */
306
	public function refresh($compileParts=["tbody"]){
307
		$this->_compileParts=$compileParts;
308
		return $this;
309
	}
310
311
312
	public function addSearchInToolbar($position=Direction::RIGHT){
313
		return $this->addInToolbar($this->getSearchField())->setPosition($position);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Ajax\common\html\HtmlDoubleElement as the method setPosition() does only exist in the following sub-classes of Ajax\common\html\HtmlDoubleElement: Ajax\bootstrap\html\content\HtmlGridCol, Ajax\semantic\html\colle...menus\HtmlAccordionMenu, Ajax\semantic\html\collections\menus\HtmlIconMenu, Ajax\semantic\html\colle...nus\HtmlLabeledIconMenu, Ajax\semantic\html\collections\menus\HtmlMenu, Ajax\semantic\html\colle...enus\HtmlPaginationMenu, Ajax\semantic\html\content\HtmlAccordionMenuItem, Ajax\semantic\html\content\HtmlDropdownItem, Ajax\semantic\html\content\HtmlMenuItem, Ajax\semantic\html\modules\HtmlPopup. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
314
	}
315
316
	public function getSearchField(){
317
		if(isset($this->_searchField)===false){
318
			$this->_searchField=new HtmlInput("search-".$this->identifier,"search","","Search...");
319
			$this->_searchField->addIcon("search",Direction::RIGHT);
320
		}
321
		return $this->_searchField;
322
	}
323
324
	/**
325
	 * The callback function called after the insertion of each row when fromDatabaseObjects is called
326
	 * callback function takes the parameters $row : the row inserted and $object: the instance of model used
327
	 * @param callable $callback
328
	 * @return DataTable
329
	 */
330
	public function onNewRow($callback) {
331
		$this->content["table"]->onNewRow($callback);
332
		return $this;
333
	}
334
335
	public function asForm(){
336
		return $this->getForm();
337
	}
338
339
340
341
	protected function getTargetSelector() {
342
		$result=$this->_targetSelector;
343
		if(!isset($result))
344
			$result="#".$this->identifier;
345
		return $result;
346
	}
347
348
	/**
349
	 * Sets the response element selector for Edit and Delete request with ajax
350
	 * @param string $_targetSelector
351
	 * @return \Ajax\semantic\widgets\datatable\DataTable
352
	 */
353
	public function setTargetSelector($_targetSelector) {
354
		$this->_targetSelector=$_targetSelector;
355
		return $this;
356
	}
357
358
	public function getRefreshSelector() {
359
		if(isset($this->_refreshSelector))
360
			return $this->_refreshSelector;
361
		return "#".$this->identifier." tbody";
362
	}
363
364
	/**
365
	 * @param string $_refreshSelector
366
	 * @return DataTable
367
	 */
368
	public function setRefreshSelector($_refreshSelector) {
369
		$this->_refreshSelector=$_refreshSelector;
370
		return $this;
371
	}
372
373
	/**
374
	 * {@inheritDoc}
375
	 * @see \Ajax\common\Widget::show()
376
	 */
377
	public function show($modelInstance){
378
		if(\is_array($modelInstance)){
379
			if(\is_array(array_values($modelInstance)[0]))
380
				$modelInstance=\json_decode(\json_encode($modelInstance), FALSE);
381
		}
382
		$this->_modelInstance=$modelInstance;
383
	}
384
385
	public function getRowClass() {
386
		return $this->_rowClass;
387
	}
388
389
	/**
390
	 * Sets the default row class (tr class)
391
	 * @param string $_rowClass
392
	 * @return DataTable
393
	 */
394
	public function setRowClass($_rowClass) {
395
		$this->_rowClass=$_rowClass;
396
		return $this;
397
	}
398
399
	/**
400
	 * Sets the message displayed when there is no record
401
	 * @param mixed $_emptyMessage
402
	 * @return DataTable
403
	 */
404
	public function setEmptyMessage($_emptyMessage) {
405
		$this->_emptyMessage=$_emptyMessage;
406
		return $this;
407
	}
408
409
	public function setSortable($colIndex=NULL) {
410
		$this->_sortable=$colIndex;
411
		return $this;
412
	}
413
414
	public function setActiveRowSelector($class="active",$event="click",$multiple=false){
415
		$this->getTable()->setActiveRowSelector($class,$event,$multiple);
416
		return $this;
417
	}
418
419
	public function hideColumn($colIndex){
420
		if(!\is_array($this->_hiddenColumns))
421
			$this->_hiddenColumns=[];
422
		$this->_hiddenColumns[]=$colIndex;
423
		return $this;
424
	}
425
426
}