Passed
Branch master (9963ba)
by Brian
03:22
created

JiraProject::getIssue()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.024

Importance

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