Passed
Pull Request — master (#1449)
by René
04:14
created

OptionService   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 425
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 4
Bugs 3 Features 0
Metric Value
wmc 64
eloc 196
c 4
b 3
f 0
dl 0
loc 425
ccs 0
cts 199
cp 0
rs 3.28

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 1
D list() 0 111 25
A get() 0 8 2
A moveModifier() 0 12 6
A add() 0 14 2
A setOrder() 0 26 6
A confirm() 0 8 2
A getHighestOrder() 0 6 3
A clone() 0 15 2
A update() 0 8 1
A reorder() 0 23 5
A setOption() 0 16 4
A sequence() 0 27 4
A delete() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like OptionService 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 OptionService, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Vinzenz Rosenkranz <[email protected]>
4
 *
5
 * @author René Gieling <[email protected]>
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 *  This program is free software: you can redistribute it and/or modify
10
 *  it under the terms of the GNU Affero General Public License as
11
 *  published by the Free Software Foundation, either version 3 of the
12
 *  License, or (at your option) any later version.
13
 *
14
 *  This program is distributed in the hope that it will be useful,
15
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 *  GNU Affero General Public License for more details.
18
 *
19
 *  You should have received a copy of the GNU Affero General Public License
20
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 */
23
24
namespace OCA\Polls\Service;
25
26
use DateTime;
27
use OCP\AppFramework\Db\DoesNotExistException;
28
use OCA\Polls\Exceptions\NotAuthorizedException;
29
use OCA\Polls\Exceptions\BadRequestException;
30
use OCA\Polls\Exceptions\DuplicateEntryException;
31
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
32
33
use OCA\Polls\Db\OptionMapper;
34
use OCA\Polls\Db\VoteMapper;
35
use OCA\Polls\Db\Option;
36
use OCA\Polls\Db\PollMapper;
37
use OCA\Polls\Db\Poll;
38
use OCA\Polls\Db\Watch;
39
use OCA\Polls\Model\Acl;
40
41
class OptionService {
42
43
	/** @var Acl */
44
	private $acl;
45
46
	/** @var Option */
47
	private $option;
48
49
	/** @var OptionMapper */
50
	private $optionMapper;
51
52
	/** @var PollMapper */
53
	private $pollMapper;
54
55
	/** @var VoteMapper */
56
	private $voteMapper;
57
58
	/** @var WatchService */
59
	private $watchService;
60
61
	public function __construct(
62
		Acl $acl,
63
		Option $option,
64
		OptionMapper $optionMapper,
65
		PollMapper $pollMapper,
66
		VoteMapper $voteMapper,
67
		WatchService $watchService
68
	) {
69
		$this->acl = $acl;
70
		$this->option = $option;
71
		$this->optionMapper = $optionMapper;
72
		$this->pollMapper = $pollMapper;
73
		$this->voteMapper = $voteMapper;
74
		$this->watchService = $watchService;
75
	}
76
77
	/**
78
	 * 	 * Get all options of given poll
79
	 *
80
	 * @return Option[]
81
	 *
82
	 * @psalm-return array<array-key, Option>
83
	 */
84
	public function list(?int $pollId = 0, string $token = ''): array {
85
		if ($token) {
86
			$this->acl->setToken($token);
87
			$pollId = $this->acl->getPollId();
88
		} else {
89
			$this->acl->setPollId($pollId)->request(Acl::PERMISSION_VIEW);
90
		}
91
92
		if (!$this->acl->isAllowed(Acl::PERMISSION_VIEW)) {
93
			throw new NotAuthorizedException;
94
		}
95
96
		try {
97
			$poll = $this->pollMapper->find($pollId);
0 ignored issues
show
Bug introduced by
It seems like $pollId can also be of type null; however, parameter $id of OCA\Polls\Db\PollMapper::find() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

97
			$poll = $this->pollMapper->find(/** @scrutinizer ignore-type */ $pollId);
Loading history...
98
			$options = $this->optionMapper->findByPoll($pollId);
0 ignored issues
show
Bug introduced by
It seems like $pollId can also be of type null; however, parameter $pollId of OCA\Polls\Db\OptionMapper::findByPoll() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

98
			$options = $this->optionMapper->findByPoll(/** @scrutinizer ignore-type */ $pollId);
Loading history...
99
			$votes = $this->voteMapper->findByPoll($pollId);
0 ignored issues
show
Bug introduced by
It seems like $pollId can also be of type null; however, parameter $pollId of OCA\Polls\Db\VoteMapper::findByPoll() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

99
			$votes = $this->voteMapper->findByPoll(/** @scrutinizer ignore-type */ $pollId);
Loading history...
100
			$countParticipants = count($this->voteMapper->findParticipantsByPoll($pollId));
0 ignored issues
show
Bug introduced by
It seems like $pollId can also be of type null; however, parameter $pollId of OCA\Polls\Db\VoteMapper::findParticipantsByPoll() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

100
			$countParticipants = count($this->voteMapper->findParticipantsByPoll(/** @scrutinizer ignore-type */ $pollId));
Loading history...
101
102
			foreach ($options as $option) {
103
				$option->yes = count(
104
					array_filter($votes, function ($vote) use ($option) {
105
						if ($vote->getVoteOptionText() === $option->getPollOptionText()
106
							&& $vote->getVoteAnswer() === 'yes') {
107
							return $vote;
108
						}
109
					})
110
				);
111
112
				$option->realno = count(
113
					array_filter($votes, function ($vote) use ($option) {
114
						if ($vote->getVoteOptionText() === $option->getPollOptionText()
115
							&& $vote->getVoteAnswer() === 'no') {
116
							return $vote;
117
						}
118
					})
119
				);
120
121
				$option->maybe = count(
122
					array_filter($votes, function ($vote) use ($option) {
123
						if ($vote->getVoteOptionText() === $option->getPollOptionText()
124
							&& $vote->getVoteAnswer() === 'maybe') {
125
							return $vote;
126
						}
127
					})
128
				);
129
130
				$option->isBookedUp = $poll->getOptionLimit() ? $poll->getOptionLimit() <= $option->yes : false;
131
132
				if (!$this->acl->isAllowed(Acl::PERMISSION_SEE_RESULTS)) {
133
					$option->yes = 0;
134
					$option->no = 0;
135
					$option->maybe = 0;
136
					$option->realNo = 0;
137
				} else {
138
					$option->no = $countParticipants - $option->maybe - $option->yes;
139
				}
140
			}
141
			// hide booked up options except the user has edit permission
142
			if ($poll->getHideBookedUp() && !$this->acl->isAllowed(Acl::PERMISSION_EDIT)) {
0 ignored issues
show
Bug introduced by
The method getHideBookedUp() does not exist on OCA\Polls\Db\Poll. 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

142
			if ($poll->/** @scrutinizer ignore-call */ getHideBookedUp() && !$this->acl->isAllowed(Acl::PERMISSION_EDIT)) {
Loading history...
143
144
				// Thats an ugly solution, but for now, it seems to work
145
				// Optimization proposals are welcome
146
147
				// If the user opted in, do not hide them
148
				// First: Find votes, where the user voted yes or maybe
149
				$userId = $this->acl->getUserId();
150
				$exceptVotes = array_filter($votes, function ($vote) use ($userId){
151
					if ($vote->getUserId() === $userId && in_array($vote->getVoteAnswer(), ['yes', 'maybe'])) {
152
						return $vote;
153
					}
154
				});
155
156
				// Second: Extract only the vote option texts to an array
157
				$exceptVotes = array_values(array_map(function ($vote){
158
   					return $vote->getVoteOptionText();
159
				}, $exceptVotes));
160
161
				// Third: Reduce options to options, which are not booked up or
162
				// the user has opted in via yes or maybe answer
163
				$options = array_filter($options, function ($option) use ($exceptVotes) {
164
					if (!$option->getIsBookedUp() || in_array($option->getPollOptionText(), $exceptVotes)) {
165
						return $option;
166
					}
167
				});
168
			} else if ($this->acl->isAllowed(Acl::PERMISSION_SEE_RESULTS)) {
169
170
				// sort array by yes and maybe votes
171
				usort($options, function ($a, $b) {
172
					    $diff = $b->yes - $a->yes;
173
    					return ($diff !== 0) ? $diff : $b->maybe - $a->maybe;
174
				});
175
176
				// calculate the rank
177
				for ($i=0; $i < count($options); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
178
					if ($i > 0 && $options[$i]->yes === $options[$i-1]->yes && $options[$i]->maybe === $options[$i-1]->maybe) {
179
						$options[$i]->rank = $options[$i-1]->rank;
180
					} else {
181
						$options[$i]->rank = $i + 1;
182
					}
183
				}
184
185
				// restore original order
186
				usort($options, function ($a, $b) {
187
					    return $a->getOrder() - $b->getOrder();
188
				});
189
190
			}
191
192
			return array_values($options);
193
		} catch (DoesNotExistException $e) {
194
			return [];
195
		}
196
	}
197
198
	/**
199
	 * 	 * Get option
200
	 *
201
	 * @return Option
202
	 */
203
	public function get(int $optionId): Option {
204
		$this->acl->setPollId($this->optionMapper->find($optionId)->getPollId())->request(Acl::PERMISSION_VIEW);
205
206
		if (!$this->acl->isAllowed(Acl::PERMISSION_VIEW)) {
207
			throw new NotAuthorizedException;
208
		}
209
210
		return $this->optionMapper->find($optionId);
211
	}
212
213
214
	/**
215
	 * 	 * Add a new option
216
	 *
217
	 * @return Option
218
	 */
219
	public function add(int $pollId, int $timestamp = 0, string $pollOptionText = '', ?int $duration = 0): Option {
220
		$this->acl->setPollId($pollId)->request(Acl::PERMISSION_EDIT);
221
		$this->option = new Option();
222
		$this->option->setPollId($pollId);
223
		$this->option->setOrder($this->getHighestOrder($this->option->getPollId()) + 1);
224
		$this->setOption($timestamp, $pollOptionText, $duration);
225
226
		try {
227
			$this->option = $this->optionMapper->insert($this->option);
228
			$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
229
		} catch (UniqueConstraintViolationException $e) {
230
			throw new DuplicateEntryException('This option already exists');
231
		}
232
		return $this->option;
233
	}
234
235
	/**
236
	 * 	 * Update option
237
	 *
238
	 * @return Option
239
	 */
240
	public function update(int $optionId, int $timestamp = 0, ?string $pollOptionText = '', ?int $duration = 0): Option {
241
		$this->option = $this->optionMapper->find($optionId);
242
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
243
		$this->setOption($timestamp, $pollOptionText, $duration);
244
245
		$this->option = $this->optionMapper->update($this->option);
246
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
247
		return $this->option;
248
	}
249
250
	/**
251
	 * 	 * Delete option
252
	 *
253
	 * @return Option
254
	 */
255
	public function delete(int $optionId): Option {
256
		$this->option = $this->optionMapper->find($optionId);
257
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
258
		$this->optionMapper->delete($this->option);
259
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
260
261
		return $this->option;
262
	}
263
264
	/**
265
	 * 	 * Switch optoin confirmation
266
	 *
267
	 * @return Option
268
	 */
269
	public function confirm(int $optionId): Option {
270
		$this->option = $this->optionMapper->find($optionId);
271
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
272
273
		$this->option->setConfirmed($this->option->getConfirmed() ? 0 : time());
274
		$this->option = $this->optionMapper->update($this->option);
275
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
276
		return $this->option;
277
	}
278
279
	/**
280
	 * 	 * Make a sequence of date poll options
281
	 *
282
	 * @return Option[]
283
	 *
284
	 * @psalm-return array<array-key, Option>
285
	 */
286
	public function sequence(int $optionId, int $step, string $unit, int $amount): array {
287
		$baseDate = new DateTime;
288
		$this->option = $this->optionMapper->find($optionId);
289
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
290
291
		if ($step === 0) {
292
			return $this->optionMapper->findByPoll($this->option->getPollId());
293
		}
294
295
		$baseDate->setTimestamp($this->option->getTimestamp());
296
297
		for ($i = 0; $i < $amount; $i++) {
298
			$clonedOption = new Option();
299
			$clonedOption->setPollId($this->option->getPollId());
300
			$clonedOption->setDuration($this->option->getDuration());
301
			$clonedOption->setConfirmed(0);
302
			$clonedOption->setTimestamp($baseDate->modify($step . ' ' . $unit)->getTimestamp());
303
			$clonedOption->setOrder($clonedOption->getTimestamp());
304
			$clonedOption->setPollOptionText($baseDate->format('c'));
305
			try {
306
				$this->optionMapper->insert($clonedOption);
307
			} catch (UniqueConstraintViolationException $e) {
308
				\OC::$server->getLogger()->warning('skip adding ' . $baseDate->format('c') . 'for pollId' . $this->option->getPollId() . '. Option alredy exists.');
309
			}
310
		}
311
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
312
		return $this->optionMapper->findByPoll($this->option->getPollId());
313
	}
314
315
	/**
316
	 * 	 * Copy options from $fromPoll to $toPoll
317
	 *
318
	 * @return Option[]
319
	 *
320
	 * @psalm-return array<array-key, Option>
321
	 */
322
	public function clone(int $fromPollId, int $toPollId): array {
323
		$this->acl->setPollId($fromPollId);
324
325
		foreach ($this->optionMapper->findByPoll($fromPollId) as $origin) {
326
			$option = new Option();
327
			$option->setPollId($toPollId);
328
			$option->setConfirmed(0);
329
			$option->setPollOptionText($origin->getPollOptionText());
330
			$option->setTimestamp($origin->getTimestamp());
331
			$option->setDuration($origin->getDuration());
332
			$option->setOrder($option->getOrder());
333
			$this->optionMapper->insert($option);
334
		}
335
336
		return $this->optionMapper->findByPoll($toPollId);
337
	}
338
339
	/**
340
	 * 	 * Reorder options with the order specified by $options
341
	 *
342
	 * @return Option[]
343
	 *
344
	 * @psalm-return array<array-key, Option>
345
	 */
346
	public function reorder(int $pollId, array $options): array {
347
		try {
348
			$poll = $this->pollMapper->find($pollId);
349
			$this->acl->setPoll($poll)->request(Acl::PERMISSION_EDIT);
350
351
			if ($poll->getType() === Poll::TYPE_DATE) {
352
				throw new BadRequestException("Not allowed in date polls");
353
			}
354
		} catch (DoesNotExistException $e) {
355
			throw new NotAuthorizedException;
356
		}
357
358
		$i = 0;
359
		foreach ($options as $option) {
360
			$this->option = $this->optionMapper->find($option['id']);
361
			if ($pollId === intval($this->option->getPollId())) {
362
				$this->option->setOrder(++$i);
363
				$this->optionMapper->update($this->option);
364
			}
365
		}
366
367
		$this->watchService->writeUpdate($pollId, Watch::OBJECT_OPTIONS);
368
		return $this->optionMapper->findByPoll($pollId);
369
	}
370
371
	/**
372
	 * 	 * Change order for $optionId and reorder the options
373
	 *
374
	 * @NoAdminRequired
375
	 *
376
	 * @return Option[]
377
	 *
378
	 * @psalm-return array<array-key, Option>
379
	 */
380
	public function setOrder(int $optionId, int $newOrder): array {
381
		try {
382
			$this->option = $this->optionMapper->find($optionId);
383
			$poll = $this->pollMapper->find($this->option->getPollId());
384
			$this->acl->setPoll($poll)->request(Acl::PERMISSION_EDIT);
385
386
			if ($poll->getType() === Poll::TYPE_DATE) {
387
				throw new BadRequestException("Not allowed in date polls");
388
			}
389
		} catch (DoesNotExistException $e) {
390
			throw new NotAuthorizedException;
391
		}
392
393
		if ($newOrder < 1) {
394
			$newOrder = 1;
395
		} elseif ($newOrder > $this->getHighestOrder($poll->getId())) {
396
			$newOrder = $this->getHighestOrder($poll->getId());
397
		}
398
399
		foreach ($this->optionMapper->findByPoll($poll->getId()) as $option) {
400
			$option->setOrder($this->moveModifier($this->option->getOrder(), $newOrder, $option->getOrder()));
401
			$this->optionMapper->update($option);
402
		}
403
404
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
405
		return $this->optionMapper->findByPoll($this->option->getPollId());
406
	}
407
408
	/**
409
	 * 	 * moveModifier - evaluate new order
410
	 * 	 * depending on the old and the new position of a moved array item
411
	 * 	 * $moveFrom - old position of the moved item
412
	 * 	 * $moveTo   - target posotion of the moved item
413
	 * 	 * $value    - current position of the current item
414
	 * 	 * Returns the modified new new position of the current item
415
	 *
416
	 * @return int
417
	 */
418
	private function moveModifier(int $moveFrom, int $moveTo, int $currentPosition): int {
419
		$moveModifier = 0;
420
		if ($moveFrom < $currentPosition && $currentPosition <= $moveTo) {
421
			// moving forward
422
			$moveModifier = -1;
423
		} elseif ($moveTo <= $currentPosition && $currentPosition < $moveFrom) {
424
			//moving backwards
425
			$moveModifier = 1;
426
		} elseif ($moveFrom === $currentPosition) {
427
			return $moveTo;
428
		}
429
		return $currentPosition + $moveModifier;
430
	}
431
432
	/**
433
	 * Set option entities validated
434
	 */
435
	private function setOption(int $timestamp = 0, ?string $pollOptionText = '', ?int $duration = 0): void {
436
		$poll = $this->pollMapper->find($this->option->getPollId());
437
438
		if ($poll->getType() === Poll::TYPE_DATE) {
439
			$this->option->setTimestamp($timestamp);
440
			$this->option->setOrder($timestamp);
441
			$this->option->setDuration($duration);
442
			if ($duration === 0) {
443
				$this->option->setPollOptionText(date('c', $timestamp));
444
			} elseif ($duration > 0) {
445
				$this->option->setPollOptionText(date('c', $timestamp) .' - ' . date('c', $timestamp + $duration));
446
			} else {
447
				$this->option->setPollOptionText($pollOptionText);
448
			}
449
		} else {
450
			$this->option->setPollOptionText($pollOptionText);
451
		}
452
	}
453
454
	/**
455
	 * 	 * Get the highest order number in $pollId
456
	 * 	 * Return Highest order number
457
	 *
458
	 * @return int
459
	 */
460
	private function getHighestOrder(int $pollId): int {
461
		$highestOrder = 0;
462
		foreach ($this->optionMapper->findByPoll($pollId) as $option) {
463
			$highestOrder = ($option->getOrder() > $highestOrder) ? $option->getOrder() : $highestOrder;
464
		}
465
		return $highestOrder;
466
	}
467
}
468