Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Looking for: **route performance**, **performance monitoring**, **query tracking
- ✅ WebProfiler integration with ranking information
- ✅ **Chart.js integration** - Interactive performance charts
- ✅ **Symfony UX Twig Components** - Optional modern component system
- ✅ **Nowo UiKit + FormKit** — dashboard chrome and admin forms (`nowo-tech/ui-kit-bundle`, `nowo-tech/form-kit-bundle`, `symfony/ux-icons`)
- ✅ Symfony 7.x and 8.x compatible
- ✅ **FrankenPHP** — Compatible with FrankenPHP; production Caddyfile can use worker mode, while **dev demos** use `APP_ENV=dev` so the image entrypoint swaps in `Caddyfile.dev` (no worker, comfortable local dev). See [docs/DEMO-FRANKENPHP.md](docs/DEMO-FRANKENPHP.md) and the demo READMEs.

Expand Down Expand Up @@ -126,6 +127,7 @@ For detailed installation steps (including sync-schema and migrations), see [Ins
- Symfony 7.x or 8.x
- Doctrine ORM ^2.13 || ^3.0
- Doctrine Bundle ^2.8 || ^3.0 (3.0 required for Symfony 8)
- **nowo-tech/form-kit-bundle** ^2.2, **nowo-tech/ui-kit-bundle** ^1.4, **symfony/ux-icons** (dashboard UI)

## Configuration

Expand Down
766 changes: 24 additions & 742 deletions docs/ROADMAP.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions tests/Integration/NoopDoctrineCacheSchemaListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Nowo\PerformanceBundle\Tests\Integration;

/**
* No-op replacement for DoctrineDbalCacheAdapterSchemaListener.
*
* That listener calls GenerateSchemaEventArgs::setSchema(), which requires DBAL
* Schema::edit() (doctrine/dbal ^4.5). Latest released DBAL is 4.4.x.
*/
final class NoopDoctrineCacheSchemaListener
{
public function postGenerateSchema(): void
{
}
}
39 changes: 38 additions & 1 deletion tests/Integration/TestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\UX\Icons\UXIconsBundle;

use function str_contains;

/**
* Minimal Kernel for integration tests.
* Uses SQLite in-memory so no external database is required.
Expand Down Expand Up @@ -47,7 +52,39 @@ public function registerContainerConfiguration(LoaderInterface $loader): void
IntegrationDoctrineConfig::load($loader, $confDir . '/packages');
$loader->load($confDir . '/packages/security.yaml');
$loader->load($confDir . '/packages/twig.yaml');
$loader->load($confDir . '/packages/nowo_performance.yaml');
$loader->load($confDir . '/packages/' . $this->performanceConfigFile());
$loader->load($confDir . '/services.yaml');
}

/**
* ORM 3.6 SchemaTool + DBAL 4.4: DoctrineDbalCacheAdapterSchemaListener calls
* GenerateSchemaEventArgs::setSchema(), which needs DBAL Schema::edit() (dbal ^4.5, unreleased).
*/
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(
new class implements CompilerPassInterface {
public function process(ContainerBuilder $container): void
{
foreach ($container->getDefinitions() as $definition) {
$class = $definition->getClass();
if ($class === null || !str_contains($class, '\\SchemaListener\\')) {
continue;
}

$definition
->setClass(NoopDoctrineCacheSchemaListener::class)
->setArguments([]);
}
}
},
PassConfig::TYPE_BEFORE_REMOVING,
-1024,
);
}

protected function performanceConfigFile(): string
{
return 'nowo_performance.yaml';
}
}
42 changes: 3 additions & 39 deletions tests/Integration/TestKernelDashboardDisabled.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,13 @@

namespace Nowo\PerformanceBundle\Tests\Integration;

use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Nowo\FormKitBundle\NowoFormKitBundle;
use Nowo\PerformanceBundle\NowoPerformanceBundle;
use Nowo\UiKitBundle\NowoUiKitBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\UX\Icons\UXIconsBundle;

/**
* Same as TestKernel but with the performance dashboard disabled (covers controller 403/404 branches).
*/
final class TestKernelDashboardDisabled extends BaseKernel
final class TestKernelDashboardDisabled extends TestKernel
{
public function registerBundles(): array
{
return [
new FrameworkBundle(),
new DoctrineBundle(),
new SecurityBundle(),
new TwigBundle(),
new UXIconsBundle(),
new NowoFormKitBundle(),
new NowoUiKitBundle(),
new NowoPerformanceBundle(),
];
}

public function getProjectDir(): string
{
return __DIR__;
}

public function registerContainerConfiguration(LoaderInterface $loader): void
protected function performanceConfigFile(): string
{
$confDir = $this->getProjectDir() . '/config';
$loader->load($confDir . '/packages/framework.yaml');
IntegrationDoctrineConfig::load($loader, $confDir . '/packages');
$loader->load($confDir . '/packages/security.yaml');
$loader->load($confDir . '/packages/twig.yaml');
$loader->load($confDir . '/packages/nowo_performance_dashboard_disabled.yaml');
$loader->load($confDir . '/services.yaml');
return 'nowo_performance_dashboard_disabled.yaml';
}
}
42 changes: 3 additions & 39 deletions tests/Integration/TestKernelDashboardRoleAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,13 @@

namespace Nowo\PerformanceBundle\Tests\Integration;

use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Nowo\FormKitBundle\NowoFormKitBundle;
use Nowo\PerformanceBundle\NowoPerformanceBundle;
use Nowo\UiKitBundle\NowoUiKitBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\UX\Icons\UXIconsBundle;

/**
* Dashboard enabled but requires ROLE_ADMIN (anonymous user → AccessDenied).
*/
final class TestKernelDashboardRoleAdmin extends BaseKernel
final class TestKernelDashboardRoleAdmin extends TestKernel
{
public function registerBundles(): array
{
return [
new FrameworkBundle(),
new DoctrineBundle(),
new SecurityBundle(),
new TwigBundle(),
new UXIconsBundle(),
new NowoFormKitBundle(),
new NowoUiKitBundle(),
new NowoPerformanceBundle(),
];
}

public function getProjectDir(): string
{
return __DIR__;
}

public function registerContainerConfiguration(LoaderInterface $loader): void
protected function performanceConfigFile(): string
{
$confDir = $this->getProjectDir() . '/config';
$loader->load($confDir . '/packages/framework.yaml');
IntegrationDoctrineConfig::load($loader, $confDir . '/packages');
$loader->load($confDir . '/packages/security.yaml');
$loader->load($confDir . '/packages/twig.yaml');
$loader->load($confDir . '/packages/nowo_performance_dashboard_role_admin.yaml');
$loader->load($confDir . '/services.yaml');
return 'nowo_performance_dashboard_role_admin.yaml';
}
}
4 changes: 4 additions & 0 deletions tests/Integration/config/packages/framework.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ framework:
storage_factory_id: session.storage.factory.mock_file
router:
resource: '%kernel.project_dir%/config/routes.yaml'
# Avoid DoctrineDbalCacheAdapterSchemaListener (needs DBAL Schema::edit() / dbal ^4.5).
cache:
app: cache.adapter.array
system: cache.adapter.array
125 changes: 125 additions & 0 deletions tests/Unit/DependencyInjection/PerformanceExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,129 @@ public function testLoadWithStageInEnvironments(): void

$this->assertSame(['dev', 'stage', 'prod'], $this->container->getParameter('nowo_performance.environments'));
}

public function testPrependSeedsFormKitPerformanceProfileWhenHostUnset(): void
{
$this->registerStubExtension($this->container, 'nowo_form_kit');
$this->container->registerExtension($this->extension);

$this->extension->prepend($this->container);

$found = false;
foreach ($this->container->getExtensionConfig('nowo_form_kit') as $cfg) {
if (($cfg['css_framework'] ?? null) === 'bootstrap'
&& isset($cfg['profiles']['performance']['alias'])
&& $cfg['profiles']['performance']['alias'] === 'performance'
) {
$found = true;
$this->assertSame('NowoPerformanceBundle', $cfg['profiles']['performance']['translation_domain']);
$this->assertFalse($cfg['profiles']['performance']['auto_help']);
$this->assertFalse($cfg['profiles']['performance']['auto_placeholder']);
break;
}
}
$this->assertTrue($found);
}

public function testPrependDoesNotOverrideExplicitFormKitHostConfig(): void
{
$this->registerStubExtension($this->container, 'nowo_form_kit');
$this->container->prependExtensionConfig('nowo_form_kit', [
'css_framework' => 'none',
'profiles' => [
'performance' => [
'alias' => 'performance',
'translation_domain' => 'HostDomain',
],
],
]);
$this->container->registerExtension($this->extension);

$this->extension->prepend($this->container);

$bootstrapSeed = false;
$profileReseed = false;
foreach ($this->container->getExtensionConfig('nowo_form_kit') as $cfg) {
if (($cfg['css_framework'] ?? null) === 'bootstrap') {
$bootstrapSeed = true;
}
if (($cfg['profiles']['performance']['translation_domain'] ?? null) === 'NowoPerformanceBundle') {
$profileReseed = true;
}
}
$this->assertFalse($bootstrapSeed);
$this->assertFalse($profileReseed);
}

public function testPrependSeedsUiKitFromDashboardWhenHostUnset(): void
{
$this->registerStubExtension($this->container, 'nowo_ui_kit');
$this->container->registerExtension($this->extension);
$this->container->prependExtensionConfig('nowo_performance', [
'dashboard' => ['css_framework' => 'bootstrap'],
]);

$this->extension->prepend($this->container);

$found = false;
foreach ($this->container->getExtensionConfig('nowo_ui_kit') as $cfg) {
if (($cfg['css_framework'] ?? null) === 'bootstrap5'
&& ($cfg['icon_set'] ?? null) === 'bootstrap-icons'
) {
$found = true;
break;
}
}
$this->assertTrue($found);
}

public function testPrependDoesNotOverrideExplicitUiKitHostConfig(): void
{
$this->registerStubExtension($this->container, 'nowo_ui_kit');
$this->container->prependExtensionConfig('nowo_ui_kit', [
'css_framework' => 'none',
'icon_set' => 'none',
]);
$this->container->registerExtension($this->extension);

$this->extension->prepend($this->container);

$seeded = false;
foreach ($this->container->getExtensionConfig('nowo_ui_kit') as $cfg) {
if (($cfg['css_framework'] ?? null) === 'bootstrap5'
|| ($cfg['icon_set'] ?? null) === 'bootstrap-icons'
) {
$seeded = true;
}
}
$this->assertFalse($seeded);
}

private function registerStubExtension(ContainerBuilder $container, string $alias): void
{
$container->registerExtension(new class($alias) implements ExtensionInterface {
public function __construct(private readonly string $extensionAlias)
{
}

public function load(array $configs, ContainerBuilder $container): void
{
}

public function getNamespace(): string
{
return '';
}

public function getXsdValidationBasePath(): string|false
{
return false;
}

public function getAlias(): string
{
return $this->extensionAlias;
}
});
}
}
Loading