Issue   F
last analyzed

Complexity

Total Complexity 92

Size/Duplication

Total Lines 714
Duplicated Lines 0 %

Test Coverage

Coverage 9.34%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 280
c 4
b 0
f 0
dl 0
loc 714
ccs 27
cts 289
cp 0.0934
rs 2
wmc 92

45 Methods

Rating   Name   Duplication   Size   Complexity  
A jsonSerialize() 0 18 2
A getTypeHTML() 0 7 2
C buildTooltipHTML() 0 87 12
A saveToDB() 0 5 1
A getKey() 0 6 3
A setSummary() 0 4 1
A getSummary() 0 3 1
A getLastError() 0 6 2
A setLabels() 0 7 2
A getParent() 0 3 1
A getFromService() 0 17 3
A __toString() 0 4 1
A getDescription() 0 3 1
A getIssueLinkHTML() 0 27 5
A getPriority() 0 3 1
A setComponents() 0 10 3
A getProject() 0 3 1
A isMergeRequest() 0 8 2
A getMaterialDesignTypeIcon() 0 18 2
A getInstance() 0 12 3
A __construct() 0 10 5
A setType() 0 4 1
A getDuedate() 0 3 1
A getAdditionalDataHTML() 0 22 5
A setDescription() 0 4 1
A isValid() 0 8 3
A getIssueURL() 0 6 1
A getStatus() 0 3 1
A setUpdated() 0 9 2
A getServiceName() 0 3 1
A setParent() 0 4 1
A setAssignee() 0 4 1
A getformattedIssueStatus() 0 7 2
A setVersions() 0 12 3
A getFromDB() 0 19 3
A setDuedate() 0 4 1
A setLabelData() 0 5 1
A getUpdated() 0 3 1
A calculateColor() 0 16 3
A getType() 0 3 1
A setPriority() 0 4 1
A getVersions() 0 3 1
A getComponents() 0 3 1
A setStatus() 0 4 1
A getLabels() 0 3 1

How to fix   Complexity   

Complex Class

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
2
3
namespace dokuwiki\plugin\issuelinks\classes;
4
5
/**
6
 *
7
 * Class Issue
8
 */
9
class Issue extends \DokuWiki_Plugin implements \JsonSerializable
10
{
11
12
    /** @var Issue[] */
13
    private static $instances = [];
14
    protected $issueId;
15
    protected $projectId;
16
    protected $isMergeRequest;
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;
30
    private $updated;
31
    private $parent;
32
    private $errors = [];
33
    private $isValid;
34
35
    /**
36
     * @param        $serviceName
37
     * @param string $projectKey The shortkey of the project, e.g. SPR
38
     * @param int    $issueId    The id of the issue, e.g. 42
39
     *
40
     * @param        $isMergeRequest
41
     */
42 2
    private function __construct($serviceName, $projectKey, $issueId, $isMergeRequest)
43
    {
44 2
        if (empty($serviceName) || empty($projectKey) || empty($issueId) || !is_numeric($issueId)) {
45
            throw new \InvalidArgumentException('Empty value passed to Issue constructor');
46
        }
47
48 2
        $this->issueId = $issueId;
49 2
        $this->projectId = $projectKey;
50 2
        $this->isMergeRequest = $isMergeRequest;
51 2
        $this->serviceID = $serviceName;
52
53
//        $this->getFromDB();
54 2
    }
55
56
    /**
57
     * Get the singleton instance of a issue
58
     *
59
     * @param      $serviceName
60
     * @param      $projectKey
61
     * @param      $issueId
62
     * @param bool $isMergeRequest
63
     * @param bool $forcereload create a new instace
64
     *
65
     * @return Issue
66
     */
67 2
    public static function getInstance(
68
        $serviceName,
69
        $projectKey,
70
        $issueId,
71
        $isMergeRequest = false,
72
        $forcereload = false
73
    ) {
74 2
        $issueHash = $serviceName . $projectKey . $issueId . '!' . $isMergeRequest;
75 2
        if (empty(self::$instances[$issueHash]) || $forcereload) {
76 2
            self::$instances[$issueHash] = new Issue($serviceName, $projectKey, $issueId, $isMergeRequest);
77
        }
78 2
        return self::$instances[$issueHash];
79
    }
80
81
    /**
82
     * @return bool true if issue was found in database, false otherwise
83
     */
84 2
    public function getFromDB()
85
    {
86
        /** @var \helper_plugin_issuelinks_db $db */
87 2
        $db = plugin_load('helper', 'issuelinks_db');
88 2
        $issue = $db->loadIssue($this->serviceID, $this->projectId, $this->issueId, $this->isMergeRequest);
89 2
        if (empty($issue)) {
90 2
            return false;
91
        }
92
        $this->summary = $issue['summary'] ?: '';
93
        $this->status = $issue['status'];
94
        $this->type = $issue['type'];
95
        $this->description = $issue['description'];
96
        $this->setComponents($issue['components']);
97
        $this->setLabels($issue['labels']);
98
        $this->priority = $issue['priority'];
99
        $this->duedate = $issue['duedate'];
100
        $this->setVersions($issue['versions']);
101
        $this->setUpdated($issue['updated']);
102
        return true;
103
    }
104
105
    public function __toString()
106
    {
107
        $sep = $this->pmService->getProjectIssueSeparator($this->isMergeRequest);
108
        return $this->projectId . $sep . $this->issueId;
109
    }
110
111
    /**
112
     * @return \Exception|null
113
     */
114
    public function getLastError()
115
    {
116
        if (!end($this->errors)) {
117
            return null;
118
        }
119
        return end($this->errors);
120
    }
121
122
    /**
123
     * @return bool|self
124
     */
125 2
    public function isMergeRequest($isMergeRequest = null)
126
    {
127 2
        if ($isMergeRequest === null) {
128 2
            return $this->isMergeRequest;
129
        }
130
131
        $this->isMergeRequest = $isMergeRequest;
132
        return $this;
133
    }
134
135
    /**
136
     * Specify data which should be serialized to JSON
137
     *
138
     * @link  http://php.net/manual/en/jsonserializable.jsonserialize.php
139
     * @return mixed data which can be serialized by <b>json_encode</b>,
140
     * which is a value of any type other than a resource.
141
     * @since 5.4.0
142
     *
143
     * @link  http://stackoverflow.com/a/4697671/3293343
144
     */
145
    public function jsonSerialize()
146
    {
147
        return [
148
            'service' => $this->serviceID,
149
            'project' => $this->getProject(),
150
            'id' => $this->getKey(),
151
            'isMergeRequest' => $this->isMergeRequest ? '1' : '0',
152
            'summary' => $this->getSummary(),
153
            'description' => $this->getDescription(),
154
            'type' => $this->getType(),
155
            'status' => $this->getStatus(),
156
            'parent' => $this->getParent(),
157
            'components' => $this->getComponents(),
158
            'labels' => $this->getLabels(),
159
            'priority' => $this->getPriority(),
160
            'duedate' => $this->getDuedate(),
161
            'versions' => $this->getVersions(),
162
            'updated' => $this->getUpdated(),
163
        ];
164
    }
165
166
    /**
167
     * @return string
168
     */
169 2
    public function getProject()
170
    {
171 2
        return $this->projectId;
172
    }
173
174
    /**
175
     * Returns the key, i.e. number, of the issue
176
     *
177
     * @param bool $annotateMergeRequest If true, prepends a `!` to the key of a merge requests
178
     *
179
     * @return int|string
180
     */
181 2
    public function getKey($annotateMergeRequest = false)
182
    {
183 2
        if ($annotateMergeRequest && $this->isMergeRequest) {
184
            return '!' . $this->issueId;
185
        }
186 2
        return $this->issueId;
187
    }
188
189
    public function getSummary()
190
    {
191
        return $this->summary;
192
    }
193
194
    /**
195
     * @param string $summary
196
     *
197
     * @return Issue
198
     *
199
     * todo: decide if we should test for non-empty string here
200
     */
201
    public function setSummary($summary)
202
    {
203
        $this->summary = $summary;
204
        return $this;
205
    }
206
207
    /**
208
     * @return string
209
     */
210
    public function getDescription()
211
    {
212
        return $this->description;
213
    }
214
215
    /**
216
     * @param string $description
217
     *
218
     * @return Issue
219
     */
220
    public function setDescription($description)
221
    {
222
        $this->description = $description;
223
        return $this;
224
    }
225
226
    /**
227
     * @return string
228
     */
229
    public function getType()
230
    {
231
        return $this->type;
232
    }
233
234
    /**
235
     * @param string $type
236
     *
237
     * @return Issue
238
     */
239
    public function setType($type)
240
    {
241
        $this->type = $type;
242
        return $this;
243
    }
244
245
    /**
246
     * @return string
247
     */
248
    public function getStatus()
249
    {
250
        return $this->status;
251
    }
252
253
    /**
254
     * @param string $status
255
     *
256
     * @return Issue
257
     */
258
    public function setStatus($status)
259
    {
260
        $this->status = $status;
261
        return $this;
262
    }
263
264
    /**
265
     * @return mixed
266
     */
267
    public function getParent()
268
    {
269
        return $this->parent;
270
    }
271
272
    public function setParent($key)
273
    {
274
        $this->parent = $key;
275
        return $this;
276
    }
277
278
    /**
279
     * @return array
280
     */
281
    public function getComponents()
282
    {
283
        return $this->components;
284
    }
285
286
    /**
287
     * @param array|string $components
288
     *
289
     * @return Issue
290
     */
291
    public function setComponents($components)
292
    {
293
        if (is_string($components)) {
294
            $components = array_filter(array_map('trim', explode(',', $components)));
295
        }
296
        if (!empty($components[0]['name'])) {
297
            $components = array_column($components, 'name');
298
        }
299
        $this->components = $components;
300
        return $this;
301
    }
302
303
    /**
304
     * @return array
305
     */
306
    public function getLabels()
307
    {
308
        return $this->labels;
309
    }
310
311
    /**
312
     * @param array|string $labels
313
     *
314
     * @return Issue
315
     */
316
    public function setLabels($labels)
317
    {
318
        if (!is_array($labels)) {
319
            $labels = array_filter(array_map('trim', explode(',', $labels)));
320
        }
321
        $this->labels = $labels;
322
        return $this;
323
    }
324
325
    /**
326
     * @return string
327
     */
328
    public function getPriority()
329
    {
330
        return $this->priority;
331
    }
332
333
    /**
334
     * @param string $priority
335
     *
336
     * @return Issue
337
     */
338
    public function setPriority($priority)
339
    {
340
        $this->priority = $priority;
341
        return $this;
342
    }
343
344
    /**
345
     * @return string
346
     */
347
    public function getDuedate()
348
    {
349
        return $this->duedate;
350
    }
351
352
    /**
353
     * Set the issues duedate
354
     *
355
     * @param string $duedate
356
     *
357
     * @return Issue
358
     */
359
    public function setDuedate($duedate)
360
    {
361
        $this->duedate = $duedate;
362
        return $this;
363
    }
364
365
    /**
366
     * @return array
367
     */
368
    public function getVersions()
369
    {
370
        return $this->versions;
371
    }
372
373
    /**
374
     * @param array|string $versions
375
     *
376
     * @return Issue
377
     */
378
    public function setVersions($versions)
379
    {
380
        if (!is_array($versions)) {
381
            $versions = array_map('trim', explode(',', $versions));
382
        }
383
        if (!empty($versions[0]['name'])) {
384
            $versions = array_map(function ($version) {
385
                return $version['name'];
386
            }, $versions);
387
        }
388
        $this->versions = $versions;
389
        return $this;
390
    }
391
392
    /**
393
     * @return int
394
     */
395
    public function getUpdated()
396
    {
397
        return $this->updated;
398
    }
399
400
    /**
401
     * @param string|int $updated
402
     *
403
     * @return Issue
404
     */
405
    public function setUpdated($updated)
406
    {
407
        /** @var \helper_plugin_issuelinks_util $util */
408
        $util = plugin_load('helper', 'issuelinks_util');
409
        if (!$util->isValidTimeStamp($updated)) {
410
            $updated = strtotime($updated);
411
        }
412
        $this->updated = (int)$updated;
413
        return $this;
414
    }
415
416
    /**
417
     * Get a fancy HTML-link to issue
418
     *
419
     * @param bool $addSummary add the issue's summary after it status
420
     *
421
     * @return string
422
     */
423
    public function getIssueLinkHTML($addSummary = false)
424
    {
425
        $serviceProvider = ServiceProvider::getInstance();
426
        $service = $serviceProvider->getServices()[$this->serviceID];
427
        $name = $this->projectId . $service::getProjectIssueSeparator($this->isMergeRequest) . $this->issueId;
428
        $url = $this->getIssueURL();
429
430
        $status = cleanID($this->getStatus());
431
        if ($status) {
432
            $name .= $this->getformattedIssueStatus();
433
        }
434
        if ($addSummary) {
435
            $name .= ' ' . $this->getSummary();
436
        }
437
438
        $classes = 'issuelink ' . cleanID($this->getType()) . ($this->isMergeRequest ? ' mergerequest' : '');
439
440
        $attributes = [
441
            'class' => $classes,
442
            'target' => '_blank',
443
            'rel' => 'noopener',
444
            'data-service' => $this->serviceID,
445
            'data-project' => $this->projectId,
446
            'data-issueid' => $this->issueId,
447
            'data-ismergerequest' => $this->isMergeRequest ? '1' : '0'
448
        ];
449
        return "<a href=\"$url\" " . buildAttributes($attributes, true) . '>' . $this->getTypeHTML() . "$name</a>";
450
    }
451
452
    /**
453
     * @return string
454
     */
455
    public function getIssueURL()
456
    {
457
        $serviceProvider = ServiceProvider::getInstance();
458
        $serviceClassName = $serviceProvider->getServices()[$this->serviceID];
459
        $service = $serviceClassName::getInstance();
460
        return $service->getIssueURL($this->projectId, $this->issueId, $this->isMergeRequest);
461
    }
462
463
    /**
464
     * get the status of the issue as HTML string
465
     *
466
     * @param string|null $status
467
     *
468
     * @return string
469
     */
470
    public function getformattedIssueStatus($status = null)
471
    {
472
        if ($status === null) {
473
            $status = $this->getStatus();
474
        }
475
        $status = strtolower($status);
476
        return "<span class='mm__status " . cleanID($status) . "'>$status</span>";
477
    }
478
479
    /**
480
     * @return string
481
     */
482
    public function getTypeHTML()
483
    {
484
        if ($this->isMergeRequest) {
485
            return inlineSVG(__DIR__ . '/../images/mdi-source-pull.svg');
486
        }
487
        $image = $this->getMaterialDesignTypeIcon();
488
        return "<img src='$image' alt='$this->type' />";
489
    }
490
491
    /**
492
     * ToDo: replace all with SVG
493
     *
494
     * @return string the path to the icon / base64 image if type unknown
495
     */
496
    protected function getMaterialDesignTypeIcon()
497
    {
498
        $typeIcon = [
499
            'bug' => 'mdi-bug.png',
500
            'story' => 'mdi-bookmark.png',
501
            'epic' => 'mdi-flash.png',
502
            'change_request' => 'mdi-plus.png',
503
            'improvement' => 'mdi-arrow-up-thick.png',
504
            'organisation_task' => 'mdi-calendar-text.png',
505
            'technical_task' => 'mdi-source-branch.png',
506
            'task' => 'mdi-check.png',
507
        ];
508
509
        if (!isset($typeIcon[cleanID($this->type)])) {
510
            return DOKU_URL . '/lib/plugins/issuelinks/images/mdi-help-circle-outline.png';
511
        }
512
513
        return DOKU_URL . '/lib/plugins/issuelinks/images/' . $typeIcon[cleanID($this->type)];
514
    }
515
516
    public function setAssignee($name, $avatar_url)
517
    {
518
        $this->assignee['name'] = $name;
519
        $this->assignee['avatarURL'] = $avatar_url;
520
    }
521
522
    public function getAdditionalDataHTML()
523
    {
524
        $this->getFromService();
525
        $data = [];
526
        if (!empty($this->assignee)) {
527
            $data['avatarHTML'] = "<img src=\"{$this->assignee['avatarURL']}\" alt=\"{$this->assignee['name']}\">";
528
        }
529
        if (!empty($this->labelData)) {
530
            $labels = $this->getLabels();
531
            $data['fancyLabelsHTML'] = '';
532
            foreach ($labels as $label) {
533
                $colors = '';
534
                $classes = 'label';
535
                if (isset($this->labelData[$label])) {
536
                    $colors = "style=\"background-color: {$this->labelData[$label]['background-color']};";
537
                    $colors .= " color: {$this->labelData[$label]['color']};\"";
538
                    $classes .= ' color';
539
                }
540
                $data['fancyLabelsHTML'] .= "<span class=\"$classes\" $colors>$label</span>";
541
            }
542
        }
543
        return $data;
544
    }
545
546
    public function getFromService()
547
    {
548
        $serviceProvider = ServiceProvider::getInstance();
549
        $serviceClassName = $serviceProvider->getServices()[$this->serviceID];
550
        $service = $serviceClassName::getInstance();
551
552
        try {
553
            $service->retrieveIssue($this);
554
            if ($this->isValid(true)) {
555
                $this->saveToDB();
556
            }
557
        } catch (IssueLinksException $e) {
558
            $this->errors[] = $e;
559
            $this->isValid = false;
560
            return false;
561
        }
562
        return true;
563
    }
564
565
    /**
566
     * Check if an issue is valid.
567
     *
568
     * The specific rules depend on the service and the cached value may also be set by other functions.
569
     *
570
     * @param bool $recheck force a validity check instead of using cached value if available
571
     *
572
     * @return bool
573
     */
574
    public function isValid($recheck = false)
575
    {
576
        if ($recheck || $this->isValid === null) {
577
            $serviceProvider = ServiceProvider::getInstance();
578
            $service = $serviceProvider->getServices()[$this->serviceID];
579
            $this->isValid = $service::isIssueValid($this);
580
        }
581
        return $this->isValid;
582
    }
583
584
    public function saveToDB()
585
    {
586
        /** @var \helper_plugin_issuelinks_db $db */
587
        $db = plugin_load('helper', 'issuelinks_db');
588
        return $db->saveIssue($this);
589
    }
590
591
    public function buildTooltipHTML()
592
    {
593
        $html = '<aside class="issueTooltip">';
594
        $html .= "<h1 class=\"issueTitle\">{$this->getSummary()}</h1>";
595
        $html .= "<div class='assigneeAvatar waiting'></div>";
596
597
        /** @var \helper_plugin_issuelinks_util $util */
598
        $util = plugin_load('helper', 'issuelinks_util');
599
600
        $components = $this->getComponents();
601
        if (!empty($components)) {
602
            $html .= '<p class="components">';
603
            foreach ($components as $component) {
604
                $html .= "<span class=\"component\">$component</span>";
605
            }
606
            $html .= '</p>';
607
        }
608
609
        $labels = $this->getLabels();
610
        if (!empty($labels)) {
611
            $html .= '<p class="labels">';
612
            foreach ($labels as $label) {
613
                $html .= "<span class=\"label\">$label</span>";
614
            }
615
            $html .= '</p>';
616
        }
617
618
        $html .= '<p class="descriptionTeaser">';
619
        $description = $this->getDescription();
620
        if ($description) {
621
            $lines = explode("\n", $description);
622
            $cnt = min(count($lines), 5);
623
            for ($i = 0; $i < $cnt; $i += 1) {
624
                $html .= hsc($lines[$i]) . "\n";
625
            }
626
        } else {
627
            $html .= $util->getLang('no issue description');
628
        }
629
        $html .= '</p>';
630
631
        /** @var \helper_plugin_issuelinks_data $data */
632
        $data = $this->loadHelper('issuelinks_data');
633
634
        if (!$this->isMergeRequest) {
635
            // show merge requests referencing this Issues
636
            $mrs = $data->getMergeRequestsForIssue(
637
                $this->getServiceName(),
638
                $this->getProject(),
639
                $this->issueId,
640
                $this->isMergeRequest
641
            );
642
            if (!empty($mrs)) {
643
                $html .= '<div class="mergeRequests">';
644
                $html .= '<h2>Merge Requests</h2>';
645
                $html .= '<ul>';
646
                foreach ($mrs as $mr) {
647
                    $html .= '<li>';
648
                    $a = "<a href=\"$mr[url]\">$mr[summary]</a>";
649
                    $html .= $this->getformattedIssueStatus($mr['status']) . ' ' . $a;
650
                    $html .= '</li>';
651
                }
652
                $html .= '</ul>';
653
                $html .= '</div>';
654
            }
655
        }
656
657
        $linkingPages = $data->getLinkingPages(
658
            $this->getServiceName(),
659
            $this->getProject(),
660
            $this->issueId,
661
            $this->isMergeRequest
662
        );
663
        if (count($linkingPages)) {
664
            $html .= '<div class="relatedPages📄">';
665
            $html .= '<h2>' . $util->getLang('linking pages') . '</h2>';
666
            $html .= '<ul>';
667
            foreach ($linkingPages as $linkingPage) {
668
                $html .= '<li>';
669
                $html .= html_wikilink($linkingPage['page']);
670
                $html .= '</li>';
671
            }
672
            $html .= '</ul>';
673
            $html .= '</div>';
674
        }
675
676
        $html .= '</aside>';
677
        return $html;
678
    }
679
680 2
    public function getServiceName()
681
    {
682 2
        return $this->serviceID;
683
    }
684
685
    /**
686
     * @param string $labelName the background color without the leading #
687
     * @param string $color
688
     */
689
    public function setLabelData($labelName, $color)
690
    {
691
        $this->labelData[$labelName] = [
692
            'background-color' => $color,
693
            'color' => $this->calculateColor($color),
694
        ];
695
    }
696
697
    /**
698
     * Calculate if a white or black font-color should be with the given background color
699
     *
700
     * https://www.w3.org/TR/WCAG20/#relativeluminancedef
701
     * http://stackoverflow.com/a/3943023/3293343
702
     *
703
     * @param string $color the background-color, without leading #
704
     *
705
     * @return string
706
     */
707
    private function calculateColor($color)
708
    {
709
        /** @noinspection PrintfScanfArgumentsInspection */
710
        list($r, $g, $b) = array_map(function ($color8bit) {
711
            $c = $color8bit / 255;
712
            if ($c <= 0.03928) {
713
                $cl = $c / 12.92;
714
            } else {
715
                $cl = pow(($c + 0.055) / 1.055, 2.4);
716
            }
717
            return $cl;
718
        }, sscanf($color, "%02x%02x%02x"));
719
        if ($r * 0.2126 + $g * 0.7152 + $b * 0.0722 > 0.179) {
720
            return '#000000';
721
        }
722
        return '#FFFFFF';
723
    }
724
}
725