Completed
Push — master ( 7abd10...04d259 )
by Jean-Christophe
03:18
created

DataTable::addEditButton()   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 2
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
		if(!$this->_generated){
64
			$this->_instanceViewer->setInstance($this->_model);
65
			$captions=$this->_instanceViewer->getCaptions();
66
67
			$table=$this->content["table"];
68
69
			if($this->_hasCheckboxes){
70
				$this->_generateMainCheckbox($captions);
71
			}
72
73
			$table->setRowCount(0, \sizeof($captions));
74
			$table->setHeaderValues($captions);
75
			if(isset($this->_compileParts))
76
				$table->setCompileParts($this->_compileParts);
77
			if(isset($this->_searchField) && isset($js)){
78
				$this->_searchField->postOn("change", $this->_urls,"{'s':$(this).val()}","#".$this->identifier." tbody",["preventDefault"=>false,"jqueryDone"=>"replaceWith"]);
79
			}
80
81
			$this->_generateContent($table);
82
83
			if($this->_hasCheckboxes && $table->hasPart("thead")){
84
					$table->getHeader()->getCell(0, 0)->addToProperty("class","no-sort");
85
			}
86
87
			if(isset($this->_pagination) && $this->_pagination->getVisible()){
88
				$this->_generatePagination($table);
89
			}
90
			if(isset($this->_toolbar)){
91
				$this->_setToolbarPosition($table, $captions);
92
			}
93
			$this->content=JArray::sortAssociative($this->content, [PositionInTable::BEFORETABLE,"table",PositionInTable::AFTERTABLE]);
94
			$this->_compileForm();
95
			$this->_generated=true;
96
		}
97
		return parent::compile($js,$view);
98
	}
99
100
	private function _generateMainCheckbox(&$captions){
101
		$ck=new HtmlCheckbox("main-ck-".$this->identifier,"");
102
		$ck->setOnChecked("$('#".$this->identifier." [name=%quote%selection[]%quote%]').prop('checked',true);");
103
		$ck->setOnUnchecked("$('#".$this->identifier." [name=%quote%selection[]%quote%]').prop('checked',false);");
104
		\array_unshift($captions, $ck);
105
	}
106
107
	protected function _generateContent($table){
108
		$objects=$this->_modelInstance;
109
		if(isset($this->_pagination)){
110
			$objects=$this->_pagination->getObjects($this->_modelInstance);
111
		}
112
		InstanceViewer::setIndex(0);
113
		$table->fromDatabaseObjects($objects, function($instance) use($table){
114
			$this->_instanceViewer->setInstance($instance);
115
			InstanceViewer::$index++;
116
			$values= $this->_instanceViewer->getValues();
117
			if($this->_hasCheckboxes){
118
				$ck=new HtmlCheckbox("ck-".$this->identifier,"");
119
				$field=$ck->getField();
120
				$field->setProperty("value",$this->_instanceViewer->getIdentifier());
121
				$field->setProperty("name", "selection[]");
122
				\array_unshift($values, $ck);
123
			}
124
			$result=$table->newRow();
125
			$result->setIdentifier($this->identifier."-tr-".$this->_instanceViewer->getIdentifier());
126
			$result->setValues($values);
127
			return $result;
128
		});
129
	}
130
131
	private function _generatePagination($table){
132
		$footer=$table->getFooter();
133
		$footer->mergeCol();
134
		$menu=new HtmlPaginationMenu("pagination-".$this->identifier,$this->_pagination->getPagesNumbers());
135
		$menu->floatRight();
136
		$menu->setActiveItem($this->_pagination->getPage()-1);
137
		$footer->setValues($menu);
138
		$menu->postOnClick($this->_urls,"{'p':$(this).attr('data-page')}","#".$this->identifier." tbody",["preventDefault"=>false,"jqueryDone"=>"replaceWith"]);
139
	}
140
141
	protected function _getFieldName($index){
142
		return parent::_getFieldName($index)."[]";
143
	}
144
145
	protected function _getFieldCaption($index){
146
		return null;
147
	}
148
149
	protected function _setToolbarPosition($table,$captions=NULL){
150
		switch ($this->_toolbarPosition){
151
			case PositionInTable::BEFORETABLE:
152
			case PositionInTable::AFTERTABLE:
153
				if(isset($this->_compileParts)===false){
154
					$this->content[$this->_toolbarPosition]=$this->_toolbar;
155
				}
156
				break;
157
			case PositionInTable::HEADER:
158
			case PositionInTable::FOOTER:
159
			case PositionInTable::BODY:
160
				$this->addToolbarRow($this->_toolbarPosition,$table, $captions);
161
				break;
162
		}
163
	}
164
165
	/**
166
	 * Associates a $callback function after the compilation of the field at $index position
167
	 * The $callback function can take the following arguments : $field=>the compiled field, $instance : the active instance of the object, $index: the field position
168
	 * @param int $index postion of the compiled field
169
	 * @param callable $callback function called after the field compilation
170
	 * @return \Ajax\semantic\widgets\datatable\DataTable
171
	 */
172
	public function afterCompile($index,$callback){
173
		$this->_instanceViewer->afterCompile($index,$callback);
174
		return $this;
175
	}
176
177
	private function addToolbarRow($part,$table,$captions){
178
		$hasPart=$table->hasPart($part);
179
		if($hasPart){
180
			$row=$table->getPart($part)->addRow(\sizeof($captions));
181
		}else{
182
			$row=$table->getPart($part)->getRow(0);
183
		}
184
		$row->mergeCol();
185
		$row->setValues([$this->_toolbar]);
186
	}
187
188
	public function getHtmlComponent(){
189
		return $this->content["table"];
190
	}
191
192
	public function getUrls() {
193
		return $this->_urls;
194
	}
195
196
	public function setUrls($urls) {
197
		$this->_urls=$urls;
198
		return $this;
199
	}
200
201
	public function paginate($items_per_page=10,$page=1){
202
		$this->_pagination=new Pagination($items_per_page,4,$page);
203
	}
204
205
	public function getHasCheckboxes() {
206
		return $this->_hasCheckboxes;
207
	}
208
209
	public function setHasCheckboxes($_hasCheckboxes) {
210
		$this->_hasCheckboxes=$_hasCheckboxes;
211
		return $this;
212
	}
213
214
	public function refresh($compileParts=["tbody"]){
215
		$this->_compileParts=$compileParts;
216
		return $this;
217
	}
218
	/**
219
	 * @param string $caption
220
	 * @param callable $callback
221
	 * @param boolean $visibleHover
222
	 * @return callable
223
	 */
224
	private function getFieldButtonCallable($caption,$visibleHover=true,$callback=null){
225
		return $this->getCallable("getFieldButton",[$caption,$visibleHover],$callback);
226
	}
227
228
	/**
229
	 * @param callable $thisCallback
230
	 * @param array $parameters
231
	 * @param callable $callback
232
	 * @return callable
233
	 */
234
	private function getCallable($thisCallback,$parameters,$callback=null){
235
		$result=function($instance) use($thisCallback,$parameters,$callback){
236
			$object=call_user_func_array(array($this,$thisCallback), $parameters);
237
			if(isset($callback)){
238
				if(\is_callable($callback)){
239
					$callback($object,$instance);
240
				}
241
			}
242
			if($object instanceof HtmlSemDoubleElement){
243
				$object->setProperty("data-ajax",$this->_instanceViewer->getIdentifier());
244
				if($object->propertyContains("class","visibleover")){
245
					$this->_visibleHover=true;
246
					$object->setProperty("style","visibility:hidden;");
247
				}
248
			}
249
			return $object;
250
		};
251
		return $result;
252
	}
253
254
	/**
255
	 * @param string $caption
256
	 * @return HtmlButton
257
	 */
258
	private function getFieldButton($caption,$visibleHover=true){
259
		$bt= new HtmlButton("",$caption);
260
		if($visibleHover)
261
			$this->_visibleOver($bt);
262
		return $bt;
263
	}
264
265
	/**
266
	 * Inserts a new Button for each row
267
	 * @param string $caption
268
	 * @param callable $callback
269
	 * @param boolean $visibleHover
270
	 * @return \Ajax\semantic\widgets\datatable\DataTable
271
	 */
272
	public function addFieldButton($caption,$visibleHover=true,$callback=null){
273
		$this->addField($this->getCallable("getFieldButton",[$caption,$visibleHover],$callback));
274
		return $this;
275
	}
276
277
	/**
278
	 * Inserts a new Button for each row at col $index
279
	 * @param int $index
280
	 * @param string $caption
281
	 * @param callable $callback
282
	 * @return \Ajax\semantic\widgets\datatable\DataTable
283
	 */
284
	public function insertFieldButton($index,$caption,$visibleHover=true,$callback=null){
285
		$this->insertField($index, $this->getFieldButtonCallable($caption,$visibleHover,$callback));
286
		return $this;
287
	}
288
289
	/**
290
	 * Inserts a new Button for each row in col at $index
291
	 * @param int $index
292
	 * @param string $caption
293
	 * @param callable $callback
294
	 * @return \Ajax\semantic\widgets\datatable\DataTable
295
	 */
296
	public function insertInFieldButton($index,$caption,$visibleHover=true,$callback=null){
297
		$this->insertInField($index, $this->getFieldButtonCallable($caption,$visibleHover,$callback));
298
		return $this;
299
	}
300
301
	private function addDefaultButton($icon,$class=null,$visibleHover=true,$callback=null){
302
		$this->addField($this->getCallable("getDefaultButton",[$icon,$class,$visibleHover],$callback));
303
		return $this;
304
	}
305
306
	private function insertDefaultButtonIn($index,$icon,$class=null,$visibleHover=true,$callback=null){
307
		$this->insertInField($index,$this->getCallable("getDefaultButton",[$icon,$class,$visibleHover],$callback));
308
		return $this;
309
	}
310
311
	private function getDefaultButton($icon,$class=null,$visibleHover=true){
312
		$bt=$this->getFieldButton("",$visibleHover);
313
		$bt->asIcon($icon);
314
		if(isset($class))
315
			$bt->addToProperty("class", $class);
316
		return $bt;
317
	}
318
319
	public function addDeleteButton($visibleHover=true,$callback=null){
320
		return $this->addDefaultButton("remove","delete red basic",$visibleHover,$callback);
321
	}
322
323
	public function addEditButton($visibleHover=true,$callback=null){
324
		return $this->addDefaultButton("edit","edit basic",$visibleHover,$callback);
325
	}
326
327
	public function addEditDeleteButtons($visibleHover=true,$callbackEdit=null,$callbackDelete=null){
328
		$this->addEditButton($visibleHover,$callbackEdit);
329
		$index=$this->_instanceViewer->visiblePropertiesCount()-1;
330
		$this->insertDeleteButtonIn($index,$visibleHover,$callbackDelete);
331
		return $this;
332
	}
333
334
	public function insertDeleteButtonIn($index,$visibleHover=true,$callback=null){
335
		return $this->insertDefaultButtonIn($index,"remove","delete red basic",$visibleHover,$callback);
336
	}
337
338
	public function insertEditButtonIn($index,$visibleHover=true,$callback=null){
339
		return $this->insertDefaultButtonIn($index,"edit","edit basic",$visibleHover,$callback);
340
	}
341
342
	public function addSearchInToolbar($position=Direction::RIGHT){
343
		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...
344
	}
345
346
	public function getSearchField(){
347
		if(isset($this->_searchField)===false){
348
			$this->_searchField=new HtmlInput("search-".$this->identifier,"search","","Search...");
349
			$this->_searchField->addIcon("search",Direction::RIGHT);
350
		}
351
		return $this->_searchField;
352
	}
353
354
	/**
355
	 * The callback function called after the insertion of each row when fromDatabaseObjects is called
356
	 * callback function takes the parameters $row : the row inserted and $object: the instance of model used
357
	 * @param callable $callback
358
	 * @return DataTable
359
	 */
360
	public function onNewRow($callback) {
361
		$this->content["table"]->onNewRow($callback);
362
		return $this;
363
	}
364
365
	public function asForm(){
366
		return $this->getForm();
367
	}
368
369
	public function fieldAsSubmit($index,$cssStyle=NULL,$url=NULL,$responseElement=NULL,$attributes=NULL){
370
		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...
371
			$button=new HtmlButton($id,$value,$cssStyle);
372
			$button->postOnClick($url,"$(event.target).closest('tr').find(':input').serialize()",$responseElement);
373
			return $this->_visibleOver($button);
374
		}, $index,$attributes);
375
	}
376
377
	protected function _visibleOver($element){
378
		$this->_visibleHover=true;
379
		return $element->addToProperty("class", "visibleover")->setProperty("style","visibility:hidden;");
380
	}
381
}