Total Complexity | 92 |
Total Lines | 699 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Issue 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 Issue, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
9 | class Issue extends \DokuWiki_Plugin implements \JsonSerializable |
||
10 | { |
||
11 | |||
12 | /** @var Issue[] */ |
||
13 | private static $instances = []; |
||
14 | protected $issueId = null; |
||
15 | protected $projectId = null; |
||
16 | protected $isMergeRequest = null; |
||
17 | protected $files = []; |
||
18 | protected $serviceID = ''; |
||
19 | private $summary = ''; |
||
20 | private $description = ''; |
||
21 | private $status = ''; |
||
22 | private $type = 'unknown'; |
||
23 | private $components = []; |
||
24 | private $labels = []; |
||
25 | private $priority = ''; |
||
26 | private $assignee = []; |
||
27 | private $labelData = []; |
||
28 | private $versions = []; |
||
29 | private $duedate = null; |
||
30 | private $updated = null; |
||
31 | /** @var int may be set if this issue is created in specific relation to another issue */ |
||
32 | private $relatedWeight = 0; |
||
|
|||
33 | private $parent; |
||
34 | private $errors = []; |
||
35 | private $isValid = null; |
||
36 | |||
37 | /** |
||
38 | * @param $serviceName |
||
39 | * @param string $projectKey The shortkey of the project, e.g. SPR |
||
40 | * @param int $issueId The id of the issue, e.g. 42 |
||
41 | * |
||
42 | * @param $isMergeRequest |
||
43 | */ |
||
44 | private function __construct($serviceName, $projectKey, $issueId, $isMergeRequest) |
||
45 | { |
||
46 | if (empty($serviceName) || empty($projectKey) || empty($issueId) || !is_numeric($issueId)) { |
||
47 | throw new \InvalidArgumentException('Empty value passed to Issue constructor'); |
||
48 | } |
||
49 | |||
50 | $this->issueId = $issueId; |
||
51 | $this->projectId = $projectKey; |
||
52 | $this->isMergeRequest = $isMergeRequest; |
||
53 | $this->serviceID = $serviceName; |
||
54 | |||
55 | // $this->getFromDB(); |
||
56 | } |
||
57 | |||
58 | /** |
||
59 | * Get the singleton instance of a issue |
||
60 | * |
||
61 | * @param $serviceName |
||
62 | * @param $projectKey |
||
63 | * @param $issueId |
||
64 | * @param bool $isMergeRequest |
||
65 | * @param bool $forcereload create a new instace |
||
66 | * |
||
67 | * @return Issue |
||
68 | */ |
||
69 | public static function getInstance( |
||
70 | $serviceName, |
||
71 | $projectKey, |
||
72 | $issueId, |
||
73 | $isMergeRequest = false, |
||
74 | $forcereload = false |
||
75 | ) { |
||
76 | $issueHash = $serviceName . $projectKey . $issueId . '!' . $isMergeRequest; |
||
77 | if (empty(self::$instances[$issueHash]) || $forcereload) { |
||
78 | self::$instances[$issueHash] = new Issue($serviceName, $projectKey, $issueId, $isMergeRequest); |
||
79 | } |
||
80 | return self::$instances[$issueHash]; |
||
81 | } |
||
82 | |||
83 | /** |
||
84 | * @return bool true if issue was found in database, false otherwise |
||
85 | */ |
||
86 | public function getFromDB() |
||
87 | { |
||
88 | /** @var \helper_plugin_issuelinks_db $db */ |
||
89 | $db = plugin_load('helper', 'issuelinks_db'); |
||
90 | $issue = $db->loadIssue($this->serviceID, $this->projectId, $this->issueId, $this->isMergeRequest); |
||
91 | if (empty($issue)) { |
||
92 | return false; |
||
93 | } |
||
94 | $this->summary = $issue['summary'] ?: ''; |
||
95 | $this->status = $issue['status']; |
||
96 | $this->type = $issue['type']; |
||
97 | $this->description = $issue['description']; |
||
98 | $this->setComponents($issue['components']); |
||
99 | $this->setLabels($issue['labels']); |
||
100 | $this->priority = $issue['priority']; |
||
101 | $this->duedate = $issue['duedate']; |
||
102 | $this->setVersions($issue['versions']); |
||
103 | $this->setUpdated($issue['updated']); |
||
104 | return true; |
||
105 | } |
||
106 | |||
107 | public function __toString() |
||
108 | { |
||
109 | $sep = $this->pmService->getProjectIssueSeparator($this->isMergeRequest); |
||
110 | return $this->projectId . $sep . $this->issueId; |
||
111 | } |
||
112 | |||
113 | /** |
||
114 | * @return \Exception|null |
||
115 | */ |
||
116 | public function getLastError() |
||
117 | { |
||
118 | if (!end($this->errors)) { |
||
119 | return null; |
||
120 | } |
||
121 | return end($this->errors); |
||
122 | } |
||
123 | |||
124 | /** |
||
125 | * @return bool|self |
||
126 | */ |
||
127 | public function isMergeRequest($isMergeRequest = null) |
||
128 | { |
||
129 | if ($isMergeRequest === null) { |
||
130 | return $this->isMergeRequest; |
||
131 | } |
||
132 | |||
133 | $this->isMergeRequest = $isMergeRequest; |
||
134 | return $this; |
||
135 | } |
||
136 | |||
137 | /** |
||
138 | * Specify data which should be serialized to JSON |
||
139 | * |
||
140 | * @link http://php.net/manual/en/jsonserializable.jsonserialize.php |
||
141 | * @return mixed data which can be serialized by <b>json_encode</b>, |
||
142 | * which is a value of any type other than a resource. |
||
143 | * @since 5.4.0 |
||
144 | * |
||
145 | * @link http://stackoverflow.com/a/4697671/3293343 |
||
146 | */ |
||
147 | public function jsonSerialize() |
||
165 | ]; |
||
166 | } |
||
167 | |||
168 | /** |
||
169 | * @return string |
||
170 | */ |
||
171 | public function getProject() |
||
172 | { |
||
173 | return $this->projectId; |
||
174 | } |
||
175 | |||
176 | /** |
||
177 | * Returns the key, i.e. number, of the issue |
||
178 | * |
||
179 | * @param bool $annotateMergeRequest If true, prepends a `!` to the key of a merge requests |
||
180 | * |
||
181 | * @return int|string |
||
182 | */ |
||
183 | public function getKey($annotateMergeRequest = false) |
||
184 | { |
||
185 | if ($annotateMergeRequest && $this->isMergeRequest) { |
||
186 | return '!' . $this->issueId; |
||
187 | } |
||
188 | return $this->issueId; |
||
189 | } |
||
190 | |||
191 | public function getSummary() |
||
192 | { |
||
193 | return $this->summary; |
||
194 | } |
||
195 | |||
196 | /** |
||
197 | * @param string $summary |
||
198 | * |
||
199 | * @return Issue |
||
200 | * |
||
201 | * todo: decide if we should test for non-empty string here |
||
202 | */ |
||
203 | public function setSummary($summary) |
||
204 | { |
||
205 | $this->summary = $summary; |
||
206 | return $this; |
||
207 | } |
||
208 | |||
209 | /** |
||
210 | * @return string |
||
211 | */ |
||
212 | public function getDescription() |
||
213 | { |
||
214 | return $this->description; |
||
215 | } |
||
216 | |||
217 | /** |
||
218 | * @param string $description |
||
219 | * |
||
220 | * @return Issue |
||
221 | */ |
||
222 | public function setDescription($description) |
||
223 | { |
||
224 | $this->description = $description; |
||
225 | return $this; |
||
226 | } |
||
227 | |||
228 | /** |
||
229 | * @return string |
||
230 | */ |
||
231 | public function getType() |
||
232 | { |
||
233 | return $this->type; |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * @param string $type |
||
238 | * |
||
239 | * @return Issue |
||
240 | */ |
||
241 | public function setType($type) |
||
242 | { |
||
243 | $this->type = $type; |
||
244 | return $this; |
||
245 | } |
||
246 | |||
247 | /** |
||
248 | * @return string |
||
249 | */ |
||
250 | public function getStatus() |
||
251 | { |
||
252 | return $this->status; |
||
253 | } |
||
254 | |||
255 | /** |
||
256 | * @param string $status |
||
257 | * |
||
258 | * @return Issue |
||
259 | */ |
||
260 | public function setStatus($status) |
||
261 | { |
||
262 | $this->status = $status; |
||
263 | return $this; |
||
264 | } |
||
265 | |||
266 | /** |
||
267 | * @return mixed |
||
268 | */ |
||
269 | public function getParent() |
||
270 | { |
||
271 | return $this->parent; |
||
272 | } |
||
273 | |||
274 | public function setParent($key) |
||
275 | { |
||
276 | $this->parent = $key; |
||
277 | return $this; |
||
278 | } |
||
279 | |||
280 | /** |
||
281 | * @return array |
||
282 | */ |
||
283 | public function getComponents() |
||
284 | { |
||
285 | return $this->components; |
||
286 | } |
||
287 | |||
288 | /** |
||
289 | * @param array $components |
||
290 | * |
||
291 | * @return Issue |
||
292 | */ |
||
293 | public function setComponents($components) |
||
294 | { |
||
295 | if (!is_array($components)) { |
||
296 | $components = array_filter(array_map('trim', explode(',', $components))); |
||
297 | } |
||
298 | if (!empty($components[0]['name'])) { |
||
299 | $components = array_map(function ($component) { |
||
300 | return $component['name']; |
||
301 | }, $components); |
||
302 | } |
||
303 | $this->components = $components; |
||
304 | return $this; |
||
305 | } |
||
306 | |||
307 | /** |
||
308 | * @return array |
||
309 | */ |
||
310 | public function getLabels() |
||
311 | { |
||
312 | return $this->labels; |
||
313 | } |
||
314 | |||
315 | /** |
||
316 | * @param array $labels |
||
317 | * |
||
318 | * @return Issue |
||
319 | */ |
||
320 | public function setLabels($labels) |
||
321 | { |
||
322 | if (!is_array($labels)) { |
||
323 | $labels = array_filter(array_map('trim', explode(',', $labels))); |
||
324 | } |
||
325 | $this->labels = $labels; |
||
326 | return $this; |
||
327 | } |
||
328 | |||
329 | /** |
||
330 | * @return string |
||
331 | */ |
||
332 | public function getPriority() |
||
333 | { |
||
334 | return $this->priority; |
||
335 | } |
||
336 | |||
337 | /** |
||
338 | * @param string $priority |
||
339 | * |
||
340 | * @return Issue |
||
341 | */ |
||
342 | public function setPriority($priority) |
||
343 | { |
||
344 | $this->priority = $priority; |
||
345 | return $this; |
||
346 | } |
||
347 | |||
348 | /** |
||
349 | * @return null |
||
350 | */ |
||
351 | public function getDuedate() |
||
352 | { |
||
353 | return $this->duedate; |
||
354 | } |
||
355 | |||
356 | /** |
||
357 | * @param null $duedate |
||
358 | * |
||
359 | * @return Issue |
||
360 | */ |
||
361 | public function setDuedate($duedate) |
||
362 | { |
||
363 | $this->duedate = $duedate; |
||
364 | return $this; |
||
365 | } |
||
366 | |||
367 | /** |
||
368 | * @return array |
||
369 | */ |
||
370 | public function getVersions() |
||
371 | { |
||
372 | return $this->versions; |
||
373 | } |
||
374 | |||
375 | /** |
||
376 | * @param array $versions |
||
377 | * |
||
378 | * @return Issue |
||
379 | */ |
||
380 | public function setVersions($versions) |
||
381 | { |
||
382 | if (!is_array($versions)) { |
||
383 | $versions = array_map('trim', explode(',', $versions)); |
||
384 | } |
||
385 | if (!empty($versions[0]['name'])) { |
||
386 | $versions = array_map(function ($version) { |
||
387 | return $version['name']; |
||
388 | }, $versions); |
||
389 | } |
||
390 | $this->versions = $versions; |
||
391 | return $this; |
||
392 | } |
||
393 | |||
394 | /** |
||
395 | * @return null |
||
396 | */ |
||
397 | public function getUpdated() |
||
398 | { |
||
399 | return $this->updated; |
||
400 | } |
||
401 | |||
402 | /** |
||
403 | * @param string|int $updated |
||
404 | * |
||
405 | * @return Issue |
||
406 | */ |
||
407 | public function setUpdated($updated) |
||
408 | { |
||
409 | /** @var \helper_plugin_issuelinks_util $util */ |
||
410 | $util = plugin_load('helper', 'issuelinks_util'); |
||
411 | if (!$util->isValidTimeStamp($updated)) { |
||
412 | $updated = strtotime($updated); |
||
413 | } |
||
414 | $this->updated = (int)$updated; |
||
415 | return $this; |
||
416 | } |
||
417 | |||
418 | /** |
||
419 | * Get a fancy HTML-link to issue |
||
420 | * |
||
421 | * @param bool $addSummary add the issue's summary after it status |
||
422 | * |
||
423 | * @return string |
||
424 | */ |
||
425 | public function getIssueLinkHTML($addSummary = false) |
||
426 | { |
||
427 | $serviceProvider = ServiceProvider::getInstance(); |
||
428 | $service = $serviceProvider->getServices()[$this->serviceID]; |
||
429 | $name = $this->projectId . $service::getProjectIssueSeparator($this->isMergeRequest) . $this->issueId; |
||
430 | $url = $this->getIssueURL(); |
||
431 | |||
432 | $status = cleanID($this->getStatus()); |
||
433 | if ($status) { |
||
434 | $name .= $this->getformattedIssueStatus(); |
||
435 | } |
||
436 | if ($addSummary) { |
||
437 | $name .= ' ' . $this->getSummary(); |
||
438 | } |
||
439 | |||
440 | $target = 'target="_blank" rel="noopener"'; |
||
441 | $classes = 'issuelink ' . cleanID($this->getType()) . ($this->isMergeRequest ? ' mergerequest' : ''); |
||
442 | $dataAttributes = "data-service=\"$this->serviceID\" data-project=\"$this->projectId\" data-issueid=\"$this->issueId\""; |
||
443 | $dataAttributes .= ' data-ismergerequest="' . ($this->isMergeRequest ? '1' : '0') . '"'; |
||
444 | return "<a href=\"$url\" class=\"$classes\" $dataAttributes $target>" . $this->getTypeHTML() . "$name</a>"; |
||
445 | } |
||
446 | |||
447 | /** |
||
448 | * @return string |
||
449 | */ |
||
450 | public function getIssueURL() |
||
451 | { |
||
452 | $serviceProvider = ServiceProvider::getInstance(); |
||
453 | $service = $serviceProvider->getServices()[$this->serviceID]::getInstance(); |
||
454 | return $service->getIssueURL($this->projectId, $this->issueId, $this->isMergeRequest); |
||
455 | } |
||
456 | |||
457 | /** |
||
458 | * get the status of the issue as HTML string |
||
459 | * |
||
460 | * @param string|null $status |
||
461 | * |
||
462 | * @return string |
||
463 | */ |
||
464 | public function getformattedIssueStatus($status = null) |
||
465 | { |
||
466 | if ($status === null) { |
||
467 | $status = $this->getStatus(); |
||
468 | } |
||
469 | $status = strtolower($status); |
||
470 | return "<span class='mm__status " . cleanID($status) . "'>$status</span>"; |
||
471 | } |
||
472 | |||
473 | /** |
||
474 | * @return string |
||
475 | */ |
||
476 | public function getTypeHTML() |
||
483 | } |
||
484 | |||
485 | /** |
||
486 | * ToDo: replace all with SVG |
||
487 | * |
||
488 | * @return string the path to the icon / base64 image if type unknown |
||
489 | */ |
||
490 | protected function getMaterialDesignTypeIcon() |
||
491 | { |
||
492 | $typeIcon = [ |
||
493 | 'bug' => 'mdi-bug.png', |
||
494 | 'story' => 'mdi-bookmark.png', |
||
495 | 'epic' => 'mdi-flash.png', |
||
496 | 'change_request' => 'mdi-plus.png', |
||
497 | 'improvement' => 'mdi-arrow-up-thick.png', |
||
498 | 'organisation_task' => 'mdi-calendar-text.png', |
||
499 | 'technical_task' => 'mdi-source-branch.png', |
||
500 | 'task' => 'mdi-check.png', |
||
501 | ]; |
||
502 | |||
503 | if (!isset($typeIcon[cleanID($this->type)])) { |
||
504 | return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAABDUlEQVQYGQXBLWuVcQDA0fM8272OIYLCmi+IOBBWhWEZohiHn0AQi/H3CQxaLVptgmmIacUwWLthsDiQBaOmIaYF+XsOgHb61N9Glx30qAkAtOigVbttttZGO31t1VUArXfeCwCg3S66Buhzr6Blb/rVeS+b6WEnTehuZ0206Gej0Wh0CH3pCXrXM2ijVW+bW3bS6Bbd6xiddQNogpadNrpDa40mXbYBQI+7bPS9CRotdN51gOZGo9dN0Nxo1vv2AFpr1RFAtztBD1oBtOhffwD62D7osH2gZaN/QNv9aAZd6XdPgZYtoPtdtAWgzY771nbrNHezD523BQCa2uuo0Wh02vNmAADQ1KIZAPgPQZt8UVJ7VXIAAAAASUVORK5CYII='; |
||
505 | } |
||
506 | |||
507 | return DOKU_URL . '/lib/plugins/issuelinks/images/' . $typeIcon[cleanID($this->type)]; |
||
508 | } |
||
509 | |||
510 | public function setAssignee($name, $avatar_url) |
||
511 | { |
||
512 | $this->assignee['name'] = $name; |
||
513 | $this->assignee['avatarURL'] = $avatar_url; |
||
514 | } |
||
515 | |||
516 | public function getAdditionalDataHTML() |
||
517 | { |
||
518 | $this->getFromService(); |
||
519 | $data = []; |
||
520 | if (!empty($this->assignee)) { |
||
521 | $data['avatarHTML'] = "<img src=\"{$this->assignee['avatarURL']}\" alt=\"{$this->assignee['name']}\">"; |
||
522 | } |
||
523 | if (!empty($this->labelData)) { |
||
524 | $labels = $this->getLabels(); |
||
525 | $data['fancyLabelsHTML'] = ''; |
||
526 | foreach ($labels as $label) { |
||
527 | $colors = ''; |
||
528 | $classes = 'label'; |
||
529 | if (isset($this->labelData[$label])) { |
||
530 | $colors = "style=\"background-color: {$this->labelData[$label]['background-color']};"; |
||
531 | $colors .= " color: {$this->labelData[$label]['color']};\""; |
||
532 | $classes .= ' color'; |
||
533 | } |
||
534 | $data['fancyLabelsHTML'] .= "<span class=\"$classes\" $colors>$label</span>"; |
||
535 | } |
||
536 | } |
||
537 | return $data; |
||
538 | } |
||
539 | |||
540 | public function getFromService() |
||
541 | { |
||
542 | $serviceProvider = ServiceProvider::getInstance(); |
||
543 | $service = $serviceProvider->getServices()[$this->serviceID]::getInstance(); |
||
544 | |||
545 | try { |
||
546 | $service->retrieveIssue($this); |
||
547 | if ($this->isValid(true)) { |
||
548 | $this->saveToDB(); |
||
549 | } |
||
550 | } catch (IssueLinksException $e) { |
||
551 | $this->errors[] = $e; |
||
552 | $this->isValid = false; |
||
553 | return false; |
||
554 | } |
||
555 | return true; |
||
556 | } |
||
557 | |||
558 | /** |
||
559 | * Check if an issue is valid. |
||
560 | * |
||
561 | * The specific rules depend on the service and the cached value may also be set by other functions. |
||
562 | * |
||
563 | * @param bool $recheck force a validity check instead of using cached value if available |
||
564 | * |
||
565 | * @return bool |
||
566 | */ |
||
567 | public function isValid($recheck = false) |
||
568 | { |
||
569 | if ($recheck || $this->isValid === null) { |
||
570 | $serviceProvider = ServiceProvider::getInstance(); |
||
571 | $service = $serviceProvider->getServices()[$this->serviceID]; |
||
572 | $this->isValid = $service::isIssueValid($this); |
||
573 | } |
||
574 | return $this->isValid; |
||
575 | } |
||
576 | |||
577 | public function saveToDB() |
||
578 | { |
||
579 | /** @var \helper_plugin_issuelinks_db $db */ |
||
580 | $db = plugin_load('helper', 'issuelinks_db'); |
||
581 | return $db->saveIssue($this); |
||
582 | } |
||
583 | |||
584 | public function buildTooltipHTML() |
||
663 | } |
||
664 | |||
665 | public function getServiceName() |
||
666 | { |
||
667 | return $this->serviceID; |
||
668 | } |
||
669 | |||
670 | /** |
||
671 | * @param string $labelName the background color without the leading # |
||
672 | * @param string $color |
||
673 | */ |
||
674 | public function setLabelData($labelName, $color) |
||
679 | ]; |
||
680 | } |
||
681 | |||
682 | /** |
||
683 | * Calculate if a white or black font-color should be with the given background color |
||
684 | * |
||
685 | * https://www.w3.org/TR/WCAG20/#relativeluminancedef |
||
686 | * http://stackoverflow.com/a/3943023/3293343 |
||
687 | * |
||
688 | * @param string $color the background-color, without leading # |
||
689 | * |
||
690 | * @return string |
||
691 | */ |
||
692 | private function calculateColor($color) |
||
710 |