Completed
Push — master ( 3abce3...7abd10 )
by Jean-Christophe
03:33
created

DataTable::insertDeleteButtonIn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 3
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 $_visibleHover=false;
33
34
	public function run(JsUtils $js){
35
		if($this->_hasCheckboxes && isset($js)){
36
			$js->execOn("change", "#".$this->identifier." [name='selection[]']", "
37
		var \$parentCheckbox=\$('#ck-main-ck-{$this->identifier}'),\$checkbox=\$('#{$this->identifier} [name=\"selection[]\"]'),allChecked=true,allUnchecked=true;
38
		\$checkbox.each(function() {if($(this).prop('checked')){allUnchecked = false;}else{allChecked = false;}});
39
		if(allChecked) {\$parentCheckbox.checkbox('set checked');}else if(allUnchecked){\$parentCheckbox.checkbox('set unchecked');}else{\$parentCheckbox.checkbox('set indeterminate');}");
40
		}
41
		if($this->_visibleHover){
42
			$js->execOn("mouseover", "#".$this->identifier." tr", "$(event.target).closest('tr').find('.visibleover').css('visibility', 'visible');",["preventDefault"=>false,"stopPropagation"=>true]);
43
			$js->execOn("mouseout", "#".$this->identifier." tr", "$(event.target).closest('tr').find('.visibleover').css('visibility', 'hidden');",["preventDefault"=>false,"stopPropagation"=>true]);
44
		}
45
		return parent::run($js);
46
	}
47
48
	public function __construct($identifier,$model,$modelInstance=NULL) {
49
		parent::__construct($identifier, $model,$modelInstance);
50
		$this->_init(new InstanceViewer($identifier), "table", new HtmlTable($identifier, 0,0), false);
51
	}
52
53
	/**
54
	 * {@inheritDoc}
55
	 * @see \Ajax\semantic\html\collections\table\TableTrait::getTable()
56
	 */
57
	protected function getTable() {
58
		return $this->content["table"];
59
	}
60
61
62
	public function compile(JsUtils $js=NULL,&$view=NULL){
63
		$this->_instanceViewer->setInstance($this->_model);
64
		$captions=$this->_instanceViewer->getCaptions();
65
66
		$table=$this->content["table"];
67
68
		if($this->_hasCheckboxes){
69
			$this->_generateMainCheckbox($captions);
70
		}
71
72
		$table->setRowCount(0, \sizeof($captions));
73
		$table->setHeaderValues($captions);
74
		if(isset($this->_compileParts))
75
			$table->setCompileParts($this->_compileParts);
76
		if(isset($this->_searchField) && isset($js)){
77
			$this->_searchField->postOn("change", $this->_urls,"{'s':$(this).val()}","#".$this->identifier." tbody",["preventDefault"=>false,"jqueryDone"=>"replaceWith"]);
78
		}
79
80
		$this->_generateContent($table);
81
82
		if($this->_hasCheckboxes && $table->hasPart("thead")){
83
				$table->getHeader()->getCell(0, 0)->addToProperty("class","no-sort");
84
		}
85
86
		if(isset($this->_pagination) && $this->_pagination->getVisible()){
87
			$this->_generatePagination($table);
88
		}
89
		if(isset($this->_toolbar)){
90
			$this->_setToolbarPosition($table, $captions);
91
		}
92
		$this->content=JArray::sortAssociative($this->content, [PositionInTable::BEFORETABLE,"table",PositionInTable::AFTERTABLE]);
93
		$this->_compileForm();
94
		return parent::compile($js,$view);
95
	}
96
97
	private function _generateMainCheckbox(&$captions){
98
		$ck=new HtmlCheckbox("main-ck-".$this->identifier,"");
99
		$ck->setOnChecked("$('#".$this->identifier." [name=%quote%selection[]%quote%]').prop('checked',true);");
100
		$ck->setOnUnchecked("$('#".$this->identifier." [name=%quote%selection[]%quote%]').prop('checked',false);");
101
		\array_unshift($captions, $ck);
102
	}
103
104
	protected function _generateContent($table){
105
		$objects=$this->_modelInstance;
106
		if(isset($this->_pagination)){
107
			$objects=$this->_pagination->getObjects($this->_modelInstance);
108
		}
109
		InstanceViewer::setIndex(0);
110
		$table->fromDatabaseObjects($objects, function($instance) use($table){
111
			$this->_instanceViewer->setInstance($instance);
112
			InstanceViewer::$index++;
113
			$values= $this->_instanceViewer->getValues();
114
			if($this->_hasCheckboxes){
115
				$ck=new HtmlCheckbox("ck-".$this->identifier,"");
116
				$field=$ck->getField();
117
				$field->setProperty("value",$this->_instanceViewer->getIdentifier());
118
				$field->setProperty("name", "selection[]");
119
				\array_unshift($values, $ck);
120
			}
121
			$result=$table->newRow();
122
			$result->setIdentifier($this->identifier."-tr-".$this->_instanceViewer->getIdentifier());
123
			$result->setValues($values);
124
			return $result;
125
		});
126
	}
127
128
	private function _generatePagination($table){
129
		$footer=$table->getFooter();
130
		$footer->mergeCol();
131
		$menu=new HtmlPaginationMenu("pagination-".$this->identifier,$this->_pagination->getPagesNumbers());
132
		$menu->floatRight();
133
		$menu->setActiveItem($this->_pagination->getPage()-1);
134
		$footer->setValues($menu);
135
		$menu->postOnClick($this->_urls,"{'p':$(this).attr('data-page')}","#".$this->identifier." tbody",["preventDefault"=>false,"jqueryDone"=>"replaceWith"]);
136
	}
137
138
	protected function _getFieldName($index){
139
		return parent::_getFieldName($index)."[]";
140
	}
141
142
	protected function _getFieldCaption($index){
143
		return null;
144
	}
145
146
	protected function _setToolbarPosition($table,$captions=NULL){
147
		switch ($this->_toolbarPosition){
148
			case PositionInTable::BEFORETABLE:
149
			case PositionInTable::AFTERTABLE:
150
				if(isset($this->_compileParts)===false){
151
					$this->content[$this->_toolbarPosition]=$this->_toolbar;
152
				}
153
				break;
154
			case PositionInTable::HEADER:
155
			case PositionInTable::FOOTER:
156
			case PositionInTable::BODY:
157
				$this->addToolbarRow($this->_toolbarPosition,$table, $captions);
158
				break;
159
		}
160
	}
161
162
	/**
163
	 * Associates a $callback function after the compilation of the field at $index position
164
	 * The $callback function can take the following arguments : $field=>the compiled field, $instance : the active instance of the object, $index: the field position
165
	 * @param int $index postion of the compiled field
166
	 * @param callable $callback function called after the field compilation
167
	 * @return \Ajax\semantic\widgets\datatable\DataTable
168
	 */
169
	public function afterCompile($index,$callback){
170
		$this->_instanceViewer->afterCompile($index,$callback);
171
		return $this;
172
	}
173
174
	private function addToolbarRow($part,$table,$captions){
175
		$hasPart=$table->hasPart($part);
176
		if($hasPart){
177
			$row=$table->getPart($part)->addRow(\sizeof($captions));
178
		}else{
179
			$row=$table->getPart($part)->getRow(0);
180
		}
181
		$row->mergeCol();
182
		$row->setValues([$this->_toolbar]);
183
	}
184
185
	public function getHtmlComponent(){
186
		return $this->content["table"];
187
	}
188
189
	public function getUrls() {
190
		return $this->_urls;
191
	}
192
193
	public function setUrls($urls) {
194
		$this->_urls=$urls;
195
		return $this;
196
	}
197
198
	public function paginate($items_per_page=10,$page=1){
199
		$this->_pagination=new Pagination($items_per_page,4,$page);
200
	}
201
202
	public function getHasCheckboxes() {
203
		return $this->_hasCheckboxes;
204
	}
205
206
	public function setHasCheckboxes($_hasCheckboxes) {
207
		$this->_hasCheckboxes=$_hasCheckboxes;
208
		return $this;
209
	}
210
211
	public function refresh($compileParts=["tbody"]){
212
		$this->_compileParts=$compileParts;
213
		return $this;
214
	}
215
	/**
216
	 * @param string $caption
217
	 * @param callable $callback
218
	 * @param boolean $visibleHover
219
	 * @return callable
220
	 */
221
	private function getFieldButtonCallable($caption,$visibleHover=true,$callback=null){
222
		return $this->getCallable("getFieldButton",[$caption,$visibleHover],$callback);
223
	}
224
225
	/**
226
	 * @param callable $thisCallback
227
	 * @param array $parameters
228
	 * @param callable $callback
229
	 * @return callable
230
	 */
231
	private function getCallable($thisCallback,$parameters,$callback=null){
232
		$result=function($instance) use($thisCallback,$parameters,$callback){
233
			$object=call_user_func_array(array($this,$thisCallback), $parameters);
234
			if(isset($callback)){
235
				if(\is_callable($callback)){
236
					$callback($object,$instance);
237
				}
238
			}
239
			if($object instanceof HtmlSemDoubleElement){
240
				$object->setProperty("data-ajax",$this->_instanceViewer->getIdentifier());
241
				if($object->propertyContains("class","visibleover")){
0 ignored issues
show
Bug introduced by
The method propertyContains() cannot be called from this context as it is declared protected in class Ajax\common\html\BaseHtml.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
242
					$this->_visibleHover=true;
243
					$object->setProperty("style","visibility:hidden;");
244
				}
245
			}
246
			return $object;
247
		};
248
		return $result;
249
	}
250
251
	/**
252
	 * @param string $caption
253
	 * @return HtmlButton
254
	 */
255
	private function getFieldButton($caption,$visibleHover=true){
256
		$bt= new HtmlButton("",$caption);
257
		if($visibleHover)
258
			$this->_visibleOver($bt);
259
		return $bt;
260
	}
261
262
	/**
263
	 * Inserts a new Button for each row
264
	 * @param string $caption
265
	 * @param callable $callback
266
	 * @param boolean $visibleHover
267
	 * @return \Ajax\semantic\widgets\datatable\DataTable
268
	 */
269
	public function addFieldButton($caption,$visibleHover=true,$callback=null){
270
		$this->addField($this->getCallable("getFieldButton",[$caption,$visibleHover],$callback));
271
		return $this;
272
	}
273
274
	/**
275
	 * Inserts a new Button for each row at col $index
276
	 * @param int $index
277
	 * @param string $caption
278
	 * @param callable $callback
279
	 * @return \Ajax\semantic\widgets\datatable\DataTable
280
	 */
281
	public function insertFieldButton($index,$caption,$visibleHover=true,$callback=null){
282
		$this->insertField($index, $this->getFieldButtonCallable($caption,$visibleHover,$callback));
283
		return $this;
284
	}
285
286
	/**
287
	 * Inserts a new Button for each row in col at $index
288
	 * @param int $index
289
	 * @param string $caption
290
	 * @param callable $callback
291
	 * @return \Ajax\semantic\widgets\datatable\DataTable
292
	 */
293
	public function insertInFieldButton($index,$caption,$visibleHover=true,$callback=null){
294
		$this->insertInField($index, $this->getFieldButtonCallable($caption,$visibleHover,$callback));
295
		return $this;
296
	}
297
298
	private function addDefaultButton($icon,$class=null,$visibleHover=true,$callback=null){
299
		$this->addField($this->getCallable("getDefaultButton",[$icon,$class],$visibleHover,$callback));
0 ignored issues
show
Documentation introduced by
$visibleHover is of type boolean, but the function expects a callable|null.

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...
Unused Code introduced by
The call to DataTable::getCallable() has too many arguments starting with $callback.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
300
		return $this;
301
	}
302
303
	private function insertDefaultButtonIn($index,$icon,$class=null,$visibleHover=true,$callback=null){
304
		$this->insertInField($index,$this->getCallable("getDefaultButton",[$icon,$class,$visibleHover],$callback));
305
		return $this;
306
	}
307
308
	private function getDefaultButton($icon,$class=null,$visibleHover=true){
309
		$bt=$this->getFieldButton("",$visibleHover);
310
		$bt->asIcon($icon);
311
		if(isset($class))
312
			$bt->addToProperty("class", $class);
313
		return $bt;
314
	}
315
316
	public function addDeleteButton($visibleHover=true,$callback=null){
317
		return $this->addDefaultButton("remove","delete red basic",$visibleHover,$callback);
318
	}
319
320
	public function addEditButton($visibleHover=true,$callback=null){
321
		return $this->addDefaultButton("edit","edit basic",$visibleHover,$callback);
322
	}
323
324
	public function addEditDeleteButtons($visibleHover=true,$callbackEdit=null,$callbackDelete=null){
325
		$this->addEditButton($visibleHover,$callbackEdit);
326
		$index=$this->_instanceViewer->visiblePropertiesCount()-1;
327
		$this->insertDeleteButtonIn($index,$visibleHover,$callbackDelete);
328
		return $this;
329
	}
330
331
	public function insertDeleteButtonIn($index,$visibleHover=true,$callback=null){
332
		return $this->insertDefaultButtonIn($index,"remove","delete red basic",$visibleHover,$callback);
333
	}
334
335
	public function insertEditButtonIn($index,$visibleHover=true,$callback=null){
336
		return $this->insertDefaultButtonIn($index,"edit","edit basic",$visibleHover,$callback);
337
	}
338
339
	public function addSearchInToolbar($position=Direction::RIGHT){
340
		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...
341
	}
342
343
	public function getSearchField(){
344
		if(isset($this->_searchField)===false){
345
			$this->_searchField=new HtmlInput("search-".$this->identifier,"search","","Search...");
346
			$this->_searchField->addIcon("search",Direction::RIGHT);
347
		}
348
		return $this->_searchField;
349
	}
350
351
	/**
352
	 * The callback function called after the insertion of each row when fromDatabaseObjects is called
353
	 * callback function takes the parameters $row : the row inserted and $object: the instance of model used
354
	 * @param callable $callback
355
	 * @return DataTable
356
	 */
357
	public function onNewRow($callback) {
358
		$this->content["table"]->onNewRow($callback);
359
		return $this;
360
	}
361
362
	public function asForm(){
363
		return $this->getForm();
364
	}
365
366 View Code Duplication
	public function fieldAsSubmit($index,$cssStyle=NULL,$url=NULL,$responseElement=NULL,$attributes=NULL){
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
367
		return $this->_fieldAs(function($id,$name,$value,$caption) use ($url,$responseElement,$cssStyle,$index){
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...
368
			$button=new HtmlButton($id,$value,$cssStyle);
369
			$button->postOnClick($url,"$(event.target).closest('tr').find(':input').serialize()",$responseElement);
370
			return $this->_visibleOver($button);
371
		}, $index,$attributes);
372
	}
373
374
	protected function _visibleOver($element){
375
		$this->_visibleHover=true;
376
		return $element->addToProperty("class", "visibleover")->setProperty("style","visibility:hidden;");
377
	}
378
}