GridBase   F
last analyzed

Complexity

Total Complexity 130

Size/Duplication

Total Lines 1199
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 130
eloc 260
dl 0
loc 1199
rs 2
c 5
b 0
f 0
ccs 0
cts 494
cp 0

68 Methods

Rating   Name   Duplication   Size   Complexity  
A getScopes() 0 3 1
A addCheckColumn() 0 4 2
A addTextColumn() 0 4 2
A setId() 0 3 1
A gridName() 0 4 1
A getGridRenderer() 0 3 1
A setDataProvider() 0 3 1
A hasScope() 0 3 1
A addIntColumn() 0 4 2
A exportCsvUrl() 0 3 1
A hasDataProvider() 0 3 2
A removeColumn() 0 4 2
A isScopeActive() 0 3 1
A __construct() 0 4 1
B getFilterForm() 0 21 7
A getPageSize() 0 3 1
A getProperties() 0 9 1
A addButtonColumn() 0 4 2
A getColumnGroups() 0 3 1
A setColumnGroups() 0 3 1
A prepareDisplay() 0 4 2
A setScope() 0 3 1
A addColumn() 0 14 2
A getScope() 0 19 4
A addColumnFromDefinition() 0 15 3
B exportCsv() 0 47 10
A getGridData() 0 3 1
A refreshDataProvider() 0 4 2
A hasData() 0 3 1
A processActionsOnSelected() 0 8 5
A addScope() 0 3 1
A setQueryBuilder() 0 3 1
A setColumns() 0 4 2
A getId() 0 6 2
A filterShow() 0 4 1
A setup() 0 2 1
A gridBoot() 0 18 2
A setPageSize() 0 3 1
A addDateColumn() 0 4 2
A getColumns() 0 3 1
A setPageNumber() 0 5 2
A run() 0 10 1
A configure() 0 2 1
A getQueryBuilder() 0 3 1
A processScopes() 0 12 3
A getIndexColumn() 0 5 2
A processSort() 0 7 3
A replaceRowTagsWithValues() 0 4 1
A prepareCellContentForCSV() 0 15 2
A hasIndexColumn() 0 3 1
A getSortInfo() 0 9 2
A _processScopeCounts() 0 12 3
A getScopeTotalCount() 0 3 2
A registerAssets() 0 7 1
A getColumnsVisible() 0 8 3
A hasSortDescending() 0 4 1
A hasSort() 0 4 1
A getRowSearchTags() 0 9 3
A processSearch() 0 14 6
A columnExists() 0 3 1
A callScope() 0 11 3
A getRows() 0 3 1
A getColumn() 0 5 2
A showQuery() 0 3 1
A __toString() 0 3 1
A setIndexedByColumn() 0 6 2
A load() 0 2 1
A getFilterUrl() 0 5 2

How to fix   Complexity   

Complex Class

Complex classes like GridBase 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.

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 GridBase, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link http://www.newicon.net/
4
 * @copyright Copyright (c) 2015-18 Newicon Ltd
5
 * @license http://www.newicon.net/neon/framework/license/
6
 * @author Steve O'Brien <[email protected]>
7
 * @date 12/12/2015 16:13
8
 */
9
10
namespace neon\core\grid;
11
12
use neon\core\form\Form;
13
use neon\core\grid\column\Column;
14
use neon\core\grid\column\IColumn;
15
use neon\core\helpers\Arr;
16
use neon\core\helpers\Hash;
17
use neon\core\interfaces\IProperties;
18
use neon\core\traits\PropertiesTrait;
19
use neon\core\web\View;
20
use yii\base\Component;
21
use yii\base\InvalidConfigException;
22
use \yii\helpers\ArrayHelper;
23
use \neon\core\grid\query\IQuery;
24
use ReflectionClass;
25
26
/**
27
 * The Grid class inherit from this class to create your own advanced neon Grids
28
 *
29
 * The grid goes through 4 main stages to render:
30
 * 1: $this->init(): function is called, you can add columns and settings but its cleaner to use the configure function
31
 * 2: $this->configure(): function is called, this is where you can configure the columns by adding them to the grid
32
 * (3: Deprecated $this->load($_REQUEST): data pertinent to the grid is loaded. Typically this is the request data)
33
 * 4: $this->setup(): the setup function is called, this allows customisation of the grid and columns based on the request data
34
 * 5: $this->run(): Finally run processes and renders the grid
35
 *
36
 * See the self::gridBoot function for more information
37
 *
38
 * If you want properties and filters to persist then make them public properties in child classes for example:
39
 * ```php
40
 * class MyGrid extends Grid {
41
 *      public $projectId;
42
 *      public function setup() {
43
 *          // add query
44
 *          $this->getQueryBuilder()->where('project', '=', $this->$projectId);
45
 *      }
46
 * }
47
 * ```
48
 * The grid data persists between requests and setup will be called multiple times - therefore make sure the query
49
 * is built fresh with each call to setup.
50
 *
51
 * @package neon\core\grid
52
 * @author Steve O'Brien <[email protected]>
53
 *
54
 * @property \yii\data\DataProviderInterface $dataProvider
55
 * @property string $id - the grid ID @see $this->getId()
56
 */
57
abstract class GridBase extends Component implements IProperties
58
{
59
	use PropertiesTrait;
60
61
	/**
62
	 * @inheritdoc
63
	 */
64
	public function __construct($config = [])
65
	{
66
		parent::__construct($config);
67
		$this->configure();
68
	}
69
70
	/**
71
	 * @var array the configuration for the pager widget. By default, [[LinkPager]] will be
72
	 * used to render the pager. You can use a different widget class by configuring the "class" element.
73
	 * Note that the widget must support the `pagination` property which will be populated with the
74
	 * [[\yii\data\BaseDataProvider::pagination|pagination]] value of the [[dataProvider]].
75
	 * @var \yii\widgets\LinkPager
76
	 */
77
	public $pager = ['hideOnSinglePage' => false];
78
79
	/**
80
	 * Whether to stripe odd/even rows
81
	 * @var bool
82
	 */
83
	public $stripedRows = true;
84
85
	/**
86
	 * Allows to specify a theme name for the grid
87
	 * This simply adds the theme string as a css class to the base theme element
88
	 * @var string
89
	 */
90
	public $theme = '';
91
92
	/**
93
	 * Set whether the grid fills the width of the container or can overflow
94
	 * @var boolean
95
	 */
96
	public $fillWidth = false;
97
98
	/**
99
	 * TODO: this should not be accessible by the grid directly - use the data provider
100
	 * child classes can "know" that the dataprovider has a query object and therefore access it this way
101
	 * You can use $this->grid->getDataProvider()->query or use the getDataProvider()->getQueryBuilder() for standard
102
	 * filter options
103
	 * @deprecated
104
	 * @var \yii\db\ActiveQuery
105
	 */
106
	public $query;
107
108
	/**
109
	 * @var \yii\data\ActiveDataProvider
110
	 */
111
	protected $_dataProvider;
112
113
	/**
114
	 * Whether we have a data provider
115
	 * @return bool
116
	 */
117
	public function hasDataProvider()
118
	{
119
		return $this->_dataProvider === null ? false : true;
120
	}
121
122
	/**
123
	 * Store column objects
124
	 * @var array
125
	 */
126
	protected $_columns = [];
127
128
	/**
129
	 * Store filters
130
	 * @var array
131
	 */
132
	protected $_scopes = [];
133
134
	/**
135
	 * The view file to use to render the grid body
136
	 * **NOTE** this can be overridden using the theme pathMap property
137
	 * Overriding this property in sub classes should be a last resort
138
	 * @var string
139
	 */
140
	protected $_viewBodyFile = '@neon/core/grid/views/body.tpl';
141
142
	/**
143
	 * A unique id for this grid
144
	 * it will be used to attach request data to
145
	 * @var string
146
	 */
147
	private $_id;
148
149
	/**
150
	 * The name of the grid.
151
	 *
152
	 * @var string
153
	 */
154
	public $name;
155
156
	/**
157
	 * The header title for the grid
158
	 *
159
	 * @var string
160
	 */
161
	public $title;
162
163
	/**
164
	 * Configures the search url the grid will call this by ajax.
165
	 * To return correctly the controller action must return the Grid Html by
166
	 *   simply instantiating the grid and echoing the run command
167
	 * Minimum controller required for an example MyNeonGrid:
168
	 *
169
	 * ```php
170
	 * public function actionGrid()
171
	 * {
172
	 *	  $grid = new MyNeonGrid();
173
	 *		echo $grid->run();
174
	 * }
175
	 * ```
176
	 *
177
	 * @var array|string route e.g. ['/controller/index/grid']
178
	 */
179
	public $filterUrl = '/core/grid/filter';
180
181
	/**
182
	 * DDS filter format - a placeholder for columns to manipulate filters
183
	 *
184
	 * @var array
185
	 */
186
	public $filters = [];
187
188
	/**
189
	 * @var IQuery
190
	 */
191
	public $queryBuilder = null;
192
193
	/**
194
	 * Store the query builder object for the grid
195
	 *
196
	 * @var IQuery
197
	 */
198
	protected $_queryBuilder = null;
199
200
	/**
201
	 * Specify whether or not this grid can export a CSV
202
	 * @var bool
203
	 */
204
	public $canExportCsv = true;
205
206
	/**
207
	 * The string to display in a cell when empty
208
	 * @var string
209
	 */
210
	public $emptyCellDisplay = '-';
211
212
	/**
213
	 * @inheritdoc
214
	 */
215
	public function getProperties()
216
	{
217
		/**
218
		 * Return all the properties that are needed for generic rendering
219
		 * or exporting of this grid in any form.
220
		 */
221
		return [
222
			'id', 'pageSize', 'name', 'title', 'canExportCsv',
223
			'theme', 'emptyCellDisplay', 'stripedRows', 'filterUrl'
224
		];
225
	}
226
227
	/**
228
	 * Get the query builder object responsible for building search queries for columns
229
	 *
230
	 * @return IQuery
231
	 */
232
	public function getQueryBuilder()
233
	{
234
		return $this->getDataProvider()->getQueryBuilder();
235
	}
236
237
	/**
238
	 * Set the query builder object
239
	 *
240
	 * @param IQuery $queryBuilder
241
	 */
242
	public function setQueryBuilder(IQuery $queryBuilder)
243
	{
244
		$this->_queryBuilder = $queryBuilder;
245
	}
246
247
	/**
248
	 * The current scope the grid will render
249
	 * if not defined then it will use the first scope defined in the $this->_scopes array
250
	 *
251
	 * @var string the scope key
252
	 */
253
	protected $_scope;
254
255
	/**
256
	 * The key of the default scope for the grid.
257
	 * The grid will apply this scope if no request scope exists
258
	 * If this is not defined the grid will apply the first scope defined in $this->_scopes
259
	 *
260
	 * @var string
261
	 */
262
	public $defaultScope;
263
264
	/**
265
	 * Set the pagination page number
266
	 */
267
	public function setPageNumber()
268
	{
269
		$page = neon()->request->post($this->id);
270
		if (isset($page['page'])) {
271
			$this->getDataProvider()->pagination->setPage($page['page']);
272
		}
273
	}
274
275
	/**
276
	 * Set the pagination page size
277
	 *
278
	 * @param int $size
279
	 */
280
	public function setPageSize($size)
281
	{
282
		$this->getDataProvider()->pagination->pageSize = $size;
283
	}
284
285
	/**
286
	 * Get hold of the current pagination size
287
	 */
288
	public function getPageSize()
289
	{
290
		return $this->getDataProvider()->pagination->pageSize;
291
	}
292
293
	/**
294
	 * Get a scope key string
295
	 * This returns the active scope the grid must process
296
	 * This is calculated from either request data, a defaultScope parameter or the first in the scopes list
297
	 * returns null if a scope is not found
298
	 *
299
	 * @return string|null
300
	 */
301
	public function getScope()
302
	{
303
		if (empty($this->_scopes)) {
304
			return null;
305
		}
306
		// If a scope on the request has been set
307
		// _scope property exists
308
		$postScope = $this->_scope;
309
		$scope = ArrayHelper::getValue($this->_scopes, $postScope);
310
		if ($scope) {
311
			return $scope['key'];
312
		}
313
		// No url request scope defined check if a default is set
314
		if ($this->defaultScope !== null)
315
			return $this->defaultScope;
316
317
		// No default scope so return the first in the list as the default
318
		$defaultScope = reset($this->_scopes);
319
		return $defaultScope['key'];
320
	}
321
322
	/**
323
	 * @deprecated - this function is no longer needed
324
	 */
325
	public function load()
326
	{}
327
328
	/**
329
	 * Whether the specified scope is the currently the active one
330
	 * Note that request data must have been loaded - therefore this function will not work
331
	 * when used inside the init function
332
	 *
333
	 * @param string $scopeKey
334
	 * @return bool
335
	 */
336
	public function isScopeActive($scopeKey)
337
	{
338
		return ($this->getScope() == $scopeKey);
339
	}
340
341
	/**
342
	 * Does the grid have the scope defined
343
	 * @param string $scopeKey
344
	 * @return boolean
345
	 */
346
	public function hasScope($scopeKey)
347
	{
348
		return isset($this->_scopes[$scopeKey]);
349
	}
350
351
	/**
352
	 * Set the active scope
353
	 *
354
	 * @param string $scope
355
	 */
356
	public function setScope($scope)
357
	{
358
		$this->_scope = $scope;
359
	}
360
361
	/**
362
	 * Get the array of scope definitions
363
	 *
364
	 * @return array
365
	 */
366
	public function getScopes()
367
	{
368
		return $this->_scopes;
369
	}
370
371
	/**
372
	 * Add a filter scope - typically "deleted" | "active" etc
373
	 *
374
	 * @param string $key
375
	 * @param string $name
376
	 * @param callable $function
377
	 */
378
	public function addScope($key, $name, $function=null)
379
	{
380
		$this->_scopes[$key] = compact('key', 'name', 'function');
381
	}
382
383
	/**
384
	 * Whether to show the filters
385
	 *
386
	 * @var bool
387
	 */
388
	public $filterShow = true;
389
390
	/**
391
	 * Chainable method to set filterShow property
392
	 *
393
	 * @param boolean $bool
394
	 * @return $this
395
	 */
396
	public function filterShow($bool)
397
	{
398
		$this->filterShow = $bool;
399
		return $this;
400
	}
401
402
	/**
403
	 * The default sort (adopt yii terminology)
404
	 * columnKey
405
	 *
406
	 * @var string
407
	 */
408
	public $sort;
409
410
	/**
411
	 * A private cache of the grid data
412
	 *
413
	 * @var array
414
	 */
415
	private $_gridData = [];
416
417
	/**
418
	 * Store the form object responsible for each columns filter field
419
	 *
420
	 * @var null|\neon\core\form\Form
421
	 */
422
	protected $_filterForm = null;
423
424
	/**
425
	 * Get a form object representing all the filters
426
	 * This calls each column and asks it kindly for a form field object
427
	 * that it suitable to render its filter input control
428
	 *
429
	 * @throws \Exception on incorrect column config
430
	 * @return Form
431
	 */
432
	public function getFilterForm()
433
	{
434
		if (empty($this->getColumns())) {
435
			throw new \Exception('No grid columns have been defined yet.  The filter form is built from the grid columns, make sure you have setup your grid columns before loading request data into the form');
436
		}
437
		if ($this->_filterForm === null) {
438
			$this->_filterForm = (new Form($this->getId()))
0 ignored issues
show
Documentation Bug introduced by
It seems like new neon\core\form\Form(...)->addSubForm('filter') of type neon\core\form\fields\Field is incompatible with the declared type neon\core\form\Form|null of property $_filterForm.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
439
				->setId($this->getId().'Filter')
440
				->addSubForm('filter');
441
			foreach($this->getColumns() as $key => $column) {
442
				if ($column->getFilter() && $column->getFilterField() !== false) {
443
					$field = $column->getFilterField();
444
					if (is_object($field))
445
						$field = $field->toArray();
446
					$field['name'] = $key;
447
					$field['dataKey'] = $column->getDbField();
448
					$this->_filterForm->add($field);
0 ignored issues
show
Bug introduced by
The method add() does not exist on neon\core\form\fields\Field. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

448
					$this->_filterForm->/** @scrutinizer ignore-call */ 
449
                         add($field);
Loading history...
449
				}
450
			}
451
		}
452
		return $this->_filterForm;
453
	}
454
455
	/**
456
	 * Return the original grid data that has been sent to the grid
457
	 *
458
	 * @return array
459
	 */
460
	public function getGridData()
461
	{
462
		return $this->_gridData;
463
	}
464
465
	/**
466
	 * This function is called before request data is loaded (and before setup is ran)
467
	 * This is equivalent of putting code into the init function
468
	 * This should be used to add columns and grid settings
469
	 */
470
	public function configure()
471
	{
472
	}
473
474
	/**
475
	 * Function called as the first step of the run process
476
	 * at this point any relevant request data should have been loaded typically which scope is active
477
	 * therefore the setup of the grid can be altered based on post data
478
	 * Note for filter data to be stored in the filter form the column objects must first be initialized
479
	 * (as the filter form is created from the columns on the grid)
480
	 * Its best to add columns in the init function and use the setup to tweak them
481
	 * you can access an existing column via $this->getColumn($key); or store references in the subclass
482
	 *
483
	 * @return void
484
	 */
485
	public function setup()
486
	{
487
	}
488
489
	/**
490
	 * Returns the grid name that this model class should use.
491
	 *
492
	 * The grid name is mainly used by [[\yii\widgets\ActiveForm]] to determine how to name
493
	 * the input fields for the attributes in a model. If the form name is "A" and an attribute
494
	 * name is "b", then the corresponding input name would be "A[b]". If the form name is
495
	 * an empty string, then the input name would be "b".
496
	 *
497
	 * By default, this method returns the model class name (without the namespace part)
498
	 * as the form name. You may override it when the model is used in different forms.
499
	 *
500
	 * @return string the grid name of this class.
501
	 * @throws \Exception
502
	 */
503
	public function gridName()
504
	{
505
		$reflector = new ReflectionClass($this);
506
		return $reflector->getShortName();
507
	}
508
509
	/**
510
	 * Get an id for the grid
511
	 *
512
	 * @return string
513
	 * @throws \Exception if no id has been set
514
	 */
515
	public function getId()
516
	{
517
		if ($this->_id === null) {
518
			$this->_id = $this->gridName();
519
		}
520
		return $this->_id;
521
	}
522
523
	/**
524
	 * Set the grid id, the id is used as a key to attach request data to
525
	 * and to identify the html block for the grid when rendering
526
	 *
527
	 * @param string $id
528
	 */
529
	public function setId($id)
530
	{
531
		$this->_id = $id;
532
	}
533
534
	/**
535
	 * Set a bunch of columns by an array definition
536
	 *
537
	 * ```php
538
	 * new Grid(['columns'=>[
539
	 *     'name' => [
540
	 *         'class' => 'neon\core\grid\column\Text',
541
	 *         'title' => 'First Name',
542
	 *         'dbField' => 'first_name'
543
	 *     ]
544
	 * ]])
545
	 * ```
546
	 * @throws \Exception on incorrect column config
547
	 * @param array $columns containing ['key' => ['class' => 'column class', ...]]
548
	 */
549
	public function setColumns($columns)
550
	{
551
		foreach($columns as $key => $config) {
552
			$this->addColumn($key, $config);
553
		}
554
	}
555
556
	/**
557
	 * Add a text column to the grid
558
	 *
559
	 * @param string $key
560
	 * @param array $config
561
	 * @throws \Exception on incorrect column config
562
	 * @return \neon\core\grid\column\Text
563
	 */
564
	public function addTextColumn($key, $config=[])
565
	{
566
		$config['class'] = isset($config['class']) ? $config['class'] : 'neon\core\grid\column\Text';
567
		return $this->addColumn($key, $config);
568
	}
569
570
	/**
571
	 * Add a Date column to the grid
572
	 *
573
	 * @param string $key
574
	 * @param array $config
575
	 * @throws \Exception on incorrect column config
576
	 * @return \neon\core\grid\column\Date
577
	 */
578
	public function addDateColumn($key, $config=[])
579
	{
580
		$config['class'] = isset($config['class']) ? $config['class'] : 'neon\core\grid\column\Date';
581
		return $this->addColumn($key, $config);
582
	}
583
584
	/**
585
	 * Add a int column to the grid
586
	 *
587
	 * @param string $key
588
	 * @param array $config
589
	 * @throws \Exception on incorrect column config
590
	 * @return \neon\core\grid\column\IntColumn
591
	 */
592
	public function addIntColumn($key, $config=[])
593
	{
594
		$config['class'] = isset($config['class']) ? $config['class'] : 'neon\core\grid\column\IntColumn';
595
		return $this->addColumn($key, $config);
596
	}
597
598
	/**
599
	 * Add a check column to the grid
600
	 *
601
	 * @param $key
602
	 * @param array $config
603
	 * @return \neon\core\grid\column\Check
604
	 * @throws \Exception
605
	 */
606
	public function addCheckColumn($key, $config=[])
607
	{
608
		$config['class'] = isset($config['class']) ?  $config['class'] : 'neon\core\grid\column\Check';
609
		return $this->addColumn($key, $config);
610
	}
611
612
	/**
613
	 * Add an action column to the grid
614
	 *
615
	 * @param $key
616
	 * @param $config
617
	 * @throws \Exception on incorrect column config
618
	 * @return \neon\core\grid\column\Button
619
	 */
620
	public function addButtonColumn($key, $config=[])
621
	{
622
		$config['class'] = isset($config['class']) ?  $config['class'] : 'neon\core\grid\column\Button';
623
		return $this->addColumn($key, $config);
624
	}
625
626
	/**
627
	 * Create a column based on its field definition
628
	 *
629
	 * @param string $key - a key for the column
630
	 * @param string $class - the class reference
631
	 * @param string $member - the member reference string typically from Daedalus.
632
	 *   If this is null, then the key is used as the member
633
	 * @param string $phoebeType - the phoebe type for the form definition. Defaults to 'daedalus'
634
	 * @throws \Exception
635
	 * @return Column
636
	 */
637
	public function addColumnFromDefinition($key, $class, $member=null, $phoebeType='daedalus')
638
	{
639
		static $formsDefinitionCache=[];
640
		$cacheKey = $class.$phoebeType;
641
		if (!array_key_exists($cacheKey, $formsDefinitionCache)) {
642
			$formDefinition = neon('phoebe')->getFormDefinition($phoebeType, $class);
643
			$formsDefinitionCache[$cacheKey] = $formDefinition;
644
		}
645
		$form = new \neon\core\form\Form($formsDefinitionCache[$cacheKey]);
646
		if ($member === null)
647
			$member = $key;
648
		return $this->addColumn($key, [
649
			'title' => $form->getField($member)->getLabel(),
650
			'class' => 'neon\core\grid\column\Column',
651
			'member' => $form->getField($member),
652
		]);
653
	}
654
655
	/**
656
	 * @var array
657
	 */
658
	protected $_columnGroups = [];
659
660
	/**
661
	 * @return array
662
	 */
663
	public function getColumnGroups()
664
	{
665
		return $this->_columnGroups;
666
	}
667
668
	/**
669
	 * @param $array
670
	 */
671
	public function setColumnGroups($array)
672
	{
673
		$this->_columnGroups = $array;
674
	}
675
676
	/**
677
	 * Add a column to the grid
678
	 *
679
	 * @param string $key - the internal key for the column will also be used as the key to look up the value in the
680
	 * row data. If you need this to be different then you can set the dbField option of the column to specify the
681
	 * precise key in the column data to return
682
	 * @param array $config
683
	 * @return \neon\core\grid\column\Column
684
	 * @throws \Exception if no `$config['class']` is set
685
	 */
686
	public function addColumn($key, $config=[])
687
	{
688
		if (!isset($config['class'])) {
689
			// 99% of the time you just want the standard Column class
690
			$config['class'] = \neon\core\grid\column\Column::class;
691
		}
692
		$class = Arr::remove($config, 'class');
693
		unset($config['class']);
694
		$this->_columns[$key] = \Neon::createObject(array_merge([
695
			'class' => $class,
696
			'key' => $key,
697
			'grid' => $this
698
		], $config));
699
		return $this->_columns[$key];
700
	}
701
702
	/**
703
	 * Remove a column from the grid
704
	 *
705
	 * @param $key
706
	 */
707
	public function removeColumn($key)
708
	{
709
		if ($this->columnExists($key))
710
			unset($this->_columns[$key]);
711
	}
712
713
	/**
714
	 * Set the data provider used to get the data
715
	 *
716
	 * @param \yii\data\BaseDataProvider $provider
717
	 */
718
	public function setDataProvider($provider)
719
	{
720
		$this->_dataProvider = $provider;
0 ignored issues
show
Documentation Bug introduced by
$provider is of type yii\data\BaseDataProvider, but the property $_dataProvider was declared to be of type yii\data\ActiveDataProvider. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
721
	}
722
723
	/**
724
	 * Get the grids Data provider object
725
	 *
726
	 * @return \yii\data\ActiveDataProvider
727
	 */
728
	abstract public function getDataProvider();
729
730
	/**
731
	 * Get the columns.
732
	 * Returns an array of column objects implementing IColumn interface key => column object
733
	 *
734
	 * @return \neon\core\grid\column\IColumn[]
735
	 */
736
	public function getColumns()
737
	{
738
		return $this->_columns;
739
	}
740
741
	/**
742
	 * Whether the grid has any data set
743
	 *
744
	 * @return bool
745
	 */
746
	public function hasData()
747
	{
748
		return ! empty($this->_gridData);
749
	}
750
751
	/**
752
	 * We must refresh the data provider before calling scopes
753
	 * This removes scopes that have been added previously but keeps initial setup query conditions
754
	 */
755
	public function refreshDataProvider()
756
	{
757
		if ($this->_dataProvider)
758
			$this->_dataProvider = clone $this->getDataProvider();
759
	}
760
761
	/**
762
	 * The main render function to draw the grid and run the search etc
763
	 * @throws \Exception
764
	 * @return string
765
	 */
766
	public function run()
767
	{
768
		$this->gridBoot();
769
		$this->setPageNumber();
770
		$this->registerAssets();
771
		// executes the query and stores the data results in models accessible via `$this->getDataProvider()->getModels()`
772
		$this->getDataProvider()->prepare();
773
		// enable columns to prepare for display (i.e. load maps based on current result set)
774
		$this->prepareDisplay();
775
		return $this->getGridRenderer()->doRender();
776
	}
777
778
	/**
779
	 * @throws InvalidConfigException
780
	 */
781
	public function gridBoot()
782
	{
783
		if (! $this->hasData())
784
			$this->_gridData = Arr::get($_REQUEST, $this->id, null);
785
		// load the active scope
786
		$this->_scope = Arr::get($this->_gridData, "scope", []);
0 ignored issues
show
Documentation Bug introduced by
It seems like neon\core\helpers\Arr::g...Data, 'scope', array()) can also be of type array. However, the property $_scope is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
787
		// load request data into the form
788
		// note: columns should have already been added to the grid otherwise the form will be empty
789
		$this->getFilterForm()->load(Arr::get($this->_gridData, "filter", []));
790
		// enable the grid to configure columns visibility and filters based on active scopes and request data
791
		$this->setup();
792
		// force the data provider to load (only necessary when this is a subclass of YiiGridView
793
		$this->getDataProvider();
794
		$this->refreshDataProvider();
795
		$this->processActionsOnSelected();
796
		$this->processScopes();
797
		$this->processSearch();
798
		$this->processSort();
799
	}
800
801
	/**
802
	 * Return the string url to the export link
803
	 * @return string
804
	 */
805
	public function exportCsvUrl()
806
	{
807
		return url(['/core/grid/export-csv', 'token' => Hash::setObjectToToken($this)]);
808
	}
809
810
	/**
811
	 * Export the grid data in a format suitable for CSV
812
	 * @param int $page  the starting page in the data
813
	 * @param int $pageSize  the number of rows in a page. Maximum 1000.
814
	 * @param bool $asRows  false if you want the data imploded into a string
815
	 *   and true if you want the data returned as an array
816
	 * @return string | array  depending on value of $asRows
817
	 */
818
	public function exportCsv($page=0, $pageSize=1000, $asRows=false)
819
	{
820
		// check we're allowed to do this
821
		if (!$this->canExportCsv)
822
			return null;
823
824
		$this->gridBoot();
825
		// Get header column names
826
		$headerCols = [];
827
		$columns = $this->getColumns();
828
829
		foreach ($columns as $colKey => $column) {
830
			$headerCols[] = $this->prepareCellContentForCSV($column->getTitle());
831
		}
832
		// For some reason excel throws an SYLK file error if the first cell is ID uppercase
833
834
		if (isset($headerCols[0]) && $headerCols[0] == 'ID') {
835
			$headerCols[0] = 'Id';
836
		}
837
838
		// Render the rows
839
		$dataProvider = $this->getDataProvider();
840
		$pagination = $dataProvider->pagination;
0 ignored issues
show
Documentation Bug introduced by
It seems like $dataProvider->pagination can also be of type boolean. However, the property $pagination is declared as type false|yii\data\Pagination. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
841
		$pagination->page=$page;
842
		$pagination->pageSize = min(1000,$pageSize);
843
		$models = $dataProvider->getModels();
844
		if (empty($models))
845
			return null;
846
847
		$rows = [];
848
		foreach($models as $index => $row) {
849
			$singleRow = [];
850
			// use the columns set as not all columns exists in row data e.g. derived ones
851
			foreach ($columns as $colKey => $column) {
852
				$singleRow[] = $this->prepareCellContentForCSV($column->callRenderDataCellContentFunction($row, $colKey, 0));
853
			}
854
			$rows[] = implode(',', $singleRow);
855
		}
856
857
		// clear the models to prevent ooming
858
		$dataProvider->models = null;
859
860
		// Add headers columns as the first row if we are on the first page
861
		if ($page==0)
862
			array_unshift($rows, implode(',', $headerCols));
863
864
		return $asRows ? $rows : implode("\n", $rows);
865
	}
866
867
	/**
868
	 * Get a grid renderer object
869
	 */
870
	public function getGridRenderer()
871
	{
872
		return new GridRenderer($this);
873
	}
874
875
	/**
876
	 * Prepare the columns for display
877
	 */
878
	public function prepareDisplay()
879
	{
880
		foreach ($this->getColumns() as $column) {
881
			$column->prepareDisplay();
882
		}
883
	}
884
885
	/**
886
	 * Checks if there are selected row actions and runs them
887
	 */
888
	public function processActionsOnSelected()
889
	{
890
		// run actions on any selected rows
891
		// TODO: tidy up running actions
892
		if (isset($_POST[$this->id]['action']) && isset($_POST[$this->id]['action_index']) && $_POST[$this->id]['action'] != 'undefined') {
893
			$rows = explode(',', $_POST[$this->id]['action_index']);
894
			foreach ($rows as $id) {
895
				call_user_func_array(array($this, $_POST[$this->id]['action']), array($id));
896
			}
897
		}
898
	}
899
900
	/**
901
	 * Applies scope filter criteria
902
	 * @throws \Exception
903
	 */
904
	public function processScopes()
905
	{
906
		$this->_processScopeCounts();
907
		// apply default scope
908
		if (!empty($this->getScopes())) {
909
			$activeScope = $this->getScope();
910
			// remove previous scopes added
911
			$scope = ArrayHelper::getValue($this->getScopes(), $activeScope);
912
			if ($scope) {
913
				// before calling a scope we will tell the query builder to label any rules added
914
				// by the scope - this allows us to remove them later
915
				$this->callScope($scope, $this->getQueryBuilder());
916
			}
917
		}
918
	}
919
920
	private $_scopeCounts = [];
921
922
	/**
923
	 * Perform a count for each scope and store ready for rendering.
924
	 * Populates $this->_scopeCounts with total count for each scope.
925
	 * This function must be called before applying the default scope otherwise we cannot easily remove the
926
	 * default scope from the query builder object. Thus ending up with inaccurate counts
927
	 *
928
	 * Note - counts are extremely expensive, and can take a long time to return
929
	 * for database tables with items in the millions of rows especially if joins
930
	 * are involved. So counts should not be made more than once if possible.
931
	 *
932
	 * @throws \Exception if no scope function exists for specified key
933
	 */
934
	private function _processScopeCounts()
935
	{
936
		foreach ($this->getScopes() as $key => $scope) {
937
			// counts across whole tables are very very expensive. Don't count more than once
938
			if (isset($this->_scopeCounts[$key]))
939
				continue;
940
			$dp = clone $this->getDataProvider();
941
			// call the scope function defined by `'scope'.$key` functions in child classes
942
			$this->callScope($scope, $dp->getQueryBuilder());
943
			// reset cached counts
944
			$dp->setTotalCount(null);
945
			$this->_scopeCounts[$key] = $dp->getTotalCount();
946
		}
947
	}
948
949
	/**
950
	 * return the count for a given scope key
951
	 * @param string $key
952
	 * @return int
953
	 */
954
	public function getScopeTotalCount($key)
955
	{
956
		return isset($this->_scopeCounts[$key]) ? $this->_scopeCounts[$key] : '?';
957
	}
958
959
	/**
960
	 * Checks search data and applied correct filter modifications to the query/filter object
961
	 *
962
	 * @throws InvalidConfigException
963
	 */
964
	public function processSearch()
965
	{
966
		$data = $this->getGridData();
967
		if (!isset($data['filter']))
968
			return;
969
		foreach ($this->getColumns() as $key => $column) {
970
			// check there is a form filter field
971
			// check that the filter field has request data
972
			if ($column->getFilter() && $column->hasRequestData()) {
973
				// if there is valid search data:
974
				if ($column->getSearchFunction() !== null) {
975
					$column->callSearchFunction();
976
				} else {
977
					$column->processSearch($this->getQueryBuilder());
978
				}
979
			}
980
		}
981
	}
982
983
	/**
984
	 * Attach sort information to the data provider.
985
	 * Typically a column will be identified to be sorted, this information can be
986
	 * found out from `$this->getSortInfo()`
987
	 *
988
	 * @see getSortInfo()
989
	 */
990
	public function processSort()
991
	{
992
		list ($columnKey, $desc) = $this->getSortInfo();
993
		if ($columnKey && $this->columnExists($columnKey)) {
994
			// lookup the column db field to apply the sorting to
995
			$column = $this->getColumn($columnKey);
996
			$column->processSort($this->getQueryBuilder(), $desc);
997
		}
998
	}
999
1000
	/**
1001
	 * Whether a column currently has a sort set
1002
	 *
1003
	 * @param $columnKey
1004
	 * @return bool
1005
	 */
1006
	public function hasSort($columnKey)
1007
	{
1008
		list ($columnSort, $desc) = $this->getSortInfo();
1009
		return $columnSort == $columnKey;
1010
	}
1011
1012
	/**
1013
	 * Whether the current sort is in descending order
1014
	 *
1015
	 * @return mixed
1016
	 */
1017
	public function hasSortDescending()
1018
	{
1019
		list ($columnSort, $desc) = $this->getSortInfo();
1020
		return $desc;
1021
	}
1022
1023
	/**
1024
	 * example useage:
1025
	 *
1026
	 * ```PHP
1027
	 *	 list($columnKey, $descending) = $this->getSortInfo();
1028
	 * ```
1029
	 * Note that the column key (the first key in the returned array) will be null
1030
	 * if there is currently no sort
1031
	 *
1032
	 * @return array first key columnKey the second a boolean representing descending
1033
	 */
1034
	public function getSortInfo()
1035
	{
1036
		$sort = ArrayHelper::getValue($this->getGridData(), 'sort');
1037
		$descending = false;
1038
		if (strncmp($sort, '-', 1) === 0) {
1039
			$descending = true;
1040
			$sort = substr($sort, 1);
1041
		}
1042
		return [$sort, $descending];
1043
	}
1044
1045
	/**
1046
	 * @param $columnKey
1047
	 * @return \neon\core\grid\column\Column
1048
	 * @throws \Exception
1049
	 */
1050
	public function getColumn($columnKey)
1051
	{
1052
		if (!isset($this->_columns[$columnKey]))
1053
			throw new \Exception("No column with key '$columnKey' exists in the grid");
1054
		return $this->_columns[$columnKey];
1055
	}
1056
1057
	/**
1058
	 * Check a column exists
1059
	 * @param string $columnKey - the column key
1060
	 * @return bool
1061
	 */
1062
	public function columnExists($columnKey)
1063
	{
1064
		return isset($this->_columns[$columnKey]);
1065
	}
1066
1067
	/**
1068
	 * Calls the scope function to action the scope filter
1069
	 *
1070
	 * @param array $scope the scope array containing ['function' => ..., 'key' => '...']
1071
	 * @param IQuery $query
1072
	 * @throws \Exception
1073
	 */
1074
	public function callScope($scope, IQuery $query)
1075
	{
1076
		if ($scope['function'] == null) {
1077
			$func = 'scope' . ucfirst($scope['key']);
1078
			if ($this->hasMethod($func, false)) {
1079
				call_user_func(array($this, $func), $query);
1080
			} else {
1081
				throw new \Exception("A scope function with name '$func' is not defined in the grid class.");
1082
			}
1083
		} else {
1084
			call_user_func($scope['function'], $query);
1085
		}
1086
	}
1087
1088
	/**
1089
	 * Get the grid row data
1090
	 *
1091
	 * @return array
1092
	 */
1093
	public function getRows()
1094
	{
1095
		return $this->getDataProvider()->getModels();
1096
	}
1097
1098
	public function getFilterUrl()
1099
	{
1100
		$filterUrl = is_array($this->filterUrl) ? $this->filterUrl : [$this->filterUrl];
1101
		$filterUrl['token'] = Hash::setObjectToToken($this);
1102
		return url($filterUrl);
1103
	}
1104
1105
	/**
1106
	 * Registers client assets
1107
	 */
1108
	protected function registerAssets()
1109
	{
1110
		GridAssets::register(neon()->view);
1111
		$options = [];
1112
		$options['filterUrl'] = $this->getFilterUrl();
1113
		neon()->view->registerJs('(new Vue()).$mount("#'.$this->id.'");', View::POS_END, $this->id.'vue');
1114
		neon()->view->registerJs('$("#'.$this->id.'").neonGrid('.json_encode($options).')', View::POS_READY, $this->id.'neonGrid');
1115
	}
1116
1117
	/**
1118
	 * Gets a debug result message showing the query performed to
1119
	 * fetch the current set of grid results
1120
	 *
1121
	 * @throws \Exception
1122
	 * @return string
1123
	 */
1124
	public function showQuery()
1125
	{
1126
		return json_encode($this->getQueryBuilder()->getFilter());
1127
	}
1128
1129
	/**
1130
	 * Get an array of only visible columns
1131
	 * format:
1132
	 * ```
1133
	 * [
1134
	 *	 ['key' => {column object}],
1135
	 * ]
1136
	 * ```
1137
	 *
1138
	 * @return array
1139
	 */
1140
	public function getColumnsVisible()
1141
	{
1142
		$columns = [];
1143
		foreach($this->getColumns() as $key => $column) {
1144
			if ($column->visible)
0 ignored issues
show
Bug introduced by
Accessing visible on the interface neon\core\grid\column\IColumn suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1145
				$columns[$key] = $column;
1146
		}
1147
		return $columns;
1148
	}
1149
1150
	/**
1151
	 * When outputting the object as a string call the run function
1152
	 *
1153
	 * @return string
1154
	 */
1155
	public function __toString()
1156
	{
1157
		return $this->run();
1158
	}
1159
1160
	/**
1161
	 * Key of the column this grid is indexed by
1162
	 *
1163
	 * @var string
1164
	 */
1165
	protected $_indexedByColumn;
1166
1167
	/**
1168
	 * Set a column as the index of the grid
1169
	 * An index column assumes that the value in of that column can be used to find the row of data from the data store
1170
	 * This is useful for grid actions passing the value of that rows index column - the row id to the action
1171
	 *
1172
	 * @param string $columnKey
1173
	 * @throws \Exception if no column exists with the $columnKey in the $this->_columns array
1174
	 */
1175
	public function setIndexedByColumn($columnKey)
1176
	{
1177
		if (!isset($this->_columns[$columnKey])) {
1178
			throw new \Exception("No column exists with key '$columnKey'");
1179
		}
1180
		$this->_indexedByColumn = $columnKey;
1181
	}
1182
1183
	/**
1184
	 * Get the column that has been set as the index
1185
	 * @return IColumn
1186
	 * @throws \Exception - if no index column exists
1187
	 */
1188
	public function getIndexColumn()
1189
	{
1190
		if (!isset($this->_columns[$this->_indexedByColumn]))
1191
			throw new \Exception("No index column exists with key '$this->_indexedByColumn'.");
1192
		return $this->_columns[$this->_indexedByColumn];
1193
	}
1194
1195
	/**
1196
	 * Whether ter is a column on the grid that has been set to be the index
1197
	 * @return bool
1198
	 */
1199
	public function hasIndexColumn()
1200
	{
1201
		return isset($this->_columns[$this->_indexedByColumn]);
1202
	}
1203
1204
	/**
1205
	 * Replace field tags in a string with the values from the row of data.  A field tag is denoted with a leading colon.
1206
	 * For example a row containing ['name' => 'steve'] and a string of 'hello {$name}' will give 'hello steve'
1207
	 *
1208
	 * @param string|array $string - a string or array of strings containing field tags e.g. ':field'
1209
	 * @param array $row - The row data
1210
	 * @return string
1211
	 */
1212
	public function replaceRowTagsWithValues($string, $row)
1213
	{
1214
		$tags = $this->getRowSearchTags($row);
1215
		return str_replace(array_keys($tags), array_values($tags), $string);
1216
	}
1217
1218
	/**
1219
	 * Generates an array of search tags based on the keys of the row
1220
	 * A tag is simply the key of a row item wrapped with a particular string
1221
	 * It assumes that each row in the grid will have a consistent keys
1222
	 *
1223
	 * @param array $row
1224
	 * @return array
1225
	 */
1226
	protected function getRowSearchTags($row)
1227
	{
1228
		$tags = [];
1229
		foreach ($row as $key => $val) {
1230
			if (!is_array($val)){
1231
				$tags['{{' . $key . '}}'] = $val;
1232
			}
1233
		}
1234
		return $tags;
1235
	}
1236
1237
	/**
1238
	 * Prepare the content for CSV export by removing tags, html entities etc
1239
	 * @param string $content  the content to be converted
1240
	 */
1241
	protected function prepareCellContentForCSV($content)
1242
	{
1243
		/**
1244
		 * From experimentation with real data:
1245
		 * grid cells and headers can have html tags added
1246
		 * To export properly to csv, any html tags need to be removed and
1247
		 * html entities reverted. These can leave newlines in the data so
1248
		 * need to clear those out too.
1249
		 */
1250
		$plain = html_entity_decode(
1251
				str_replace(["\n", '"'], ['', '\''], strip_tags($content)),
1252
				ENT_HTML401 | ENT_QUOTES | ENT_HTML5);
1253
		if ($plain === $this->emptyCellDisplay)
1254
			$plain = '';
1255
		return '"'.$plain.'"';
1256
	}
1257
}
1258