Passed
Pull Request — master (#18)
by Brian
05:45
created

JiraProject::configure()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 34
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 8.048

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 25
nc 6
nop 0
dl 0
loc 34
ccs 16
cts 26
cp 0.6153
crap 8.048
rs 8.8977
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
     * Config Object.
33
     * @var \JiraRestApi\Configuration\ArrayConfiguration
34
     */
35
    public $ConfigObj;
36
37
    /**
38
     * The key for the project.
39
     * @var string|null
40
     */
41
    public $projectKey = null;
42
43
    /**
44
     * The project service object.
45
     * @var \JiraRestApi\Project\ProjectService
46
     */
47
    public $ProjectService;
48
49
    /**
50
     * The project object.
51
     * @var \JiraRestApi\Project\Project
52
     */
53
    protected $Project;
54
55
    /**
56
     * The list of a Project's Versions.
57
     * @var array
58
     */
59
    protected $Versions = [];
60
61
    /**
62
     * The project service object.
63
     * @var \JiraRestApi\Issue\IssueService
64
     */
65
    public $IssueService;
66
67
    /**
68
     * The Cached list of issues.
69
     * @var array
70
     */
71
    protected $Issues = [];
72
73
    /**
74
     * The cached list of returned issue info from the below getIssue() method.
75
     * @var array
76
     */
77
    protected $issuesCache = [];
78
79
    /**
80
     * Valid Types.
81
     * Used to ensure we're getting a valid type when filtering.
82
     * Currently only support Jira Core and Software.
83
     * @see https://confluence.atlassian.com/adminjiracloud/issue-types-844500742.html
84
     * @var array
85
     */
86
    protected $validTypes = [
87
        'Bug',
88
        'Epic',
89
        'Story',
90
        'Subtask',
91
        'Task',
92
    ];
93
94
    /**
95
     * Types of issues allowed to be submitted.
96
     * @var array
97
     */
98
    protected $allowedTypes = [
99
        'Task' => [
100
            'jiraType' => 'Task', // Must be one of the types in the $this->validTypes.
101
            'jiraLabels' => 'task-submitted', // The label used to tag user submitted bugs.
102
            // The form's field information.
103
            'formData' => [
104
                'fields' => [
105
                    'summary' => [
106
                        'type' => 'text',
107
                        'required' => true,
108
                    ],
109
                    'details' => [
110
                        'type' => 'textarea',
111
                        'required' => true,
112
                    ],
113
                ],
114
            ],
115
        ],
116
        'Bug' => [
117
            'jiraType' => 'Bug', // Must be one of the types in the $this->validTypes.
118
            'jiraLabels' => 'bug-submitted', // The label used to tag user submitted bugs.
119
            // The form's field information.
120
            'formData' => [
121
                'fields' => [
122
                    'summary' => [
123
                        'type' => 'text',
124
                        'required' => true,
125
                    ],
126
                    'details' => [
127
                        'type' => 'textarea',
128
                        'required' => true,
129
                    ],
130
                ],
131
            ],
132
        ],
133
        'FeatureRequest' => [
134
            'jiraType' => 'Story', // Must be one of the types in the $this->validTypes.
135
            'jiraLabels' => 'feature-request', // The label used to tag feature requests.
136
            // The form's field information.
137
            'formData' => [
138
                'fields' => [
139
                    'summary' => [
140
                        'type' => 'text',
141
                        'required' => true,
142
                    ],
143
                    'details' => [
144
                        'type' => 'textarea',
145
                        'required' => true,
146
                    ],
147
                ],
148
            ],
149
        ],
150
    ];
151
152
    /**
153
     * This is here for the Form object (or any other object) to use.
154
     * It tacks all errors, even if an exception is thrown.
155
     * @var array
156
     */
157
    protected $errors = [];
158
159
    /**
160
     * Constructor
161
     *
162
     * Reads the configuration, and crdate a config object to be passed to the other objects.
163
     *
164
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException When the project can't be found.
165
     * @return void
166
     */
167 27
    public function __construct()
168
    {
169 27
        $this->configure();
170
171
        // setup the objects
172 27
        $this->ProjectService = new ProjectService($this->ConfigObj);
173
        try {
174 27
            $this->Project = $this->ProjectService->get($this->projectKey);
175
        } catch (JiraException $e) {
176
            $this->setError($this->projectKey, 'MissingProjectException');
0 ignored issues
show
Bug introduced by
It seems like $this->projectKey can also be of type null; however, parameter $msg of Fr3nch13\Jira\Lib\JiraProject::setError() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

176
            $this->setError(/** @scrutinizer ignore-type */ $this->projectKey, 'MissingProjectException');
Loading history...
177
            throw new MissingProjectException($this->projectKey);
178
        }
179
180 27
        $this->Versions = $this->ProjectService->getVersions($this->projectKey);
181 27
        $this->IssueService = new IssueService($this->ConfigObj);
182 27
    }
183
184
    /**
185
     * Configures the object.
186
     * Broken out of construct.
187
     *
188
     * @throws \Fr3nch13\Jira\Exception\MissingConfigException When a config setting isn't set.
189
     * @return void
190
     */
191 27
    public function configure(): void
192
    {
193 27
        $schema = Configure::read('Jira.schema');
194 27
        if (!$schema) {
195
            $this->setError('schema', 'MissingConfigException');
196
            throw new MissingConfigException('schema');
197
        }
198 27
        $host = Configure::read('Jira.host');
199 27
        if (!$host) {
200
            $this->setError('host', 'MissingConfigException');
201
            throw new MissingConfigException('host');
202
        }
203 27
        $username = Configure::read('Jira.username');
204 27
        if (!$username) {
205
            $this->setError('username', 'MissingConfigException');
206
            throw new MissingConfigException('username');
207
        }
208 27
        $apiKey = Configure::read('Jira.apiKey');
209 27
        if (!$apiKey) {
210
            $this->setError('apiKey', 'MissingConfigException');
211
            throw new MissingConfigException('apiKey');
212
        }
213 27
        $projectKey = Configure::read('Jira.projectKey');
214 27
        if (!$projectKey) {
215
            $this->setError('projectKey', 'MissingConfigException');
216
            throw new MissingConfigException('projectKey');
217
        }
218 27
        $this->ConfigObj = new ArrayConfiguration([
219 27
            'jiraHost' => $schema . '://' . $host,
220 27
            'jiraUser' => $username,
221 27
            'jiraPassword' => $apiKey,
222
        ]);
223
224 27
        $this->projectKey = $projectKey;
225 27
    }
226
227
    /**
228
     * Get the Project's Info.
229
     *
230
     * @return \JiraRestApi\Project\Project The information about the project.
231
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException If the project can't be found.
232
     */
233 4
    public function getInfo(): \JiraRestApi\Project\Project
234
    {
235 4
        return $this->Project;
236
    }
237
238
    /**
239
     * Get the Project's Versions.
240
     *
241
     * @return array A list of version objects.
242
     */
243 2
    public function getVersions(): array
244
    {
245 2
        return $this->Versions;
246
    }
247
248
    /**
249
     * Get the Project's Issues.
250
     *
251
     * @param string|null $type Filter the Issues by type.
252
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
253
     */
254 4
    public function getIssues(?string $type = null): \JiraRestApi\Issue\IssueSearchResult
255
    {
256 4
        $cacheKey = 'all';
257 4
        if ($type) {
258 2
            $cacheKey .= '-' . $type;
259
        }
260 4
        if (!isset($this->Issues[$cacheKey])) {
261 4
            $jql = new JqlQuery();
262
263 4
            $jql->setProject($this->projectKey);
264 4
            if ($type && in_array($type, $this->validTypes)) {
265 2
                $jql->setType($type);
266
            }
267 4
            $jql->addAnyExpression('ORDER BY key DESC');
268
269 4
            $this->Issues[$cacheKey] = $this->IssueService->search($jql->getQuery(), 0, 1000);
270
        }
271
272 4
        return $this->Issues[$cacheKey];
273
    }
274
275
    /**
276
     * Get the Project's Open Issues.
277
     *
278
     * @param string|null $type Filter the Issues by type.
279
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
280
     */
281 4
    public function getOpenIssues(?string $type = null): \JiraRestApi\Issue\IssueSearchResult
282
    {
283 4
        $cacheKey = 'open';
284 4
        if ($type) {
285 2
            $cacheKey .= '-' . $type;
286
        }
287 4
        if (!isset($this->Issues[$cacheKey])) {
288 4
            $jql = new JqlQuery();
289
290 4
            $jql->setProject($this->projectKey);
291 4
            if ($type && in_array($type, $this->validTypes)) {
292 2
                $jql->setType($type);
293
            }
294 4
            $jql->addAnyExpression('AND resolution is EMPTY');
295 4
            $jql->addAnyExpression('ORDER BY key DESC');
296
297 4
            $this->Issues[$cacheKey] = $this->IssueService->search($jql->getQuery(), 0, 1000);
298
        }
299
300 4
        return $this->Issues[$cacheKey];
301
    }
302
303
    /**
304
     * Gets info on a particular issue within your project.
305
     *
306
     * @param int|null $id The issue id. The integer part without the project key.
307
     * @return \JiraRestApi\Issue\Issue|\JiraRestApi\Issue\IssueV3 the object that has the info of that issue.
308
     * @throws \Fr3nch13\Jira\Exception\Exception If the issue's id isn't given.
309
     * @throws \Fr3nch13\Jira\Exception\MissingIssueException If the project's issue can't be found.
310
     */
311 2
    public function getIssue(?int $id = null): \JiraRestApi\Issue\Issue
312
    {
313 2
        if (!is_int($id)) {
314
            $this->setError(__('Missing the Issue\'s ID.'), 'Exception');
315
            throw new Exception(__('Missing the Issue\'s ID.'));
316
        }
317 2
        $key = $this->projectKey . '-' . $id;
318 2
        if (!isset($this->issuesCache[$key])) {
319 2
            $this->issuesCache[$key] = $this->IssueService->get($key);
320 2
            if (!$this->issuesCache[$key]) {
321
                $this->setError($key, 'MissingIssueException');
322
                throw new MissingIssueException($key);
323
            }
324
        }
325
326 2
        return $this->issuesCache[$key];
327
    }
328
329
    /**
330
     * Gets a list of issues that are considered bugs.
331
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
332
     */
333 2
    public function getBugs(): \JiraRestApi\Issue\IssueSearchResult
334
    {
335 2
        return $this->getIssues('Bug');
336
    }
337
338
    /**
339
     * Gets a list of open issues that are considered bugs.
340
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
341
     */
342 2
    public function getOpenBugs(): \JiraRestApi\Issue\IssueSearchResult
343
    {
344 2
        return $this->getOpenIssues('Bug');
345
    }
346
347
    /**
348
     * Methods used to submit an Issue to Jira.
349
     */
350
351
    /**
352
     * Returns the allowed types and their settings
353
     *
354
     * @param string|null $type The type of issue you want to get.
355
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If a type is given, and that type is not configured.
356
     * @return array the content of $this->allowedTypes.
357
     */
358 12
    public function getAllowedTypes(?string $type = null): array
359
    {
360 12
        if ($type) {
361 2
            if (!isset($this->allowedTypes[$type])) {
362
                $this->setError($type, 'MissingAllowedTypeException');
363
                throw new MissingAllowedTypeException($type);
364
            }
365
366 2
            return $this->allowedTypes[$type];
367
        }
368
369 11
        return $this->allowedTypes;
370
    }
371
372
    /**
373
     * Allows you to modify the form allowdTypes to fir your situation.
374
     *
375
     * @param string $type The type of issue you want to add/modify.
376
     * @param array $settings The settings for the type.
377
     * @throws \Fr3nch13\Jira\Exception\MissingIssueFieldException If we're adding a new issue type, and the summary field isn't defined.
378
     * @return void
379
     */
380 5
    public function modifyAllowedTypes(string $type, array $settings = []): void
381
    {
382 5
        if (!isset($this->allowedTypes[$type])) {
383 5
            $this->allowedTypes[$type] = [];
384 5
            if (!isset($settings['jiraType'])) {
385
                $this->setError('jiraType', 'MissingIssueFieldException');
386
                throw new MissingIssueFieldException('jiraType');
387
            }
388 5
            if (!isset($settings['formData'])) {
389
                $this->setError('formData', 'MissingIssueFieldException');
390
                throw new MissingIssueFieldException('formData');
391
            }
392 5
            if (!isset($settings['formData']['fields'])) {
393
                $this->setError('formData.fields', 'MissingIssueFieldException');
394
                throw new MissingIssueFieldException('formData.fields');
395
            }
396 5
            if (!isset($settings['formData']['fields']['summary'])) {
397
                $this->setError('formData.fields.summary', 'MissingIssueFieldException');
398
                throw new MissingIssueFieldException('formData.fields.summary');
399
            }
400
        }
401
402 5
        $this->allowedTypes[$type] += $settings;
403 5
    }
404
405
    /**
406
     * Checks to see if a type is allowed.
407
     *
408
     * @param string $type The type to check.
409
     * @return bool if it's allowed or not.
410
     */
411 12
    public function isAllowedType(string $type): bool
412
    {
413 12
        return isset($this->allowedTypes[$type]) ? true : false;
414
    }
415
416
    /**
417
     * Gets the array for the forms when submitting an issue to Jira.
418
     *
419
     * @param string|null $type The type of issue we're submitting.
420
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that type is not configured.
421
     * @throws \Fr3nch13\Jira\Exception\Exception If the form data for that type is missing.
422
     * @return array The array of data to fill in the form with.
423
     */
424 10
    public function getFormData(?string $type = null): array
425
    {
426 10
        if (!$type) {
427
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
428
            throw new MissingAllowedTypeException('[$type is not set]');
429
        }
430
431 10
        if (!$this->isAllowedType($type)) {
432
            $this->setError($type, 'MissingAllowedTypeException');
433
            throw new MissingAllowedTypeException($type);
434
        }
435
436 10
        $allowedTypes = $this->getAllowedTypes();
437
438 10
        if (!isset($allowedTypes[$type]['formData'])) {
439
            $this->setError('No form data is set.', 'Exception');
440
            throw new Exception(__('No form data is set.'));
441
        }
442
443 10
        return $allowedTypes[$type]['formData'];
444
    }
445
446
    /**
447
     * Sets the formData variable if you want to modify the default/initial values.
448
     *
449
     * @param string $type The type you want to set the data for.
450
     *  - Needs to be in the allowedTypes already.
451
     * @param array $data The definition of the allowed types
452
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that type is not configured.
453
     * @return void
454
     */
455 9
    public function setFormData(string $type, array $data = []): void
456
    {
457 9
        if (!$type) {
458
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
459
            throw new MissingAllowedTypeException('[$type is not set]');
460
        }
461
462 9
        if (!$this->isAllowedType($type)) {
463
            $this->setError($type, 'MissingAllowedTypeException');
464
            throw new MissingAllowedTypeException($type);
465
        }
466
467 9
        $this->allowedTypes[$type]['formData'] = $data;
468 9
    }
469
470
    /**
471
     * Submits the Issue
472
     *
473
     * @param string $type The type you want to set the data for.
474
     *  - Needs to be in the allowedTypes already.
475
     * @param array $data The array of details about the issue.
476
     * @throws \Fr3nch13\Jira\Exception\IssueSubmissionException If submitting the issue fails.
477
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that issue type is not configured.
478
     * @throws \Fr3nch13\Jira\Exception\MissingIssueFieldException If we're adding a new issue, and required fields aren't defined.
479
     * @return int > 0 If the request was successfully submitted.
480
     */
481 2
    public function submitIssue(string $type, array $data = []): int
482
    {
483 2
        if (!$type) {
484
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
485
            throw new MissingAllowedTypeException('[$type is not set]');
486
        }
487
488 2
        if (!$this->isAllowedType($type)) {
489
            $this->setError($type, 'MissingAllowedTypeException');
490
            throw new MissingAllowedTypeException($type);
491
        }
492
493 2
        if (!isset($data['summary'])) {
494
            $this->setError('summary', 'MissingIssueFieldException');
495
            throw new MissingIssueFieldException('summary');
496
        }
497
498 2
        $issueField = $this->buildSubmittedIssue($type, $data);
499
500 2
        $issueService = new IssueService($this->ConfigObj);
501
502
        try {
503 2
            $ret = $issueService->create($issueField);
504
        } catch (JiraException $e) {
505
            //Sample return error with json in it.
506
            //Pasting here so I can mock this return message in the unit tests.
507
            //CURL HTTP Request Failed: Status Code : 400, URL:https://[hostname]/rest/api/2/issue
508
            //Error Message : {"errorMessages":[],"errors":{"user_type":"Field 'user_type' cannot be set. It is not on the appropriate screen, or unknown."}}             */
509
            $msg = $e->getMessage();
510
            if (strpos($msg, '{') !== false) {
511
                $msgArray = str_split($msg);
512
                // extract the json message.
513
                $json = '';
514
                $in = 0;
515
                foreach ($msgArray as $i => $char) {
516
                    if ($char == '{') {
517
                        $in++;
518
                    }
519
                    if ($in) {
520
                        $json .= $msg[$i];
521
                    }
522
                    if ($char == '}') {
523
                        $in--;
524
                    }
525
                }
526
                if ($json) {
527
                    $json = json_decode($json, true);
528
                }
529
                if ($json) {
530
                    $newMsg = [];
531
                    if (isset($json['errorMessages'])) {
532
                        foreach ($json['errorMessages'] as $jsonMsg) {
533
                            $newMsg[] = $jsonMsg;
534
                        }
535
                        foreach ($json['errors'] as $jsonMsg) {
536
                            $newMsg[] = $jsonMsg;
537
                        }
538
                        $msg = implode("\n", $newMsg);
539
                    }
540
                }
541
            }
542
            $this->setError($msg, 'IssueSubmissionException');
543
            throw new IssueSubmissionException($msg);
544
        }
545
546 2
        if ($ret instanceof Issue && $ret->id) {
547 2
            return (int)$ret->id;
548
        }
549
550
        return 0;
551
    }
552
553
    /**
554
     * Creates the issue to send to the server.
555
     *
556
     * @param string $type The type of isse we're creating.
557
     * @param array $data The data from the submitted form.
558
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException If submitting the issue fails.
559
     * @return \JiraRestApi\Issue\IssueField
560
     */
561 2
    public function buildSubmittedIssue(string $type, array $data = []): \JiraRestApi\Issue\IssueField
562
    {
563 2
        $typeInfo = $this->getAllowedTypes($type);
564
565
        // make sure we can get the project info first.
566
        // getInfo will throw an exception if it can't find the project.
567
        // putting a try/catch around it so scrutinizer stops complaining.
568
        try {
569 2
            $project = $this->getInfo();
570
        } catch (MissingProjectException $e) {
571
            $this->setError($this->projectKey, 'MissingProjectException');
0 ignored issues
show
Bug introduced by
It seems like $this->projectKey can also be of type null; however, parameter $msg of Fr3nch13\Jira\Lib\JiraProject::setError() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

571
            $this->setError(/** @scrutinizer ignore-type */ $this->projectKey, 'MissingProjectException');
Loading history...
572
            throw $e;
573
        }
574
575 2
        $issueField = new IssueField();
576 2
        $issueField->setProjectKey($this->projectKey)
577 2
            ->setIssueType($typeInfo['jiraType']);
578
579 2
        if (isset($data['summary'])) {
580 2
            $issueField->setSummary($data['summary']);
581
        }
582 2
        if (isset($data['description'])) {
583 1
            $issueField->setDescription($data['description']);
584
        }
585 2
        if (isset($data['priority'])) {
586
            $issueField->setPriorityName($data['priority']);
587
        }
588 2
        if (isset($data['assignee'])) {
589
            $issueField->setPriorityName($data['assignee']);
590
        }
591 2
        if (isset($data['version'])) {
592
            $issueField->addVersion($data['version']);
593
        }
594 2
        if (isset($data['components'])) {
595
            $issueField->addComponents($data['components']);
596
        }
597 2
        if (isset($data['duedate'])) {
598
            $issueField->setDueDate($data['duedate']);
599
        }
600
601
        // labels should be space seperated
602 2
        if (isset($typeInfo['jiraLabels'])) {
603 2
            if (is_string($typeInfo['jiraLabels'])) {
604 2
                $typeInfo['jiraLabels'] = preg_split('/\s+/', $typeInfo['jiraLabels']);
605
            }
606
            // track the type with a label
607 2
            $typeInfo['jiraLabels'][] = 'user-submitted-type-' . $type;
608 2
            foreach ($typeInfo['jiraLabels'] as $jiralabel) {
609 2
                $issueField->addLabel($jiralabel);
610
            }
611
        }
612
613 2
        return $issueField;
614
    }
615
616
    /**
617
     * Sets an error
618
     *
619
     * @param string $msg The error message.
620
     * @param string $key The key to use in the this->errors array.
621
     * @return bool If saved or not.
622
     */
623
    public function setError(string $msg = '', string $key = ''): bool
624
    {
625
        if (!trim($msg)) {
626
            return false;
627
        }
628
        if ($key) {
629
            $this->errors[$key] = $msg;
630
        } else {
631
            $this->errors[] = $msg;
632
        }
633
634
        return true;
635
    }
636
637
    /**
638
     * Gets the accumulated error messages.
639
     * If a key is given, return that specific message. If that key doesn't exist, return false.
640
     *
641
     * @param string|null $key The key to the specific message to get.
642
     * @return array|string|false
643
     */
644
    public function getErrors(?string $key = null)
645
    {
646
        if ($key) {
647
            if (isset($this->errors[$key])) {
648
                return $this->errors[$key];
649
            } else {
650
                return false;
651
            }
652
        }
653
654
        return $this->errors;
655
    }
656
}
657