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>>', + ], + ]; + } +}