Completed
Pull Request — master (#375)
by Dalibor
02:34
created

NextrasDataSource::getAggregationData()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
cc 3
eloc 13
nc 3
nop 0
1
<?php
2
3
/**
4
 * @copyright   Copyright (c) 2015 ublaboo <[email protected]>
5
 * @author      Pavel Janda <[email protected]>
6
 * @package     Ublaboo
7
 */
8
9
namespace Ublaboo\DataGrid\DataSource;
10
11
use Nextras\Dbal\SqlProcessor;
12
use Ublaboo\DataGrid\Filter;
13
use Nextras\Orm\Mapper\Dbal\DbalCollection;
14
use Nette\Utils\Strings;
15
use Nextras\Orm\Collection\ICollection;
16
use Ublaboo\DataGrid\Utils\ArraysHelper;
17
use Ublaboo\DataGrid\Utils\DateTimeHelper;
18
use Ublaboo\DataGrid\Utils\Sorting;
19
20
class NextrasDataSource extends FilterableDataSource implements IDataSource
21
{
22
23
	/**
24
	 * @var DbalCollection
25
	 */
26
	protected $data_source;
27
28
	/**
29
	 * @var array
30
	 */
31
	protected $data = [];
32
33
	/**
34
	 * @var array
35
	 */
36
	protected $aggregations = [];
37
38
	/**
39
	 * @var string
40
	 */
41
	protected $primary_key;
42
43
44
	/**
45
	 * @param ICollection  $data_source
46
	 * @param string       $primary_key
47
	 */
48
	public function __construct(ICollection $data_source, $primary_key)
49
	{
50
		$this->data_source = $data_source;
0 ignored issues
show
Documentation Bug introduced by
$data_source is of type object<Nextras\Orm\Collection\ICollection>, but the property $data_source was declared to be of type object<Nextras\Orm\Mapper\Dbal\DbalCollection>. 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...
51
		$this->primary_key = $primary_key;
52
	}
53
54
55
	/********************************************************************************
56
	 *                          IDataSource implementation                          *
57
	 ********************************************************************************/
58
59
60
	/**
61
	 * Get count of data
62
	 * @return int
63
	 */
64
	public function getCount()
65
	{
66
		return $this->data_source->countStored();
67
	}
68
69
	/**
70
	 * Get the data
71
	 * @return array
72
	 */
73
	public function getData()
74
	{
75
		/**
76
		 * Paginator is better if the query uses ManyToMany associations
77
		 */
78
		return $this->data ?: $this->data_source->fetchAll();
79
	}
80
81
82
	/**
83
	 * Filter data - get one row
84
	 * @param array $condition
85
	 * @return static
86
	 */
87
	public function filterOne(array $condition)
88
	{
89
		$cond = [];
90
		foreach ($condition as $key => $value) {
91
			$cond[$this->prepareColumn($key)] = $value;
92
		}
93
		$this->data_source = $this->data_source->findBy($cond);
94
95
		return $this;
96
	}
97
98
99
	/**
100
	 * Filter by date
101
	 * @param  Filter\FilterDate $filter
102
	 * @return static
103
	 */
104
	public function applyFilterDate(Filter\FilterDate $filter)
105
	{
106
		foreach ($filter->getCondition() as $column => $value) {
107
			$date = DateTimeHelper::tryConvertToDateTime($value, [$filter->getPhpFormat()]);
108
			$date_end = clone $date;
109
110
			$this->data_source = $this->data_source->findBy([
111
				$this->prepareColumn($column) . '>=' => $date->setTime(0, 0, 0),
112
				$this->prepareColumn($column) . '<=' => $date_end->setTime(23, 59, 59)
113
			]);
114
		}
115
116
		return $this;
117
	}
118
119
120
	/**
121
	 * Filter by date range
122
	 * @param  Filter\FilterDateRange $filter
123
	 * @return void
124
	 */
125
	public function applyFilterDateRange(Filter\FilterDateRange $filter)
126
	{
127
		$conditions = $filter->getCondition();
128
129
		$value_from = $conditions[$filter->getColumn()]['from'];
130
		$value_to = $conditions[$filter->getColumn()]['to'];
131
132
		$dataCondition = [];
133
		if ($value_from) {
134
			$date_from = DateTimeHelper::tryConvertToDateTime($value_from, [$filter->getPhpFormat()]);
135
			$dataCondition[$this->prepareColumn($filter->getColumn()) . '>='] = $date_from->setTime(0, 0, 0);
0 ignored issues
show
Bug introduced by
It seems like $filter->getColumn() targeting Ublaboo\DataGrid\Filter\Filter::getColumn() can also be of type array; however, Ublaboo\DataGrid\DataSou...Source::prepareColumn() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
136
		}
137
138
		if ($value_to) {
139
			$date_to = DateTimeHelper::tryConvertToDateTime($value_to, [$filter->getPhpFormat()]);
140
			$dataCondition[$this->prepareColumn($filter->getColumn()) . '<='] = $date_to->setTime(23, 59, 59);
0 ignored issues
show
Bug introduced by
It seems like $filter->getColumn() targeting Ublaboo\DataGrid\Filter\Filter::getColumn() can also be of type array; however, Ublaboo\DataGrid\DataSou...Source::prepareColumn() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
141
		}
142
143
		if (!empty($dataCondition)) {
144
			$this->data_source = $this->data_source->findBy($dataCondition);
145
		}
146
	}
147
148
149
	/**
150
	 * Filter by range
151
	 * @param  Filter\FilterRange $filter
152
	 * @return void
153
	 */
154
	public function applyFilterRange(Filter\FilterRange $filter)
155
	{
156
		$conditions = $filter->getCondition();
157
158
		$value_from = $conditions[$filter->getColumn()]['from'];
159
		$value_to = $conditions[$filter->getColumn()]['to'];
160
161
		$dataCondition = [];
162
163
		if ($value_from) {
164
			$dataCondition[$this->prepareColumn($filter->getColumn()) . '>='] = $value_from;
0 ignored issues
show
Bug introduced by
It seems like $filter->getColumn() targeting Ublaboo\DataGrid\Filter\Filter::getColumn() can also be of type array; however, Ublaboo\DataGrid\DataSou...Source::prepareColumn() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
165
		}
166
167
		if ($value_to) {
168
			$dataCondition[$this->prepareColumn($filter->getColumn()) . '<='] = $value_to;
0 ignored issues
show
Bug introduced by
It seems like $filter->getColumn() targeting Ublaboo\DataGrid\Filter\Filter::getColumn() can also be of type array; however, Ublaboo\DataGrid\DataSou...Source::prepareColumn() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
169
		}
170
171
		if (!empty($dataCondition)) {
172
			$this->data_source = $this->data_source->findBy($dataCondition);
173
		}
174
	}
175
176
177
	/**
178
	 * Filter by keyword
179
	 * @param  Filter\FilterText $filter
180
	 * @return void
181
	 */
182
	public function applyFilterText(Filter\FilterText $filter)
183
	{
184
		$condition = $filter->getCondition();
185
		$expr = '(';
186
		$params = [];
187
188
		foreach ($condition as $column => $value) {
189
			if ($filter->isExactSearch()) {
190
				$expr .= "%column = %s OR ";
191
				$params[] = $column;
192
				$params[] = "$value";
193
				continue;
194
			}
195
196 View Code Duplication
			if ($filter->hasSplitWordsSearch() === FALSE) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
197
				$words = [$value];
198
			} else {
199
				$words = explode(' ', $value);
200
			}
201
202
			foreach ($words as $word) {
203
				$expr .= "%column LIKE %s OR ";
204
				$params[] = $column;
205
				$params[] = "%$word%";
206
			}
207
		}
208
209
		$expr = preg_replace('/ OR $/', ')', $expr);
210
211
		array_unshift($params, $expr);
212
213
		call_user_func_array([$this->data_source->getQueryBuilder(), 'andWhere'], $params);
214
	}
215
216
217
	/**
218
	 * Filter by multi select value
219
	 * @param  Filter\FilterMultiSelect $filter
220
	 * @return void
221
	 */
222
	public function applyFilterMultiSelect(Filter\FilterMultiSelect $filter)
223
	{
224
		$condition = $filter->getCondition();
225
		$values = $condition[$filter->getColumn()];
226
		$expr = '(';
227
228
		foreach ($values as $value) {
229
			$expr .= "%column = %any OR ";
230
			$params[] = $filter->getColumn();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$params was never initialized. Although not strictly required by PHP, it is generally a good practice to add $params = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
231
			$params[] = "$value";
232
		}
233
234
		$expr = preg_replace('/ OR $/', ')', $expr);
235
236
		array_unshift($params, $expr);
237
238
		call_user_func_array([$this->data_source->getQueryBuilder(), 'andWhere'], $params);
239
	}
240
241
242
	/**
243
	 * Filter by select value
244
	 * @param  Filter\FilterSelect $filter
245
	 * @return void
246
	 */
247
	public function applyFilterSelect(Filter\FilterSelect $filter)
248
	{
249
		$this->data_source = $this->data_source->findBy([$this->prepareColumn($filter->getColumn()) => $filter->getValue()]);
0 ignored issues
show
Bug introduced by
It seems like $filter->getColumn() targeting Ublaboo\DataGrid\Filter\Filter::getColumn() can also be of type array; however, Ublaboo\DataGrid\DataSou...Source::prepareColumn() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
250
	}
251
252
253
	/**
254
	 * Apply limit and offset on data
255
	 * @param int $offset
256
	 * @param int $limit
257
	 * @return static
258
	 */
259
	public function limit($offset, $limit)
260
	{
261
		$this->data_source = $this->data_source->limitBy($limit, $offset);
262
263
		return $this;
264
	}
265
266
267
	/**
268
	 * Sort data
269
	 * @param  Sorting $sorting
270
	 * @return static
271
	 */
272
	public function sort(Sorting $sorting)
273
	{
274
		if (is_callable($sorting->getSortCallback())) {
275
			call_user_func(
276
				$sorting->getSortCallback(),
277
				$this->data_source,
278
				$sorting->getSort()
279
			);
280
281
			return $this;
282
		}
283
284
		$sort = $sorting->getSort();
285
286
		if (!empty($sort)) {
287
			foreach ($sort as $column => $order) {
288
				$this->data_source = $this->data_source->orderBy($column, $order);
289
			}
290
		} else {
291
			/**
292
			 * Has the statement already a order by clause?
293
			 */
294
			$order = $this->data_source->getQueryBuilder()->getClause('order');
295
296
			if (ArraysHelper::testEmpty($order)) {
297
				$this->data_source = $this->data_source->orderBy($this->primary_key);
298
			}
299
		}
300
301
		return $this;
302
	}
303
304
	/**
305
	 * Adjust column from DataGrid 'foreignKey.column' to Nextras 'this->foreignKey->column'
306
	 * @param string $column
307
	 * @return string
308
	 */
309
	private function prepareColumn($column)
310
	{
311
	if (Strings::contains($column, '.')) {
312
		return 'this->' . str_replace('.', '->', $column);
313
	}
314
	return $column;
315
	}
316
317
	/**
318
	 * @param string $aggregation_type
319
	 * @param string $column
320
	 * @return mixed
321
	 */
322
	public function addAggregationColumn($aggregation_type, $column)
323
	{
324
		$this->aggregations[$column] = $aggregation_type;
325
	}
326
327
	/**
328
	 * get aggregation row
329
	 * @return array
330
	 */
331
	public function getAggregationData()
332
	{
333
		if (count($this->aggregations) == 0) {
334
			return FALSE;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return FALSE; (false) is incompatible with the return type declared by the interface Ublaboo\DataGrid\DataSou...rce::getAggregationData of type array.

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...
335
		}
336
		$sql = 'SELECT ';
337
		foreach ($this->aggregations as $column => $aggregation_type) {
338
			$sql .= $aggregation_type . '(' . $column . ') as ' . $column . ',';
339
		}
340
341
		$sql = rtrim($sql, ',') . ' FROM ( ' . $this->data_source->getQueryBuilder()->getQuerySql() . ' ) as data';
342
343
		$params = $this->data_source->getQueryBuilder()->getQueryParameters();
344
		array_unshift($params, $sql);
345
		$sqlPreprocessor = new SqlProcessor($this->data_source->getQueryBuilder()->driver);
0 ignored issues
show
Bug introduced by
The property driver does not seem to exist in Nextras\Dbal\QueryBuilder\QueryBuilder.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
346
		$sql = $sqlPreprocessor->process($params);
347
348
349
		$result = $this->data_source->getQueryBuilder()->driver->query($sql)->fetch()->toArray();
350
351
352
		return $result;
353
	}
354
}
355