Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a CLI command to generate an HTML report showing how classes are distributed across DDD categories (aggregate roots, entities, value objects, etc.) and whether there are forbidden relations between them.
Changes:
- Adds Symfony Console command
tactix:reportto generate static HTML reports with interactive charts - Integrates Symfony Bundle (TactixBundle) with dependency injection support for use in Symfony applications
- Provides static HTML/CSS/JavaScript report templates with ECharts visualization
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| src/TactixBundle.php | New Symfony bundle class for integration with Symfony applications |
| src/DependencyInjection/TactixExtension.php | Symfony DI extension to load services configuration |
| src/DependencyInjection/Configuration.php | Symfony configuration tree builder (placeholder configuration) |
| src/Command/ReportCommand.php | Main CLI command that analyzes classes and generates HTML reports |
| src/Command/Report.php | Data class to categorize classes by DDD attributes |
| resources/report/*.{html,js,css} | Static templates for the HTML report with charts and tables |
| resources/config/services.yaml | Symfony service configuration for the ReportCommand |
| src/IgnoreableTypes.php | Removed typed constant declaration (PHP 8.3+ feature) for PHP 8.2 compatibility |
| src/Analyzer/Class/Name.php | Removed typed constant declaration (PHP 8.3+ feature) for PHP 8.2 compatibility |
| docker/php/Dockerfile | Downgraded from PHP 8.4 to PHP 8.2 |
| composer.json | Added Symfony dependencies and adjusted PHPUnit version |
| README.md | Added documentation for the new report command and fixed spelling |
| AGENTS.md | Updated with new project structure and command information |
| Makefile | Updated container commands to use compose exec instead of run |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
resources/report/report.js
Outdated
| @@ -0,0 +1 @@ | |||
| const reportData = {"folder":"src/TBone/","classes":{"aggregate_roots":["App\\TBone\\Depot\\Depot","App\\TBone\\Depot\\WatchList"],"entities":["App\\TBone\\Index\\iShares\\StoxxEurope600","App\\TBone\\Index\\Spider\\SP500","App\\TBone\\Index\\Yahoo\\YahooIndex","App\\TBone\\Index\\Yahoo\\Component"],"factories":["App\\TBone\\Depot\\PositionsFactory","App\\TBone\\Index\\iShares\\SectorNameFactory","App\\TBone\\Index\\iShares\\iShares","App\\TBone\\Index\\iShares\\HoldingFactory","App\\TBone\\Index\\Spider\\SectorNameFactory","App\\TBone\\Index\\Spider\\HoldingFactory","App\\TBone\\Index\\Spider\\SectorIdFactory","App\\TBone\\ConfirmationFactory"],"repositories":[],"services":["App\\TBone\\Depot\\Positions","App\\TBone\\Depot\\Invested","App\\TBone\\Depot\\AmountCalculator","App\\TBone\\Depot\\Gain","App\\TBone\\Depot\\PurchaseCalculator","App\\TBone\\Instrumentation\\Tracer","App\\TBone\\DistinctUntilChanged","App\\TBone\\Index\\HoldingComparer","App\\TBone\\Index\\Spider\\Spider","App\\TBone\\Index\\Spider\\SectorWeight","App\\TBone\\Index\\Yahoo\\FloatComparer","App\\TBone\\Index\\Yahoo\\ConfirmationDtoComparer","App\\TBone\\Index\\Yahoo\\TimestampComparer","App\\TBone\\Index\\Yahoo\\PerformanceProvider","App\\TBone\\Index\\Yahoo\\Currency","App\\TBone\\Index\\Yahoo\\AddressProvider"],"value_objects":["App\\TBone\\InitialAverage","App\\TBone\\Average","App\\TBone\\Depot\\Watch","App\\TBone\\Depot\\Adjustment","App\\TBone\\Depot\\DepotEvent","App\\TBone\\Depot\\Position","App\\TBone\\Price","App\\TBone\\Depot\\Cash","App\\TBone\\Depot\\Risk","App\\TBone\\Depot\\StopLoss","App\\TBone\\Depot\\TakeProfit","App\\TBone\\TrueRange","App\\TBone\\Depot\\Resume","App\\TBone\\Depot\\Performance","App\\TBone\\Candle","App\\TBone\\LinearRegression","App\\TBone\\TimeRange","App\\TBone\\Confirmation","App\\TBone\\MovingAverages","App\\TBone\\Indicates","App\\TBone\\MovingAverage","App\\TBone\\Index\\Holding","App\\TBone\\Index\\Sector","App\\TBone\\Index\\FinanzenNet\\Share","App\\TBone\\Index\\UserAgent","App\\TBone\\Index\\Yahoo\\SymbolLookupResult","App\\TBone\\Index\\Yahoo\\ConfirmationDto","App\\TBone\\Index\\Yahoo\\CandleRange","App\\TBone\\Performance","App\\TBone\\Index\\Yahoo\\Quote","App\\TBone\\Index\\Yahoo\\YahooPrice","App\\TBone\\Index\\Yahoo\\HistoryDocument\\TradingPeriod","App\\TBone\\Index\\Yahoo\\HistoryDocument\\DTO","App\\TBone\\Index\\Yahoo\\HistoryDocument\\Quote","App\\TBone\\Index\\Yahoo\\HistoryDocument\\Indicators","App\\TBone\\Index\\Yahoo\\HistoryDocument\\Meta","App\\TBone\\Index\\Yahoo\\HistoryDocument\\Result","App\\TBone\\Index\\Yahoo\\HistoryDocument\\Chart","App\\TBone\\Index\\Yahoo\\HistoryDocument\\AdjustedClose","App\\TBone\\Index\\Yahoo\\HistoryDocument\\Period","App\\TBone\\Slope"],"interfaces":["App\\TBone\\Depot\\PositionsInterface","App\\TBone\\Depot\\DepotEventStoreInterface","App\\TBone\\CurrentUserInterface","App\\TBone\\Instrumentation\\LoggingInterface","App\\TBone\\Depot\\QuoteFetcherInterface","App\\TBone\\CacheInterface","App\\TBone\\Instrumentation\\TraceInterface","App\\TBone\\Instrumentation\\TracingInterface","App\\TBone\\Instrumentation\\MetricsInterface","App\\TBone\\Index\\IndexInterface","App\\TBone\\Index\\Yahoo\\DocumentProviderInterface","App\\TBone\\DenormalizerInterface","App\\TBone\\NormalizerInterface"],"exceptions":["App\\TBone\\NotEnoughCandlesException","App\\TBone\\TBoneException","App\\TBone\\Depot\\CancelledException","App\\TBone\\Depot\\InsufficientFundsException","InvalidArgumentException","App\\TBone\\Depot\\DepotException","OutOfBoundsException","LogicException","App\\TBone\\Index\\Yahoo\\HistoryDocument\\InvalidDocumentException","App\\TBone\\Index\\Yahoo\\YahooException","App\\TBone\\NoConfirmationsException","App\\TBone\\TimeRangeException"],"uncategorized":["App\\TBone\\TriggerIndicator","App\\TBone\\CandleColor","App\\TBone\\Depot\\SubjectType","App\\TBone\\Depot\\DepotEventType","App\\TBone\\Depot\\SuggestionType","App\\TBone\\ConfirmationIndicator","App\\TBone\\TrendIndicator","App\\TBone\\TimeRangeEnum","App\\TBone\\Frequency","App\\TBone\\Index\\SortOrder","App\\TBone\\Index\\SectorName","App\\TBone\\Index\\iShares\\iSharesHoldingCsvColumn","App\\TBone\\Index\\Spider\\SpiderHoldingCsvColumn","App\\TBone\\Index\\FinanzenNet\\IndexCode","App\\TBone\\Index\\Yahoo\\IndexName","App\\TBone\\Index\\Yahoo\\SortOrder","App\\TBone\\Index\\Yahoo\\FloatComparerResult","App\\TBone\\Index\\Yahoo\\YahooPriceType"]},"forbidden":[]} No newline at end of file | |||
There was a problem hiding this comment.
The report.js file contains hardcoded example data from "src/TBone/" which appears to be from a specific project. This file should either be:
- Excluded from the repository (added to .gitignore) since it will be generated by the command
- Or contain placeholder/example data that is clearly marked as a template
Having project-specific data committed to the repository is confusing and could mislead users about what this file should contain.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
src/Command/ReportCommand.php
Outdated
| file_put_contents($destination, 'const reportData = '.$json.';'); | ||
|
|
||
| // Ensure static assets (chart.js, styles.css) are available in the report directory. | ||
| // Prefer packaged templates under resources/report, otherwise create sensible defaults. | ||
| $resourceDir = __DIR__.'/../../resources/report'; | ||
|
|
||
| // chart.js | ||
| if (is_file($resourceDir.'/chart.js')) { | ||
| copy($resourceDir.'/chart.js', $dir.'/chart.js'); | ||
| } else { | ||
| // default minimal chart script inspired by tbone's chart.js using ECharts | ||
| $defaultChartJs = <<<'JS' | ||
| // Basic chart renderer expecting global reportData and echarts | ||
| document.addEventListener("DOMContentLoaded", function () { | ||
| const { classes, forbidden, folder } = reportData; | ||
| document.getElementById("reportFolder").textContent = `Report for ${folder}`; | ||
|
|
||
| const colors = [ | ||
| "#3498db", "#e74c3c", "#1abc9c", "#95a5a6", "#2ecc71", "#f39c12", "#f1c40f", "#9b59b6", "#7f8c8d" | ||
| ]; | ||
|
|
||
| const labels = Object.keys(classes).filter(key => Array.isArray(classes[key])); | ||
| const values = labels.map(key => classes[key].length); | ||
| const readableLabels = labels.map(key => key.replace(/_/g, ' ')); | ||
|
|
||
| const pieEl = document.getElementById('pieChart'); | ||
| const barEl = document.getElementById('barChart'); | ||
| if (pieEl && barEl && window.echarts) { | ||
| const pieChart = echarts.init(pieEl); | ||
| const pieData = readableLabels.map((label, index) => ({ name: label, value: values[index] })); | ||
| pieChart.setOption({ | ||
| tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' }, | ||
| legend: { orient: 'vertical', left: 'left' }, | ||
| series: [{ name: 'Classes', type: 'pie', radius: '50%', data: pieData, color: colors }] | ||
| }); | ||
|
|
||
| const barChart = echarts.init(barEl); | ||
| barChart.setOption({ | ||
| tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, | ||
| xAxis: { type: 'category', data: readableLabels, axisLabel: { rotate: 45 } }, | ||
| yAxis: { type: 'value', minInterval: 1 }, | ||
| series: [{ name: 'Count', type: 'bar', data: values, itemStyle: { color: params => colors[params.dataIndex % colors.length] } }] | ||
| }); | ||
|
|
||
| window.addEventListener('resize', () => { pieChart.resize(); barChart.resize(); }); | ||
| } | ||
|
|
||
| // populate classes table if present | ||
| const container = document.getElementById('classesTableContainer'); | ||
| if (container) { | ||
| const all = []; | ||
| labels.forEach(category => { | ||
| classes[category].forEach(c => all.push({ name: c, category })); | ||
| }); | ||
| all.sort((a,b) => a.name.localeCompare(b.name)); | ||
| const table = document.createElement('table'); | ||
| table.style.width = '100%'; | ||
| table.style.borderCollapse = 'collapse'; | ||
| const thead = document.createElement('thead'); | ||
| const headerRow = document.createElement('tr'); | ||
| ['Class','Category'].forEach(h => { | ||
| const th = document.createElement('th'); th.textContent = h; th.style.border = '1px solid #ddd'; th.style.padding = '8px'; th.style.background = '#f5f5f5'; headerRow.appendChild(th); | ||
| }); | ||
| thead.appendChild(headerRow); table.appendChild(thead); | ||
| const tbody = document.createElement('tbody'); | ||
| all.forEach((item, idx) => { | ||
| const tr = document.createElement('tr'); if (idx%2===0) tr.style.background='#f9f9f9'; | ||
| const tdName = document.createElement('td'); tdName.textContent = item.name; tdName.style.border='1px solid #ddd'; tdName.style.padding='8px'; | ||
| const tdCat = document.createElement('td'); tdCat.textContent = item.category; tdCat.style.border='1px solid #ddd'; tdCat.style.padding='8px'; | ||
| tr.appendChild(tdName); tr.appendChild(tdCat); tbody.appendChild(tr); | ||
| }); | ||
| table.appendChild(tbody); container.appendChild(table); | ||
| } | ||
| }); | ||
| JS; | ||
| file_put_contents($dir.'/chart.js', $defaultChartJs); | ||
| } | ||
|
|
||
| // styles.css | ||
| if (is_file($resourceDir.'/styles.css')) { | ||
| copy($resourceDir.'/styles.css', $dir.'/styles.css'); | ||
| } else { | ||
| $defaultCss = <<<'CSS' | ||
| body { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; margin: 16px; } | ||
| h1 { text-align: center; } | ||
| .chart-row { display:flex; gap:20px; justify-content:center; margin:20px 0; } | ||
| .chart-container { border:1px solid #ddd; border-radius:8px; padding:6px; } | ||
| #classesTableContainer { max-width: 1000px; margin: 0 auto; } | ||
| CSS; | ||
| file_put_contents($dir.'/styles.css', $defaultCss); | ||
| } | ||
|
|
||
| // index.html referencing the static assets. Use ECharts CDN and include chart.js and report.js | ||
| $indexHtml = <<<'HTML' | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | ||
| <title>Charts Report</title> | ||
|
|
||
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet"> | ||
| <link rel="stylesheet" href="styles.css"> | ||
|
|
||
| <script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.4.3/echarts.min.js"></script> | ||
| <script src="report.js"></script> | ||
| </head> | ||
| <body> | ||
| <h1 id="reportFolder" style="text-align: center;"></h1> | ||
|
|
||
| <h2 style="text-align: center;">Class distribution</h2> | ||
|
|
||
| <div class="chart-row" style="display: flex; gap: 20px; justify-content: center; margin: 20px 0;"> | ||
| <div class="chart-container" style="width: 45%; height: 400px; border: 1px solid #ddd; border-radius: 8px;"> | ||
| <div id="pieChart" style="width: 100%; height: 100%;"></div> | ||
| </div> | ||
| <div class="chart-container" style="width: 45%; height: 400px; border: 1px solid #ddd; border-radius: 8px;"> | ||
| <div id="barChart" style="width: 100%; height: 100%;"></div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <h2 style="text-align: center; margin-top: 40px;">All classes</h2> | ||
| <div id="classesTableContainer"></div> | ||
|
|
||
| <script src="chart.js"></script> | ||
| </body> | ||
| </html> | ||
| HTML; | ||
|
|
||
| file_put_contents($dir.'/index.html', $indexHtml); |
There was a problem hiding this comment.
The file_put_contents, copy, and mkdir operations don't check for failures. These operations can fail due to permissions, disk space, or other I/O issues. Consider checking the return values and throwing meaningful exceptions when operations fail, so users get clear error messages instead of silent failures or PHP warnings.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
src/Command/ReportCommand.php
Outdated
| shouldNotStartWith: [ | ||
| 'App\\Kernel', | ||
| 'App\\CLI\\', | ||
| 'App\\DDD\\', | ||
| 'Doctrine\\', | ||
| 'Symfony\\', | ||
| 'Psr\\', | ||
| 'PhpParser\\', | ||
| 'phpDocumentor\\', | ||
| 'Monolog\\', | ||
| 'OpenTelemetry\\', | ||
| 'Rx\\', | ||
| 'React\\', | ||
| 'InfluxDB2\\', | ||
| 'EasyCorp\\Bundle\\EasyAdminBundle\\', | ||
| ], |
There was a problem hiding this comment.
The hardcoded list of namespaces to filter (App\Kernel, App\CLI, App\DDD, etc.) appears to be specific to a particular project rather than being general-purpose. For a library that can be reused across different projects, these filters should be:
- Configurable through command options (e.g., --exclude-namespace)
- Or moved to a configuration file
- Or at minimum, documented as example filters that users should customize
This makes the command less reusable without modification.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
|
|
||
| document.addEventListener("DOMContentLoaded", function () { | ||
|
|
||
| const { classes, forbidden, folder } = reportData; |
There was a problem hiding this comment.
Unused variable forbidden.
| const { classes, forbidden, folder } = reportData; | |
| const { classes, folder } = reportData; |
Co-authored-by: makomweb <1567373+makomweb@users.noreply.github.com>
Co-authored-by: makomweb <1567373+makomweb@users.noreply.github.com>
Co-authored-by: makomweb <1567373+makomweb@users.noreply.github.com>
Co-authored-by: makomweb <1567373+makomweb@users.noreply.github.com>
Make it English Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: makomweb <1567373+makomweb@users.noreply.github.com>
Co-authored-by: makomweb <1567373+makomweb@users.noreply.github.com>
Co-authored-by: makomweb <1567373+makomweb@users.noreply.github.com>
Co-authored-by: makomweb <1567373+makomweb@users.noreply.github.com>
Make namespace filters configurable in ReportCommand
Align CI workflow PHP version with Dockerfile
Add test coverage for ReportCommand and Report classes
Remove hardcoded project-specific data from report template
Uh oh!
There was an error while loading. Please reload this page.