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

DoctrineCollectionDataSource   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 303
Duplicated Lines 18.15 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 41
lcom 1
cbo 11
dl 55
loc 303
rs 8.2769
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getFilteredCollection() 0 4 1
A getCount() 0 4 1
A getData() 0 4 1
A filterOne() 0 9 2
A applyFilterDate() 0 12 2
A applyFilterDateRange() 16 22 3
A applyFilterRange() 0 14 3
B applyFilterText() 5 24 5
A applyFilterMultiSelect() 0 7 1
A applyFilterSelect() 0 7 2
A limit() 0 6 1
A sort() 0 21 3
A addAggregationColumn() 0 4 1
C getAggregationData() 34 34 14

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DoctrineCollectionDataSource 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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

1
<?php
2
3
/**
4
 * @copyright   Copyright (c) 2015 ublaboo <[email protected]>
5
 * @author      Martin Procházka <[email protected]>
6
 * @package     Ublaboo
7
 */
8
9
namespace Ublaboo\DataGrid\DataSource;
10
11
use Doctrine\Common\Collections\Collection;
12
use Doctrine\Common\Collections\Criteria;
13
use Ublaboo\DataGrid\Column\ColumnAggregationFunction;
14
use Ublaboo\DataGrid\Filter;
15
use Ublaboo\DataGrid\Utils\Sorting;
16
17
final class DoctrineCollectionDataSource extends FilterableDataSource implements IDataSource
18
{
19
20
	/**
21
	 * @var Collection
22
	 */
23
	private $data_source;
24
25
	/**
26
	 * @var string
27
	 */
28
	private $primary_key;
29
30
	/**
31
	 * @var Criteria
32
	 */
33
	private $criteria;
34
35
	/**
36
	 * @var array
37
	 */
38
	protected $aggregations = [];
39
40
	/**
41
	 * @param Collection  $collection
42
	 * @param string      $primary_key
43
	 */
44
	public function __construct(Collection $collection, $primary_key)
45
	{
46
		$this->criteria = Criteria::create();
47
		$this->data_source = $collection;
48
		$this->primary_key = $primary_key;
49
	}
50
51
52
	/**
53
	 * @return Collection
54
	 */
55
	private function getFilteredCollection()
56
	{
57
		return $this->data_source->matching($this->criteria);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\Common\Collections\Collection as the method matching() does only exist in the following implementations of said interface: Doctrine\Common\Collections\ArrayCollection, Doctrine\ORM\LazyCriteriaCollection, Doctrine\ORM\PersistentCollection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
58
	}
59
60
61
	/********************************************************************************
62
	 *                          IDataSource implementation                          *
63
	 ********************************************************************************/
64
65
66
	/**
67
	 * Get count of data
68
	 * @return int
69
	 */
70
	public function getCount()
71
	{
72
		return $this->getFilteredCollection()->count();
73
	}
74
75
76
	/**
77
	 * Get the data
78
	 * @return array
79
	 */
80
	public function getData()
81
	{
82
		return $this->getFilteredCollection()->toArray();
83
	}
84
85
86
	/**
87
	 * Filter data - get one row
88
	 * @param array $condition
89
	 * @return static
90
	 */
91
	public function filterOne(array $condition)
92
	{
93
		foreach ($condition as $column => $value) {
94
			$expr = Criteria::expr()->eq($column, $value);
95
			$this->criteria->andWhere($expr);
96
		}
97
98
		return $this;
99
	}
100
101
102
	/**
103
	 * Filter by date
104
	 * @param  Filter\FilterDate $filter
105
	 * @return void
106
	 */
107
	public function applyFilterDate(Filter\FilterDate $filter)
108
	{
109
		foreach ($filter->getCondition() as $column => $value) {
110
			$date = \DateTime::createFromFormat($filter->getPhpFormat(), $value)
111
				->format('Y-m-d H:i:s');
112
113
			$from = Criteria::expr()->gte($filter->getColumn(), $date->format('Y-m-d 00:00:00'));
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, Doctrine\Common\Collecti...xpressionBuilder::gte() 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...
114
			$to = Criteria::expr()->lte($filter->getColumn(), $date->format('Y-m-d 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, Doctrine\Common\Collecti...xpressionBuilder::lte() 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...
115
116
			$this->criteria->andWhere($from)->andWhere($to);
117
		}
118
	}
119
120
121
	/**
122
	 * Filter by date range
123
	 * @param  Filter\FilterDateRange $filter
124
	 * @return void
125
	 */
126
	public function applyFilterDateRange(Filter\FilterDateRange $filter)
127
	{
128
		$values = $conditions[$filter->getColumn()];
0 ignored issues
show
Bug introduced by
The variable $conditions does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
129
130 View Code Duplication
		if ($value_from = $values['from']) {
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...
131
			$date_from = \DateTime::createFromFormat($filter->getPhpFormat(), $value_from)
132
				->setTime(0, 0, 0)
133
				->format('Y-m-d H:i:s');
134
135
			$expr = Criteria::expr()->gte($filter->getColumn(), $date_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, Doctrine\Common\Collecti...xpressionBuilder::gte() 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
			$this->criteria->andWhere($expr);
137
		}
138
139 View Code Duplication
		if ($value_to = $values['to']) {
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...
140
			$date_to = \DateTime::createFromFormat($filter->getPhpFormat(), $value_to)
141
				->setTime(23, 59, 59)
142
				->format('Y-m-d H:i:s');
143
144
			$expr = Criteria::expr()->lte($filter->getColumn(), $date_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, Doctrine\Common\Collecti...xpressionBuilder::lte() 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...
145
			$this->criteria->andWhere($expr);
146
		}
147
	}
148
149
150
	/**
151
	 * Filter by range
152
	 * @param  Filter\FilterRange $filter
153
	 * @return void
154
	 */
155
	public function applyFilterRange(Filter\FilterRange $filter)
156
	{
157
		$values = $conditions[$filter->getColumn()];
0 ignored issues
show
Bug introduced by
The variable $conditions does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
158
159
		if ($value_from = $values['from']) {
160
			$expr = Criteria::expr()->gte($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, Doctrine\Common\Collecti...xpressionBuilder::gte() 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...
161
			$this->criteria->andWhere($expr);
162
		}
163
164
		if ($value_to = $values['to']) {
165
			$expr = Criteria::expr()->lte($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, Doctrine\Common\Collecti...xpressionBuilder::lte() 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...
166
			$this->criteria->andWhere($expr);
167
		}
168
	}
169
170
171
	/**
172
	 * Filter by keyword
173
	 * @param  Filter\FilterText $filter
174
	 * @return void
175
	 */
176
	public function applyFilterText(Filter\FilterText $filter)
177
	{
178
		$exprs = [];
179
180
		foreach ($filter->getCondition() as $column => $value) {
181
			if($filter->isExactSearch()) {
182
				$exprs[] = Criteria::expr()->eq($column, $value);
183
				continue;
184
			}
185
186 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...
187
				$words = [$value];
188
			} else {
189
				$words = explode(' ', $value);
190
			}
191
192
			foreach ($words as $word) {
193
				$exprs[] = Criteria::expr()->contains($column, $word);
194
			}
195
		}
196
197
		$expr = call_user_func_array([Criteria::expr(), 'orX'], $exprs);
198
		$this->criteria->andWhere($expr);
199
	}
200
201
202
	/**
203
	 * Filter by multi select value
204
	 * @param  Filter\FilterMultiSelect $filter
205
	 * @return void
206
	 */
207
	public function applyFilterMultiSelect(Filter\FilterMultiSelect $filter)
208
	{
209
		$values = $filter->getCondition()[$filter->getColumn()];
210
211
		$expr = Criteria::expr()->in($filter->getColumn(), $values);
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, Doctrine\Common\Collecti...ExpressionBuilder::in() 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...
212
		$this->criteria->andWhere($expr);
213
	}
214
215
216
	/**
217
	 * Filter by select value
218
	 * @param  Filter\FilterSelect $filter
219
	 * @return void
220
	 */
221
	public function applyFilterSelect(Filter\FilterSelect $filter)
222
	{
223
		foreach ($filter->getCondition() as $column => $value) {
224
			$expr = Criteria::expr()->eq($column, $value);
225
			$this->criteria->andWhere($expr);
226
		}
227
	}
228
229
230
	/**
231
	 * Apply limit and offset on data
232
	 * @param int $offset
233
	 * @param int $limit
234
	 * @return static
235
	 */
236
	public function limit($offset, $limit)
237
	{
238
		$this->criteria->setFirstResult($offset)->setMaxResults($limit);
239
240
		return $this;
241
	}
242
243
244
	/**
245
	 * Sort data
246
	 * @param Sorting $sorting
247
	 * @return static
248
	 */
249
	public function sort(Sorting $sorting)
250
	{
251
		if (is_callable($sorting->getSortCallback())) {
252
			call_user_func(
253
				$sorting->getSortCallback(),
254
				$this->criteria,
255
				$sorting->getSort()
256
			);
257
258
			return $this;
259
		}
260
261
		if ($sort = $sorting->getSort()) {
262
			$this->criteria->orderBy($sort);
263
			return $this;
264
		}
265
266
		$this->criteria->orderBy([$this->primary_key => 'ASC']);
267
268
		return $this;
269
	}
270
271
	/**
272
	 * @param string $aggregation_type
273
	 * @param string $column
274
	 * @return mixed
275
	 */
276
	public function addAggregationColumn($aggregation_type, $column)
277
	{
278
		$this->aggregations[$column] = $aggregation_type;
279
	}
280
281
	/**
282
	 * get aggregation row
283
	 * @return array
284
	 */
285 View Code Duplication
	public function getAggregationData()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
286
	{
287
		$result = [];
288
		foreach ($this->data_source as $row) {
289
			foreach ($this->aggregations as $column => $aggregation_type) {
290
				switch ($aggregation_type) {
291
					case ColumnAggregationFunction::AGGREGATION_TYPE_SUM:
292
					case ColumnAggregationFunction::AGGREGATION_TYPE_AVG:
293
						if (!isset($result[$column])) {
294
							$result[$column] = 0;
295
						}
296
						$result[$column] += $row[$column];
297
						break;
298
					case ColumnAggregationFunction::AGGREGATION_TYPE_MIN:
299
						if (!isset($result[$column]) || $row[$column] < $result[$column]) {
300
							$result[$column] = $row[$column];
301
						}
302
						break;
303
					case ColumnAggregationFunction::AGGREGATION_TYPE_MAX:
304
						if (!isset($result[$column]) || $row[$column] > $result[$column]) {
305
							$result[$column] = $row[$column];
306
						}
307
						break;
308
				}
309
			}
310
		}
311
312
		foreach ($this->aggregations as $column => $aggregation_type) {
313
			if ($aggregation_type == ColumnAggregationFunction::AGGREGATION_TYPE_AVG) {
314
				$result[$column] =  $result[$column] / $this->data_source->count();
315
			}
316
		}
317
		return $result;
318
	}
319
}
320