Completed
Push — master ( 772d83...24138a )
by Jean-Christophe
10:15
created

DataTable::insertInFieldButton()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 4
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\elements\HtmlButton;
12
use Ajax\semantic\html\base\constants\Direction;
13
use Ajax\service\JArray;
14
use Ajax\semantic\html\base\HtmlSemDoubleElement;
15
use Ajax\semantic\widgets\base\InstanceViewer;
16
use Ajax\semantic\html\collections\table\traits\TableTrait;
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;
27
	protected $_searchField;
28
	protected $_urls;
29
	protected $_pagination;
30
	protected $_hasCheckboxes;
31
	protected $_compileParts;
32
	protected $_hasDelete=false;
33
	protected $_hasEdit=false;
34
	protected $_visibleHover=false;
35
	protected $_targetSelector;
36
37
	public function __construct($identifier,$model,$modelInstance=NULL) {
38
		parent::__construct($identifier, $model,$modelInstance);
39
		$this->_init(new InstanceViewer($identifier), "table", new HtmlTable($identifier, 0,0), false);
40
		$this->_urls=[];
41
	}
42
43
	public function run(JsUtils $js){
44
		if($this->_hasCheckboxes && isset($js)){
45
			$js->execOn("change", "#".$this->identifier." [name='selection[]']", "
46
					var \$parentCheckbox=\$('#ck-main-ck-{$this->identifier}'),\$checkbox=\$('#{$this->identifier} [name=\"selection[]\"]'),allChecked=true,allUnchecked=true;
47
					\$checkbox.each(function() {if($(this).prop('checked')){allUnchecked = false;}else{allChecked = false;}});
48
					if(allChecked) {\$parentCheckbox.checkbox('set checked');}else if(allUnchecked){\$parentCheckbox.checkbox('set unchecked');}else{\$parentCheckbox.checkbox('set indeterminate');}");
49
		}
50
		if($this->_visibleHover){
51
			$js->execOn("mouseover", "#".$this->identifier." tr", "$(event.target).closest('tr').find('.visibleover').css('visibility', 'visible');",["preventDefault"=>false,"stopPropagation"=>true]);
52
			$js->execOn("mouseout", "#".$this->identifier." tr", "$(event.target).closest('tr').find('.visibleover').css('visibility', 'hidden');",["preventDefault"=>false,"stopPropagation"=>true]);
53
		}
54
		if($this->_hasDelete)
55
			$this->_generateBehavior("delete", $js);
56
		if($this->_hasEdit)
57
			$this->_generateBehavior("edit", $js);
58
		return parent::run($js);
59
	}
60
61
	protected function _generateBehavior($op,JsUtils $js){
62
		if(isset($this->_urls[$op]))
63
			$js->getOnClick("#".$this->identifier." .".$op, $this->_urls[$op],$this->getTargetSelector(),["preventDefault"=>false,"attr"=>"data-ajax"]);
64
	}
65
66
	/**
67
	 * {@inheritDoc}
68
	 * @see \Ajax\semantic\html\collections\table\TableTrait::getTable()
69
	 */
70
	protected function getTable() {
71
		return $this->content["table"];
72
	}
73
74
75
	public function compile(JsUtils $js=NULL,&$view=NULL){
76
		if(!$this->_generated){
77
			$this->_instanceViewer->setInstance($this->_model);
78
			$captions=$this->_instanceViewer->getCaptions();
79
80
			$table=$this->content["table"];
81
82
			if($this->_hasCheckboxes){
83
				$this->_generateMainCheckbox($captions);
84
			}
85
86
			$table->setRowCount(0, \sizeof($captions));
87
			$table->setHeaderValues($captions);
88
			if(isset($this->_compileParts))
89
				$table->setCompileParts($this->_compileParts);
90
91
			if(isset($this->_searchField) && isset($js)){
92
				if(isset($this->_urls["refresh"]))
93
					$this->_searchField->postOn("change", $this->_urls["refresh"],"{'s':$(this).val()}","#".$this->identifier." tbody",["preventDefault"=>false,"jqueryDone"=>"replaceWith"]);
94
			}
95
96
			$this->_generateContent($table);
97
98
			if($this->_hasCheckboxes && $table->hasPart("thead")){
99
					$table->getHeader()->getCell(0, 0)->addToProperty("class","no-sort");
100
			}
101
102
			if(isset($this->_pagination) && $this->_pagination->getVisible()){
103
				$this->_generatePagination($table);
104
			}
105
			if(isset($this->_toolbar)){
106
				$this->_setToolbarPosition($table, $captions);
107
			}
108
			$this->content=JArray::sortAssociative($this->content, [PositionInTable::BEFORETABLE,"table",PositionInTable::AFTERTABLE]);
109
			$this->_compileForm();
110
			$this->_generated=true;
111
		}
112
		return parent::compile($js,$view);
113
	}
114
115
	private function _generateMainCheckbox(&$captions){
116
		$ck=new HtmlCheckbox("main-ck-".$this->identifier,"");
117
		$ck->setOnChecked("$('#".$this->identifier." [name=%quote%selection[]%quote%]').prop('checked',true);");
118
		$ck->setOnUnchecked("$('#".$this->identifier." [name=%quote%selection[]%quote%]').prop('checked',false);");
119
		\array_unshift($captions, $ck);
120
	}
121
122
	protected function _generateContent($table){
123
		$objects=$this->_modelInstance;
124
		if(isset($this->_pagination)){
125
			$objects=$this->_pagination->getObjects($this->_modelInstance);
126
		}
127
		InstanceViewer::setIndex(0);
128
		$table->fromDatabaseObjects($objects, function($instance) use($table){
129
			$this->_instanceViewer->setInstance($instance);
130
			InstanceViewer::$index++;
131
			$values= $this->_instanceViewer->getValues();
132
			if($this->_hasCheckboxes){
133
				$ck=new HtmlCheckbox("ck-".$this->identifier,"");
134
				$field=$ck->getField();
135
				$field->setProperty("value",$this->_instanceViewer->getIdentifier());
136
				$field->setProperty("name", "selection[]");
137
				\array_unshift($values, $ck);
138
			}
139
			$result=$table->newRow();
140
			$result->setIdentifier($this->identifier."-tr-".$this->_instanceViewer->getIdentifier());
141
			$result->setValues($values);
142
			return $result;
143
		});
144
	}
145
146
	private function _generatePagination($table){
147
		$footer=$table->getFooter();
148
		$footer->mergeCol();
149
		$menu=new HtmlPaginationMenu("pagination-".$this->identifier,$this->_pagination->getPagesNumbers());
150
		$menu->floatRight();
151
		$menu->setActiveItem($this->_pagination->getPage()-1);
152
		$footer->setValues($menu);
153
		if(isset($this->_urls["refresh"]))
154
			$menu->postOnClick($this->_urls["refresh"],"{'p':$(this).attr('data-page')}","#".$this->identifier." tbody",["preventDefault"=>false,"jqueryDone"=>"replaceWith"]);
155
	}
156
157
	protected function _getFieldName($index){
158
		return parent::_getFieldName($index)."[]";
159
	}
160
161
	protected function _getFieldCaption($index){
162
		return null;
163
	}
164
165
	protected function _setToolbarPosition($table,$captions=NULL){
166
		switch ($this->_toolbarPosition){
167
			case PositionInTable::BEFORETABLE:
168
			case PositionInTable::AFTERTABLE:
169
				if(isset($this->_compileParts)===false){
170
					$this->content[$this->_toolbarPosition]=$this->_toolbar;
171
				}
172
				break;
173
			case PositionInTable::HEADER:
174
			case PositionInTable::FOOTER:
175
			case PositionInTable::BODY:
176
				$this->addToolbarRow($this->_toolbarPosition,$table, $captions);
177
				break;
178
		}
179
	}
180
181
	/**
182
	 * Associates a $callback function after the compilation of the field at $index position
183
	 * The $callback function can take the following arguments : $field=>the compiled field, $instance : the active instance of the object, $index: the field position
184
	 * @param int $index postion of the compiled field
185
	 * @param callable $callback function called after the field compilation
186
	 * @return \Ajax\semantic\widgets\datatable\DataTable
187
	 */
188
	public function afterCompile($index,$callback){
189
		$this->_instanceViewer->afterCompile($index,$callback);
190
		return $this;
191
	}
192
193
	private function addToolbarRow($part,$table,$captions){
194
		$hasPart=$table->hasPart($part);
195
		if($hasPart){
196
			$row=$table->getPart($part)->addRow(\sizeof($captions));
197
		}else{
198
			$row=$table->getPart($part)->getRow(0);
199
		}
200
		$row->mergeCol();
201
		$row->setValues([$this->_toolbar]);
202
	}
203
204
	public function getHtmlComponent(){
205
		return $this->content["table"];
206
	}
207
208
	public function getUrls() {
209
		return $this->_urls;
210
	}
211
212
	/**
213
	 * Sets the associative array of urls for refreshing, updating or deleting
214
	 * @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
215
	 * @return \Ajax\semantic\widgets\datatable\DataTable
216
	 */
217
	public function setUrls($urls) {
218
		if(\is_array($urls)){
219
			$this->_urls["refresh"]=JArray::getValue($urls, "refresh",0);
220
			$this->_urls["edit"]=JArray::getValue($urls, "edit",1);
221
			$this->_urls["delete"]=JArray::getValue($urls, "delete",2);
222
		}else{
223
			$this->_urls=["refresh"=>$urls,"edit"=>$urls,"delete"=>$urls];
224
		}
225
		return $this;
226
	}
227
228
	public function paginate($items_per_page=10,$page=1){
229
		$this->_pagination=new Pagination($items_per_page,4,$page);
230
	}
231
232
	public function getHasCheckboxes() {
233
		return $this->_hasCheckboxes;
234
	}
235
236
	public function setHasCheckboxes($_hasCheckboxes) {
237
		$this->_hasCheckboxes=$_hasCheckboxes;
238
		return $this;
239
	}
240
241
	public function refresh($compileParts=["tbody"]){
242
		$this->_compileParts=$compileParts;
243
		return $this;
244
	}
245
	/**
246
	 * @param string $caption
247
	 * @param callable $callback
248
	 * @param boolean $visibleHover
249
	 * @return callable
250
	 */
251
	private function getFieldButtonCallable($caption,$visibleHover=true,$callback=null){
252
		return $this->getCallable("getFieldButton",[$caption,$visibleHover],$callback);
253
	}
254
255
	/**
256
	 * @param callable $thisCallback
257
	 * @param array $parameters
258
	 * @param callable $callback
259
	 * @return callable
260
	 */
261
	private function getCallable($thisCallback,$parameters,$callback=null){
262
		$result=function($instance) use($thisCallback,$parameters,$callback){
263
			$object=call_user_func_array(array($this,$thisCallback), $parameters);
264
			if(isset($callback)){
265
				if(\is_callable($callback)){
266
					$callback($object,$instance);
267
				}
268
			}
269
			if($object instanceof HtmlSemDoubleElement){
270
				$object->setProperty("data-ajax",$this->_instanceViewer->getIdentifier());
271
				if($object->propertyContains("class","visibleover")){
272
					$this->_visibleHover=true;
273
					$object->setProperty("style","visibility:hidden;");
274
				}
275
			}
276
			return $object;
277
		};
278
		return $result;
279
	}
280
281
	/**
282
	 * @param string $caption
283
	 * @return HtmlButton
284
	 */
285
	private function getFieldButton($caption,$visibleHover=true){
286
		$bt= new HtmlButton("",$caption);
287
		if($visibleHover)
288
			$this->_visibleOver($bt);
289
		return $bt;
290
	}
291
292
	/**
293
	 * Inserts a new Button for each row
294
	 * @param string $caption
295
	 * @param callable $callback
296
	 * @param boolean $visibleHover
297
	 * @return \Ajax\semantic\widgets\datatable\DataTable
298
	 */
299
	public function addFieldButton($caption,$visibleHover=true,$callback=null){
300
		$this->addField($this->getCallable("getFieldButton",[$caption,$visibleHover],$callback));
301
		return $this;
302
	}
303
304
	/**
305
	 * Inserts a new Button for each row at col $index
306
	 * @param int $index
307
	 * @param string $caption
308
	 * @param callable $callback
309
	 * @return \Ajax\semantic\widgets\datatable\DataTable
310
	 */
311
	public function insertFieldButton($index,$caption,$visibleHover=true,$callback=null){
312
		$this->insertField($index, $this->getFieldButtonCallable($caption,$visibleHover,$callback));
313
		return $this;
314
	}
315
316
	/**
317
	 * Inserts a new Button for each row in col at $index
318
	 * @param int $index
319
	 * @param string $caption
320
	 * @param callable $callback
321
	 * @return \Ajax\semantic\widgets\datatable\DataTable
322
	 */
323
	public function insertInFieldButton($index,$caption,$visibleHover=true,$callback=null){
324
		$this->insertInField($index, $this->getFieldButtonCallable($caption,$visibleHover,$callback));
325
		return $this;
326
	}
327
328
	private function addDefaultButton($icon,$class=null,$visibleHover=true,$callback=null){
329
		$this->addField($this->getCallable("getDefaultButton",[$icon,$class,$visibleHover],$callback));
330
		return $this;
331
	}
332
333
	private function insertDefaultButtonIn($index,$icon,$class=null,$visibleHover=true,$callback=null){
334
		$this->insertInField($index,$this->getCallable("getDefaultButton",[$icon,$class,$visibleHover],$callback));
335
		return $this;
336
	}
337
338
	private function getDefaultButton($icon,$class=null,$visibleHover=true){
339
		$bt=$this->getFieldButton("",$visibleHover);
340
		$bt->asIcon($icon);
341
		if(isset($class))
342
			$bt->addToProperty("class", $class);
343
		return $bt;
344
	}
345
346
	public function addDeleteButton($visibleHover=true,$generateBehavior=true,$callback=null){
347
		$this->_hasDelete=$generateBehavior;
348
		return $this->addDefaultButton("remove","delete red basic",$visibleHover,$callback);
349
	}
350
351
	public function addEditButton($visibleHover=true,$generateBehavior=true,$callback=null){
352
		$this->_hasEdit=$generateBehavior;
353
		return $this->addDefaultButton("edit","edit basic",$visibleHover,$callback);
354
	}
355
356
	public function addEditDeleteButtons($visibleHover=true,$generateBehavior=true,$callbackEdit=null,$callbackDelete=null){
357
		$this->addEditButton($visibleHover,$generateBehavior,$callbackEdit);
358
		$index=$this->_instanceViewer->visiblePropertiesCount()-1;
359
		$this->insertDeleteButtonIn($index,$visibleHover,$generateBehavior,$callbackDelete);
360
		return $this;
361
	}
362
363
	public function insertDeleteButtonIn($index,$visibleHover=true,$generateBehavior=true,$callback=null){
364
		$this->_hasDelete=$generateBehavior;
365
		return $this->insertDefaultButtonIn($index,"remove","delete red basic",$visibleHover,$callback);
366
	}
367
368
	public function insertEditButtonIn($index,$visibleHover=true,$generateBehavior=true,$callback=null){
369
		$this->_hasEdit=$generateBehavior;
370
		return $this->insertDefaultButtonIn($index,"edit","edit basic",$visibleHover,$callback);
371
	}
372
373
	public function addSearchInToolbar($position=Direction::RIGHT){
374
		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...
375
	}
376
377
	public function getSearchField(){
378
		if(isset($this->_searchField)===false){
379
			$this->_searchField=new HtmlInput("search-".$this->identifier,"search","","Search...");
380
			$this->_searchField->addIcon("search",Direction::RIGHT);
381
		}
382
		return $this->_searchField;
383
	}
384
385
	/**
386
	 * The callback function called after the insertion of each row when fromDatabaseObjects is called
387
	 * callback function takes the parameters $row : the row inserted and $object: the instance of model used
388
	 * @param callable $callback
389
	 * @return DataTable
390
	 */
391
	public function onNewRow($callback) {
392
		$this->content["table"]->onNewRow($callback);
393
		return $this;
394
	}
395
396
	public function asForm(){
397
		return $this->getForm();
398
	}
399
400
	/**
401
	 * Creates a submit button at $index position
402
	 * @param int $index
403
	 * @param string $cssStyle
404
	 * @param string $url
405
	 * @param string $responseElement
406
	 * @param array $attributes
407
	 * @return \Ajax\semantic\widgets\datatable\DataTable
408
	 */
409
	public function fieldAsSubmit($index,$cssStyle=NULL,$url=NULL,$responseElement=NULL,$attributes=NULL){
410
		return $this->_fieldAs(function($id,$name,$value,$caption) use ($url,$responseElement,$cssStyle,$index,$attributes){
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->_fieldAs(f..., $index, $attributes); (Ajax\semantic\widgets\datatable\DataTable) is incompatible with the return type of the parent method Ajax\common\Widget::fieldAsSubmit of type Ajax\semantic\widgets\base\FieldAsTrait.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
Unused Code introduced by
The parameter $caption is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
411
			$button=new HtmlButton($id,$value,$cssStyle);
412
			$button->postOnClick($url,"$(event.target).closest('tr').find(':input').serialize()",$responseElement,$attributes["ajax"]);
413
			if(!isset($attributes["visibleHover"]) || $attributes["visibleHover"])
414
				$this->_visibleOver($button);
415
			return $button;
416
		}, $index,$attributes);
417
	}
418
419
	protected function _visibleOver($element){
420
		$this->_visibleHover=true;
421
		return $element->addToProperty("class", "visibleover")->setProperty("style","visibility:hidden;");
422
	}
423
424
	protected function getTargetSelector() {
425
		$result=$this->_targetSelector;
426
		if(!isset($result))
427
			$result="#".$this->identifier;
428
		return $result;
429
	}
430
431
	/**
432
	 * Sets the response element selector for Edit and Delete request with ajax
433
	 * @param string $_targetSelector
434
	 * @return \Ajax\semantic\widgets\datatable\DataTable
435
	 */
436
	public function setTargetSelector($_targetSelector) {
437
		$this->_targetSelector=$_targetSelector;
438
		return $this;
439
	}
440
441
}