Completed
Push — master ( ca057c...baf19c )
by Jean-Christophe
03:05
created

DataTable::getDefaultButton()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
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\widgets\base\InstanceViewer;
15
use Ajax\semantic\html\collections\table\traits\TableTrait;
16
use Ajax\semantic\html\elements\HtmlLabel;
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;
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 $_hasCheckedMessage=false;
36
	protected $_targetSelector;
37
	protected $_checkedMessage;
38
39
	public function __construct($identifier,$model,$modelInstance=NULL) {
40
		parent::__construct($identifier, $model,$modelInstance);
41
		$this->_init(new InstanceViewer($identifier), "table", new HtmlTable($identifier, 0,0), false);
42
		$this->_urls=[];
43
	}
44
45
	public function run(JsUtils $js){
46
		if($this->_hasCheckboxes && isset($js)){
47
			$this->_runCheckboxes($js);
48
		}
49
		if($this->_visibleHover){
50
			$js->execOn("mouseover", "#".$this->identifier." tr", "$(event.target).closest('tr').find('.visibleover').css('visibility', 'visible');",["preventDefault"=>false,"stopPropagation"=>true]);
51
			$js->execOn("mouseout", "#".$this->identifier." tr", "$(event.target).closest('tr').find('.visibleover').css('visibility', 'hidden');",["preventDefault"=>false,"stopPropagation"=>true]);
52
		}
53
		if($this->_hasDelete)
54
			$this->_generateBehavior("delete", $js);
55
		if($this->_hasEdit)
56
			$this->_generateBehavior("edit", $js);
57
		return parent::run($js);
58
	}
59
60
	protected function _runCheckboxes(JsUtils $js){
61
		$checkedMessageCall="";
62
		if($this->_hasCheckedMessage){
63
			$msg=$this->getCheckedMessage();
64
			$checkedMessageFunction="function updateChecked(){var msg='".$msg[0]."',count=\$('#{$this->identifier} [name=\"selection[]\"]:checked').length,all=\$('#{$this->identifier} [name=\"selection[]\"]').length;
65
			if(count==1) msg='".$msg[1]."';
66
						else if(count>1) msg='".$msg["other"]."';
67
						\$('#checked-count-".$this->identifier."').contents().filter(function() {return this.nodeType == 3;}).each(function(){this.textContent = msg.replace('{count}',count);});
68
								\$('#toolbar-{$this->identifier} .visibleOnChecked').toggle(count>0);}\$('#toolbar-".$this->identifier." .visibleOnChecked').hide();";
69
			$checkedMessageCall="updateChecked();";
70
			$js->exec($checkedMessageFunction,true);
71
		}
72
		$js->execOn("change", "#".$this->identifier." [name='selection[]']", "
73
				var \$parentCheckbox=\$('#ck-main-ck-{$this->identifier}'),\$checkbox=\$('#{$this->identifier} [name=\"selection[]\"]'),allChecked=true,allUnchecked=true;
74
				\$checkbox.each(function() {if($(this).prop('checked')){allUnchecked = false;}else{allChecked = false;}});
75
				if(allChecked) {\$parentCheckbox.checkbox('set checked');}else if(allUnchecked){\$parentCheckbox.checkbox('set unchecked');}else{\$parentCheckbox.checkbox('set indeterminate');};".$checkedMessageCall);
76
	}
77
78
	protected function _generateBehavior($op,JsUtils $js){
79
		if(isset($this->_urls[$op]))
80
			$js->getOnClick("#".$this->identifier." .".$op, $this->_urls[$op],$this->getTargetSelector(),["preventDefault"=>false,"attr"=>"data-ajax"]);
81
	}
82
83
	/**
84
	 * {@inheritDoc}
85
	 * @see \Ajax\semantic\html\collections\table\TableTrait::getTable()
86
	 */
87
	protected function getTable() {
88
		return $this->content["table"];
89
	}
90
91
92
	public function compile(JsUtils $js=NULL,&$view=NULL){
93
		if(!$this->_generated){
94
			$this->_instanceViewer->setInstance($this->_model);
95
			$captions=$this->_instanceViewer->getCaptions();
96
97
			$table=$this->content["table"];
98
99
			if($this->_hasCheckboxes){
100
				$this->_generateMainCheckbox($captions);
101
			}
102
103
			$table->setRowCount(0, \sizeof($captions));
104
			$table->setHeaderValues($captions);
105
			if(isset($this->_compileParts))
106
				$table->setCompileParts($this->_compileParts);
107
108
			if(isset($this->_searchField) && isset($js)){
109
				if(isset($this->_urls["refresh"]))
110
					$this->_searchField->postOn("change", $this->_urls["refresh"],"{'s':$(this).val()}","#".$this->identifier." tbody",["preventDefault"=>false,"jqueryDone"=>"replaceWith"]);
111
			}
112
113
			$this->_generateContent($table);
114
115
			if($this->_hasCheckboxes && $table->hasPart("thead")){
116
					$table->getHeader()->getCell(0, 0)->addClass("no-sort");
117
			}
118
119
			if(isset($this->_pagination) && $this->_pagination->getVisible()){
120
				$this->_generatePagination($table);
121
			}
122
			if(isset($this->_toolbar)){
123
				$this->_setToolbarPosition($table, $captions);
124
			}
125
			$this->content=JArray::sortAssociative($this->content, [PositionInTable::BEFORETABLE,"table",PositionInTable::AFTERTABLE]);
126
			$this->_compileForm();
127
			$this->_generated=true;
128
		}
129
		return parent::compile($js,$view);
130
	}
131
132
	private function _generateMainCheckbox(&$captions){
133
		$ck=new HtmlCheckbox("main-ck-".$this->identifier,"");
134
		$checkedMessageCall="";
135
		if($this->_hasCheckedMessage)
136
			$checkedMessageCall="updateChecked();";
137
		$ck->setOnChecked("$('#".$this->identifier." [name=%quote%selection[]%quote%]').prop('checked',true);".$checkedMessageCall);
138
		$ck->setOnUnchecked("$('#".$this->identifier." [name=%quote%selection[]%quote%]').prop('checked',false);".$checkedMessageCall);
139
		\array_unshift($captions, $ck);
140
	}
141
142
	protected function _generateContent($table){
143
		$objects=$this->_modelInstance;
144
		if(isset($this->_pagination)){
145
			$objects=$this->_pagination->getObjects($this->_modelInstance);
146
		}
147
		InstanceViewer::setIndex(0);
148
		$table->fromDatabaseObjects($objects, function($instance) use($table){
149
			$this->_instanceViewer->setInstance($instance);
150
			InstanceViewer::$index++;
151
			$values= $this->_instanceViewer->getValues();
152
			if($this->_hasCheckboxes){
153
				$ck=new HtmlCheckbox("ck-".$this->identifier,"");
154
				$field=$ck->getField();
155
				$field->setProperty("value",$this->_instanceViewer->getIdentifier());
156
				$field->setProperty("name", "selection[]");
157
				\array_unshift($values, $ck);
158
			}
159
			$result=$table->newRow();
160
			$result->setIdentifier($this->identifier."-tr-".$this->_instanceViewer->getIdentifier());
161
			$result->setValues($values);
162
			return $result;
163
		});
164
	}
165
166
	private function _generatePagination($table){
167
		$footer=$table->getFooter();
168
		$footer->mergeCol();
169
		$menu=new HtmlPaginationMenu("pagination-".$this->identifier,$this->_pagination->getPagesNumbers());
170
		$menu->floatRight();
171
		$menu->setActiveItem($this->_pagination->getPage()-1);
172
		$footer->setValues($menu);
173
		if(isset($this->_urls["refresh"]))
174
			$menu->postOnClick($this->_urls["refresh"],"{'p':$(this).attr('data-page')}","#".$this->identifier." tbody",["preventDefault"=>false,"jqueryDone"=>"replaceWith"]);
175
	}
176
177
	protected function _getFieldName($index){
178
		return parent::_getFieldName($index)."[]";
179
	}
180
181
	protected function _getFieldCaption($index){
182
		return null;
183
	}
184
185
	protected function _setToolbarPosition($table,$captions=NULL){
186
		switch ($this->_toolbarPosition){
187
			case PositionInTable::BEFORETABLE:
188
			case PositionInTable::AFTERTABLE:
189
				if(isset($this->_compileParts)===false){
190
					$this->content[$this->_toolbarPosition]=$this->_toolbar;
191
				}
192
				break;
193
			case PositionInTable::HEADER:
194
			case PositionInTable::FOOTER:
195
			case PositionInTable::BODY:
196
				$this->addToolbarRow($this->_toolbarPosition,$table, $captions);
197
				break;
198
		}
199
	}
200
201
	/**
202
	 * Associates a $callback function after the compilation of the field at $index position
203
	 * The $callback function can take the following arguments : $field=>the compiled field, $instance : the active instance of the object, $index: the field position
204
	 * @param int $index postion of the compiled field
205
	 * @param callable $callback function called after the field compilation
206
	 * @return \Ajax\semantic\widgets\datatable\DataTable
207
	 */
208
	public function afterCompile($index,$callback){
209
		$this->_instanceViewer->afterCompile($index,$callback);
210
		return $this;
211
	}
212
213
	private function addToolbarRow($part,$table,$captions){
214
		$hasPart=$table->hasPart($part);
215
		if($hasPart){
216
			$row=$table->getPart($part)->addRow(\sizeof($captions));
217
		}else{
218
			$row=$table->getPart($part)->getRow(0);
219
		}
220
		$row->mergeCol();
221
		$row->setValues([$this->_toolbar]);
222
	}
223
224
	public function getHtmlComponent(){
225
		return $this->content["table"];
226
	}
227
228
	public function getUrls() {
229
		return $this->_urls;
230
	}
231
232
	/**
233
	 * Sets the associative array of urls for refreshing, updating or deleting
234
	 * @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
235
	 * @return \Ajax\semantic\widgets\datatable\DataTable
236
	 */
237
	public function setUrls($urls) {
238
		if(\is_array($urls)){
239
			$this->_urls["refresh"]=JArray::getValue($urls, "refresh",0);
240
			$this->_urls["edit"]=JArray::getValue($urls, "edit",1);
241
			$this->_urls["delete"]=JArray::getValue($urls, "delete",2);
242
		}else{
243
			$this->_urls=["refresh"=>$urls,"edit"=>$urls,"delete"=>$urls];
244
		}
245
		return $this;
246
	}
247
248
	public function paginate($items_per_page=10,$page=1){
249
		$this->_pagination=new Pagination($items_per_page,4,$page);
250
	}
251
252
	public function getHasCheckboxes() {
253
		return $this->_hasCheckboxes;
254
	}
255
256
	public function setHasCheckboxes($_hasCheckboxes) {
257
		$this->_hasCheckboxes=$_hasCheckboxes;
258
		return $this;
259
	}
260
261
	public function refresh($compileParts=["tbody"]){
262
		$this->_compileParts=$compileParts;
263
		return $this;
264
	}
265
266
267
	public function addSearchInToolbar($position=Direction::RIGHT){
268
		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...
269
	}
270
271
	public function getSearchField(){
272
		if(isset($this->_searchField)===false){
273
			$this->_searchField=new HtmlInput("search-".$this->identifier,"search","","Search...");
274
			$this->_searchField->addIcon("search",Direction::RIGHT);
275
		}
276
		return $this->_searchField;
277
	}
278
279
	/**
280
	 * The callback function called after the insertion of each row when fromDatabaseObjects is called
281
	 * callback function takes the parameters $row : the row inserted and $object: the instance of model used
282
	 * @param callable $callback
283
	 * @return DataTable
284
	 */
285
	public function onNewRow($callback) {
286
		$this->content["table"]->onNewRow($callback);
287
		return $this;
288
	}
289
290
	public function asForm(){
291
		return $this->getForm();
292
	}
293
294
	/**
295
	 * Creates a submit button at $index position
296
	 * @param int $index
297
	 * @param string $cssStyle
298
	 * @param string $url
299
	 * @param string $responseElement
300
	 * @param array $attributes associative array (<b>ajax</b> key is for ajax post)
301
	 * @return \Ajax\semantic\widgets\datatable\DataTable
302
	 */
303
	public function fieldAsSubmit($index,$cssStyle=NULL,$url=NULL,$responseElement=NULL,$attributes=NULL){
304
		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...
305
			$button=new HtmlButton($id,$value,$cssStyle);
306
			$button->postOnClick($url,"$(event.target).closest('tr').find(':input').serialize()",$responseElement,$attributes["ajax"]);
307
			if(!isset($attributes["visibleHover"]) || $attributes["visibleHover"])
308
				$this->_visibleOver($button);
309
			return $button;
310
		}, $index,$attributes);
311
	}
312
313
	protected function _visibleOver($element){
314
		$this->_visibleHover=true;
315
		return $element->addToProperty("class", "visibleover")->setProperty("style","visibility:hidden;");
316
	}
317
318
	protected function getTargetSelector() {
319
		$result=$this->_targetSelector;
320
		if(!isset($result))
321
			$result="#".$this->identifier;
322
		return $result;
323
	}
324
325
	/**
326
	 * Sets the response element selector for Edit and Delete request with ajax
327
	 * @param string $_targetSelector
328
	 * @return \Ajax\semantic\widgets\datatable\DataTable
329
	 */
330
	public function setTargetSelector($_targetSelector) {
331
		$this->_targetSelector=$_targetSelector;
332
		return $this;
333
	}
334
335
	protected function getCheckedMessage() {
336
		$result= $this->_checkedMessage;
337
		if(!isset($result)){
338
			$result=[0=>"none selected",1=>"one item selected","other"=>"{count} items selected"];
339
		}
340
		return $result;
341
	}
342
343
	/**
344
	 * Defines the message displayed when checkboxes are checked or unchecked
345
	 * with an associative array 0=>no selection,1=>one item selected, other=>{count} items selected
346
	 * @param array $_checkedMessage
347
	 * @return \Ajax\semantic\widgets\datatable\DataTable
348
	 */
349
	public function setCheckedMessage(array $_checkedMessage) {
350
		$this->_checkedMessage=$_checkedMessage;
351
		return $this;
352
	}
353
354
	/**
355
	 * @param array $checkedMessage
356
	 * @param callable $callback
357
	 */
358
	public function addCountCheckedInToolbar(array $checkedMessage=null,$callback=null){
359
		if(isset($checkedMessage))
360
			$this->_checkedMessage=$checkedMessage;
361
		$checkedMessage=$this->getCheckedMessage();
362
		$this->_hasCheckboxes=true;
363
		$this->_hasCheckedMessage=true;
364
		$element=new HtmlLabel("checked-count-".$this->identifier,$checkedMessage[0]);
365
		$this->addInToolbar($element,$callback);
366
	}
367
368
369
}