Completed
Pull Request — master (#739)
by Sean
04:53
created

DNDeployment   D

Complexity

Total Complexity 61

Size/Duplication

Total Lines 409
Duplicated Lines 11.49 %

Coupling/Cohesion

Components 2
Dependencies 15

Importance

Changes 0
Metric Value
wmc 61
lcom 2
cbo 15
dl 47
loc 409
rs 4.054
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
B flag_new_deploy_enabled() 13 13 8
A setResqueToken() 0 3 1
A getFiniteState() 0 3 1
A setFiniteState() 0 4 1
A getStatus() 0 3 1
A getMachine() 0 3 1
A Link() 0 7 2
A LogLink() 0 3 1
A canView() 0 3 1
A logfile() 0 7 1
A log() 0 3 1
A LogContent() 0 3 1
A ResqueStatus() 0 3 1
A getRepository() 0 6 2
A getCommit() 12 12 3
A getCommitURL() 0 15 4
A getCommitMessage() 11 11 3
A getTags() 0 16 4
B getFullDeployMessages() 0 27 5
A getTag() 0 7 2
A getDeploymentStrategy() 0 6 1
D getChanges() 0 26 10
B enqueueDeployment() 11 39 3
A getSigFile() 0 12 2
A setSignal() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DNDeployment often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DNDeployment, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Class representing a single deplyoment (passed or failed) at a time to a particular environment
5
 *
6
 * @property string $SHA
7
 * @property string $ResqueToken
8
 * @property string $State
9
 * @property int $RefType
10
 * @property SS_Datetime $DeployStarted
11
 * @property SS_Datetime $DeployRequested
12
 *
13
 * @method DNEnvironment Environment()
14
 * @property int EnvironmentID
15
 * @method Member Deployer()
16
 * @property int DeployerID
17
 */
18
class DNDeployment extends DataObject implements Finite\StatefulInterface, HasStateMachine {
0 ignored issues
show
Coding Style introduced by
The property $has_one is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $default_sort is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style introduced by
The property $summary_fields is not named in camelCase.

This check marks property names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
19
20
	const STATE_NEW = 'New';
21
	const STATE_SUBMITTED = 'Submitted';
22
	const STATE_INVALID = 'Invalid';
23
	const STATE_APPROVED = 'Approved';
24
	const STATE_REJECTED = 'Rejected';
25
	const STATE_QUEUED = 'Queued';
26
	const STATE_DEPLOYING = 'Deploying';
27
	const STATE_ABORTING = 'Aborting';
28
	const STATE_COMPLETED = 'Completed';
29
	const STATE_FAILED = 'Failed';
30
31
	const TR_NEW = 'new';
32
	const TR_SUBMIT = 'submit';
33
	const TR_INVALIDATE = 'invalidate';
34
	const TR_APPROVE = 'approve';
35
	const TR_REJECT = 'reject';
36
	const TR_QUEUE = 'queue';
37
	const TR_DEPLOY = 'deploy';
38
	const TR_ABORT = 'abort';
39
	const TR_COMPLETE = 'complete';
40
	const TR_FAIL = 'fail';
41
42
	/**
43
	 * @var array
44
	 */
45
	private static $db = array(
46
		"SHA" => "GitSHA",
47
		"ResqueToken" => "Varchar(255)",
48
		// The branch that was used to deploy this. Can't really be inferred from Git history because
49
		// the commit could appear in lots of branches that are irrelevant to the user when it comes
50
		// to deployment history, and the branch may have been deleted.
51
		"Branch" => "Varchar(255)",
52
		// is it a branch, tag etc, see GitDispatcher REF_TYPE_* constants
53
		"RefType" => "Int",
54
		"State" => "Enum('New, Submitted, Invalid, Approved, Rejected, Queued, Deploying, Aborting, Completed, Failed', 'New')",
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 122 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
55
		// JSON serialised DeploymentStrategy.
56
		"Strategy" => "Text",
57
		"Title" => "Varchar(255)",
58
		"Summary" => "Text",
59
		// the date and time the deploy was queued
60
		"DeployStarted" => "SS_Datetime",
61
		// the date and time a deployment was requested to be approved
62
		"DeployRequested" => "SS_Datetime"
63
	);
64
65
	/**
66
	 * @var array
67
	 */
68
	private static $has_one = array(
69
		"Environment" => "DNEnvironment",
70
		"Deployer" => "Member",
71
		"Approver" => "Member",
72
		"BackupDataTransfer" => "DNDataTransfer" // denotes an automated backup done for this deployment
73
	);
74
75
	private static $default_sort = '"LastEdited" DESC';
76
77
	private static $dependencies = [
78
		'stateMachineFactory' => '%$StateMachineFactory'
79
	];
80
81
	private static $summary_fields = array(
82
		'LastEdited' => 'Last Edited',
83
		'SHA' => 'SHA',
84
		'State' => 'State',
85
		'Deployer.Name' => 'Deployer'
86
	);
87
88
	/**
89
	 * Check for feature flags:
90
	 * - FLAG_NEWDEPLOY_ENABLED: set to true to enable globally
91
	 * - FLAG_NEWDEPLOY_ENABLED_FOR_MEMBERS: set to semicolon-separated list of email addresses of allowed users.
92
	 *
93
	 * @return boolean
94
	 */
95 View Code Duplication
	public static function flag_new_deploy_enabled() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
96
		if (defined('FLAG_NEWDEPLOY_ENABLED') && FLAG_NEWDEPLOY_ENABLED) {
97
			return true;
98
		}
99
		if (defined('FLAG_NEWDEPLOY_ENABLED_FOR_MEMBERS') && FLAG_NEWDEPLOY_ENABLED_FOR_MEMBERS) {
100
			$allowedMembers = explode(';', FLAG_NEWDEPLOY_ENABLED_FOR_MEMBERS);
101
			$member = Member::currentUser();
102
			if ($allowedMembers && $member && in_array($member->Email, $allowedMembers)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowedMembers 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...
103
				return true;
104
			}
105
		}
106
		return false;
107
	}
108
109
	public function setResqueToken($token) {
110
		$this->ResqueToken = $token;
111
	}
112
113
	public function getFiniteState() {
114
		return $this->State;
115
	}
116
117
	public function setFiniteState($state) {
118
		$this->State = $state;
119
		$this->write();
120
	}
121
122
	public function getStatus() {
123
		return $this->State;
124
	}
125
126
	public function getMachine() {
127
		return $this->stateMachineFactory->forDNDeployment($this);
0 ignored issues
show
Documentation introduced by
The property stateMachineFactory does not exist on object<DNDeployment>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
128
	}
129
130
	public function Link() {
131
		if (self::flag_new_deploy_enabled()) {
132
			return \Controller::join_links($this->Environment()->Link('overview'), 'deployment', $this->ID);
133
		} else {
134
			return \Controller::join_links($this->Environment()->Link(), 'deploy', $this->ID);
135
		}
136
	}
137
138
	public function LogLink() {
139
		return $this->Link() . '/log';
140
	}
141
142
	public function canView($member = null) {
143
		return $this->Environment()->canView($member);
144
	}
145
146
	/**
147
	 * Return a path to the log file.
148
	 * @return string
149
	 */
150
	protected function logfile() {
151
		return sprintf(
152
			'%s.%s.log',
153
			$this->Environment()->getFullName('.'),
154
			$this->ID
155
		);
156
	}
157
158
	/**
159
	 * @return DeploynautLogFile
160
	 */
161
	public function log() {
162
		return Injector::inst()->createWithArgs('DeploynautLogFile', array($this->logfile()));
163
	}
164
165
	public function LogContent() {
166
		return $this->log()->content();
167
	}
168
169
	/**
170
	 * This remains here for backwards compatibility - we don't want to expose Resque status in here.
171
	 * Resque job (DeployJob) will change statuses as part of its execution.
172
	 *
173
	 * @return string
174
	 */
175
	public function ResqueStatus() {
176
		return $this->State;
177
	}
178
179
	/**
180
	 * Fetch the git repository
181
	 *
182
	 * @return \Gitonomy\Git\Repository|null
183
	 */
184
	public function getRepository() {
185
		if(!$this->SHA) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->SHA of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
186
			return null;
187
		}
188
		return $this->Environment()->Project()->getRepository();
189
	}
190
191
	/**
192
	 * Gets the commit from source. The result is cached upstream in Repository.
193
	 *
194
	 * @return \Gitonomy\Git\Commit|null
195
	 */
196 View Code Duplication
	public function getCommit() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
197
		$repo = $this->getRepository();
198
		if($repo) {
199
			try {
200
				return $this->Environment()->getCommit($this->SHA);
201
			} catch(Gitonomy\Git\Exception\ReferenceNotFoundException $ex) {
202
				return null;
203
			}
204
		}
205
206
		return null;
207
	}
208
209
	/**
210
	 * Get the commit URL to the commit associated with this deployment.
211
	 * @return null|string
212
	 */
213
	public function getCommitURL() {
214
		$environment = $this->Environment();
215
		if (!$environment) {
216
			return null;
217
		}
218
		$project = $environment->Project();
219
		if (!$project) {
220
			return null;
221
		}
222
		$interface = $project->getRepositoryInterface();
223
		if (!$interface) {
224
			return null;
225
		}
226
		return $interface->CommitURL . '/' . $this->SHA;
227
	}
228
229
	/**
230
	 * Gets the commit message.
231
	 *
232
	 * @return string|null
233
	 */
234 View Code Duplication
	public function getCommitMessage() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
235
		$commit = $this->getCommit();
236
		if($commit) {
237
			try {
238
				return Convert::raw2xml($this->Environment()->getCommitMessage($commit));
239
			} catch(Gitonomy\Git\Exception\ReferenceNotFoundException $e) {
240
				return null;
241
			}
242
		}
243
		return null;
244
	}
245
246
	/**
247
	 * Return all tags for the deployed commit.
248
	 *
249
	 * @return ArrayList
250
	 */
251
	public function getTags() {
252
		$commit = $this->Environment()->getCommit($this->SHA);
253
		if(!$commit) {
254
			return new ArrayList([]);
255
		}
256
		$tags = $this->Environment()->getCommitTags($commit);
257
		$returnTags = [];
258
		if (!empty($tags)) {
259
			foreach($tags as $tag) {
260
				$field = Varchar::create('Tag', '255');
261
				$field->setValue($tag->getName());
262
				$returnTags[] = $field;
263
			}
264
		}
265
		return new ArrayList($returnTags);
266
	}
267
268
	/**
269
	 * Collate the list of additional flags to affix to this deployment.
270
	 * Elements of the array will be rendered literally.
271
	 *
272
	 * @return ArrayList
273
	 */
274
	public function getFullDeployMessages() {
275
		$strategy = $this->getDeploymentStrategy();
276
		if ($strategy->getActionCode()!=='full') return null;
277
278
		$changes = $strategy->getChangesModificationNeeded();
279
		$messages = [];
280
		foreach ($changes as $change => $details) {
281
			if ($change==='Code version') continue;
282
283
			$messages[] = [
284
				'Flag' => sprintf(
285
					'<span class="label label-default full-deploy-info-item">%s</span>',
286
					$change[0]
287
				),
288
				'Text' => sprintf('%s changed', $change)
289
			];
290
		}
291
292
		if (empty($messages)) {
293
			$messages[] = [
294
				'Flag' => '',
295
				'Text' => '<i>Environment changes have been made.</i>'
296
			];
297
		}
298
299
		return new ArrayList($messages);
300
	}
301
302
	/**
303
	 * Fetches the latest tag for the deployed commit
304
	 *
305
	 * @return \Varchar|null
306
	 */
307
	public function getTag() {
308
		$tags = $this->getTags();
309
		if($tags->count() > 0) {
310
			return $tags->last();
311
		}
312
		return null;
313
	}
314
315
	/**
316
	 * @return DeploymentStrategy
317
	 */
318
	public function getDeploymentStrategy() {
319
		$environment = $this->Environment();
320
		$strategy = new DeploymentStrategy($environment);
321
		$strategy->fromJSON($this->Strategy);
0 ignored issues
show
Documentation introduced by
The property Strategy does not exist on object<DNDeployment>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
322
		return $strategy;
323
	}
324
325
	/**
326
	 * Return a list of things that are going to be deployed, such
327
	 * as the code version, and any infrastructural changes.
328
	 *
329
	 * @return ArrayList
330
	 */
331
	public function getChanges() {
332
		$list = new ArrayList();
333
		$strategy = $this->getDeploymentStrategy();
334
		foreach($strategy->getChanges() as $name => $change) {
335
			$changed = (isset($change['from']) && isset($change['to'])) ? $change['from'] != $change['to'] : null;
336
			$description = isset($change['description']) ? $change['description'] : '';
337
			$compareUrl = null;
338
339
			// if there is a compare URL, and a description or a change (something actually changed)
340
			// then show the URL. Otherwise don't show anything, as there is no comparison to be made.
341
			if ($changed || $description) {
342
				$compareUrl = isset($change['compareUrl']) ? $change['compareUrl'] : '';
343
			}
344
345
			$list->push(new ArrayData([
346
				'Name' => $name,
347
				'From' => isset($change['from']) ? $change['from'] : null,
348
				'To' => isset($change['to']) ? $change['to'] : null,
349
				'Description' => $description,
350
				'Changed' => $changed,
351
				'CompareUrl' => $compareUrl
352
			]));
353
		}
354
355
		return $list;
356
	}
357
358
	/**
359
	 * Start a resque job for this deployment
360
	 *
361
	 * @return string Resque token
362
	 */
363
	public function enqueueDeployment() {
364
		$environment = $this->Environment();
365
		$project = $environment->Project();
366
		$log = $this->log();
367
368
		$args = array(
369
			'environmentName' => $environment->Name,
370
			'repository' => $project->getLocalCVSPath(),
371
			'logfile' => $this->logfile(),
372
			'projectName' => $project->Name,
373
			'env' => $project->getProcessEnv(),
374
			'deploymentID' => $this->ID,
375
			'sigFile' => $this->getSigFile(),
376
		);
377
378
		$strategy = $this->getDeploymentStrategy();
379
		// Inject options.
380
		$args = array_merge($args, $strategy->getOptions());
381
		// Make sure we use the SHA as it was written into this DNDeployment.
382
		$args['sha'] = $this->SHA;
383
384
		if(!$this->DeployerID) {
385
			$this->DeployerID = Member::currentUserID();
386
		}
387
388 View Code Duplication
		if($this->DeployerID) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
389
			$deployer = $this->Deployer();
390
			$message = sprintf(
391
				'Deploy to %s initiated by %s (%s), with IP address %s',
392
				$environment->getFullName(),
393
				$deployer->getName(),
394
				$deployer->Email,
395
				\Controller::curr()->getRequest()->getIP()
396
			);
397
			$log->write($message);
398
		}
399
400
		return Resque::enqueue('deploy', 'DeployJob', $args, true);
401
	}
402
403
	public function getSigFile() {
404
		$dir = DNData::inst()->getSignalDir();
405
		if (!is_dir($dir)) {
406
			`mkdir $dir`;
407
		}
408
		return sprintf(
409
			'%s/deploynaut-signal-%s-%s',
410
			DNData::inst()->getSignalDir(),
411
			$this->ClassName,
412
			$this->ID
413
		);
414
	}
415
416
	/**
417
	 * Signal the worker to self-abort. If we had a reliable way of figuring out the right PID,
418
	 * we could posix_kill directly, but Resque seems to not provide a way to find out the PID
419
	 * from the job nor worker.
420
	 */
421
	public function setSignal($signal) {
422
		$sigFile = $this->getSigFile();
423
		// 2 is SIGINT - we can't use SIGINT constant in the Apache context, only available in workers.
424
		file_put_contents($sigFile, $signal);
425
	}
426
}
427