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 classes like DNProject 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 DNProject, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
15 | class DNProject extends DataObject { |
||
16 | |||
17 | /** |
||
18 | * @var array |
||
19 | */ |
||
20 | public static $db = array( |
||
21 | "Name" => "Varchar", |
||
22 | "CVSPath" => "Varchar(255)", |
||
23 | "DiskQuotaMB" => "Int", |
||
24 | "AllowedEnvironmentType" => "Varchar(255)", |
||
25 | ); |
||
26 | |||
27 | /** |
||
28 | * @var array |
||
29 | */ |
||
30 | public static $has_many = array( |
||
31 | "Environments" => "DNEnvironment", |
||
32 | "CreateEnvironments" => "DNCreateEnvironment" |
||
33 | ); |
||
34 | |||
35 | /** |
||
36 | * @var array |
||
37 | */ |
||
38 | public static $many_many = array( |
||
39 | "Viewers" => "Group", |
||
40 | 'StarredBy' => "Member" |
||
41 | ); |
||
42 | |||
43 | /** |
||
44 | * @var array |
||
45 | */ |
||
46 | public static $summary_fields = array( |
||
47 | "Name", |
||
48 | "ViewersList", |
||
49 | ); |
||
50 | |||
51 | /** |
||
52 | * @var array |
||
53 | */ |
||
54 | public static $searchable_fields = array( |
||
55 | "Name", |
||
56 | ); |
||
57 | |||
58 | /** |
||
59 | * @var string |
||
60 | */ |
||
61 | private static $singular_name = 'Project'; |
||
62 | |||
63 | /** |
||
64 | * @var string |
||
65 | */ |
||
66 | private static $plural_name = 'Projects'; |
||
67 | |||
68 | /** |
||
69 | * @var string |
||
70 | */ |
||
71 | private static $default_sort = 'Name'; |
||
72 | |||
73 | /** |
||
74 | * Display the repository URL on the project page. |
||
75 | * |
||
76 | * @var bool |
||
77 | */ |
||
78 | private static $show_repository_url = false; |
||
79 | |||
80 | /** |
||
81 | * In-memory cache for currentBuilds per environment since fetching them from |
||
82 | * disk is pretty resource hungry. |
||
83 | * |
||
84 | * @var array |
||
85 | */ |
||
86 | protected static $relation_cache = array(); |
||
87 | |||
88 | /** |
||
89 | * In-memory cache to determine whether clone repo was called. |
||
90 | * @var array |
||
91 | */ |
||
92 | private static $has_cloned_cache = array(); |
||
93 | |||
94 | /** |
||
95 | * @var bool|Member |
||
96 | */ |
||
97 | protected static $_current_member_cache = null; |
||
98 | |||
99 | /** |
||
100 | * Used by the sync task |
||
101 | * |
||
102 | * @param string $path |
||
103 | * @return \DNProject |
||
104 | */ |
||
105 | public static function create_from_path($path) { |
||
106 | $project = DNProject::create(); |
||
107 | $project->Name = $path; |
||
108 | $project->write(); |
||
109 | |||
110 | // add the administrators group as the viewers of the new project |
||
111 | $adminGroup = Group::get()->filter('Code', 'administrators')->first(); |
||
112 | if($adminGroup && $adminGroup->exists()) { |
||
113 | $project->Viewers()->add($adminGroup); |
||
114 | } |
||
115 | return $project; |
||
116 | } |
||
117 | |||
118 | /** |
||
119 | * Return the used quota in MB. |
||
120 | * |
||
121 | * @param int $round Number of decimal places to round to |
||
122 | * @return double The used quota size in MB |
||
123 | */ |
||
124 | public function getUsedQuotaMB($round = 2) { |
||
125 | $size = 0; |
||
126 | |||
127 | foreach($this->Environments() as $environment) { |
||
128 | foreach($environment->DataArchives()->filter('IsBackup', 0) as $archive) { |
||
129 | $size += $archive->ArchiveFile()->getAbsoluteSize(); |
||
130 | } |
||
131 | } |
||
132 | |||
133 | // convert bytes to megabytes and round |
||
134 | return round(($size / 1024) / 1024, $round); |
||
135 | } |
||
136 | |||
137 | /** |
||
138 | * Getter for DiskQuotaMB field to provide a default for existing |
||
139 | * records that have no quota field set, as it will need to default |
||
140 | * to a globally set size. |
||
141 | * |
||
142 | * @return string|int The quota size in MB |
||
143 | */ |
||
144 | public function getDiskQuotaMB() { |
||
145 | $size = $this->getField('DiskQuotaMB'); |
||
146 | |||
147 | if(empty($size)) { |
||
148 | $defaults = $this->config()->get('defaults'); |
||
149 | $size = (isset($defaults['DiskQuotaMB'])) ? $defaults['DiskQuotaMB'] : 0; |
||
150 | } |
||
151 | |||
152 | return $size; |
||
153 | } |
||
154 | |||
155 | /** |
||
156 | * Has the disk quota been exceeded? |
||
157 | * |
||
158 | * @return boolean |
||
159 | */ |
||
160 | public function HasExceededDiskQuota() { |
||
163 | |||
164 | /** |
||
165 | * Is there a disk quota set for this project? |
||
166 | * |
||
167 | * @return boolean |
||
168 | */ |
||
169 | public function HasDiskQuota() { |
||
172 | |||
173 | /** |
||
174 | * Returns the current disk quota usage as a percentage |
||
175 | * |
||
176 | * @return int |
||
177 | */ |
||
178 | public function DiskQuotaUsagePercent() { |
||
179 | $quota = $this->getDiskQuotaMB(); |
||
180 | if($quota > 0) { |
||
181 | return $this->getUsedQuotaMB() * 100 / $quota; |
||
182 | } |
||
183 | return 100; |
||
184 | } |
||
185 | |||
186 | /** |
||
187 | * Get the menu to be shown on projects |
||
188 | * |
||
189 | * @return ArrayList |
||
190 | */ |
||
191 | public function Menu() { |
||
192 | $list = new ArrayList(); |
||
193 | |||
194 | $controller = Controller::curr(); |
||
195 | $actionType = $controller->getField('CurrentActionType'); |
||
196 | |||
197 | if(DNRoot::FlagSnapshotsEnabled() && $this->isProjectReady()) { |
||
198 | $list->push(new ArrayData(array( |
||
199 | 'Link' => sprintf('naut/project/%s/snapshots', $this->Name), |
||
200 | 'Title' => 'Snapshots', |
||
201 | 'IsCurrent' => $this->isSection() && $controller->getAction() == 'snapshots', |
||
202 | 'IsSection' => $this->isSection() && $actionType == DNRoot::ACTION_SNAPSHOT |
||
203 | ))); |
||
204 | } |
||
205 | |||
206 | $this->extend('updateMenu', $list); |
||
207 | |||
208 | return $list; |
||
209 | } |
||
210 | |||
211 | /** |
||
212 | * Is this project currently at the root level of the controller that handles it? |
||
213 | * |
||
214 | * @return bool |
||
215 | */ |
||
216 | public function isCurrent() { |
||
219 | |||
220 | /** |
||
221 | * Return the current object from $this->Menu() |
||
222 | * Good for making titles and things |
||
223 | * |
||
224 | * @return DataObject |
||
225 | */ |
||
226 | public function CurrentMenu() { |
||
229 | |||
230 | /** |
||
231 | * Is this project currently in a controller that is handling it or performing a sub-task? |
||
232 | * |
||
233 | * @return bool |
||
234 | */ |
||
235 | public function isSection() { |
||
236 | $controller = Controller::curr(); |
||
237 | $project = $controller->getField('CurrentProject'); |
||
238 | return $project && $this->ID == $project->ID; |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * Restrict access to viewing this project |
||
243 | * |
||
244 | * @param Member|null $member |
||
245 | * @return boolean |
||
246 | */ |
||
247 | public function canView($member = null) { |
||
248 | if(!$member) { |
||
249 | $member = Member::currentUser(); |
||
250 | } |
||
251 | |||
252 | if(Permission::checkMember($member, 'ADMIN')) { |
||
253 | return true; |
||
254 | } |
||
255 | |||
256 | return $member->inGroups($this->Viewers()); |
||
257 | } |
||
258 | |||
259 | /** |
||
260 | * @param Member|null $member |
||
261 | * |
||
262 | * @return bool |
||
263 | */ |
||
264 | View Code Duplication | public function canRestore($member = null) { |
|
|
|||
265 | if ($this->allowedAny( |
||
266 | array( |
||
267 | DNRoot::ALLOW_PROD_SNAPSHOT, |
||
268 | DNRoot::ALLOW_NON_PROD_SNAPSHOT |
||
269 | ), |
||
270 | $member |
||
271 | )) { |
||
272 | return true; |
||
273 | } |
||
274 | |||
275 | return (bool)$this->Environments()->filterByCallback(function($env) use($member) { |
||
276 | return $env->canRestore($member); |
||
277 | })->Count(); |
||
278 | } |
||
279 | |||
280 | /** |
||
281 | * @param Member|null $member |
||
282 | * @return bool |
||
283 | */ |
||
284 | View Code Duplication | public function canBackup($member = null) { |
|
285 | if ($this->allowedAny( |
||
286 | array( |
||
287 | DNRoot::ALLOW_PROD_SNAPSHOT, |
||
288 | DNRoot::ALLOW_NON_PROD_SNAPSHOT |
||
289 | ), |
||
290 | $member |
||
291 | )) { |
||
292 | return true; |
||
293 | } |
||
294 | |||
295 | return (bool)$this->Environments()->filterByCallback(function($env) use($member) { |
||
296 | return $env->canBackup($member); |
||
297 | })->Count(); |
||
298 | } |
||
299 | |||
300 | /** |
||
301 | * @param Member|null $member |
||
302 | * @return bool |
||
303 | */ |
||
304 | View Code Duplication | public function canUploadArchive($member = null) { |
|
305 | if ($this->allowedAny( |
||
306 | array( |
||
307 | DNRoot::ALLOW_PROD_SNAPSHOT, |
||
308 | DNRoot::ALLOW_NON_PROD_SNAPSHOT |
||
309 | ), |
||
310 | $member |
||
311 | )) { |
||
312 | return true; |
||
313 | } |
||
314 | |||
315 | return (bool)$this->Environments()->filterByCallback(function($env) use($member) { |
||
316 | return $env->canUploadArchive($member); |
||
317 | })->Count(); |
||
318 | } |
||
319 | |||
320 | /** |
||
321 | * @param Member|null $member |
||
322 | * @return bool |
||
323 | */ |
||
324 | View Code Duplication | public function canDownloadArchive($member = null) { |
|
325 | if ($this->allowedAny( |
||
326 | array( |
||
327 | DNRoot::ALLOW_PROD_SNAPSHOT, |
||
328 | DNRoot::ALLOW_NON_PROD_SNAPSHOT |
||
329 | ), |
||
330 | $member |
||
331 | )) { |
||
332 | return true; |
||
333 | } |
||
334 | |||
335 | return (bool)$this->Environments()->filterByCallback(function($env) use($member) { |
||
336 | return $env->canDownloadArchive($member); |
||
337 | })->Count(); |
||
338 | } |
||
339 | |||
340 | /** |
||
341 | * This is a permission check for the front-end only. |
||
342 | * |
||
343 | * Only admins can create environments for now. Also, we need to check the value |
||
344 | * of AllowedEnvironmentType which dictates which backend to use to render the form. |
||
345 | * |
||
346 | * @param Member|null $member |
||
347 | * |
||
348 | * @return bool |
||
349 | */ |
||
350 | public function canCreateEnvironments($member = null) { |
||
351 | $envType = $this->AllowedEnvironmentType; |
||
352 | if($envType) { |
||
353 | $env = Injector::inst()->get($envType); |
||
354 | if($env instanceof EnvironmentCreateBackend) { |
||
355 | return $this->allowed(DNRoot::ALLOW_CREATE_ENVIRONMENT, $member); |
||
356 | } |
||
357 | } |
||
358 | return false; |
||
359 | } |
||
360 | |||
361 | /** |
||
362 | * @return DataList |
||
363 | */ |
||
364 | public function DataArchives() { |
||
368 | |||
369 | /** |
||
370 | * Return all archives which are "manual upload requests", |
||
371 | * meaning they don't have a file attached to them (yet). |
||
372 | * |
||
373 | * @return DataList |
||
374 | */ |
||
375 | public function PendingManualUploadDataArchives() { |
||
378 | |||
379 | /** |
||
380 | * Build an environment variable array to be used with this project. |
||
381 | * |
||
382 | * This is relevant if every project needs to use an individual SSH pubkey. |
||
383 | * |
||
384 | * Include this with all Gitonomy\Git\Repository, and |
||
385 | * \Symfony\Component\Process\Processes. |
||
386 | * |
||
387 | * @return array |
||
388 | */ |
||
389 | public function getProcessEnv() { |
||
390 | if(file_exists($this->getPrivateKeyPath())) { |
||
391 | // Key-pair is available, use it. |
||
392 | $processEnv = array( |
||
393 | 'IDENT_KEY' => $this->getPrivateKeyPath(), |
||
394 | 'GIT_SSH' => BASE_PATH . "/deploynaut/git-deploy.sh" |
||
395 | ); |
||
396 | } else { |
||
397 | $processEnv = array(); |
||
398 | } |
||
399 | $this->extend('updateProcessEnv', $processEnv); |
||
400 | |||
401 | return $processEnv; |
||
402 | } |
||
403 | |||
404 | /** |
||
405 | * Get a string of people allowed to view this project |
||
406 | * |
||
407 | * @return string |
||
408 | */ |
||
409 | public function getViewersList() { |
||
412 | |||
413 | /** |
||
414 | * @return DNData |
||
415 | */ |
||
416 | public function DNData() { |
||
419 | |||
420 | /** |
||
421 | * Provides a DNBuildList of builds found in this project. |
||
422 | * |
||
423 | * @return DNReferenceList |
||
424 | */ |
||
425 | public function DNBuildList() { |
||
428 | |||
429 | /** |
||
430 | * Provides a list of the branches in this project. |
||
431 | * |
||
432 | * @return DNBranchList |
||
433 | */ |
||
434 | public function DNBranchList() { |
||
435 | if($this->CVSPath && !$this->repoExists()) { |
||
436 | $this->cloneRepo(); |
||
437 | } |
||
438 | return DNBranchList::create($this, $this->DNData()); |
||
439 | } |
||
440 | |||
441 | /** |
||
442 | * Provides a list of the tags in this project. |
||
443 | * |
||
444 | * @return DNReferenceList |
||
445 | */ |
||
446 | public function DNTagList() { |
||
447 | if($this->CVSPath && !$this->repoExists()) { |
||
448 | $this->cloneRepo(); |
||
449 | } |
||
450 | return DNReferenceList::create($this, $this->DNData(), null, null, true); |
||
451 | } |
||
452 | |||
453 | /** |
||
454 | * @return false|Gitonomy\Git\Repository |
||
455 | */ |
||
456 | public function getRepository() { |
||
457 | if(!$this->repoExists()) { |
||
458 | return false; |
||
459 | } |
||
460 | |||
461 | return new Gitonomy\Git\Repository($this->getLocalCVSPath()); |
||
462 | } |
||
463 | |||
464 | /** |
||
465 | * Provides a list of environments found in this project. |
||
466 | * CAUTION: filterByCallback will change this into an ArrayList! |
||
467 | * |
||
468 | * @return ArrayList |
||
469 | */ |
||
470 | public function DNEnvironmentList() { |
||
471 | |||
472 | if(!self::$_current_member_cache) { |
||
473 | self::$_current_member_cache = Member::currentUser(); |
||
474 | } |
||
475 | |||
476 | if(self::$_current_member_cache === false) { |
||
477 | return new ArrayList(); |
||
478 | } |
||
479 | |||
480 | $currentMember = self::$_current_member_cache; |
||
481 | return $this->Environments() |
||
482 | ->filterByCallBack(function($item) use ($currentMember) { |
||
483 | return $item->canView($currentMember); |
||
484 | }); |
||
485 | } |
||
486 | |||
487 | /** |
||
488 | * @param string $usage |
||
489 | * @return ArrayList |
||
490 | */ |
||
491 | public function EnvironmentsByUsage($usage) { |
||
494 | |||
495 | /** |
||
496 | * Returns a map of envrionment name to build name |
||
497 | * |
||
498 | * @return false|DNDeployment |
||
499 | */ |
||
500 | public function currentBuilds() { |
||
501 | if(!isset(self::$relation_cache['currentBuilds.'.$this->ID])) { |
||
502 | $currentBuilds = array(); |
||
503 | foreach($this->Environments() as $env) { |
||
504 | $currentBuilds[$env->Name] = $env->CurrentBuild(); |
||
505 | } |
||
506 | self::$relation_cache['currentBuilds.'.$this->ID] = $currentBuilds; |
||
507 | } |
||
508 | return self::$relation_cache['currentBuilds.'.$this->ID]; |
||
509 | } |
||
510 | |||
511 | /** |
||
512 | * @param string |
||
513 | * @return string |
||
514 | */ |
||
515 | public function Link($action = '') { |
||
518 | |||
519 | /** |
||
520 | * @return string|null |
||
521 | */ |
||
522 | public function CreateEnvironmentLink() { |
||
523 | if($this->canCreateEnvironments()) { |
||
524 | return $this->Link('createenv'); |
||
525 | } |
||
526 | return null; |
||
527 | } |
||
528 | |||
529 | /** |
||
530 | * @return string |
||
531 | */ |
||
532 | public function ToggleStarLink() { |
||
535 | |||
536 | /** |
||
537 | * @return bool |
||
538 | */ |
||
539 | public function IsStarred() { |
||
540 | $member = Member::currentUser(); |
||
541 | if($member === null) { |
||
542 | return false; |
||
543 | } |
||
544 | $favourited = $this->StarredBy()->filter('MemberID', $member->ID); |
||
545 | if($favourited->count() == 0) { |
||
546 | return false; |
||
547 | } |
||
548 | return true; |
||
549 | } |
||
550 | |||
551 | /** |
||
552 | * @param string $action |
||
553 | * @return string |
||
554 | */ |
||
555 | public function APILink($action) { |
||
558 | |||
559 | /** |
||
560 | * @return FieldList |
||
561 | */ |
||
562 | public function getCMSFields() { |
||
563 | $fields = parent::getCMSFields(); |
||
564 | |||
565 | /** @var GridField $environments */ |
||
566 | $environments = $fields->dataFieldByName("Environments"); |
||
567 | |||
568 | $fields->fieldByName("Root")->removeByName("Viewers"); |
||
569 | $fields->fieldByName("Root")->removeByName("Environments"); |
||
570 | $fields->fieldByName("Root")->removeByName("LocalCVSPath"); |
||
571 | |||
572 | $diskQuotaDesc = 'This is the maximum amount of disk space (in megabytes) that all environments within this ' |
||
573 | . 'project can use for stored snapshots'; |
||
574 | $fields->dataFieldByName('DiskQuotaMB')->setDescription($diskQuotaDesc); |
||
575 | |||
576 | $projectNameDesc = 'Changing the name will <strong>reset</strong> the deploy configuration and avoid using non' |
||
577 | . 'alphanumeric characters'; |
||
578 | $fields->fieldByName('Root.Main.Name') |
||
579 | ->setTitle('Project name') |
||
580 | ->setDescription($projectNameDesc); |
||
581 | |||
582 | $fields->fieldByName('Root.Main.CVSPath') |
||
583 | ->setTitle('Git repository') |
||
584 | ->setDescription('E.g. [email protected]:silverstripe/silverstripe-installer.git'); |
||
585 | |||
586 | $workspaceField = new ReadonlyField('LocalWorkspace', 'Git workspace', $this->getLocalCVSPath()); |
||
587 | $workspaceField->setDescription('This is where the GIT repository are located on this server'); |
||
588 | $fields->insertAfter($workspaceField, 'CVSPath'); |
||
589 | |||
590 | $readAccessGroups = ListboxField::create('Viewers', 'Project viewers', Group::get()->map()->toArray()) |
||
591 | ->setMultiple(true) |
||
592 | ->setDescription('These groups can view the project in the front-end.'); |
||
593 | $fields->addFieldToTab("Root.Main", $readAccessGroups); |
||
594 | |||
595 | $this->setCreateProjectFolderField($fields); |
||
596 | $this->setEnvironmentFields($fields, $environments); |
||
597 | |||
598 | $environmentTypes = ClassInfo::implementorsOf('EnvironmentCreateBackend'); |
||
599 | $types = array(); |
||
600 | foreach($environmentTypes as $type) { |
||
601 | $types[$type] = $type; |
||
602 | } |
||
603 | |||
604 | $fields->addFieldsToTab('Root.Main', array( |
||
605 | DropdownField::create( |
||
606 | 'AllowedEnvironmentType', |
||
607 | 'Allowed Environment Type', |
||
608 | $types |
||
609 | )->setDescription('This defined which form to show on the front end for ' |
||
610 | . 'environment creation. This will not affect backend functionality.') |
||
611 | ->setEmptyString(' - None - '), |
||
612 | )); |
||
613 | |||
614 | return $fields; |
||
615 | } |
||
616 | |||
617 | /** |
||
618 | * If there isn't a capistrano env project folder, show options to create one |
||
619 | * |
||
620 | * @param FieldList $fields |
||
621 | */ |
||
622 | public function setCreateProjectFolderField(&$fields) { |
||
623 | // Check if the capistrano project folder exists |
||
624 | if(!$this->Name) { |
||
625 | return; |
||
626 | } |
||
627 | |||
628 | if($this->projectFolderExists()) { |
||
629 | return; |
||
630 | } |
||
631 | |||
632 | $createFolderNotice = new LabelField('CreateEnvFolderNotice', 'Warning: No Capistrano project folder exists'); |
||
633 | $createFolderNotice->addExtraClass('message warning'); |
||
634 | $fields->insertBefore($createFolderNotice, 'Name'); |
||
635 | $createFolderField = new CheckboxField('CreateEnvFolder', 'Create folder'); |
||
636 | $createFolderField->setDescription('Would you like to create the capistrano project folder?'); |
||
637 | $fields->insertAfter($createFolderField, 'CreateEnvFolderNotice'); |
||
638 | } |
||
639 | |||
640 | /** |
||
641 | * @return boolean |
||
642 | */ |
||
643 | public function projectFolderExists() { |
||
644 | return file_exists($this->getProjectFolderPath()); |
||
645 | } |
||
646 | |||
647 | /** |
||
648 | * @return bool |
||
649 | */ |
||
650 | public function repoExists() { |
||
653 | |||
654 | /** |
||
655 | * Setup a job to clone a git repository. |
||
656 | * @return string resque token |
||
657 | */ |
||
658 | public function cloneRepo() { |
||
659 | // Avoid this being called multiple times in the same request |
||
660 | if(!isset(self::$has_cloned_cache[$this->ID])) { |
||
661 | $fetch = DNGitFetch::create(); |
||
662 | $fetch->ProjectID = $this->ID; |
||
663 | $fetch->write(); |
||
664 | |||
665 | // passing true here tells DNGitFetch to force a git clone, otherwise |
||
666 | // it will just update the repo if it already exists. We want to ensure |
||
667 | // we're always cloning a new repo in this case, as the git URL may have changed. |
||
668 | $fetch->start(true); |
||
669 | |||
670 | self::$has_cloned_cache[$this->ID] = true; |
||
671 | } |
||
672 | } |
||
673 | |||
674 | /** |
||
675 | * @return string |
||
676 | */ |
||
677 | public function getLocalCVSPath() { |
||
678 | return sprintf('%s/%s', DEPLOYNAUT_LOCAL_VCS_PATH, $this->Name); |
||
679 | } |
||
680 | |||
681 | public function onBeforeWrite() { |
||
682 | parent::onBeforeWrite(); |
||
683 | |||
684 | if($this->CreateEnvFolder && !file_exists($this->getProjectFolderPath())) { |
||
685 | mkdir($this->getProjectFolderPath()); |
||
686 | } |
||
687 | } |
||
688 | |||
689 | public function onAfterWrite() { |
||
690 | parent::onAfterWrite(); |
||
691 | |||
692 | if(!$this->CVSPath) { |
||
693 | return; |
||
694 | } |
||
695 | |||
696 | $changedFields = $this->getChangedFields(true, 2); |
||
697 | if(isset($changedFields['CVSPath']) || isset($changedFields['Name'])) { |
||
698 | $this->cloneRepo(); |
||
699 | } |
||
700 | } |
||
701 | |||
702 | /** |
||
703 | * Delete related environments and folders |
||
704 | */ |
||
705 | public function onAfterDelete() { |
||
706 | parent::onAfterDelete(); |
||
707 | |||
708 | // Delete related environments |
||
709 | foreach($this->Environments() as $env) { |
||
710 | $env->delete(); |
||
711 | } |
||
712 | |||
713 | // Delete local repository |
||
714 | if(file_exists($this->getLocalCVSPath())) { |
||
715 | Filesystem::removeFolder($this->getLocalCVSPath()); |
||
716 | } |
||
717 | |||
718 | // Delete project template |
||
719 | if(file_exists($this->getProjectFolderPath()) && Config::inst()->get('DNEnvironment', 'allow_web_editing')) { |
||
720 | Filesystem::removeFolder($this->getProjectFolderPath()); |
||
721 | } |
||
722 | |||
723 | // Delete the deploy key |
||
724 | if(file_exists($this->getKeyDir())) { |
||
725 | Filesystem::removeFolder($this->getKeyDir()); |
||
726 | } |
||
727 | } |
||
728 | |||
729 | /** |
||
730 | * Fetch the public key for this project. |
||
731 | * |
||
732 | * @return string|void |
||
733 | */ |
||
734 | public function getPublicKey() { |
||
735 | $key = $this->getPublicKeyPath(); |
||
736 | |||
737 | if(file_exists($key)) { |
||
738 | return trim(file_get_contents($key)); |
||
739 | } |
||
740 | } |
||
741 | |||
742 | /** |
||
743 | * This returns that path of the public key if a key directory is set. It doesn't check whether the file exists. |
||
744 | * |
||
745 | * @return string|null |
||
746 | */ |
||
747 | public function getPublicKeyPath() { |
||
748 | if($privateKey = $this->getPrivateKeyPath()) { |
||
749 | return $privateKey . '.pub'; |
||
750 | } |
||
751 | return null; |
||
752 | } |
||
753 | |||
754 | /** |
||
755 | * This returns that path of the private key if a key directory is set. It doesn't check whether the file exists. |
||
756 | * |
||
757 | * @return string|null |
||
758 | */ |
||
759 | public function getPrivateKeyPath() { |
||
760 | $keyDir = $this->getKeyDir(); |
||
761 | if(!empty($keyDir)) { |
||
762 | $filter = FileNameFilter::create(); |
||
763 | $name = $filter->filter($this->Name); |
||
764 | return $keyDir . '/' . $name; |
||
765 | } |
||
766 | return null; |
||
767 | } |
||
768 | |||
769 | /** |
||
770 | * Returns the location of the projects key dir if one exists. |
||
771 | * |
||
772 | * @return string|null |
||
773 | */ |
||
774 | public function getKeyDir() { |
||
775 | $keyDir = $this->DNData()->getKeyDir(); |
||
776 | if(!$keyDir) { |
||
777 | return null; |
||
778 | } |
||
779 | |||
780 | $filter = FileNameFilter::create(); |
||
781 | $name = $filter->filter($this->Name); |
||
782 | |||
783 | return $this->DNData()->getKeyDir() . '/' . $name; |
||
784 | } |
||
785 | |||
786 | /** |
||
787 | * Setup a gridfield for the environment configs |
||
788 | * |
||
789 | * @param FieldList $fields |
||
790 | * @param GridField $environments |
||
791 | */ |
||
792 | protected function setEnvironmentFields(&$fields, $environments) { |
||
793 | if(!$environments) { |
||
794 | return; |
||
795 | } |
||
796 | |||
797 | $environments->getConfig()->addComponent(new GridFieldAddNewMultiClass()); |
||
798 | $environments->getConfig()->removeComponentsByType('GridFieldAddNewButton'); |
||
799 | $environments->getConfig()->removeComponentsByType('GridFieldAddExistingAutocompleter'); |
||
800 | $environments->getConfig()->removeComponentsByType('GridFieldDeleteAction'); |
||
801 | $environments->getConfig()->removeComponentsByType('GridFieldPageCount'); |
||
802 | if(Config::inst()->get('DNEnvironment', 'allow_web_editing')) { |
||
803 | $addNewRelease = new GridFieldAddNewButton('toolbar-header-right'); |
||
804 | $addNewRelease->setButtonName('Add'); |
||
805 | $environments->getConfig()->addComponent($addNewRelease); |
||
806 | } |
||
807 | |||
808 | $fields->addFieldToTab("Root.Main", $environments); |
||
809 | } |
||
810 | |||
811 | /** |
||
812 | * Provide current repository URL to the users. |
||
813 | * |
||
814 | * @return void|string |
||
815 | */ |
||
816 | public function getRepositoryURL() { |
||
817 | $showUrl = Config::inst()->get($this->class, 'show_repository_url'); |
||
818 | if($showUrl) { |
||
819 | return $this->CVSPath; |
||
820 | } |
||
821 | } |
||
822 | |||
823 | /** |
||
824 | * Whitelist configuration that describes how to convert a repository URL into a link |
||
825 | * to a web user interface for that URL |
||
826 | * |
||
827 | * Consists of a hash of "full.lower.case.domain" => {configuration} key/value pairs |
||
828 | * |
||
829 | * {configuration} can either be boolean true to auto-detect both the host and the |
||
830 | * name of the UI provider, or a nested array that overrides either one or both |
||
831 | * of the auto-detected valyes |
||
832 | * |
||
833 | * @var array |
||
834 | */ |
||
835 | static private $repository_interfaces = array( |
||
836 | 'github.com' => array( |
||
837 | 'icon' => 'deploynaut/img/github.png', |
||
838 | 'name' => 'Github.com', |
||
839 | ), |
||
840 | 'bitbucket.org' => array( |
||
841 | 'commit' => 'commits', |
||
842 | 'name' => 'Bitbucket.org', |
||
843 | ), |
||
844 | 'repo.or.cz' => array( |
||
845 | 'scheme' => 'http', |
||
846 | 'name' => 'repo.or.cz', |
||
847 | 'regex' => array('^(.*)$' => '/w$1'), |
||
848 | ), |
||
849 | |||
850 | /* Example for adding your own gitlab repository and override all auto-detected values (with their defaults) |
||
851 | 'gitlab.mysite.com' => array( |
||
852 | 'icon' => 'deploynaut/img/git.png', |
||
853 | 'host' => 'gitlab.mysite.com', |
||
854 | 'name' => 'Gitlab', |
||
855 | 'regex' => array('.git$' => ''), |
||
856 | 'commit' => "commit" |
||
857 | ), |
||
858 | */ |
||
859 | ); |
||
860 | |||
861 | /** |
||
862 | * Get a ViewableData structure describing the UI tool that lets the user view the repository code |
||
863 | * |
||
864 | * @return ArrayData |
||
865 | */ |
||
866 | public function getRepositoryInterface() { |
||
867 | $interfaces = $this->config()->repository_interfaces; |
||
868 | |||
869 | /* Look for each whitelisted hostname */ |
||
870 | foreach($interfaces as $host => $interface) { |
||
871 | /* See if the CVS Path is for this hostname, followed by some junk (maybe a port), then the path */ |
||
872 | if(preg_match('{^[^.]*' . $host . '(.*?)([/a-zA-Z].+)}', $this->CVSPath, $match)) { |
||
873 | |||
874 | $path = $match[2]; |
||
875 | |||
876 | $scheme = isset($interface['scheme']) ? $interface['scheme'] : 'https'; |
||
877 | $host = isset($interface['host']) ? $interface['host'] : $host; |
||
878 | $regex = isset($interface['regex']) ? $interface['regex'] : array('\.git$' => ''); |
||
879 | |||
880 | $components = explode('.', $host); |
||
881 | |||
882 | foreach($regex as $pattern => $replacement) { |
||
883 | $path = preg_replace('/' . $pattern . '/', $replacement, $path); |
||
884 | } |
||
885 | |||
886 | $uxurl = Controller::join_links($scheme . '://', $host, $path); |
||
887 | |||
888 | if(array_key_exists('commit', $interface) && $interface['commit'] == false) { |
||
889 | $commiturl = false; |
||
890 | } else { |
||
891 | $commiturl = Controller::join_links( |
||
892 | $uxurl, |
||
893 | isset($interface['commit']) ? $interface['commit'] : 'commit' |
||
894 | ); |
||
895 | } |
||
896 | |||
897 | return new ArrayData(array( |
||
898 | 'Name' => isset($interface['name']) ? $interface['name'] : ucfirst($components[0]), |
||
899 | 'Icon' => isset($interface['icon']) ? $interface['icon'] : 'deploynaut/img/git.png', |
||
900 | 'URL' => $uxurl, |
||
901 | 'CommitURL' => $commiturl |
||
902 | )); |
||
903 | } |
||
904 | } |
||
905 | } |
||
906 | |||
907 | /** |
||
908 | * @return string |
||
909 | */ |
||
910 | protected function getProjectFolderPath() { |
||
913 | |||
914 | /** |
||
915 | * Convenience wrapper for a single permission code. |
||
916 | * |
||
917 | * @param string $code |
||
918 | * @return SS_List |
||
919 | */ |
||
920 | public function whoIsAllowed($code) { |
||
923 | |||
924 | /** |
||
925 | * List members who have $codes on this project. |
||
926 | * Does not support Permission::DENY_PERMISSION malarky, same as Permission::get_groups_by_permission anyway... |
||
927 | * |
||
928 | * @param array|string $codes |
||
929 | * @return SS_List |
||
930 | */ |
||
931 | public function whoIsAllowedAny($codes) { |
||
932 | if(!is_array($codes)) $codes = array($codes); |
||
933 | |||
934 | $SQLa_codes = Convert::raw2sql($codes); |
||
935 | $SQL_codes = join("','", $SQLa_codes); |
||
936 | |||
937 | return DataObject::get('Member') |
||
938 | ->where("\"PermissionRoleCode\".\"Code\" IN ('$SQL_codes') OR \"Permission\".\"Code\" IN ('$SQL_codes')") |
||
939 | ->filter("DNProject_Viewers.DNProjectID", $this->ID) |
||
940 | ->leftJoin('Group_Members', "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\"") |
||
941 | ->leftJoin('Group', "\"Group_Members\".\"GroupID\" = \"Group\".\"ID\"") |
||
942 | ->leftJoin('DNProject_Viewers', "\"DNProject_Viewers\".\"GroupID\" = \"Group\".\"ID\"") |
||
943 | ->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"") |
||
944 | ->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"") |
||
945 | ->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"") |
||
946 | ->leftJoin('PermissionRoleCode', "\"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\""); |
||
947 | } |
||
948 | |||
949 | /** |
||
950 | * Convenience wrapper for a single permission code. |
||
951 | * |
||
952 | * @param string $code |
||
953 | * @param Member|null $member |
||
954 | * |
||
955 | * @return bool |
||
956 | */ |
||
957 | public function allowed($code, $member = null) { |
||
960 | |||
961 | /** |
||
962 | * Checks if a group is allowed to the project and the permission code |
||
963 | * |
||
964 | * @param string $permissionCode |
||
965 | * @param Group $group |
||
966 | * |
||
967 | * @return bool |
||
968 | */ |
||
969 | public function groupAllowed($permissionCode, Group $group) { |
||
970 | $viewers = $this->Viewers(); |
||
971 | if(!$viewers->find('ID', $group->ID)) { |
||
972 | return false; |
||
973 | } |
||
974 | $groups = Permission::get_groups_by_permission($permissionCode); |
||
975 | if(!$groups->find('ID', $group->ID)) { |
||
976 | return false; |
||
977 | } |
||
978 | return true; |
||
979 | } |
||
980 | |||
981 | /** |
||
982 | * Check if member has a permission code in this project. |
||
983 | * |
||
984 | * @param array|string $codes |
||
985 | * @param Member|null $member |
||
986 | * |
||
987 | * @return bool |
||
988 | */ |
||
989 | public function allowedAny($codes, $member = null) { |
||
999 | |||
1000 | /** |
||
1001 | * Checks if the environment has been fully built. |
||
1002 | * |
||
1003 | * @return bool |
||
1004 | */ |
||
1005 | public function isProjectReady() { |
||
1006 | if($this->getRunningInitialEnvironmentCreations()->count() > 0) { |
||
1007 | // We're still creating the initial environments for this project so we're |
||
1008 | // not quite done |
||
1009 | return false; |
||
1010 | } |
||
1011 | |||
1012 | // Provide a hook for further checks. Logic stolen from |
||
1013 | // {@see DataObject::extendedCan()} |
||
1014 | $isDone = $this->extend('isProjectReady'); |
||
1015 | if($isDone && is_array($isDone)) { |
||
1016 | $isDone = array_filter($isDone, function($val) { |
||
1017 | return !is_null($val); |
||
1018 | }); |
||
1019 | |||
1020 | // If anything returns false then we're not ready. |
||
1021 | if($isDone) return min($isDone); |
||
1022 | } |
||
1023 | |||
1024 | return true; |
||
1025 | } |
||
1026 | |||
1027 | /** |
||
1028 | * Returns a list of environments still being created. |
||
1029 | * |
||
1030 | * @return SS_List |
||
1031 | */ |
||
1032 | public function getRunningEnvironmentCreations() { |
||
1033 | return $this->CreateEnvironments() |
||
1034 | ->filter('Status', ['Queued', 'Started']); |
||
1035 | } |
||
1036 | |||
1037 | /** |
||
1038 | * Returns a list of initial environments created for this project. |
||
1039 | * |
||
1040 | * @return DataList |
||
1041 | */ |
||
1042 | public function getInitialEnvironmentCreations() { |
||
1045 | |||
1046 | /** |
||
1047 | * Only returns initial environments that are being created. |
||
1048 | * |
||
1049 | * @return DataList |
||
1050 | */ |
||
1051 | public function getRunningInitialEnvironmentCreations() { |
||
1055 | |||
1056 | /** |
||
1057 | * Returns a list of completed initial environment creations. This includes failed tasks. |
||
1058 | * |
||
1059 | * @return DataList |
||
1060 | */ |
||
1061 | public function getCompleteInitialEnvironmentCreations() { |
||
1065 | |||
1066 | /** |
||
1067 | * @return ValidationResult |
||
1068 | */ |
||
1069 | protected function validate() { |
||
1095 | |||
1096 | /** |
||
1097 | * @param Member $member |
||
1098 | * |
||
1099 | * @return bool |
||
1100 | */ |
||
1101 | public function canCreate($member = null) { |
||
1112 | |||
1113 | } |
||
1114 | |||
1115 |
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.