OptionService::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 18
ccs 0
cts 7
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 8
crap 2

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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