OptionService   F
last analyzed

Complexity

Total Complexity 74

Size/Duplication

Total Lines 532
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 8
Bugs 0 Features 4
Metric Value
wmc 74
eloc 231
dl 0
loc 532
ccs 0
cts 199
cp 0
rs 2.48
c 8
b 0
f 4

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 1
A list() 0 26 6
A filterBookedUp() 0 4 2
A getUsersVotes() 0 14 2
A get() 0 4 1
A calculateRanks() 0 20 6
A add() 0 28 5
A moveModifier() 0 12 6
A setOrder() 0 26 6
A confirm() 0 8 2
A getHighestOrder() 0 6 3
A clone() 0 15 2
A update() 0 11 3
A reorder() 0 23 5
A setOption() 0 16 4
A sequence() 0 32 5
A shift() 0 21 4
A delete() 0 16 4
B calculateVotes() 0 33 7

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 Psr\Log\LoggerInterface;
27
use DateTime;
28
use OCP\AppFramework\Db\DoesNotExistException;
29
use OCA\Polls\Exceptions\NotAuthorizedException;
30
use OCA\Polls\Exceptions\BadRequestException;
31
use OCA\Polls\Exceptions\DuplicateEntryException;
32
use OCA\Polls\Exceptions\InvalidPollTypeException;
33
use OCA\Polls\Exceptions\InvalidOptionPropertyException;
34
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
35
36
use OCA\Polls\Db\OptionMapper;
37
use OCA\Polls\Db\VoteMapper;
38
use OCA\Polls\Db\Vote;
39
use OCA\Polls\Db\Option;
40
use OCA\Polls\Db\PollMapper;
41
use OCA\Polls\Db\Poll;
42
use OCA\Polls\Db\Watch;
43
use OCA\Polls\Model\Acl;
44
45
class OptionService {
46
47
	/** @var LoggerInterface */
48
	private $logger;
49
50
	/** @var string */
51
	private $appName;
52
53
	/** @var Acl */
54
	private $acl;
55
56
	/** @var Option */
57
	private $option;
58
59
	/** @var int */
60
	private $countParticipants;
61
62
	/** @var Poll */
63
	private $poll;
64
65
	/** @var Option[] */
66
	private $options;
67
68
	/** @var Vote[] */
69
	private $votes;
70
71
	/** @var OptionMapper */
72
	private $optionMapper;
73
74
	/** @var PollMapper */
75
	private $pollMapper;
76
77
	/** @var VoteMapper */
78
	private $voteMapper;
79
80
	/** @var WatchService */
81
	private $watchService;
82
83
	public function __construct(
84
		string $AppName,
85
		LoggerInterface $logger,
86
		Acl $acl,
87
		Option $option,
88
		OptionMapper $optionMapper,
89
		PollMapper $pollMapper,
90
		VoteMapper $voteMapper,
91
		WatchService $watchService
92
	) {
93
		$this->appName = $AppName;
94
		$this->logger = $logger;
95
		$this->acl = $acl;
96
		$this->option = $option;
97
		$this->optionMapper = $optionMapper;
98
		$this->pollMapper = $pollMapper;
99
		$this->voteMapper = $voteMapper;
100
		$this->watchService = $watchService;
101
	}
102
103
	/**
104
	 * Get all options of given poll
105
	 *
106
	 * @return Option[]
107
	 *
108
	 * @psalm-return array<array-key, Option>
109
	 */
110
	public function list(int $pollId = 0, string $token = ''): array {
111
		if ($token) {
112
			$this->acl->setToken($token)->request(Acl::PERMISSION_VIEW);
113
			$pollId = $this->acl->getPollId();
114
		} else {
115
			$this->acl->setPollId($pollId)->request(Acl::PERMISSION_VIEW);
116
		}
117
118
		try {
119
			$this->poll = $this->pollMapper->find($pollId);
120
			$this->options = $this->optionMapper->findByPoll($pollId);
121
			$this->votes = $this->voteMapper->findByPoll($pollId);
122
			$this->countParticipants = count($this->voteMapper->findParticipantsByPoll($pollId));
123
124
			$this->calculateVotes();
125
126
			if ($this->poll->getHideBookedUp() && !$this->acl->isAllowed(Acl::PERMISSION_EDIT)) {
127
				// hide booked up options except the user has edit permission
128
				$this->filterBookedUp();
129
			} elseif ($this->acl->isAllowed(Acl::PERMISSION_SEE_RESULTS)) {
130
				$this->calculateRanks();
131
			}
132
133
			return array_values($this->options);
134
		} catch (DoesNotExistException $e) {
135
			return [];
136
		}
137
	}
138
139
	/**
140
	 * Get option
141
	 *
142
	 * @return Option
143
	 */
144
	public function get(int $optionId): Option {
145
		$this->acl->setPollId($this->optionMapper->find($optionId)->getPollId())
146
			->request(Acl::PERMISSION_VIEW);
147
		return $this->optionMapper->find($optionId);
148
	}
149
150
151
	/**
152
	 * Add a new option
153
	 *
154
	 * @return Option
155
	 */
156
	public function add(int $pollId, int $timestamp = 0, string $pollOptionText = '', ?int $duration = 0, string $token = ''): Option {
157
		if ($token) {
158
			$this->acl->setToken($token)->request(Acl::PERMISSION_ADD_OPTIONS);
159
			$pollId = $this->acl->getPollId();
160
		} else {
161
			$this->acl->setPollId($pollId)->request(Acl::PERMISSION_ADD_OPTIONS);
162
		}
163
164
		$this->option = new Option();
165
		$this->option->setPollId($pollId);
166
		$this->option->setOrder($this->getHighestOrder($this->option->getPollId()) + 1);
167
		$this->setOption($timestamp, $pollOptionText, $duration);
168
169
		if (!$this->acl->getIsOwner()) {
170
			$this->option->setOwner($this->acl->getUserId());
171
		}
172
173
		if (!$this->acl->getIsOwner()) {
174
			$this->option->setOwner($this->acl->getUserId());
175
		}
176
177
		try {
178
			$this->option = $this->optionMapper->insert($this->option);
179
			$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
180
		} catch (UniqueConstraintViolationException $e) {
181
			throw new DuplicateEntryException('This option already exists');
182
		}
183
		return $this->option;
184
	}
185
186
	/**
187
	 * Update option
188
	 *
189
	 * @return Option
190
	 */
191
	public function update(int $optionId, int $timestamp = 0, ?string $pollOptionText = '', ?int $duration = 0): Option {
192
		$this->option = $this->optionMapper->find($optionId);
193
		if (!$this->acl->setPollId($this->option->getPollId())->isAllowed(Acl::PERMISSION_EDIT)
194
			&& $this->option->getOwner() !== $this->acl->getUserId()) {
195
			throw new NotAuthorizedException('You are not allowed to delete this option');
196
		}
197
		$this->setOption($timestamp, $pollOptionText, $duration);
198
199
		$this->option = $this->optionMapper->update($this->option);
200
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
201
		return $this->option;
202
	}
203
204
	/**
205
	 * Delete option
206
	 *
207
	 * @return Option
208
	 */
209
	public function delete(int $optionId, string $token = ''): Option {
210
		$this->option = $this->optionMapper->find($optionId);
211
		if ($token) {
212
			$this->acl->setToken($token);
213
		} else {
214
			$this->acl->setPollId($this->option->getPollId());
215
		}
216
217
		if (!$this->acl->isAllowed(Acl::PERMISSION_EDIT)
218
			&& $this->option->getOwner() !== $this->acl->getUserId()) {
219
			throw new NotAuthorizedException('You are not allowed to delete this option');
220
		}
221
		$this->optionMapper->delete($this->option);
222
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
223
224
		return $this->option;
225
	}
226
227
	/**
228
	 * Switch option confirmation
229
	 *
230
	 * @return Option
231
	 */
232
	public function confirm(int $optionId): Option {
233
		$this->option = $this->optionMapper->find($optionId);
234
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
235
236
		$this->option->setConfirmed($this->option->getConfirmed() ? 0 : time());
237
		$this->option = $this->optionMapper->update($this->option);
238
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
239
		return $this->option;
240
	}
241
242
	/**
243
	 * Make a sequence of date poll options
244
	 * @param int $optionId
245
	 * @param int $step - The step for creating the sequence
246
	 * @param string $unit - The timeunit (year, month, ...)
247
	 * @param int $amount - Number of sequence items to create
248
	 *
249
	 * @return Option[]
250
	 *
251
	 * @psalm-return array<array-key, Option>
252
	 */
253
	public function sequence(int $optionId, int $step, string $unit, int $amount): array {
254
		$baseDate = new DateTime;
255
		$this->option = $this->optionMapper->find($optionId);
256
		$this->acl->setPollId($this->option->getPollId())->request(Acl::PERMISSION_EDIT);
257
258
		if ($this->pollMapper->find($this->acl->getPollId())->getType() !== Poll::TYPE_DATE) {
259
			throw new InvalidPollTypeException('Only allowed in date polls');
260
		}
261
262
263
		if ($step === 0) {
264
			return $this->optionMapper->findByPoll($this->option->getPollId());
265
		}
266
267
		$baseDate->setTimestamp($this->option->getTimestamp());
268
269
		for ($i = 0; $i < $amount; $i++) {
270
			$clonedOption = new Option();
271
			$clonedOption->setPollId($this->option->getPollId());
272
			$clonedOption->setDuration($this->option->getDuration());
273
			$clonedOption->setConfirmed(0);
274
			$clonedOption->setTimestamp($baseDate->modify($step . ' ' . $unit)->getTimestamp());
275
			$clonedOption->setOrder($clonedOption->getTimestamp());
276
			$clonedOption->setPollOptionText($baseDate->format('c'));
277
			try {
278
				$this->optionMapper->insert($clonedOption);
279
			} catch (UniqueConstraintViolationException $e) {
280
				$this->logger->warning('skip adding ' . $baseDate->format('c') . 'for pollId' . $this->option->getPollId() . '. Option alredy exists.');
281
			}
282
		}
283
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
284
		return $this->optionMapper->findByPoll($this->option->getPollId());
285
	}
286
287
	/**
288
	 * Shift all date options
289
	 * @param int $pollId
290
	 * @param int $step - The step for creating the sequence
291
	 * @param string $unit - The timeunit (year, month, ...)
292
	 *
293
	 * @return Option[]
294
	 *
295
	 * @psalm-return array<array-key, Option>
296
	 */
297
	public function shift(int $pollId, int $step, string $unit): array {
298
		$this->acl->setPollId($pollId)->request(Acl::PERMISSION_EDIT);
299
		if ($this->pollMapper->find($pollId)->getType() !== Poll::TYPE_DATE) {
300
			throw new InvalidPollTypeException('Only allowed in date polls');
301
		}
302
303
		$this->options = $this->optionMapper->findByPoll($pollId);
304
		$shiftedDate = new DateTime;
305
306
		if ($step > 0) {
307
			// avoid contraint errors
308
			$this->options = array_reverse($this->options);
309
		}
310
311
		foreach ($this->options as $option) {
312
			$shiftedDate->setTimestamp($option->getTimestamp());
313
			$option->setTimestamp($shiftedDate->modify($step . ' ' . $unit)->getTimestamp());
314
			$this->optionMapper->update($option);
315
		}
316
317
		return $this->optionMapper->findByPoll($pollId);
318
	}
319
320
	/**
321
	 * Copy options from $fromPoll to $toPoll
322
	 *
323
	 * @return Option[]
324
	 *
325
	 * @psalm-return array<array-key, Option>
326
	 */
327
	public function clone(int $fromPollId, int $toPollId): array {
328
		$this->acl->setPollId($fromPollId);
329
330
		foreach ($this->optionMapper->findByPoll($fromPollId) as $origin) {
331
			$option = new Option();
332
			$option->setPollId($toPollId);
333
			$option->setConfirmed(0);
334
			$option->setPollOptionText($origin->getPollOptionText());
335
			$option->setTimestamp($origin->getTimestamp());
336
			$option->setDuration($origin->getDuration());
337
			$option->setOrder($option->getOrder());
338
			$this->optionMapper->insert($option);
339
		}
340
341
		return $this->optionMapper->findByPoll($toPollId);
342
	}
343
344
	/**
345
	 * Reorder options with the order specified by $options
346
	 *
347
	 * @return Option[]
348
	 *
349
	 * @psalm-return array<array-key, Option>
350
	 */
351
	public function reorder(int $pollId, array $options): array {
352
		try {
353
			$this->poll = $this->pollMapper->find($pollId);
354
			$this->acl->setPoll($this->poll)->request(Acl::PERMISSION_EDIT);
355
356
			if ($this->poll->getType() === Poll::TYPE_DATE) {
357
				throw new BadRequestException('Not allowed in date polls');
358
			}
359
		} catch (DoesNotExistException $e) {
360
			throw new NotAuthorizedException;
361
		}
362
363
		$i = 0;
364
		foreach ($options as $option) {
365
			$this->option = $this->optionMapper->find($option['id']);
366
			if ($pollId === intval($this->option->getPollId())) {
367
				$this->option->setOrder(++$i);
368
				$this->optionMapper->update($this->option);
369
			}
370
		}
371
372
		$this->watchService->writeUpdate($pollId, Watch::OBJECT_OPTIONS);
373
		return $this->optionMapper->findByPoll($pollId);
374
	}
375
376
	/**
377
	 * Change order for $optionId and reorder the options
378
	 *
379
	 * @return Option[]
380
	 *
381
	 * @psalm-return array<array-key, Option>
382
	 */
383
	public function setOrder(int $optionId, int $newOrder): array {
384
		try {
385
			$this->option = $this->optionMapper->find($optionId);
386
			$this->poll = $this->pollMapper->find($this->option->getPollId());
387
			$this->acl->setPoll($this->poll)->request(Acl::PERMISSION_EDIT);
388
389
			if ($this->poll->getType() === Poll::TYPE_DATE) {
390
				throw new InvalidPollTypeException('Not allowed in date polls');
391
			}
392
		} catch (DoesNotExistException $e) {
393
			throw new NotAuthorizedException;
394
		}
395
396
		if ($newOrder < 1) {
397
			$newOrder = 1;
398
		} elseif ($newOrder > $this->getHighestOrder($this->poll->getId())) {
399
			$newOrder = $this->getHighestOrder($this->poll->getId());
400
		}
401
402
		foreach ($this->optionMapper->findByPoll($this->poll->getId()) as $option) {
403
			$option->setOrder($this->moveModifier($this->option->getOrder(), $newOrder, $option->getOrder()));
404
			$this->optionMapper->update($option);
405
		}
406
407
		$this->watchService->writeUpdate($this->option->getPollId(), Watch::OBJECT_OPTIONS);
408
		return $this->optionMapper->findByPoll($this->option->getPollId());
409
	}
410
411
	/**
412
	 * moveModifier - evaluate new order depending on the old and
413
	 * the new position of a moved array item
414
	 * @param int $moveFrom - old position of the moved item
415
	 * @param int $moveTo - target posotion of the moved item
416
	 * @param int $currentPosition - current position of the current item
417
	 *
418
	 * @return int - The modified new new position of the current item
419
	 */
420
	private function moveModifier(int $moveFrom, int $moveTo, int $currentPosition): int {
421
		$moveModifier = 0;
422
		if ($moveFrom < $currentPosition && $currentPosition <= $moveTo) {
423
			// moving forward
424
			$moveModifier = -1;
425
		} elseif ($moveTo <= $currentPosition && $currentPosition < $moveFrom) {
426
			//moving backwards
427
			$moveModifier = 1;
428
		} elseif ($moveFrom === $currentPosition) {
429
			return $moveTo;
430
		}
431
		return $currentPosition + $moveModifier;
432
	}
433
434
	/**
435
	 * Set option entities validated
436
	 *
437
	 * @return void
438
	 */
439
	private function setOption(int $timestamp = 0, ?string $pollOptionText = '', ?int $duration = 0): void {
440
		$this->poll = $this->pollMapper->find($this->option->getPollId());
441
442
		if ($this->poll->getType() === Poll::TYPE_DATE) {
443
			$this->option->setTimestamp($timestamp);
444
			$this->option->setOrder($timestamp);
445
			$this->option->setDuration($duration ?? 0);
446
			if ($duration > 0) {
447
				$this->option->setPollOptionText(date('c', $timestamp) . ' - ' . date('c', $timestamp + $duration));
448
			} else {
449
				$this->option->setPollOptionText(date('c', $timestamp));
450
			}
451
		} elseif ($pollOptionText) {
452
			$this->option->setPollOptionText($pollOptionText);
453
		} else {
454
			throw new InvalidOptionPropertyException('Option must have a value');
455
		}
456
	}
457
458
	/**
459
	 * Get all voteOptionTexts of the options, the user opted in
460
	 * with yes or maybe
461
	 *
462
	 * @return array
463
	 */
464
	private function getUsersVotes() {
465
		// Thats an ugly solution, but for now, it seems to work
466
		// Optimization proposals are welcome
467
468
		// First: Find votes, where the user voted yes or maybe
469
		$userId = $this->acl->getUserId();
470
		$exceptVotes = array_filter($this->votes, function ($vote) use ($userId) {
471
			return $vote->getUserId() === $userId && in_array($vote->getVoteAnswer(), ['yes', 'maybe']);
472
		});
473
474
		// Second: Extract only the vote option texts to an array
475
		return array_values(array_map(function ($vote) {
476
			return $vote->getVoteOptionText();
477
		}, $exceptVotes));
478
	}
479
480
	/**
481
	 * Remove booked up options, because they are not votable
482
	 *
483
	 * @return void
484
	 */
485
	private function filterBookedUp() {
486
		$exceptVotes = $this->getUsersVotes();
487
		$this->options = array_filter($this->options, function ($option) use ($exceptVotes) {
488
			return (!$option->isBookedUp || in_array($option->getPollOptionText(), $exceptVotes));
489
		});
490
	}
491
492
	/**
493
	 * Calculate the votes of each option and determines if the option is booked up
494
	 * - unvoted counts as no
495
	 * - realNo reports the actually opted out votes
496
	 *
497
	 * @return void
498
	 */
499
	private function calculateVotes() {
500
		foreach ($this->options as $option) {
501
			$option->yes = count(
502
				array_filter($this->votes, function ($vote) use ($option) {
503
					return ($vote->getVoteOptionText() === $option->getPollOptionText()
504
						&& $vote->getVoteAnswer() === 'yes') ;
505
				})
506
			);
507
508
			$option->realNo = count(
509
				array_filter($this->votes, function ($vote) use ($option) {
510
					return ($vote->getVoteOptionText() === $option->getPollOptionText()
511
						&& $vote->getVoteAnswer() === 'no');
512
				})
513
			);
514
515
			$option->maybe = count(
516
				array_filter($this->votes, function ($vote) use ($option) {
517
					return ($vote->getVoteOptionText() === $option->getPollOptionText()
518
						&& $vote->getVoteAnswer() === 'maybe');
519
				})
520
			);
521
522
			$option->isBookedUp = $this->poll->getOptionLimit() ? $this->poll->getOptionLimit() <= $option->yes : false;
523
524
			// remove details, if the results shall be hidden
525
			if (!$this->acl->isAllowed(Acl::PERMISSION_SEE_RESULTS)) {
526
				$option->yes = 0;
527
				$option->no = 0;
528
				$option->maybe = 0;
529
				$option->realNo = 0;
530
			} else {
531
				$option->no = $this->countParticipants - $option->maybe - $option->yes;
532
			}
533
		}
534
	}
535
536
	/**
537
	 * Calculate the rank of each option based on the
538
	 * yes and maybe votes and recognize equal ranked options
539
	 *
540
	 * @return void
541
	 */
542
	private function calculateRanks() {
543
		// sort array by yes and maybe votes
544
		usort($this->options, function (Option $a, Option $b):int {
545
			$diff = $b->yes - $a->yes;
546
			return ($diff !== 0) ? $diff : $b->maybe - $a->maybe;
547
		});
548
549
		// calculate the rank
550
		$count = count($this->options);
551
		for ($i = 0; $i < $count; $i++) {
552
			if ($i > 0 && $this->options[$i]->yes === $this->options[$i - 1]->yes && $this->options[$i]->maybe === $this->options[$i - 1]->maybe) {
553
				$this->options[$i]->rank = $this->options[$i - 1]->rank;
554
			} else {
555
				$this->options[$i]->rank = $i + 1;
556
			}
557
		}
558
559
		// restore original order
560
		usort($this->options, function (Option $a, Option $b):int {
561
			return $a->getOrder() - $b->getOrder();
562
		});
563
	}
564
565
	/**
566
	 * Get the highest order number in $pollId
567
	 * Return Highest order number
568
	 *
569
	 * @return int
570
	 */
571
	private function getHighestOrder(int $pollId): int {
572
		$highestOrder = 0;
573
		foreach ($this->optionMapper->findByPoll($pollId) as $option) {
574
			$highestOrder = ($option->getOrder() > $highestOrder) ? $option->getOrder() : $highestOrder;
575
		}
576
		return $highestOrder;
577
	}
578
}
579