Passed
Push — develop ( 5f0710...340c0b )
by Neill
12:23 queued 14s
created

GridBase::exportCsvUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 1
b 0
f 0
cc 1
nc 1
nop 0
ccs 0
cts 3
cp 0
crap 2
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';
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
	 * @throws \Exception if no scope function exists for specified key
928
	 */
929
	private function _processScopeCounts()
930
	{
931
		foreach ($this->getScopes() as $key => $scope) {
932
			$dp = clone $this->getDataProvider();
933
			// call the scope function defined by `'scope'.$key` functions in child classes
934
			$this->callScope($scope, $dp->getQueryBuilder());
935
			// reset cached counts
936
			$dp->setTotalCount(null);
937
			$this->_scopeCounts[$key] = $dp->getTotalCount();
938
		}
939
	}
940
941
	/**
942
	 * return the count for a given scope key
943
	 * @param string $key
944
	 * @return int
945
	 */
946
	public function getScopeTotalCount($key)
947
	{
948
		return isset($this->_scopeCounts[$key]) ? $this->_scopeCounts[$key] : '?';
949
	}
950
951
	/**
952
	 * Checks search data and applied correct filter modifications to the query/filter object
953
	 *
954
	 * @throws InvalidConfigException
955
	 */
956
	public function processSearch()
957
	{
958
		$data = $this->getGridData();
959
		if (!isset($data['filter']))
960
			return;
961
		foreach ($this->getColumns() as $key => $column) {
962
			// check there is a form filter field
963
			// check that the filter field has request data
964
			if ($column->getFilter() && $column->hasRequestData()) {
965
				// if there is valid search data:
966
				if ($column->getSearchFunction() !== null) {
967
					$column->callSearchFunction();
968
				} else {
969
					$column->processSearch($this->getQueryBuilder());
970
				}
971
			}
972
		}
973
	}
974
975
	/**
976
	 * Attach sort information to the data provider.
977
	 * Typically a column will be identified to be sorted, this information can be
978
	 * found out from `$this->getSortInfo()`
979
	 *
980
	 * @see getSortInfo()
981
	 */
982
	public function processSort()
983
	{
984
		list ($columnKey, $desc) = $this->getSortInfo();
985
		if ($columnKey && $this->columnExists($columnKey)) {
986
			// lookup the column db field to apply the sorting to
987
			$column = $this->getColumn($columnKey);
988
			$column->processSort($this->getQueryBuilder(), $desc);
989
		}
990
	}
991
992
	/**
993
	 * Whether a column currently has a sort set
994
	 *
995
	 * @param $columnKey
996
	 * @return bool
997
	 */
998
	public function hasSort($columnKey)
999
	{
1000
		list ($columnSort, $desc) = $this->getSortInfo();
1001
		return $columnSort == $columnKey;
1002
	}
1003
1004
	/**
1005
	 * Whether the current sort is in descending order
1006
	 *
1007
	 * @return mixed
1008
	 */
1009
	public function hasSortDescending()
1010
	{
1011
		list ($columnSort, $desc) = $this->getSortInfo();
1012
		return $desc;
1013
	}
1014
1015
	/**
1016
	 * example useage:
1017
	 *
1018
	 * ```PHP
1019
	 *	 list($columnKey, $descending) = $this->getSortInfo();
1020
	 * ```
1021
	 * Note that the column key (the first key in the returned array) will be null
1022
	 * if there is currently no sort
1023
	 *
1024
	 * @return array first key columnKey the second a boolean representing descending
1025
	 */
1026
	public function getSortInfo()
1027
	{
1028
		$sort = ArrayHelper::getValue($this->getGridData(), 'sort');
1029
		$descending = false;
1030
		if (strncmp($sort, '-', 1) === 0) {
1031
			$descending = true;
1032
			$sort = substr($sort, 1);
1033
		}
1034
		return [$sort, $descending];
1035
	}
1036
1037
	/**
1038
	 * @param $columnKey
1039
	 * @return \neon\core\grid\column\Column
1040
	 * @throws \Exception
1041
	 */
1042
	public function getColumn($columnKey)
1043
	{
1044
		if (!isset($this->_columns[$columnKey]))
1045
			throw new \Exception("No column with key '$columnKey' exists in the grid");
1046
		return $this->_columns[$columnKey];
1047
	}
1048
1049
	/**
1050
	 * Check a column exists
1051
	 * @param string $columnKey - the column key
1052
	 * @return bool
1053
	 */
1054
	public function columnExists($columnKey)
1055
	{
1056
		return isset($this->_columns[$columnKey]);
1057
	}
1058
1059
	/**
1060
	 * Calls the scope function to action the scope filter
1061
	 *
1062
	 * @param array $scope the scope array containing ['function' => ..., 'key' => '...']
1063
	 * @param IQuery $query
1064
	 * @throws \Exception
1065
	 */
1066
	public function callScope($scope, IQuery $query)
1067
	{
1068
		if ($scope['function'] == null) {
1069
			$func = 'scope' . ucfirst($scope['key']);
1070
			if ($this->hasMethod($func, false)) {
1071
				call_user_func(array($this, $func), $query);
1072
			} else {
1073
				throw new \Exception("A scope function with name '$func' is not defined in the grid class.");
1074
			}
1075
		} else {
1076
			call_user_func($scope['function'], $query);
1077
		}
1078
	}
1079
1080
	/**
1081
	 * Get the grid row data
1082
	 *
1083
	 * @return array
1084
	 */
1085
	public function getRows()
1086
	{
1087
		return $this->getDataProvider()->getModels();
1088
	}
1089
1090
	public function getFilterUrl()
1091
	{
1092
		$filterUrl = is_array($this->filterUrl) ? $this->filterUrl : [$this->filterUrl];
1093
		$filterUrl['token'] = Hash::setObjectToToken($this);
1094
		return url($filterUrl);
1095
	}
1096
1097
	/**
1098
	 * Registers client assets
1099
	 */
1100
	protected function registerAssets()
1101
	{
1102
		GridAssets::register(neon()->view);
1103
		$options = [];
1104
		$options['filterUrl'] = $this->getFilterUrl();
1105
		neon()->view->registerJs('(new Vue()).$mount("#'.$this->id.'");', View::POS_END, $this->id.'vue');
1106
		neon()->view->registerJs('$("#'.$this->id.'").neonGrid('.json_encode($options).')', View::POS_READY, $this->id.'neonGrid');
1107
	}
1108
1109
	/**
1110
	 * Gets a debug result message showing the query performed to
1111
	 * fetch the current set of grid results
1112
	 *
1113
	 * @throws \Exception
1114
	 * @return string
1115
	 */
1116
	public function showQuery()
1117
	{
1118
		return json_encode($this->getQueryBuilder()->getFilter());
1119
	}
1120
1121
	/**
1122
	 * Get an array of only visible columns
1123
	 * format:
1124
	 * ```
1125
	 * [
1126
	 *	 ['key' => {column object}],
1127
	 * ]
1128
	 * ```
1129
	 *
1130
	 * @return array
1131
	 */
1132
	public function getColumnsVisible()
1133
	{
1134
		$columns = [];
1135
		foreach($this->getColumns() as $key => $column) {
1136
			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...
1137
				$columns[$key] = $column;
1138
		}
1139
		return $columns;
1140
	}
1141
1142
	/**
1143
	 * When outputting the object as a string call the run function
1144
	 *
1145
	 * @return string
1146
	 */
1147
	public function __toString()
1148
	{
1149
		return $this->run();
1150
	}
1151
1152
	/**
1153
	 * Key of the column this grid is indexed by
1154
	 *
1155
	 * @var string
1156
	 */
1157
	protected $_indexedByColumn;
1158
1159
	/**
1160
	 * Set a column as the index of the grid
1161
	 * An index column assumes that the value in of that column can be used to find the row of data from the data store
1162
	 * This is useful for grid actions passing the value of that rows index column - the row id to the action
1163
	 *
1164
	 * @param string $columnKey
1165
	 * @throws \Exception if no column exists with the $columnKey in the $this->_columns array
1166
	 */
1167
	public function setIndexedByColumn($columnKey)
1168
	{
1169
		if (!isset($this->_columns[$columnKey])) {
1170
			throw new \Exception("No column exists with key '$columnKey'");
1171
		}
1172
		$this->_indexedByColumn = $columnKey;
1173
	}
1174
1175
	/**
1176
	 * Get the column that has been set as the index
1177
	 * @return IColumn
1178
	 * @throws \Exception - if no index column exists
1179
	 */
1180
	public function getIndexColumn()
1181
	{
1182
		if (!isset($this->_columns[$this->_indexedByColumn]))
1183
			throw new \Exception("No index column exists with key '$this->_indexedByColumn'.");
1184
		return $this->_columns[$this->_indexedByColumn];
1185
	}
1186
1187
	/**
1188
	 * Whether ter is a column on the grid that has been set to be the index
1189
	 * @return bool
1190
	 */
1191
	public function hasIndexColumn()
1192
	{
1193
		return isset($this->_columns[$this->_indexedByColumn]);
1194
	}
1195
1196
	/**
1197
	 * Replace field tags in a string with the values from the row of data.  A field tag is denoted with a leading colon.
1198
	 * For example a row containing ['name' => 'steve'] and a string of 'hello {$name}' will give 'hello steve'
1199
	 *
1200
	 * @param string|array $string - a string or array of strings containing field tags e.g. ':field'
1201
	 * @param array $row - The row data
1202
	 * @return string
1203
	 */
1204
	public function replaceRowTagsWithValues($string, $row)
1205
	{
1206
		$tags = $this->getRowSearchTags($row);
1207
		return str_replace(array_keys($tags), array_values($tags), $string);
1208
	}
1209
1210
	/**
1211
	 * Generates an array of search tags based on the keys of the row
1212
	 * A tag is simply the key of a row item wrapped with a particular string
1213
	 * It assumes that each row in the grid will have a consistent keys
1214
	 *
1215
	 * @param array $row
1216
	 * @return array
1217
	 */
1218
	protected function getRowSearchTags($row)
1219
	{
1220
		$tags = [];
1221
		foreach ($row as $key => $val) {
1222
			if (!is_array($val)){
1223
				$tags['{{' . $key . '}}'] = $val;
1224
			}
1225
		}
1226
		return $tags;
1227
	}
1228
1229
	/**
1230
	 * Prepare the content for CSV export by removing tags, html entities etc
1231
	 * @param string $content  the content to be converted
1232
	 */
1233
	protected function prepareCellContentForCSV($content)
1234
	{
1235
		/**
1236
		 * From experimentation with real data:
1237
		 * grid cells and headers can have html tags added
1238
		 * To export properly to csv, any html tags need to be removed and
1239
		 * html entities reverted. These can leave newlines in the data so
1240
		 * need to clear those out too.
1241
		 */
1242
		$plain = html_entity_decode(
1243
				str_replace(["\n", '"'], ['', '\''], strip_tags($content)),
1244
				ENT_HTML401 | ENT_QUOTES | ENT_HTML5);
1245
		if ($plain === $this->emptyCellDisplay)
1246
			$plain = '';
1247
		return '"'.$plain.'"';
1248
	}
1249
}
1250