Complex classes like ReleaseController 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 ReleaseController, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
43 | class ReleaseController extends Controller |
||
44 | { |
||
45 | public $defaultAction = 'release'; |
||
46 | |||
47 | /** |
||
48 | * @var string base path to use for releases. |
||
49 | */ |
||
50 | public $basePath; |
||
51 | /** |
||
52 | * @var bool whether to make actual changes. If true, it will run without changing or pushing anything. |
||
53 | */ |
||
54 | public $dryRun = false; |
||
55 | /** |
||
56 | * @var bool whether to fetch latest tags. |
||
57 | */ |
||
58 | public $update = false; |
||
59 | |||
60 | |||
61 | public function options($actionID) |
||
71 | |||
72 | |||
73 | public function beforeAction($action) |
||
84 | |||
85 | /** |
||
86 | * Shows information about current framework and extension versions. |
||
87 | */ |
||
88 | public function actionInfo() |
||
130 | |||
131 | private function minWidth($a) |
||
132 | { |
||
133 | $w = 1; |
||
134 | foreach($a as $s) { |
||
135 | if (($l = mb_strlen($s)) > $w) { |
||
136 | $w = $l; |
||
137 | } |
||
138 | } |
||
139 | return $w; |
||
140 | } |
||
141 | |||
142 | /** |
||
143 | * Automation tool for making Yii framework and official extension releases. |
||
144 | * |
||
145 | * Usage: |
||
146 | * |
||
147 | * To make a release, make sure your git is clean (no uncommitted changes) and run the following command in |
||
148 | * the yii dev repo root: |
||
149 | * |
||
150 | * ``` |
||
151 | * ./build/build release framework |
||
152 | * ``` |
||
153 | * |
||
154 | * or |
||
155 | * |
||
156 | * ``` |
||
157 | * ./build/build release redis,bootstrap,apidoc |
||
158 | * ``` |
||
159 | * |
||
160 | * You may use the `--dryRun` switch to test the command without changing or pushing anything: |
||
161 | * |
||
162 | * ``` |
||
163 | * ./build/build release redis --dryRun |
||
164 | * ``` |
||
165 | * |
||
166 | * The command will guide you through the complete release process including changing of files, |
||
167 | * committing and pushing them. Each git command must be confirmed and can be skipped individually. |
||
168 | * You may adjust changes in a separate shell or your IDE while the command is waiting for confirmation. |
||
169 | * |
||
170 | * @param array $what what do you want to release? this can either be: |
||
171 | * |
||
172 | * - an extension name such as `redis` or `bootstrap`, |
||
173 | * - an application indicated by prefix `app-`, e.g. `app-basic`, |
||
174 | * - or `framework` if you want to release a new version of the framework itself. |
||
175 | * |
||
176 | * @return int |
||
177 | */ |
||
178 | public function actionRelease(array $what) |
||
228 | |||
229 | /** |
||
230 | * This will generate application packages for download page. |
||
231 | * |
||
232 | * Usage: |
||
233 | * |
||
234 | * ``` |
||
235 | * ./build/build release/package app-basic |
||
236 | * ``` |
||
237 | * |
||
238 | * @param array $what what do you want to package? this can either be: |
||
239 | * |
||
240 | * - an application indicated by prefix `app-`, e.g. `app-basic`, |
||
241 | * |
||
242 | * @return int |
||
243 | */ |
||
244 | public function actionPackage(array $what) |
||
289 | |||
290 | protected function printWhat(array $what, $newVersions, $versions) |
||
308 | |||
309 | protected function printWhatUrls(array $what, $oldVersions) |
||
322 | |||
323 | /** |
||
324 | * @param array $what list of items |
||
325 | * @param array $limit list of things to allow, or empty to allow any, can be `app`, `framework`, `extension` |
||
326 | * @throws \yii\base\Exception |
||
327 | */ |
||
328 | protected function validateWhat(array $what, $limit = []) |
||
358 | |||
359 | |||
360 | protected function releaseFramework($frameworkPath, $version) |
||
488 | |||
489 | protected function releaseApplication($name, $path, $version) |
||
565 | |||
566 | private $_oldAlias; |
||
567 | |||
568 | protected function setAppAliases($app, $path) |
||
581 | |||
582 | protected function resetAppAliases() |
||
586 | |||
587 | protected function packageApplication($name, $version, $packagePath) |
||
602 | |||
603 | protected function releaseExtension($name, $path, $version) |
||
604 | { |
||
605 | $this->stdout("\n"); |
||
606 | $this->stdout($h = "Preparing release for extension $name version $version", Console::BOLD); |
||
607 | $this->stdout("\n" . str_repeat('-', strlen($h)) . "\n\n", Console::BOLD); |
||
608 | |||
609 | $this->runGit('git checkout master', $path); // TODO add compatibility for other release branches |
||
610 | $this->runGit('git pull', $path); // TODO add compatibility for other release branches |
||
611 | |||
612 | // adjustments |
||
613 | |||
614 | $this->stdout("fixing various PHPdoc style issues...\n", Console::BOLD); |
||
615 | $this->dryRun || Yii::$app->runAction('php-doc/fix', [$path]); |
||
616 | $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); |
||
617 | |||
618 | $this->stdout("updating PHPdoc @property annotations...\n", Console::BOLD); |
||
619 | $this->dryRun || Yii::$app->runAction('php-doc/property', [$path]); |
||
620 | $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); |
||
621 | |||
622 | $this->stdout('sorting changelogs...', Console::BOLD); |
||
623 | $this->dryRun || $this->resortChangelogs([$name], $version); |
||
624 | $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); |
||
625 | |||
626 | $this->stdout('closing changelogs...', Console::BOLD); |
||
627 | $this->dryRun || $this->closeChangelogs([$name], $version); |
||
628 | $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); |
||
629 | |||
630 | $this->stdout("\nIn the following you can check the above changes using git diff.\n\n"); |
||
631 | do { |
||
632 | $this->runGit("git diff --color", $path); |
||
633 | $this->stdout("\n\n\nCheck whether the above diff is okay, if not you may change things as needed before continuing.\n"); |
||
634 | $this->stdout("You may abort the program with Ctrl + C and reset the changes by running `git checkout -- .` in the repo.\n\n"); |
||
635 | } while(!$this->confirm("Type `yes` to continue, `no` to view git diff again. Continue?")); |
||
636 | |||
637 | $this->stdout("\n\n"); |
||
638 | $this->stdout(" **** RELEASE TIME! ****\n", Console::FG_YELLOW, Console::BOLD); |
||
639 | $this->stdout(" **** Commit, Tag and Push it! ****\n", Console::FG_YELLOW, Console::BOLD); |
||
640 | $this->stdout("\n\nHint: if you decide 'no' for any of the following, the command will not be executed. You may manually run them later if needed. E.g. try the release locally without pushing it.\n\n"); |
||
641 | |||
642 | $this->runGit("git commit -a -m \"release version $version\"", $path); |
||
643 | $this->runGit("git tag -a $version -m\"version $version\"", $path); |
||
644 | $this->runGit("git push origin master", $path); |
||
645 | $this->runGit("git push --tags", $path); |
||
646 | |||
647 | $this->stdout("\n\n"); |
||
648 | $this->stdout("CONGRATULATIONS! You have just released extension ", Console::FG_YELLOW, Console::BOLD); |
||
649 | $this->stdout($name, Console::FG_RED, Console::BOLD); |
||
650 | $this->stdout(" version ", Console::FG_YELLOW, Console::BOLD); |
||
651 | $this->stdout($version, Console::BOLD); |
||
652 | $this->stdout("!\n\n", Console::FG_YELLOW, Console::BOLD); |
||
653 | |||
654 | // prepare next release |
||
655 | |||
656 | $this->stdout("Time to prepare the next release...\n\n", Console::FG_YELLOW, Console::BOLD); |
||
657 | |||
658 | $this->stdout('opening changelogs...', Console::BOLD); |
||
659 | $nextVersion = $this->getNextVersions([$name => $version], self::PATCH); // TODO support other versions |
||
660 | $this->dryRun || $this->openChangelogs([$name], $nextVersion[$name]); |
||
661 | $this->stdout("done.\n", Console::FG_GREEN, Console::BOLD); |
||
662 | |||
663 | $this->stdout("\n"); |
||
664 | $this->runGit("git diff --color", $path); |
||
665 | $this->stdout("\n\n"); |
||
666 | $this->runGit("git commit -a -m \"prepare for next release\"", $path); |
||
667 | $this->runGit("git push origin master", $path); |
||
668 | |||
669 | $this->stdout("\n\nDONE!", Console::FG_YELLOW, Console::BOLD); |
||
670 | |||
671 | $this->stdout("\n\nThe following steps are left for you to do manually:\n\n"); |
||
672 | $nextVersion2 = $this->getNextVersions($nextVersion, self::PATCH); // TODO support other versions |
||
673 | $this->stdout("- close the $version milestone on github and open new ones for {$nextVersion[$name]} and {$nextVersion2[$name]}: https://github.com/yiisoft/yii2-$name/milestones\n"); |
||
674 | $this->stdout("- release news and announcement.\n"); |
||
675 | $this->stdout("- update the website (will be automated soon and is only relevant for the new website).\n"); |
||
676 | |||
677 | $this->stdout("\n"); |
||
678 | } |
||
679 | |||
680 | |||
681 | protected function runCommand($cmd, $path) |
||
696 | |||
697 | protected function runGit($cmd, $path) |
||
713 | |||
714 | protected function ensureGitClean($path) |
||
715 | { |
||
716 | chdir($path); |
||
717 | exec('git status --porcelain -uno', $changes, $ret); |
||
718 | if ($ret != 0) { |
||
719 | throw new Exception('Command "git status --porcelain -uno" failed with code ' . $ret); |
||
720 | } |
||
721 | if (!empty($changes)) { |
||
722 | throw new Exception("You have uncommitted changes in $path: " . print_r($changes, true)); |
||
723 | } |
||
724 | } |
||
725 | |||
726 | protected function gitFetchTags($path) |
||
727 | { |
||
728 | chdir($path); |
||
729 | exec('git fetch --tags', $output, $ret); |
||
730 | if ($ret != 0) { |
||
731 | throw new Exception('Command "git fetch --tags" failed with code ' . $ret); |
||
732 | } |
||
733 | } |
||
734 | |||
735 | |||
736 | protected function checkComposer($fwPath) |
||
742 | |||
743 | |||
744 | protected function closeChangelogs($what, $version) |
||
745 | { |
||
746 | $v = str_replace('\\-', '[\\- ]', preg_quote($version, '/')); |
||
747 | $headline = $version . ' ' . date('F d, Y'); |
||
748 | $this->sed( |
||
749 | '/'.$v.' under development\n(-+?)\n/', |
||
750 | $headline . "\n" . str_repeat('-', strlen($headline)) . "\n", |
||
751 | $this->getChangelogs($what) |
||
752 | ); |
||
753 | } |
||
754 | |||
755 | protected function openChangelogs($what, $version) |
||
756 | { |
||
757 | $headline = "\n$version under development\n"; |
||
758 | $headline .= str_repeat('-', strlen($headline) - 2) . "\n\n- no changes in this release.\n"; |
||
759 | foreach($this->getChangelogs($what) as $file) { |
||
760 | $lines = explode("\n", file_get_contents($file)); |
||
761 | $hl = [ |
||
762 | array_shift($lines), |
||
763 | array_shift($lines), |
||
764 | ]; |
||
765 | array_unshift($lines, $headline); |
||
766 | |||
767 | file_put_contents($file, implode("\n", array_merge($hl, $lines))); |
||
768 | } |
||
769 | } |
||
770 | |||
771 | protected function resortChangelogs($what, $version) |
||
772 | { |
||
773 | foreach($this->getChangelogs($what) as $file) { |
||
774 | // split the file into relevant parts |
||
775 | list($start, $changelog, $end) = $this->splitChangelog($file, $version); |
||
776 | $changelog = $this->resortChangelog($changelog); |
||
777 | file_put_contents($file, implode("\n", array_merge($start, $changelog, $end))); |
||
778 | } |
||
779 | } |
||
780 | |||
781 | /** |
||
782 | * Extract changelog content for a specific version |
||
783 | */ |
||
784 | protected function splitChangelog($file, $version) |
||
785 | { |
||
786 | $lines = explode("\n", file_get_contents($file)); |
||
787 | |||
788 | // split the file into relevant parts |
||
789 | $start = []; |
||
790 | $changelog = []; |
||
791 | $end = []; |
||
792 | |||
793 | $state = 'start'; |
||
794 | foreach($lines as $l => $line) { |
||
795 | // starting from the changelogs headline |
||
796 | if (isset($lines[$l-2]) && strpos($lines[$l-2], $version) !== false && |
||
797 | isset($lines[$l-1]) && strncmp($lines[$l-1], '---', 3) === 0) { |
||
798 | $state = 'changelog'; |
||
799 | } |
||
800 | if ($state === 'changelog' && isset($lines[$l+1]) && strncmp($lines[$l+1], '---', 3) === 0) { |
||
801 | $state = 'end'; |
||
802 | } |
||
803 | ${$state}[] = $line; |
||
804 | } |
||
805 | return [$start, $changelog, $end]; |
||
806 | } |
||
807 | |||
808 | /** |
||
809 | * Ensure sorting of the changelog lines |
||
810 | */ |
||
811 | protected function resortChangelog($changelog) |
||
812 | { |
||
813 | // cleanup whitespace |
||
814 | foreach($changelog as $i => $line) { |
||
815 | $changelog[$i] = rtrim($line); |
||
816 | } |
||
817 | $changelog = array_filter($changelog); |
||
818 | |||
819 | $i = 0; |
||
820 | ArrayHelper::multisort($changelog, function($line) use (&$i) { |
||
821 | if (preg_match('/^- (Chg|Enh|Bug|New)( #\d+(, #\d+)*)?: .+$/', $line, $m)) { |
||
822 | $o = ['Bug' => 'C', 'Enh' => 'D', 'Chg' => 'E', 'New' => 'F']; |
||
823 | return $o[$m[1]] . ' ' . (!empty($m[2]) ? $m[2] : 'AAAA' . $i++); |
||
824 | } |
||
825 | return 'B' . $i++; |
||
826 | }, SORT_ASC, SORT_NATURAL); |
||
827 | |||
828 | // re-add leading and trailing lines |
||
829 | array_unshift($changelog, ''); |
||
830 | $changelog[] = ''; |
||
831 | $changelog[] = ''; |
||
832 | |||
833 | return $changelog; |
||
834 | } |
||
835 | |||
836 | protected function getChangelogs($what) |
||
837 | { |
||
838 | $changelogs = []; |
||
839 | if (in_array('framework', $what)) { |
||
840 | $changelogs[] = $this->getFrameworkChangelog(); |
||
841 | } |
||
842 | |||
843 | return array_merge($changelogs, $this->getExtensionChangelogs($what)); |
||
844 | } |
||
845 | |||
846 | protected function getFrameworkChangelog() |
||
847 | { |
||
848 | return $this->basePath . '/framework/CHANGELOG.md'; |
||
849 | } |
||
850 | |||
851 | protected function getExtensionChangelogs($what) |
||
852 | { |
||
853 | return array_filter(glob($this->basePath . '/extensions/*/CHANGELOG.md'), function($elem) use ($what) { |
||
854 | foreach($what as $ext) { |
||
855 | if (strpos($elem, "extensions/$ext/CHANGELOG.md") !== false) { |
||
856 | return true; |
||
857 | } |
||
858 | } |
||
859 | return false; |
||
860 | }); |
||
861 | } |
||
862 | |||
863 | protected function composerSetStability($what, $version) |
||
864 | { |
||
865 | $apps = []; |
||
866 | if (in_array('app-advanced', $what)) { |
||
867 | $apps[] = $this->basePath . '/apps/advanced/composer.json'; |
||
868 | } |
||
869 | if (in_array('app-basic', $what)) { |
||
870 | $apps[] = $this->basePath . '/apps/basic/composer.json'; |
||
871 | } |
||
872 | if (in_array('app-benchmark', $what)) { |
||
873 | $apps[] = $this->basePath . '/apps/benchmark/composer.json'; |
||
874 | } |
||
875 | if (empty($apps)) { |
||
876 | return; |
||
877 | } |
||
878 | |||
879 | $stability = 'stable'; |
||
880 | if (strpos($version, 'alpha') !== false) { |
||
881 | $stability = 'alpha'; |
||
882 | } elseif (strpos($version, 'beta') !== false) { |
||
883 | $stability = 'beta'; |
||
884 | } elseif (strpos($version, 'rc') !== false) { |
||
885 | $stability = 'RC'; |
||
886 | } elseif (strpos($version, 'dev') !== false) { |
||
887 | $stability = 'dev'; |
||
888 | } |
||
889 | |||
890 | $this->sed( |
||
891 | '/"minimum-stability": "(.+?)",/', |
||
892 | '"minimum-stability": "' . $stability . '",', |
||
893 | $apps |
||
894 | ); |
||
895 | } |
||
896 | |||
897 | protected function updateYiiVersion($frameworkPath, $version) |
||
898 | { |
||
899 | $this->sed( |
||
900 | '/function getVersion\(\)\n \{\n return \'(.+?)\';/', |
||
901 | "function getVersion()\n {\n return '$version';", |
||
902 | $frameworkPath . '/BaseYii.php'); |
||
903 | } |
||
904 | |||
905 | protected function sed($pattern, $replace, $files) |
||
906 | { |
||
907 | foreach((array) $files as $file) { |
||
908 | file_put_contents($file, preg_replace($pattern, $replace, file_get_contents($file))); |
||
909 | } |
||
910 | } |
||
911 | |||
912 | protected function getCurrentVersions(array $what) |
||
933 | |||
934 | const MINOR = 'minor'; |
||
935 | const PATCH = 'patch'; |
||
936 | |||
937 | protected function getNextVersions(array $versions, $type) |
||
938 | { |
||
939 | foreach($versions as $k => $v) { |
||
940 | if (empty($v)) { |
||
941 | $versions[$k] = '2.0.0'; |
||
942 | continue; |
||
943 | } |
||
944 | $parts = explode('.', $v); |
||
945 | switch($type) { |
||
946 | case self::MINOR: |
||
947 | $parts[1]++; |
||
948 | break; |
||
959 | } |
||
960 |
If an expression can have both
false
, andnull
as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.