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