Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add HTML formatter #258

Merged
merged 13 commits into from
Mar 8, 2022
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@
"roave/no-floaters": "^1.1",
"symfony/var-dumper": "^4.2",
"symplify/easy-coding-standard": "^6.0",
"thecodingmachine/phpstan-strict-rules": "^0.11.0"
"thecodingmachine/phpstan-strict-rules": "^0.11.0",
"twig/twig": "^2.0"
},
"suggest": {
"twig/twig": "Twig:^2.0 is needed to support the HTML formattter"
},
"suggest": {
"ext-simplexml": "It is needed for the checkstyle formatter"
Expand Down
2,102 changes: 2,102 additions & 0 deletions dashboard.html

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion docs/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,26 @@ For changing the output format you can add the `format` flag. The following form
- console
- json
- checkstyle
- html

```bash
./vendor/bin/phpinsights analyse --format=json
```

::: Twig for HTML
The HTML formatter requires Twig to render.
So you have to add it do your dependencies if you want to use it.
`composer require --dev twig/twig: ^2.0`
:::

## Saving output to file

You can pipe the result to a file or to anywhere you like.
A common use case is parsing the output formatted as json to a json file.
A common use case is parsing the output formatted as json or html to a file.

```bash
./vendor/bin/phpinsights analyse --format=json > test.json
./vendor/bin/phpinsights analyse --format=html > test.html
```

When piping the result remember to add the no interaction flag `-n`, as the part where you need to interact is also getting piped. (the json format does not have any interaction)
Expand Down
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ parameters:
- '#Access to an undefined property PHP_CodeSniffer\\Sniffs\\Sniff::\$#'
- '#NunoMaduro\\PhpInsights\\Application\\Console\\Formatters\\Json has an unused parameter \$input#'
- '#NunoMaduro\\PhpInsights\\Application\\Console\\Formatters\\Checkstyle has an unused parameter \$input#'
- '#NunoMaduro\\PhpInsights\\Application\\Console\\Formatters\\Html has an unused parameter \$input#'
- '#In method "NunoMaduro\\PhpInsights\\Domain\\File::process", caught "Throwable" must be rethrown#'
- '#Casting to string something that#'
- '#Instanceof between Illuminate\\Contracts\\Foundation\\Application and#'
Expand Down
2 changes: 1 addition & 1 deletion src/Application/Console/Definitions/AnalyseDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public static function get(): InputDefinition
'format',
null,
InputOption::VALUE_REQUIRED,
'Format to output the result in [console, json, checkstyle]',
'Format to output the result in [console, json, checkstyle, html]',
'console'
),
]);
Expand Down
1 change: 1 addition & 0 deletions src/Application/Console/Formatters/FormatResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ final class FormatResolver
'console' => Console::class,
'json' => Json::class,
'checkstyle' => Checkstyle::class,
'html' => Html::class,
];

public static function resolve(
Expand Down
70 changes: 70 additions & 0 deletions src/Application/Console/Formatters/Html.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\PhpInsights\Application\Console\Formatters;

use Exception;
use NunoMaduro\PhpInsights\Application\Console\Contracts\Formatter;
use NunoMaduro\PhpInsights\Domain\Insights\InsightCollection;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Twig\Environment as Twig;
use Twig\Loader\FilesystemLoader;
use Twig\TwigFilter;

/**
* @internal
*/
final class Html implements Formatter
{
/** @var OutputInterface */
private $output;

public function __construct(InputInterface $input, OutputInterface $output)
{
$this->output = $output;
}

/**
* Format the result to the desired format.
*
* @param InsightCollection $insightCollection
* @param string $dir
* @param array<string> $metrics
*
* @throws Exception
*/
public function format(
InsightCollection $insightCollection,
string $dir,
array $metrics
): void {
$this->output->write($this->getTwig()->render('dashboard.html.twig', [
'dir' => $dir,
'results' => $insightCollection->results(),
'insights' => $insightCollection,
]), false, OutputInterface::OUTPUT_RAW);
}

private function getTwig(): Twig
{
$loader = new FilesystemLoader(__DIR__.'/../../../../views');
$twig = new Twig($loader, [
'cache' => false,
'debug' => true,
]);

$twig->addFilter(new TwigFilter('sluggify', static function (string $slug): string {
$slug = preg_replace('/<(.*?)>/u', '', (string) $slug);
$slug = preg_replace('/[\'"‘’“”]/u', '', (string) $slug);
$slug = mb_strtolower((string) $slug, 'UTF-8');

preg_match_all('/[\p{L}\p{N}\.]+/u', (string) $slug, $words);

return implode('-', array_filter($words[0]));
}));

return $twig;
}
}
8 changes: 8 additions & 0 deletions src/Domain/Insights/InsightCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ public function getCollector(): Collector
return $this->collector;
}

/**
* @return array<string, array<\NunoMaduro\PhpInsights\Domain\Contracts\Insight>>
*/
public function getInsightsPerMetric(): array
{
return $this->insightsPerMetric;
}

/**
* Gets all insights.
*
Expand Down
46 changes: 46 additions & 0 deletions views/dashboard.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha256-YLGeXaapI0/5IgZopewRJcFXomhRMlYYjugPLSyNjTY=" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.2/css/all.min.css" integrity="sha256-zmfNZmXoNWBMemUOo1XUGFfc0ihGGLYdgtJS3KCr/l0=" crossorigin="anonymous" />

<title>PHP Insights</title>
</head>
<body>
<div class="container-fluid">

<h1>PHP Insights</h1>

<p>
<time datetime="{{ 'now'|date('Y-m-d H:i:s') }}">{{ 'now'|date('Y-m-d') }}</time>
<code>{{ dir }}</code>
</p>

<div class="row">
<div class="col">{% include 'partials/stats_per_metric.html.twig' with {'id': 'quality', 'title': 'code quality', 'score': results.codeQuality} %}</div>
<div class="col">{% include 'partials/stats_per_metric.html.twig' with {'id': 'complexity', 'title': 'complexity', 'score': results.complexity} %}</div>
<div class="col">{% include 'partials/stats_per_metric.html.twig' with {'id': 'structure', 'title': 'structure', 'score': results.structure} %}</div>
<div class="col">{% include 'partials/stats_per_metric.html.twig' with {'id': 'style', 'title': 'style', 'score': results.style} %}</div>
</div>

<div class="alert alert-light">
score scale:
<span class="text-danger">■</span> 1 - 49
<span class="text-warning">■</span> 50 - 79
<span class="text-success">■</span> 80 - 100
</div>

{% include 'partials/insights_per_metric.html.twig' with {'id': 'quality', 'title': 'code quality', 'metricNamespacePart': '\\Code\\'} %}
{% include 'partials/insights_per_metric.html.twig' with {'id': 'complexity', 'title': 'complexity', 'metricNamespacePart': '\\Complexity\\'} %}
{% include 'partials/insights_per_metric.html.twig' with {'id': 'structure', 'title': 'structure', 'metricNamespacePart': '\\Architecture\\'} %}
{% include 'partials/insights_per_metric.html.twig' with {'id': 'style', 'title': 'style', 'metricNamespacePart': '\\Style\\'} %}

</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.bundle.min.js" integrity="sha256-fzFFyH01cBVPYzl16KT40wqjhgPtq6FFUB6ckN2+GGw=" crossorigin="anonymous"></script>
</body>
</html>
35 changes: 35 additions & 0 deletions views/partials/insights_per_metric.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<h2 id="{{ id }}">{{ title }}</h2>
<div class="list-group mb-3">
{% for metric, insightsPerMetric in insights.insightsPerMetric %}
{% if metricNamespacePart in metric %}
{% for insight in insightsPerMetric %}
{% if insight.hasIssue() %}
<div class="list-group-item">
<div class="clearfix" data-toggle="collapse" href="#code-{{ insight.insightClass|sluggify }}" >
<i class="fas text-danger fa-times"></i>
<strong>{{ insight.title }}</strong>
<span class="text-muted">({{ insight.details|length }})</span>
<code class="float-right">{{ insight.insightClass }}</code>
</div>
<div class="collapse" id="code-{{ insight.insightClass|sluggify }}">
<ul>
{% for detail in insight.details %}
<li>
{% if detail.hasFile() %}
<code>
{{ detail.file }}{% if detail.hasLine() %}:{{ detail.line }}{% endif %}{% if detail.hasFunction() %}:{{ detail.function }}{% endif %}
</code>
{% endif %}
{% if detail.hasMessage() %}
<span>{{ detail.message }}</span>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
</div>
5 changes: 5 additions & 0 deletions views/partials/stats_per_metric.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="alert text-center alert-{{ score >= 80 ? 'success' : (score >= 50 ? 'warning' : 'danger') }}">
<strong>{{ title }}</strong>
<a href="#{{ id }}" class="alert-link"><i class="fas fa-anchor"></i></a>
<div class="lead">{{ score|number_format(1, '.') }} %</div>
</div>