diff --git a/CHANGELOG.md b/CHANGELOG.md
index 733f91f..065e3ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/src/Resources/config/services-profiler.yaml b/src/Resources/config/services-profiler.yaml
index ffd5505..50e0b18 100644
--- a/src/Resources/config/services-profiler.yaml
+++ b/src/Resources/config/services-profiler.yaml
@@ -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']
diff --git a/src/Resources/views/Profiler/index.html.twig b/src/Resources/views/Profiler/index.html.twig
index 9f33e96..45f8887 100644
--- a/src/Resources/views/Profiler/index.html.twig
+++ b/src/Resources/views/Profiler/index.html.twig
@@ -96,6 +96,10 @@
.col--warning {
background-color: {{ colors.warning|raw }};
}
+
+ small.small {
+ color: gray;
+ }
@@ -660,27 +664,27 @@
{% macro colWrap(value, colspan = null) %}
{% if value is defined and value.formatted is defined and value.isWide %}
- {{ value.original }}
+ {{ value.original|genericClass }}
|
- {{ value.formatted }}
+ {{ value.formatted|genericClass }}
|
{% elseif value is defined and value.formatted is defined and value.original is null %}
- {{ value.formatted }}
+ {{ value.formatted|genericClass }}
|
{% elseif value is defined and value.formatted is defined and colspan is not null %}
- {{ value.original }}
+ {{ value.original|genericClass }}
|
- {{ value.formatted }}
+ {{ value.formatted|genericClass }}
|
{% else %}
- {{ value }}
+ {{ value|genericClass }}
|
{% endif %}
{% endmacro %}
diff --git a/src/Service/ClassExtension.php b/src/Service/ClassExtension.php
new file mode 100644
index 0000000..dc35096
--- /dev/null
+++ b/src/Service/ClassExtension.php
@@ -0,0 +1,97 @@
+ $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%s',
+ $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%s<%s>',
+ $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;
+ }
+}
diff --git a/tests/Service/ClassExtensionTest.php b/tests/Service/ClassExtensionTest.php
new file mode 100644
index 0000000..215f262
--- /dev/null
+++ b/tests/Service/ClassExtensionTest.php
@@ -0,0 +1,63 @@
+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\ServiceName'],
+ 'class with generic parameter' => [
+ 'Root\Service\Generic\ServiceName',
+ 'Root\Service\Generic\ServiceName<T>',
+ ],
+ 'generic class' => [
+ 'Root\Service\Generic\ServiceName',
+ 'Root\Service\Generic\ServiceName<Root\Value\Foo>',
+ ],
+ 'generic class with multiple generic arguments' => [
+ 'Root\Service\Generic\ServiceName',
+ 'Root\Service\Generic\ServiceName<Root\Value\Foo, Root\Value\Bar>',
+ ],
+ 'generic class with generic class argument' => [
+ 'Root\Service\Generic\ServiceName>',
+ 'Root\Service\Generic\ServiceName<Root\Value\Foo<Root\Value\Bar>>',
+ ],
+ 'generic class with duplicity in name' => [
+ 'Root\Service\Generic\ServiceName',
+ 'Root\Service\Generic\ServiceName<Root\Value\Foo\Foo>',
+ ],
+ 'generic class with multiple duplicities' => [
+ 'Root\Service\Generic\Foo>',
+ 'Root\Service\Generic\Foo<Root\Value\Foo<Root\Foo\Foo, Root\Foo\Foo>>',
+ ],
+ ];
+ }
+}