Skip to content

Commit 79e5517

Browse files
Refactoring for maintainability (#53)
* Refactor exporters to extend AbstractExporter and improve file handling * Refactor Markdown export functionality to use dedicated factories and traits for improved maintainability
1 parent 9a4afdb commit 79e5517

19 files changed

Lines changed: 436 additions & 135 deletions

src/Business/Churn/ChangeCounter/ChangeCounterInterface.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
namespace Phauthentic\CognitiveCodeAnalysis\Business\Churn\ChangeCounter;
66

7-
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetrics;
8-
97
interface ChangeCounterInterface
108
{
119
public function getNumberOfChangesForFile(string $filename, string $since): int;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\CognitiveCodeAnalysis\Business\Churn\Exporter;
6+
7+
use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException;
8+
9+
abstract class AbstractExporter implements DataExporterInterface
10+
{
11+
/**
12+
* @throws CognitiveAnalysisException
13+
*/
14+
protected function writeFile(string $filename, string $content): void
15+
{
16+
if (file_put_contents($filename, $content) === false) {
17+
throw new CognitiveAnalysisException("Unable to write to file: $filename");
18+
}
19+
}
20+
21+
/**
22+
* @throws CognitiveAnalysisException
23+
*/
24+
protected function assertFileIsWritable(string $filename): void
25+
{
26+
if (file_exists($filename) && !is_writable($filename)) {
27+
throw new CognitiveAnalysisException(sprintf('File %s is not writable', $filename));
28+
}
29+
30+
$dir = dirname($filename);
31+
if (!is_dir($dir) || !is_writable($dir)) {
32+
throw new CognitiveAnalysisException(sprintf('Directory %s does not exist for file %s', $dir, $filename));
33+
}
34+
}
35+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\CognitiveCodeAnalysis\Business\Churn\Exporter;
6+
7+
use InvalidArgumentException;
8+
9+
/**
10+
* Factory for creating churn data exporters.
11+
*/
12+
class ChurnExporterFactory
13+
{
14+
/**
15+
* Create an exporter instance based on the report type.
16+
*
17+
* @param string $type The type of exporter to create (json, csv, html, markdown, svg-treemap)
18+
* @return DataExporterInterface
19+
* @throws InvalidArgumentException If the type is not supported
20+
*/
21+
public function create(string $type): DataExporterInterface
22+
{
23+
return match ($type) {
24+
'json' => new JsonExporter(),
25+
'csv' => new CsvExporter(),
26+
'html' => new HtmlExporter(),
27+
'markdown' => new MarkdownExporter(),
28+
'svg-treemap', 'svg' => new SvgTreemapExporter(),
29+
default => throw new InvalidArgumentException("Unsupported exporter type: {$type}"),
30+
};
31+
}
32+
33+
/**
34+
* Get list of supported exporter types.
35+
*
36+
* @return array<string>
37+
*/
38+
public function getSupportedTypes(): array
39+
{
40+
return ['json', 'csv', 'html', 'markdown', 'svg-treemap', 'svg'];
41+
}
42+
43+
/**
44+
* Check if a type is supported.
45+
*
46+
* @param string $type
47+
* @return bool
48+
*/
49+
public function isSupported(string $type): bool
50+
{
51+
return in_array($type, $this->getSupportedTypes(), true);
52+
}
53+
}

src/Business/Churn/Exporter/CsvExporter.php

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
/**
1010
* CsvExporter for Churn metrics.
1111
*/
12-
class CsvExporter implements DataExporterInterface
12+
class CsvExporter extends AbstractExporter
1313
{
1414
/**
1515
* @var array<string>
@@ -22,21 +22,6 @@ class CsvExporter implements DataExporterInterface
2222
'Times Changed',
2323
];
2424

25-
/**
26-
* @throws CognitiveAnalysisException
27-
*/
28-
private function assertFileIsWritable(string $filename): void
29-
{
30-
if (file_exists($filename) && !is_writable($filename)) {
31-
throw new CognitiveAnalysisException(sprintf('File %s is not writable', $filename));
32-
}
33-
34-
$dir = dirname($filename);
35-
if (!is_dir($dir) || !is_writable($dir)) {
36-
throw new CognitiveAnalysisException(sprintf('Directory %s does not exist for file %s', $dir, $filename));
37-
}
38-
}
39-
4025
/**
4126
* @param array<string, array<string, mixed>> $classes
4227
* @param string $filename

src/Business/Churn/Exporter/HtmlExporter.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
/**
1010
* HtmlExporter for Churn metrics.
1111
*/
12-
class HtmlExporter implements DataExporterInterface
12+
class HtmlExporter extends AbstractExporter
1313
{
1414
/**
1515
* @var array<string>
@@ -27,11 +27,11 @@ class HtmlExporter implements DataExporterInterface
2727
*/
2828
public function export(array $classes, string $filename): void
2929
{
30+
$this->assertFileIsWritable($filename);
31+
3032
$html = $this->generateHtml($classes);
3133

32-
if (file_put_contents($filename, $html) === false) {
33-
throw new CognitiveAnalysisException('Could not write to file');
34-
}
34+
$this->writeFile($filename, $html);
3535
}
3636

3737
private function escape(string $string): string

src/Business/Churn/Exporter/JsonExporter.php

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,23 @@
88
use Phauthentic\CognitiveCodeAnalysis\Business\Utility\Datetime;
99
use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException;
1010

11-
/**
12-
*
13-
*/
14-
class JsonExporter implements DataExporterInterface
11+
class JsonExporter extends AbstractExporter
1512
{
1613
/**
1714
* @param array<string, array<string, mixed>> $classes
1815
* @throws JsonException|CognitiveAnalysisException
1916
*/
2017
public function export(array $classes, string $filename): void
2118
{
19+
$this->assertFileIsWritable($filename);
20+
2221
$data = [
2322
'createdAt' => (new DateTime())->format('Y-m-d H:i:s'),
2423
'classes' => $classes,
2524
];
2625

2726
$jsonData = json_encode($data, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
2827

29-
if (file_put_contents($filename, $jsonData) === false) {
30-
throw new CognitiveAnalysisException("Unable to write to file: $filename");
31-
}
28+
$this->writeFile($filename, $jsonData);
3229
}
3330
}

src/Business/Churn/Exporter/MarkdownExporter.php

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@
44

55
namespace Phauthentic\CognitiveCodeAnalysis\Business\Churn\Exporter;
66

7+
use Phauthentic\CognitiveCodeAnalysis\Business\Exporter\MarkdownFormatterTrait;
8+
use Phauthentic\CognitiveCodeAnalysis\Business\Traits\CoverageDataDetector;
79
use Phauthentic\CognitiveCodeAnalysis\Business\Utility\Datetime;
810
use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException;
911

1012
/**
1113
* MarkdownExporter for Churn metrics.
1214
*/
13-
class MarkdownExporter implements DataExporterInterface
15+
class MarkdownExporter extends AbstractExporter
1416
{
17+
use MarkdownFormatterTrait;
18+
use CoverageDataDetector;
19+
1520
/**
1621
* @var array<string>
1722
*/
@@ -42,11 +47,11 @@ class MarkdownExporter implements DataExporterInterface
4247
*/
4348
public function export(array $classes, string $filename): void
4449
{
50+
$this->assertFileIsWritable($filename);
51+
4552
$markdown = $this->generateMarkdown($classes);
4653

47-
if (file_put_contents($filename, $markdown) === false) {
48-
throw new CognitiveAnalysisException("Unable to write to file: $filename");
49-
}
54+
$this->writeFile($filename, $markdown);
5055
}
5156

5257
/**
@@ -63,64 +68,48 @@ private function generateMarkdown(array $classes): string
6368
$markdown .= "Total Classes: " . count($classes) . "\n\n";
6469

6570
// Create table header
66-
$markdown .= "| " . implode(" | ", $header) . " |\n";
67-
$markdown .= "|" . str_repeat(" --- |", count($header)) . "\n";
71+
$markdown .= $this->buildMarkdownTableHeader($header) . "\n";
72+
$markdown .= $this->buildMarkdownTableSeparator(count($header)) . "\n";
6873

6974
// Add rows
7075
foreach ($classes as $className => $data) {
7176
if ($data['score'] == 0 || $data['churn'] == 0) {
7277
continue;
7378
}
7479

75-
$row = [
76-
$this->escapeMarkdown($className),
77-
(string)$data['score'],
78-
(string)round($data['churn'], 3),
79-
];
80-
81-
if ($hasCoverageData) {
82-
$row[] = $data['riskChurn'] !== null ? (string)round($data['riskChurn'], 3) : 'N/A';
83-
}
84-
85-
$row[] = (string)$data['timesChanged'];
86-
87-
if ($hasCoverageData) {
88-
$row[] = $data['coverage'] !== null ? sprintf('%.2f%%', $data['coverage'] * 100) : 'N/A';
89-
$row[] = $data['riskLevel'] ?? 'N/A';
90-
}
91-
92-
$markdown .= "| " . implode(" | ", $row) . " |\n";
80+
$markdown .= $this->addRow($className, $data, $hasCoverageData);
9381
}
9482

9583
return $markdown;
9684
}
9785

9886
/**
99-
* Check if any class has coverage data
87+
* Add a single row to the markdown table
10088
*
101-
* @param array<string, mixed> $classes
102-
* @return bool
89+
* @param string $className
90+
* @param array<string, mixed> $data
91+
* @param bool $hasCoverageData
92+
* @return string
10393
*/
104-
private function hasCoverageData(array $classes): bool
94+
private function addRow(string $className, array $data, bool $hasCoverageData): string
10595
{
106-
foreach ($classes as $data) {
107-
if (array_key_exists('coverage', $data) && $data['coverage'] !== null) {
108-
return true;
109-
}
96+
$row = [
97+
$this->escapeMarkdown($className),
98+
(string)$data['score'],
99+
(string)round((float)$data['churn'], 3),
100+
];
101+
102+
if ($hasCoverageData) {
103+
$row[] = $data['riskChurn'] !== null ? (string)round((float)$data['riskChurn'], 3) : 'N/A';
110104
}
111105

112-
return false;
113-
}
106+
$row[] = (string)$data['timesChanged'];
114107

115-
/**
116-
* Escape special markdown characters in strings
117-
*
118-
* @param string $string
119-
* @return string
120-
*/
121-
private function escapeMarkdown(string $string): string
122-
{
123-
// Escape pipe characters which would break table formatting
124-
return str_replace('|', '\\|', $string);
108+
if ($hasCoverageData) {
109+
$row[] = $data['coverage'] !== null ? sprintf('%.2f%%', $data['coverage'] * 100) : 'N/A';
110+
$row[] = $data['riskLevel'] ?? 'N/A';
111+
}
112+
113+
return "| " . implode(" | ", $row) . " |\n";
125114
}
126115
}

src/Business/Churn/Exporter/SvgTreemapExporter.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*
1616
* @SuppressWarnings("PHPMD.ShortVariable")
1717
*/
18-
class SvgTreemapExporter implements DataExporterInterface
18+
class SvgTreemapExporter extends AbstractExporter
1919
{
2020
private const SVG_WIDTH = 1200;
2121
private const SVG_HEIGHT = 800;
@@ -35,6 +35,8 @@ public function __construct()
3535
*/
3636
public function export(array $classes, string $filename): void
3737
{
38+
$this->assertFileIsWritable($filename);
39+
3840
$svg = $this->generateSvgTreemap(classes: $classes);
3941

4042
if (file_put_contents($filename, $svg) === false) {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Exporter;
6+
7+
use InvalidArgumentException;
8+
use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig;
9+
10+
/**
11+
* Factory for creating cognitive metrics exporters.
12+
*/
13+
class CognitiveExporterFactory
14+
{
15+
public function __construct(
16+
private readonly CognitiveConfig $config
17+
) {
18+
}
19+
20+
/**
21+
* Create an exporter instance based on the report type.
22+
*
23+
* @param string $type The type of exporter to create (json, csv, html, markdown)
24+
* @return DataExporterInterface
25+
* @throws InvalidArgumentException If the type is not supported
26+
*/
27+
public function create(string $type): DataExporterInterface
28+
{
29+
return match ($type) {
30+
'json' => new JsonExporter(),
31+
'csv' => new CsvExporter(),
32+
'html' => new HtmlExporter(),
33+
'markdown' => new MarkdownExporter($this->config),
34+
default => throw new InvalidArgumentException("Unsupported exporter type: {$type}"),
35+
};
36+
}
37+
38+
/**
39+
* Get list of supported exporter types.
40+
*
41+
* @return array<string>
42+
*/
43+
public function getSupportedTypes(): array
44+
{
45+
return ['json', 'csv', 'html', 'markdown'];
46+
}
47+
48+
/**
49+
* Check if a type is supported.
50+
*
51+
* @param string $type
52+
* @return bool
53+
*/
54+
public function isSupported(string $type): bool
55+
{
56+
return in_array($type, $this->getSupportedTypes(), true);
57+
}
58+
}

0 commit comments

Comments
 (0)