Skip to content

Commit

Permalink
Format generic classes in profiler
Browse files Browse the repository at this point in the history
  • Loading branch information
MortalFlesh committed Apr 5, 2022
1 parent c7a1a44 commit fa2c9b9
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

## Unreleased
- Allow setting a profiler bag verbosity in bundle configuration
- Format generic classes in profiler

## 1.2.0 - 2021-08-10
- Allow an `$initiator` in `ResponseDecoders` `supports` method
Expand Down
3 changes: 3 additions & 0 deletions src/Resources/config/services-profiler.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ services:
Lmc\Cqrs\Bundle\Service\ErrorProfilerFormatter:
tags:
- { name: lmc_cqrs.profiler_formatter, priority: -1 }

Lmc\Cqrs\Bundle\Service\ClassExtension:
tags: ['twig.extension']
16 changes: 10 additions & 6 deletions src/Resources/views/Profiler/index.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@
.col--warning {
background-color: {{ colors.warning|raw }};
}
small.small {
color: gray;
}
</style>

<div class="metrics">
Expand Down Expand Up @@ -660,27 +664,27 @@
{% macro colWrap(value, colspan = null) %}
{% if value is defined and value.formatted is defined and value.isWide %}
<td{% if colspan is not null %} colspan="{{ colspan }}"{% endif %} title="Original">
<div style="word-wrap: break-word; max-width: 800px;">{{ value.original }}</div>
<div style="word-wrap: break-word; max-width: 800px;">{{ value.original|genericClass }}</div>
</td>
</tr>
<tr>
<td{% if colspan is not null %} colspan="{{ colspan + 1 }}"{% endif %} title="Formatted">
{{ value.formatted }}
{{ value.formatted|genericClass }}
</td>
{% elseif value is defined and value.formatted is defined and value.original is null %}
<td{% if colspan is not null %} colspan="{{ colspan }}"{% endif %} title="Formatted">
<div style="word-wrap: break-word; max-width: 800px;">{{ value.formatted }}</div>
<div style="word-wrap: break-word; max-width: 800px;">{{ value.formatted|genericClass }}</div>
</td>
{% elseif value is defined and value.formatted is defined and colspan is not null %}
<td colspan="{{ (colspan / 2)|round(0, 'floor') }}" title="Original">
<div style="word-wrap: break-word; max-width: 400px;">{{ value.original }}</div>
<div style="word-wrap: break-word; max-width: 400px;">{{ value.original|genericClass }}</div>
</td>
<td colspan="{{ (colspan / 2)|round(0, 'ceil') }}" title="Formatted">
<div style="word-wrap: break-word; max-width: 400px;">{{ value.formatted }}</div>
<div style="word-wrap: break-word; max-width: 400px;">{{ value.formatted|genericClass }}</div>
</td>
{% else %}
<td{% if colspan is not null %} colspan="{{ colspan }}"{% endif %}>
<div style="word-wrap: break-word; max-width: 800px;">{{ value }}</div>
<div style="word-wrap: break-word; max-width: 800px;">{{ value|genericClass }}</div>
</td>
{% endif %}
{% endmacro %}
Expand Down
97 changes: 97 additions & 0 deletions src/Service/ClassExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php declare(strict_types=1);

namespace Lmc\Cqrs\Bundle\Service;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

class ClassExtension extends AbstractExtension
{
public function getFilters()
{
return [
new TwigFilter(
'genericClass',
fn (string $class) => $this->formatGenericClass($class),
['is_safe' => ['html']]
),
];
}

private function formatGenericClass(string $class): string
{
if (empty($class)) {
return $class;
}

try {
if ($this->tryParseClassWithoutGenerics($class, $shortName)) {
return sprintf(
'%s<strong>%s</strong>',
$this->replaceOnceFromEnd($shortName, '', $class),
$shortName
);
} elseif ($this->tryParseClassWithGenerics($class, $shortName, $genericArguments)) {
if ($this->tryParseClassWithGenerics($genericArguments)) {
$generics = $this->formatGenericClass($genericArguments);
} else {
$generics = array_map(
fn (string $genericArgument) => $this->formatGenericClass(trim($genericArgument)),
explode(',', $genericArguments)
);

$generics = implode(', ', $generics);
}

[$classWithoutGenerics] = explode('<', $class, 2);

return sprintf(
'%s<strong>%s</strong>&lt;%s&gt;',
$this->replaceOnceFromEnd($shortName, '', $classWithoutGenerics),
$shortName,
$generics,
);
}

return $class;
} catch (\Throwable $e) {
return $class;
}
}

private function replaceOnceFromEnd(string $search, string $replace, string $value): string
{
$position = mb_strrpos($value, $search);
if ($position === false) {
return $value;
}

return substr_replace($value, $replace, $position, mb_strlen($search));
}

private function tryParseClassWithoutGenerics(string $class, ?string &$shortClassName = null): bool
{
if (preg_match('/^([A-Z][A-Za-z0-9]*\\\\)*([A-Z][A-Za-z]*?)$/', $class, $matches) === 1) {
$shortClassName = array_pop($matches);

return true;
}

return false;
}

private function tryParseClassWithGenerics(
string $class,
?string &$shortClassName = null,
?string &$genericArguments = null
): bool {
if (preg_match('/^([A-Z][A-Za-z0-9]*\\\\)*([A-Z][A-Za-z]*?)<(.*)>$/', $class, $matches) === 1) {
$genericArguments = array_pop($matches);
$shortClassName = array_pop($matches);

return true;
}

return false;
}
}
63 changes: 63 additions & 0 deletions tests/Service/ClassExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types=1);

namespace Lmc\Cqrs\Bundle\Service;

use PHPUnit\Framework\TestCase;

class ClassExtensionTest extends TestCase
{
private ClassExtension $extension;

protected function setUp(): void
{
$this->extension = new ClassExtension();
}

/**
* @test
* @dataProvider provideClass
*/
public function shouldFormatClassString(string $string, string $expected): void
{
$filter = $this->extension->getFilters()[0];
$this->assertSame('genericClass', $filter->getName());

$result = call_user_func($filter->getCallable(), $string);

$this->assertSame($expected, $result);
}

public function provideClass(): array
{
return [
// input, expected
'empty' => ['', ''],
'not a class' => ['foo', 'foo'],
'class without generics' => ['Root\Service\ServiceName', 'Root\Service\<strong>ServiceName</strong>'],
'class with generic parameter' => [
'Root\Service\Generic\ServiceName<T>',
'Root\Service\Generic\<strong>ServiceName</strong>&lt;<strong>T</strong>&gt;',
],
'generic class' => [
'Root\Service\Generic\ServiceName<Root\Value\Foo>',
'Root\Service\Generic\<strong>ServiceName</strong>&lt;Root\Value\<strong>Foo</strong>&gt;',
],
'generic class with multiple generic arguments' => [
'Root\Service\Generic\ServiceName<Root\Value\Foo, Root\Value\Bar>',
'Root\Service\Generic\<strong>ServiceName</strong>&lt;Root\Value\<strong>Foo</strong>, Root\Value\<strong>Bar</strong>&gt;',
],
'generic class with generic class argument' => [
'Root\Service\Generic\ServiceName<Root\Value\Foo<Root\Value\Bar>>',
'Root\Service\Generic\<strong>ServiceName</strong>&lt;Root\Value\<strong>Foo</strong>&lt;Root\Value\<strong>Bar</strong>&gt;&gt;',
],
'generic class with duplicity in name' => [
'Root\Service\Generic\ServiceName<Root\Value\Foo\Foo>',
'Root\Service\Generic\<strong>ServiceName</strong>&lt;Root\Value\Foo\<strong>Foo</strong>&gt;',
],
'generic class with multiple duplicities' => [
'Root\Service\Generic\Foo<Root\Value\Foo<Root\Foo\Foo, Root\Foo\Foo>>',
'Root\Service\Generic\<strong>Foo</strong>&lt;Root\Value\<strong>Foo</strong>&lt;Root\Foo\<strong>Foo</strong>, Root\Foo\<strong>Foo</strong>&gt;&gt;',
],
];
}
}

0 comments on commit fa2c9b9

Please sign in to comment.