44
55namespace Phauthentic \CognitiveCodeAnalysis \Command ;
66
7+ use Exception ;
78use Phauthentic \CognitiveCodeAnalysis \Business \CodeCoverage \CloverReader ;
89use Phauthentic \CognitiveCodeAnalysis \Business \CodeCoverage \CoberturaReader ;
910use Phauthentic \CognitiveCodeAnalysis \Business \CodeCoverage \CoverageReportReaderInterface ;
1011use Phauthentic \CognitiveCodeAnalysis \Business \MetricsFacade ;
1112use Phauthentic \CognitiveCodeAnalysis \CognitiveAnalysisException ;
1213use Phauthentic \CognitiveCodeAnalysis \Command \Handler \ChurnReportHandler ;
1314use Phauthentic \CognitiveCodeAnalysis \Command \Presentation \ChurnTextRenderer ;
15+ use Phauthentic \CognitiveCodeAnalysis \Command \ChurnSpecifications \ChurnCommandContext ;
16+ use Phauthentic \CognitiveCodeAnalysis \Command \ChurnSpecifications \CompositeChurnValidationSpecification ;
17+ use Phauthentic \CognitiveCodeAnalysis \Command \ChurnSpecifications \CoverageFileExistsSpecification ;
18+ use Phauthentic \CognitiveCodeAnalysis \Command \ChurnSpecifications \CoverageFormatExclusivitySpecification ;
19+ use Phauthentic \CognitiveCodeAnalysis \Command \ChurnSpecifications \CoverageFormatSupportedSpecification ;
20+ use Phauthentic \CognitiveCodeAnalysis \Command \ChurnSpecifications \ReportOptionsCompleteSpecification ;
1421use Symfony \Component \Console \Attribute \AsCommand ;
1522use Symfony \Component \Console \Command \Command ;
1623use Symfony \Component \Console \Input \InputArgument ;
@@ -36,6 +43,8 @@ class ChurnCommand extends Command
3643 public const OPTION_COVERAGE_COBERTURA = 'coverage-cobertura ' ;
3744 public const OPTION_COVERAGE_CLOVER = 'coverage-clover ' ;
3845
46+ private CompositeChurnValidationSpecification $ validationSpecification ;
47+
3948 /**
4049 * Constructor to initialize dependencies.
4150 */
@@ -45,6 +54,17 @@ public function __construct(
4554 readonly private ChurnReportHandler $ report
4655 ) {
4756 parent ::__construct ();
57+ $ this ->initializeValidationSpecification ();
58+ }
59+
60+ private function initializeValidationSpecification (): void
61+ {
62+ $ this ->validationSpecification = new CompositeChurnValidationSpecification ([
63+ new CoverageFormatExclusivitySpecification (),
64+ new CoverageFileExistsSpecification (),
65+ new CoverageFormatSupportedSpecification (),
66+ new ReportOptionsCompleteSpecification (),
67+ ]);
4868 }
4969
5070 /**
@@ -118,61 +138,64 @@ protected function configure(): void
118138 */
119139 protected function execute (InputInterface $ input , OutputInterface $ output ): int
120140 {
121- $ coberturaFile = $ input ->getOption (self ::OPTION_COVERAGE_COBERTURA );
122- $ cloverFile = $ input ->getOption (self ::OPTION_COVERAGE_CLOVER );
141+ $ context = new ChurnCommandContext ($ input );
123142
124- // Validate that only one coverage option is specified
125- if ($ coberturaFile !== null && $ cloverFile !== null ) {
126- $ output ->writeln ('<error>Only one coverage format can be specified at a time.</error> ' );
143+ // Validate all specifications
144+ if (!$ this ->validationSpecification ->isSatisfiedBy ($ context )) {
145+ $ errorMessage = $ this ->validationSpecification ->getDetailedErrorMessage ($ context );
146+ $ output ->writeln ('<error> ' . $ errorMessage . '</error> ' );
127147 return self ::FAILURE ;
128148 }
129149
130- $ coverageFile = $ coberturaFile ?? $ cloverFile ;
131- $ coverageFormat = $ coberturaFile !== null ? 'cobertura ' : ($ cloverFile !== null ? 'clover ' : null );
132-
133- if (!$ this ->coverageFileExists ($ coverageFile , $ output )) {
134- return self ::FAILURE ;
150+ // Load configuration if provided
151+ if ($ context ->hasConfigFile ()) {
152+ $ configFile = $ context ->getConfigFile ();
153+ if ($ configFile !== null && !$ this ->loadConfiguration ($ configFile , $ output )) {
154+ return self ::FAILURE ;
155+ }
135156 }
136157
137- $ coverageReader = $ this ->loadCoverageReader ($ coverageFile , $ coverageFormat , $ output );
158+ // Load coverage reader
159+ $ coverageReader = $ this ->loadCoverageReader ($ context , $ output );
138160 if ($ coverageReader === false ) {
139161 return self ::FAILURE ;
140162 }
141163
164+ // Calculate churn metrics
142165 $ classes = $ this ->metricsFacade ->calculateChurn (
143- path: $ input -> getArgument ( self :: ARGUMENT_PATH ),
144- vcsType: $ input -> getOption ( self :: OPTION_VCS ),
145- since: $ input -> getOption ( self :: OPTION_SINCE ),
166+ path: $ context -> getPath ( ),
167+ vcsType: $ context -> getVcsType ( ),
168+ since: $ context -> getSince ( ),
146169 coverageReader: $ coverageReader
147170 );
148171
149- $ reportType = $ input ->getOption (self ::OPTION_REPORT_TYPE );
150- $ reportFile = $ input ->getOption (self ::OPTION_REPORT_FILE );
151-
152- if ($ reportType !== null || $ reportFile !== null ) {
153- return $ this ->report ->exportToFile ($ classes , $ reportType , $ reportFile );
172+ // Handle report generation or display
173+ if ($ context ->hasReportOptions ()) {
174+ return $ this ->report ->exportToFile (
175+ $ classes ,
176+ $ context ->getReportType (),
177+ $ context ->getReportFile ()
178+ );
154179 }
155180
156- $ this ->renderer ->renderChurnTable (
157- classes: $ classes
158- );
159-
181+ $ this ->renderer ->renderChurnTable (classes: $ classes );
160182 return self ::SUCCESS ;
161183 }
162184
163185 /**
164186 * Load coverage reader from file
165187 *
166- * @param string|null $coverageFile Path to coverage file or null
167- * @param string|null $format Coverage format ('cobertura', 'clover') or null for auto-detect
188+ * @param ChurnCommandContext $context Command context containing coverage file information
168189 * @param OutputInterface $output Output interface for error messages
169190 * @return CoverageReportReaderInterface|null|false Returns reader instance, null if no file provided, or false on error
170191 */
171192 private function loadCoverageReader (
172- ?string $ coverageFile ,
173- ?string $ format ,
193+ ChurnCommandContext $ context ,
174194 OutputInterface $ output
175195 ): CoverageReportReaderInterface |null |false {
196+ $ coverageFile = $ context ->getCoverageFile ();
197+ $ format = $ context ->getCoverageFormat ();
198+
176199 if ($ coverageFile === null ) {
177200 return null ;
178201 }
@@ -224,24 +247,22 @@ private function detectCoverageFormat(string $coverageFile): ?string
224247 return null ;
225248 }
226249
227- private function coverageFileExists (?string $ coverageFile , OutputInterface $ output ): bool
228- {
229- // If no coverage file is provided, validation passes (backward compatibility)
230- if ($ coverageFile === null ) {
231- return true ;
232- }
233250
234- // If coverage file is provided, check if it exists
235- if (file_exists ($ coverageFile )) {
251+ /**
252+ * Loads configuration and handles errors.
253+ *
254+ * @param string $configFile
255+ * @param OutputInterface $output
256+ * @return bool Success or failure.
257+ */
258+ private function loadConfiguration (string $ configFile , OutputInterface $ output ): bool
259+ {
260+ try {
261+ $ this ->metricsFacade ->loadConfig ($ configFile );
236262 return true ;
263+ } catch (Exception $ e ) {
264+ $ output ->writeln ('<error>Failed to load configuration: ' . $ e ->getMessage () . '</error> ' );
265+ return false ;
237266 }
238-
239- // Coverage file was provided but doesn't exist - show error
240- $ output ->writeln (sprintf (
241- '<error>Coverage file not found: %s</error> ' ,
242- $ coverageFile
243- ));
244-
245- return false ;
246267 }
247268}
0 commit comments