Passed
Push — master ( 69a302...297af8 )
by
unknown
01:35 queued 12s
created

JiraProject::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2.1481

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
nc 2
nop 0
dl 0
loc 15
ccs 6
cts 9
cp 0.6667
crap 2.1481
rs 9.9666
c 1
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * JiraProjectReader
6
 */
7
8
namespace Fr3nch13\Jira\Lib;
9
10
use Cake\Core\Configure;
11
use Fr3nch13\Jira\Exception\Exception;
12
use Fr3nch13\Jira\Exception\IssueSubmissionException;
13
use Fr3nch13\Jira\Exception\MissingAllowedTypeException;
14
use Fr3nch13\Jira\Exception\MissingConfigException;
15
use Fr3nch13\Jira\Exception\MissingIssueException;
16
use Fr3nch13\Jira\Exception\MissingIssueFieldException;
17
use Fr3nch13\Jira\Exception\MissingProjectException;
18
use JiraRestApi\Configuration\ArrayConfiguration;
19
use JiraRestApi\Issue\Issue;
20
use JiraRestApi\Issue\IssueField;
21
use JiraRestApi\Issue\IssueService;
22
use JiraRestApi\Issue\JqlQuery;
23
use JiraRestApi\JiraException;
24
use JiraRestApi\Project\ProjectService;
25
26
/**
27
 * Jira Project class
28
 */
29
class JiraProject
30
{
31
    /**
32
     * @var \JiraRestApi\Configuration\ArrayConfiguration Config Object.
33
     */
34
    public $ConfigObj;
35
36
    /**
37
     * @var string|null The key for the project.
38
     */
39
    public $projectKey = null;
40
41
    /**
42
     * @var \JiraRestApi\Project\ProjectService The project service object.
43
     */
44
    public $ProjectService;
45
46
    /**
47
     * @var \JiraRestApi\Project\Project The project object.
48
     */
49
    protected $Project;
50
51
    /**
52
     * @var array<\JiraRestApi\Issue\Version> The list of a Project's Versions.
53
     */
54
    protected $Versions;
55
56
    /**
57
     * @var \JiraRestApi\Issue\IssueService The project service object.
58
     */
59
    public $IssueService;
60
61
    /**
62
     * @var array The Cached list of issues.
63
     */
64
    protected $Issues = [];
65
66
    /**
67
     * @var array The cached list of returned issue info from the below getIssue() method.
68
     */
69
    protected $issuesCache = [];
70
71
    /**
72
     * Valid Types.
73
     * Used to ensure we're getting a valid type when filtering.
74
     * Currently only support Jira Core and Software.
75
     *
76
     * @see https://confluence.atlassian.com/adminjiracloud/issue-types-844500742.html
77
     * @var array
78
     */
79
    protected $validTypes = [
80
        'Bug',
81
        'Epic',
82
        'Story',
83
        'Subtask',
84
        'Task',
85
    ];
86
87
    /**
88
     * @var array Types of issues allowed to be submitted.
89
     */
90
    protected $allowedTypes = [
91
        'Task' => [
92
            'jiraType' => 'Task', // Must be one of the types in the $this->validTypes.
93
            'jiraLabels' => 'task-submitted', // The label used to tag user submitted bugs.
94
            // The form's field information.
95
            'formData' => [
96
                'fields' => [
97
                    'summary' => [
98
                        'type' => 'text',
99
                        'required' => true,
100
                    ],
101
                    'details' => [
102
                        'type' => 'textarea',
103
                        'required' => true,
104
                    ],
105
                ],
106
            ],
107
        ],
108
        'Bug' => [
109
            'jiraType' => 'Bug', // Must be one of the types in the $this->validTypes.
110
            'jiraLabels' => 'bug-submitted', // The label used to tag user submitted bugs.
111
            // The form's field information.
112
            'formData' => [
113
                'fields' => [
114
                    'summary' => [
115
                        'type' => 'text',
116
                        'required' => true,
117
                    ],
118
                    'details' => [
119
                        'type' => 'textarea',
120
                        'required' => true,
121
                    ],
122
                ],
123
            ],
124
        ],
125
        'FeatureRequest' => [
126
            'jiraType' => 'Story', // Must be one of the types in the $this->validTypes.
127
            'jiraLabels' => 'feature-request', // The label used to tag feature requests.
128
            // The form's field information.
129
            'formData' => [
130
                'fields' => [
131
                    'summary' => [
132
                        'type' => 'text',
133
                        'required' => true,
134
                    ],
135
                    'details' => [
136
                        'type' => 'textarea',
137
                        'required' => true,
138
                    ],
139
                ],
140
            ],
141
        ],
142
    ];
143
144
    /**
145
     * This is here for the Form object (or any other object) to use.
146
     * It tacks all errors, even if an exception is thrown.
147
     *
148
     * @var array
149
     */
150
    protected $errors = [];
151
152
    /**
153
     * Constructor
154
     *
155
     * Reads the configuration, and crdate a config object to be passed to the other objects.
156
     *
157
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException When the project can't be found.
158
     * @return void
159
     */
160 28
    public function __construct()
161
    {
162 28
        $this->configure();
163
164
        // setup the objects
165 28
        $this->ProjectService = new ProjectService($this->ConfigObj);
166
        try {
167 28
            $this->Project = $this->ProjectService->get($this->projectKey);
168
        } catch (JiraException $e) {
169
            $this->setError($this->projectKey, 'MissingProjectException');
170
            throw new MissingProjectException($this->projectKey);
171
        }
172
173 28
        $this->Versions = (array)$this->ProjectService->getVersions($this->projectKey);
174 28
        $this->IssueService = new IssueService($this->ConfigObj);
175 28
    }
176
177
    /**
178
     * Configures the object.
179
     * Broken out of construct.
180
     *
181
     * @throws \Fr3nch13\Jira\Exception\MissingConfigException When a config setting isn't set.
182
     * @return void
183
     */
184 28
    public function configure(): void
185
    {
186 28
        $schema = Configure::read('Jira.schema');
187 28
        if (!$schema) {
188
            $this->setError('schema', 'MissingConfigException');
189
            throw new MissingConfigException('schema');
190
        }
191 28
        $host = Configure::read('Jira.host');
192 28
        if (!$host) {
193
            $this->setError('host', 'MissingConfigException');
194
            throw new MissingConfigException('host');
195
        }
196 28
        $username = Configure::read('Jira.username');
197 28
        if (!$username) {
198
            $this->setError('username', 'MissingConfigException');
199
            throw new MissingConfigException('username');
200
        }
201 28
        $apiKey = Configure::read('Jira.apiKey');
202 28
        if (!$apiKey) {
203
            $this->setError('apiKey', 'MissingConfigException');
204
            throw new MissingConfigException('apiKey');
205
        }
206 28
        $projectKey = Configure::read('Jira.projectKey');
207 28
        if (!$projectKey) {
208
            $this->setError('projectKey', 'MissingConfigException');
209
            throw new MissingConfigException('projectKey');
210
        }
211 28
        $this->ConfigObj = new ArrayConfiguration([
212 28
            'jiraHost' => $schema . '://' . $host,
213 28
            'jiraUser' => $username,
214 28
            'jiraPassword' => $apiKey,
215
        ]);
216
217 28
        $this->projectKey = $projectKey;
218 28
    }
219
220
    /**
221
     * Get the Project's Info.
222
     *
223
     * @return \JiraRestApi\Project\Project The information about the project.
224
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException If the project can't be found.
225
     */
226 4
    public function getInfo(): \JiraRestApi\Project\Project
227
    {
228 4
        return $this->Project;
229
    }
230
231
    /**
232
     * Get the Project's Versions.
233
     *
234
     * @return array<\JiraRestApi\Issue\Version> A list of version objects.
235
     */
236 2
    public function getVersions(): array
237
    {
238 2
        return $this->Versions;
239
    }
240
241
    /**
242
     * Get the Project's Issues.
243
     *
244
     * @param string|null $type Filter the Issues by type.
245
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
246
     */
247 4
    public function getIssues(?string $type = null): \JiraRestApi\Issue\IssueSearchResult
248
    {
249 4
        $cacheKey = 'all';
250 4
        if ($type) {
251 2
            $cacheKey .= '-' . $type;
252
        }
253 4
        if (!isset($this->Issues[$cacheKey])) {
254 4
            $jql = new JqlQuery();
255
256 4
            $jql->setProject($this->projectKey);
257 4
            if ($type && in_array($type, $this->validTypes)) {
258 2
                $jql->setType($type);
259
            }
260 4
            $jql->addAnyExpression('ORDER BY key DESC');
261
262 4
            $this->Issues[$cacheKey] = $this->IssueService->search($jql->getQuery(), 0, 1000);
263
        }
264
265 4
        return $this->Issues[$cacheKey];
266
    }
267
268
    /**
269
     * Get the Project's Open Issues.
270
     *
271
     * @param string|null $type Filter the Issues by type.
272
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
273
     */
274 4
    public function getOpenIssues(?string $type = null): \JiraRestApi\Issue\IssueSearchResult
275
    {
276 4
        $cacheKey = 'open';
277 4
        if ($type) {
278 2
            $cacheKey .= '-' . $type;
279
        }
280 4
        if (!isset($this->Issues[$cacheKey])) {
281 4
            $jql = new JqlQuery();
282
283 4
            $jql->setProject($this->projectKey);
284 4
            if ($type && in_array($type, $this->validTypes)) {
285 2
                $jql->setType($type);
286
            }
287 4
            $jql->addAnyExpression('AND resolution is EMPTY');
288 4
            $jql->addAnyExpression('ORDER BY key DESC');
289
290 4
            $this->Issues[$cacheKey] = $this->IssueService->search($jql->getQuery(), 0, 1000);
291
        }
292
293 4
        return $this->Issues[$cacheKey];
294
    }
295
296
    /**
297
     * Gets info on a particular issue within your project.
298
     *
299
     * @param int|null $id The issue id. The integer part without the project key.
300
     * @return \JiraRestApi\Issue\Issue|\JiraRestApi\Issue\IssueV3 the object that has the info of that issue.
301
     * @throws \Fr3nch13\Jira\Exception\Exception If the issue's id isn't given.
302
     * @throws \Fr3nch13\Jira\Exception\MissingIssueException If the project's issue can't be found.
303
     */
304 3
    public function getIssue(?int $id = null): \JiraRestApi\Issue\Issue
305
    {
306 3
        if (!is_int($id)) {
307
            $this->setError(__('Missing the Issue\'s ID.'), 'Exception');
308
            throw new Exception(__('Missing the Issue\'s ID.'));
309
        }
310 3
        $key = $this->projectKey . '-' . $id;
311 3
        if (!isset($this->issuesCache[$key])) {
312
            try {
313 3
                $this->issuesCache[$key] = $this->IssueService->get($key);
314 1
            } catch (JiraException $e) {
315 1
                $this->setError($this->projectKey, 'MissingIssueException');
316 1
                throw new MissingIssueException($key);
317
            }
318
        }
319
320 2
        return $this->issuesCache[$key];
321
    }
322
323
    /**
324
     * Gets a list of issues that are considered bugs.
325
     *
326
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
327
     */
328 2
    public function getBugs(): \JiraRestApi\Issue\IssueSearchResult
329
    {
330 2
        return $this->getIssues('Bug');
331
    }
332
333
    /**
334
     * Gets a list of open issues that are considered bugs.
335
     *
336
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
337
     */
338 2
    public function getOpenBugs(): \JiraRestApi\Issue\IssueSearchResult
339
    {
340 2
        return $this->getOpenIssues('Bug');
341
    }
342
343
    /**
344
     * Methods used to submit an Issue to Jira.
345
     */
346
347
    /**
348
     * Returns the allowed types and their settings
349
     *
350
     * @param string|null $type The type of issue you want to get.
351
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If a type is given, and that type is not configured.
352
     * @return array the content of $this->allowedTypes.
353
     */
354 12
    public function getAllowedTypes(?string $type = null): array
355
    {
356 12
        if ($type) {
357 2
            if (!isset($this->allowedTypes[$type])) {
358
                $this->setError($type, 'MissingAllowedTypeException');
359
                throw new MissingAllowedTypeException($type);
360
            }
361
362 2
            return $this->allowedTypes[$type];
363
        }
364
365 11
        return $this->allowedTypes;
366
    }
367
368
    /**
369
     * Allows you to modify the form allowdTypes to fir your situation.
370
     *
371
     * @param string $type The type of issue you want to add/modify.
372
     * @param array $settings The settings for the type.
373
     * @throws \Fr3nch13\Jira\Exception\MissingIssueFieldException If we're adding a new issue type, and the summary field isn't defined.
374
     * @return void
375
     */
376 5
    public function modifyAllowedTypes(string $type, array $settings = []): void
377
    {
378 5
        if (!isset($this->allowedTypes[$type])) {
379 5
            $this->allowedTypes[$type] = [];
380 5
            if (!isset($settings['jiraType'])) {
381
                $this->setError('jiraType', 'MissingIssueFieldException');
382
                throw new MissingIssueFieldException('jiraType');
383
            }
384 5
            if (!isset($settings['formData'])) {
385
                $this->setError('formData', 'MissingIssueFieldException');
386
                throw new MissingIssueFieldException('formData');
387
            }
388 5
            if (!isset($settings['formData']['fields'])) {
389
                $this->setError('formData.fields', 'MissingIssueFieldException');
390
                throw new MissingIssueFieldException('formData.fields');
391
            }
392 5
            if (!isset($settings['formData']['fields']['summary'])) {
393
                $this->setError('formData.fields.summary', 'MissingIssueFieldException');
394
                throw new MissingIssueFieldException('formData.fields.summary');
395
            }
396
        }
397
398 5
        $this->allowedTypes[$type] += $settings;
399 5
    }
400
401
    /**
402
     * Checks to see if a type is allowed.
403
     *
404
     * @param string $type The type to check.
405
     * @return bool if it's allowed or not.
406
     */
407 12
    public function isAllowedType(string $type): bool
408
    {
409 12
        return isset($this->allowedTypes[$type]) ? true : false;
410
    }
411
412
    /**
413
     * Gets the array for the forms when submitting an issue to Jira.
414
     *
415
     * @param string|null $type The type of issue we're submitting.
416
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that type is not configured.
417
     * @throws \Fr3nch13\Jira\Exception\Exception If the form data for that type is missing.
418
     * @return array The array of data to fill in the form with.
419
     */
420 10
    public function getFormData(?string $type = null): array
421
    {
422 10
        if (!$type) {
423
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
424
            throw new MissingAllowedTypeException('[$type is not set]');
425
        }
426
427 10
        if (!$this->isAllowedType($type)) {
428
            $this->setError($type, 'MissingAllowedTypeException');
429
            throw new MissingAllowedTypeException($type);
430
        }
431
432 10
        $allowedTypes = $this->getAllowedTypes();
433
434 10
        if (!isset($allowedTypes[$type]['formData'])) {
435
            $this->setError('No form data is set.', 'Exception');
436
            throw new Exception(__('No form data is set.'));
437
        }
438
439 10
        return $allowedTypes[$type]['formData'];
440
    }
441
442
    /**
443
     * Sets the formData variable if you want to modify the default/initial values.
444
     *
445
     * @param string $type The type you want to set the data for.
446
     *  - Needs to be in the allowedTypes already.
447
     * @param array $data The definition of the allowed types
448
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that type is not configured.
449
     * @return void
450
     */
451 9
    public function setFormData(string $type, array $data = []): void
452
    {
453 9
        if (!$type) {
454
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
455
            throw new MissingAllowedTypeException('[$type is not set]');
456
        }
457
458 9
        if (!$this->isAllowedType($type)) {
459
            $this->setError($type, 'MissingAllowedTypeException');
460
            throw new MissingAllowedTypeException($type);
461
        }
462
463 9
        $this->allowedTypes[$type]['formData'] = $data;
464 9
    }
465
466
    /**
467
     * Submits the Issue
468
     *
469
     * @param string $type The type you want to set the data for.
470
     *  - Needs to be in the allowedTypes already.
471
     * @param array $data The array of details about the issue.
472
     * @throws \Fr3nch13\Jira\Exception\IssueSubmissionException If submitting the issue fails.
473
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that issue type is not configured.
474
     * @throws \Fr3nch13\Jira\Exception\MissingIssueFieldException If we're adding a new issue, and required fields aren't defined.
475
     * @return int > 0 If the request was successfully submitted.
476
     */
477 2
    public function submitIssue(string $type, array $data = []): int
478
    {
479 2
        if (!$type) {
480
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
481
            throw new MissingAllowedTypeException('[$type is not set]');
482
        }
483
484 2
        if (!$this->isAllowedType($type)) {
485
            $this->setError($type, 'MissingAllowedTypeException');
486
            throw new MissingAllowedTypeException($type);
487
        }
488
489 2
        if (!isset($data['summary'])) {
490
            $this->setError('summary', 'MissingIssueFieldException');
491
            throw new MissingIssueFieldException('summary');
492
        }
493
494 2
        $issueField = $this->buildSubmittedIssue($type, $data);
495
496 2
        $issueService = new IssueService($this->ConfigObj);
497
498
        try {
499 2
            $ret = $issueService->create($issueField);
500
        } catch (JiraException $e) {
501
            //Sample return error with json in it.
502
            //Pasting here so I can mock this return message in the unit tests.
503
            //CURL HTTP Request Failed: Status Code : 400, URL:https://[hostname]/rest/api/2/issue
504
            //Error Message : {"errorMessages":[],"errors":{"user_type":"Field 'user_type' cannot be set. It is not on the appropriate screen, or unknown."}}             */
505
            $msg = $e->getMessage();
506
            if (strpos($msg, '{') !== false) {
507
                $msgArray = str_split($msg);
508
                // extract the json message.
509
                $json = '';
510
                $in = 0;
511
                foreach ($msgArray as $i => $char) {
512
                    if ($char == '{') {
513
                        $in++;
514
                    }
515
                    if ($in) {
516
                        $json .= $msg[$i];
517
                    }
518
                    if ($char == '}') {
519
                        $in--;
520
                    }
521
                }
522
                if ($json) {
523
                    $json = json_decode($json, true);
524
                }
525
                if ($json) {
526
                    $newMsg = [];
527
                    if (isset($json['errorMessages'])) {
528
                        foreach ($json['errorMessages'] as $jsonMsg) {
529
                            $newMsg[] = $jsonMsg;
530
                        }
531
                        foreach ($json['errors'] as $jsonMsg) {
532
                            $newMsg[] = $jsonMsg;
533
                        }
534
                        $msg = implode("\n", $newMsg);
535
                    }
536
                }
537
            }
538
            $this->setError($msg, 'IssueSubmissionException');
539
            throw new IssueSubmissionException($msg);
540
        }
541
542 2
        if ($ret instanceof Issue && $ret->id) {
543 2
            return (int)$ret->id;
544
        }
545
546
        return 0;
547
    }
548
549
    /**
550
     * Creates the issue to send to the server.
551
     *
552
     * @param string $type The type of isse we're creating.
553
     * @param array $data The data from the submitted form.
554
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException If submitting the issue fails.
555
     * @return \JiraRestApi\Issue\IssueField
556
     */
557 2
    public function buildSubmittedIssue(string $type, array $data = []): \JiraRestApi\Issue\IssueField
558
    {
559 2
        $typeInfo = $this->getAllowedTypes($type);
560
561
        // make sure we can get the project info first.
562
        // getInfo will throw an exception if it can't find the project.
563
        // putting a try/catch around it so scrutinizer stops complaining.
564
        try {
565 2
            $project = $this->getInfo();
566
        } catch (MissingProjectException $e) {
567
            $this->setError($this->projectKey, 'MissingProjectException');
568
            throw $e;
569
        }
570
571 2
        $issueField = new IssueField();
572 2
        $issueField->setProjectKey($this->projectKey)
573 2
            ->setIssueType($typeInfo['jiraType']);
574
575 2
        if (isset($data['summary'])) {
576 2
            $issueField->setSummary($data['summary']);
577
        }
578 2
        if (isset($data['description'])) {
579 1
            $issueField->setDescription($data['description']);
580
        }
581 2
        if (isset($data['priority'])) {
582
            $issueField->setPriorityName($data['priority']);
583
        }
584 2
        if (isset($data['assignee'])) {
585
            $issueField->setPriorityName($data['assignee']);
586
        }
587 2
        if (isset($data['version'])) {
588
            $issueField->addVersion($data['version']);
589
        }
590 2
        if (isset($data['components'])) {
591
            $issueField->addComponents($data['components']);
592
        }
593 2
        if (isset($data['duedate'])) {
594
            $issueField->setDueDate($data['duedate']);
595
        }
596
597
        // labels should be space seperated
598 2
        if (isset($typeInfo['jiraLabels'])) {
599 2
            if (is_string($typeInfo['jiraLabels'])) {
600 2
                $typeInfo['jiraLabels'] = preg_split('/\s+/', $typeInfo['jiraLabels']);
601
            }
602
            // track the type with a label
603 2
            $typeInfo['jiraLabels'][] = 'user-submitted-type-' . $type;
604 2
            foreach ($typeInfo['jiraLabels'] as $jiralabel) {
605 2
                $issueField->addLabel($jiralabel);
606
            }
607
        }
608
609 2
        return $issueField;
610
    }
611
612
    /**
613
     * Sets an error
614
     *
615
     * @param string $msg The error message.
616
     * @param string $key The key to use in the this->errors array.
617
     * @return bool If saved or not.
618
     */
619 1
    public function setError(string $msg = '', string $key = ''): bool
620
    {
621 1
        if (!trim($msg)) {
622
            return false;
623
        }
624 1
        if ($key) {
625 1
            $this->errors[$key] = $msg;
626
        } else {
627
            $this->errors[] = $msg;
628
        }
629
630 1
        return true;
631
    }
632
633
    /**
634
     * Gets the accumulated error messages.
635
     * If a key is given, return that specific message. If that key doesn't exist, return false.
636
     *
637
     * @param string|null $key The key to the specific message to get.
638
     * @return array|string|false
639
     */
640
    public function getErrors(?string $key = null)
641
    {
642
        if ($key) {
643
            if (isset($this->errors[$key])) {
644
                return $this->errors[$key];
645
            } else {
646
                return false;
647
            }
648
        }
649
650
        return $this->errors;
651
    }
652
}
653