Failed Conditions
Push — master ( 54f1b0...5f42b0 )
by Alexander
01:47
created

IssueCloner::_processProjectDetectionQueue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 11
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 19
ccs 0
cts 14
cp 0
crap 12
rs 9.9
1
<?php
2
/**
3
 * This file is part of the Jira-CLI library.
4
 * For the full copyright and license information, please view
5
 * the LICENSE file that was distributed with this source code.
6
 *
7
 * @copyright Alexander Obuhovich <[email protected]>
8
 * @link      https://github.com/console-helpers/jira-cli
9
 */
10
11
namespace ConsoleHelpers\JiraCLI\Issue;
12
13
14
use chobie\Jira\Issue;
15
use chobie\Jira\Issues\Walker;
16
use ConsoleHelpers\JiraCLI\JiraApi;
17
18
class IssueCloner
19
{
20
21
	const LINK_DIRECTION_INWARD = 1;
22
23
	const LINK_DIRECTION_OUTWARD = 2;
24
25
	/**
26
	 * Jira REST client.
27
	 *
28
	 * @var JiraApi
29
	 */
30
	protected $jiraApi;
31
32
	/**
33
	 * Specifies custom fields to copy during backporting.
34
	 *
35
	 * @var array
36
	 */
37
	private $_copyCustomFields = array(
38
		'Change Log Group', 'Change Log Message',
39
	);
40
41
	/**
42
	 * Custom fields map.
43
	 *
44
	 * @var array
45
	 */
46
	private $_customFieldsMap = array();
47
48
	/**
49
	 * Fields to query during issue search.
50
	 *
51
	 * @var array
52
	 */
53
	protected $queryFields = array('summary', 'issuelinks');
54
55
	/**
56
	 * Project of an issue.
57
	 *
58
	 * @var array
59
	 */
60
	private $_issueProjects = array();
61
62
	/**
63
	 * IssueCloner constructor.
64
	 *
65
	 * @param JiraApi $jira_api Jira REST client.
66
	 */
67 2
	public function __construct(JiraApi $jira_api)
68
	{
69 2
		$this->jiraApi = $jira_api;
70
71 2
		$this->jiraApi->setOptions(0); // Don't expand fields.
72
	}
73
74
	/**
75
	 * Returns issues.
76
	 *
77
	 * @param string  $jql               JQL.
78
	 * @param string  $link_name         Link name.
79
	 * @param integer $link_direction    Link direction.
80
	 * @param array   $link_project_keys Link project keys.
81
	 *
82
	 * @return array
83
	 */
84
	public function getIssues($jql, $link_name, $link_direction, array $link_project_keys)
85
	{
86
		$this->_buildCustomFieldsMap();
87
88
		$walker = new Walker($this->jiraApi);
89
		$walker->push($jql, implode(',', $this->_getQueryFields()));
90
91
		$cache = array();
92
93
		foreach ( $walker as $issue ) {
94
			foreach ( $link_project_keys as $link_project_key ) {
95
				$linked_issue = $this->_getLinkedIssue($issue, $link_name, $link_direction);
96
97
				if ( $linked_issue !== null ) {
98
					$this->_addToProjectDetectionQueue($linked_issue);
99
				}
100
101
				$cache[] = array($issue, $linked_issue, $link_project_key);
102
			}
103
		}
104
105
		$this->_processProjectDetectionQueue();
106
107
		$ret = array();
108
109
		foreach ( $cache as $cached_data ) {
110
			list($issue, $linked_issue, $link_project_key) = $cached_data;
111
112
			if ( $linked_issue === null ) {
113
				$ret[] = $cached_data;
114
				continue;
115
			}
116
117
			if ( $this->isLinkAccepted($issue, $linked_issue)
118
				&& $this->_getIssueProject($linked_issue) === $link_project_key
119
				&& !$this->isAlreadyProcessed($issue, $linked_issue)
120
			) {
121
				$ret[] = $cached_data;
122
			}
123
		}
124
125
		return $ret;
126
	}
127
128
	/**
129
	 * Adds an issue to the project detection queue.
130
	 *
131
	 * @param Issue $issue Issue.
132
	 *
133
	 * @return void
134
	 */
135
	private function _addToProjectDetectionQueue(Issue $issue)
136
	{
137
		$this->_issueProjects[$issue->getKey()] = null;
138
	}
139
140
	/**
141
	 * Detects projects for queued issues.
142
	 *
143
	 * @return void
144
	 */
145
	private function _processProjectDetectionQueue()
146
	{
147
		$issues_without_projects = array_keys(array_filter($this->_issueProjects, function ($project_key) {
148
			return $project_key === null;
149
		}));
150
151
		if ( !$issues_without_projects ) {
1 ignored issue
show
Bug Best Practice introduced by
The expression $issues_without_projects of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
152
			return;
153
		}
154
155
		$walker = new Walker($this->jiraApi);
156
		$walker->push(
157
			'key IN (' . implode(',', $issues_without_projects) . ')',
158
			'project'
159
		);
160
161
		foreach ( $walker as $linked_issue ) {
162
			$linked_issue_project_data = $linked_issue->get('project');
163
			$this->_issueProjects[$linked_issue->getKey()] = $linked_issue_project_data['key'];
164
		}
165
	}
166
167
	/**
168
	 * Returns an issue project.
169
	 *
170
	 * @param Issue $issue Issue.
171
	 *
172
	 * @return string
173
	 * @throws \RuntimeException When issue's project isn't available.
174
	 */
175
	private function _getIssueProject(Issue $issue)
176
	{
177
		$issue_key = $issue->getKey();
178
179
		if ( array_key_exists($issue_key, $this->_issueProjects) ) {
180
			return $this->_issueProjects[$issue_key];
181
		}
182
183
		throw new \RuntimeException('The issue "' . $issue_key . '" project wasn\'t queried.');
184
	}
185
186
	/**
187
	 * Builds custom field map.
188
	 *
189
	 * @return void
190
	 */
191
	private function _buildCustomFieldsMap()
192
	{
193
		foreach ( $this->jiraApi->getFields() as $field_key => $field_data ) {
194
			if ( substr($field_key, 0, 12) === 'customfield_' ) {
195
				$this->_customFieldsMap[$field_data['name']] = $field_key;
196
			}
197
		}
198
	}
199
200
	/**
201
	 * Returns query fields.
202
	 *
203
	 * @return array
204
	 */
205
	private function _getQueryFields()
206
	{
207
		$ret = $this->queryFields;
208
209
		foreach ( $this->_copyCustomFields as $custom_field ) {
210
			if ( isset($this->_customFieldsMap[$custom_field]) ) {
211
				$ret[] = $this->_customFieldsMap[$custom_field];
212
			}
213
		}
214
215
		return $ret;
216
	}
217
218
	/**
219
	 * Returns issue, which backports given issue (project not matched yet).
220
	 *
221
	 * @param Issue   $issue          Issue.
222
	 * @param string  $link_name      Link name.
223
	 * @param integer $link_direction Link direction.
224
	 *
225
	 * @return Issue|null
226
	 * @throws \InvalidArgumentException When link direction isn't valid.
227
	 */
228
	private function _getLinkedIssue(Issue $issue, $link_name, $link_direction)
229
	{
230
		foreach ( $issue->get('issuelinks') as $issue_link ) {
231
			if ( $issue_link['type']['name'] !== $link_name ) {
232
				continue;
233
			}
234
235
			if ( $link_direction === self::LINK_DIRECTION_INWARD ) {
236
				$check_key = 'inwardIssue';
237
			}
238
			elseif ( $link_direction === self::LINK_DIRECTION_OUTWARD ) {
239
				$check_key = 'outwardIssue';
240
			}
241
			else {
242
				throw new \InvalidArgumentException('The "' . $link_direction . '" link direction isn\'t valid.');
243
			}
244
245
			if ( array_key_exists($check_key, $issue_link) ) {
246
				return new Issue($issue_link[$check_key]);
247
			}
248
		}
249
250
		return null;
251
	}
252
253
	/**
254
	 * Creates backports issues.
255
	 *
256
	 * @param Issue   $issue          Issue.
257
	 * @param string  $project_key    Project key.
258
	 * @param string  $link_name      Link name.
259
	 * @param integer $link_direction Link direction.
260
	 * @param array   $component_ids  Component IDs.
261
	 *
262
	 * @return string
263
	 * @throws \RuntimeException When failed to create an issue.
264
	 * @throws \InvalidArgumentException When link direction isn't valid.
265
	 */
266
	public function createLinkedIssue(Issue $issue, $project_key, $link_name, $link_direction, array $component_ids)
267
	{
268
		$create_fields = array(
269
			'description' => 'See ' . $issue->getKey() . '.',
270
			'components' => array(),
271
		);
272
273
		foreach ( $this->_copyCustomFields as $custom_field ) {
274
			if ( isset($this->_customFieldsMap[$custom_field]) ) {
275
				$custom_field_id = $this->_customFieldsMap[$custom_field];
276
				$create_fields[$custom_field_id] = $this->getIssueCustomField($issue, $custom_field_id);
277
			}
278
		}
279
280
		foreach ( $component_ids as $component_id ) {
281
			$create_fields['components'][] = array('id' => (string)$component_id);
282
		}
283
284
		$create_issue_result = $this->jiraApi->createIssue(
285
			$project_key,
286
			$issue->get('summary'),
0 ignored issues
show
Bug introduced by
$issue->get('summary') of type array is incompatible with the type string expected by parameter $summary of chobie\Jira\Api::createIssue(). ( Ignorable by Annotation )

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

286
			/** @scrutinizer ignore-type */ $issue->get('summary'),
Loading history...
287
			$this->getChangelogEntryIssueTypeId(),
288
			$create_fields
289
		);
290
291
		$raw_create_issue_result = $create_issue_result->getResult();
292
293
		if ( array_key_exists('errors', $raw_create_issue_result) ) {
294
			throw new \RuntimeException(sprintf(
295
				'Failed to create linked issue for "%s" issue. Errors: ' . PHP_EOL . '%s',
296
				$issue->getKey(),
297
				print_r($raw_create_issue_result['errors'], true)
0 ignored issues
show
Bug introduced by
It seems like print_r($raw_create_issue_result['errors'], true) can also be of type true; however, parameter $values of sprintf() does only seem to accept double|integer|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

297
				/** @scrutinizer ignore-type */ print_r($raw_create_issue_result['errors'], true)
Loading history...
298
			));
299
		}
300
301
		if ( $link_direction === self::LINK_DIRECTION_INWARD ) {
302
			$issue_link_result = $this->jiraApi->api(
0 ignored issues
show
Unused Code introduced by
The assignment to $issue_link_result is dead and can be removed.
Loading history...
303
				JiraApi::REQUEST_POST,
304
				'/rest/api/2/issueLink',
305
				array(
306
					'type' => array('name' => $link_name),
307
					'inwardIssue' => array('key' => $raw_create_issue_result['key']),
308
					'outwardIssue' => array('key' => $issue->getKey()),
309
				)
310
			);
311
		}
312
		elseif ( $link_direction === self::LINK_DIRECTION_OUTWARD ) {
313
			$issue_link_result = $this->jiraApi->api(
314
				JiraApi::REQUEST_POST,
315
				'/rest/api/2/issueLink',
316
				array(
317
					'type' => array('name' => $link_name),
318
					'inwardIssue' => array('key' => $issue->getKey()),
319
					'outwardIssue' => array('key' => $raw_create_issue_result['key']),
320
				)
321
			);
322
		}
323
		else {
324
			throw new \InvalidArgumentException('The "' . $link_direction . '" link direction isn\'t valid.');
325
		}
326
327
		return $raw_create_issue_result['key'];
328
	}
329
330
	/**
331
	 * Determines if link was already processed.
332
	 *
333
	 * @param Issue $issue        Issue.
334
	 * @param Issue $linked_issue Linked issue.
335
	 *
336
	 * @return boolean
337
	 */
338
	protected function isAlreadyProcessed(Issue $issue, Issue $linked_issue)
0 ignored issues
show
Unused Code introduced by
The parameter $linked_issue is not used and could be removed. ( Ignorable by Annotation )

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

338
	protected function isAlreadyProcessed(Issue $issue, /** @scrutinizer ignore-unused */ Issue $linked_issue)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
339
	{
340
		return false;
341
	}
342
343
	/**
344
	 * Determines if link is accepted.
345
	 *
346
	 * @param Issue $issue        Issue.
347
	 * @param Issue $linked_issue Linked issue.
348
	 *
349
	 * @return boolean
350
	 */
351
	protected function isLinkAccepted(Issue $issue, Issue $linked_issue)
0 ignored issues
show
Unused Code introduced by
The parameter $linked_issue is not used and could be removed. ( Ignorable by Annotation )

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

351
	protected function isLinkAccepted(Issue $issue, /** @scrutinizer ignore-unused */ Issue $linked_issue)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
352
	{
353
		return true;
354
	}
355
356
	/**
357
	 * Returns ID of "Changelog Entry" issue type.
358
	 *
359
	 * @return integer
360
	 * @throws \LogicException When "Changelog Entry" issue type wasn't found.
361
	 */
362
	protected function getChangelogEntryIssueTypeId()
363
	{
364
		static $issue_type_id;
365
366
		if ( !isset($issue_type_id) ) {
367
			foreach ( $this->jiraApi->getIssueTypes() as $issue_type ) {
368
				if ( $issue_type->getName() === 'Changelog Entry' ) {
369
					$issue_type_id = $issue_type->getId();
370
					break;
371
				}
372
			}
373
374
			if ( !isset($issue_type_id) ) {
375
				throw new \LogicException('The "Changelog Entry" issue type not found.');
376
			}
377
		}
378
379
		return $issue_type_id;
380
	}
381
382
	/**
383
	 * Returns custom field value.
384
	 *
385
	 * @param Issue  $issue           Issue.
386
	 * @param string $custom_field_id Custom field ID.
387
	 *
388
	 * @return mixed
389
	 */
390
	protected function getIssueCustomField(Issue $issue, $custom_field_id)
391
	{
392
		$custom_field_data = $issue->get($custom_field_id);
393
394
		if ( is_array($custom_field_data) ) {
0 ignored issues
show
introduced by
The condition is_array($custom_field_data) is always true.
Loading history...
395
			return array('value' => $custom_field_data['value']);
396
		}
397
398
		return $custom_field_data;
399
	}
400
401
	/**
402
	 * Returns issue status name.
403
	 *
404
	 * @param Issue $issue Issue.
405
	 *
406
	 * @return string
407
	 */
408
	public function getIssueStatusName(Issue $issue)
409
	{
410
		$status = $issue->get('status');
411
412
		return $status['name'];
413
	}
414
415
}
416