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: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
env:
# Phalcon v5 C extension constraint (installed from source via PIE)
PHALCON_CONSTRAINT: "^5.0"
EXTENSIONS: "fileinfo, json, mbstring, pdo, xml"
EXTENSIONS: "fileinfo, json, mbstring, pdo, pdo_sqlite, xml"

permissions: { }

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ All notable changes to `phalcon/debugbar` are documented here. The format is bas
- `Core` boots the bar with no MVC application and no DI container. [#40](https://github.com/phalcon/debugbar/issues/40)
- `ADR\Provider` boots the bar in `Phalcon\ADR` applications (Phalcon `v5.18.0` or later, or Phalcon 6). [#14](https://github.com/phalcon/debugbar/issues/14)
- The database, view, cache and route collectors take data through public methods, with no events manager. [#44](https://github.com/phalcon/debugbar/issues/44)
- The Database panel shows `Phalcon\DataMapper` queries in ADR applications (Phalcon `v5.19.0` or later, or Phalcon 6). [#16](https://github.com/phalcon/debugbar/issues/16)

### Fixed

Expand Down
25 changes: 23 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The bar runs only in development. It refuses to boot when the environment is und
## Requirements

- PHP 8.1 or later.
- A Phalcon runtime: the Phalcon 5 C extension, or the Phalcon 6 PHP implementation (`phalcon/phalcon`). ADR support needs Phalcon `v5.18.0` or later, or Phalcon 6.
- A Phalcon runtime: the Phalcon 5 C extension, or the Phalcon 6 PHP implementation (`phalcon/phalcon`). ADR support needs Phalcon `v5.18.0` or later, or Phalcon 6. The Database panel in an ADR application needs Phalcon `v5.19.0` or later, or Phalcon 6.
- `matthiasmullie/minify`, pulled in automatically by Composer. It minifies the bar's CSS and JavaScript before they are inlined.

## Installation
Expand Down Expand Up @@ -91,11 +91,32 @@ The provider reads the events manager (`Phalcon\Contracts\Events\Manager`) and t

Register the provider after all other providers. It resolves the events manager and the request during registration. After that, `extend()` on these services throws `CannotExtendResolved`, and `set()` has no effect. In a blocked environment, the provider resolves no service.

The `database` panel reads the `dm:*` events of `Phalcon\DataMapper\Pdo\Connection`. It needs Phalcon `v5.19.0` or later, or Phalcon 6. A connection fires these events only when it has an events manager. In `registerProviders()`, give the connection the events manager from the container:

```php
<?php

use Phalcon\Contracts\Events\Manager as EventsManager;
use Phalcon\DataMapper\Pdo\Connection;

$container->set(
Connection::class,
function () use ($container): Connection {
$connection = new Connection('mysql:host=localhost;dbname=app', 'user', 'secret');
$connection->setEventsManager($container->get(EventsManager::class));

return $connection;
}
);
```

The panel shows the values that you give to `perform()` and to the `fetch*()` methods. `exec()` and `query()` take no values, so the panel shows only their statements.

These limits apply in an ADR application:

- **One request for each process.** `Phalcon\ADR\Application::handle()` supports a reused request in a long-lived worker, but collectors have no reset method. In a worker, their data grows from request to request. `HistoryCookie::fromGlobals()` also reads only at boot.
- **Events manager.** The events manager must implement `Phalcon\Events\ManagerInterface`. The default manager does. A manager that implements only `Phalcon\Contracts\Events\Manager` gets no listener, so the bar is not injected.
- **Empty panels.** The `database`, `route` and `exceptions` panels have no ADR source yet. The `view` and `cache` panels get data only when the application gives the events manager to these services. The provider reads no `config` service, so the `config` panel is empty.
- **Empty panels.** The `route` and `exceptions` panels have no ADR source yet. The `database`, `view` and `cache` panels get data only when the application gives the events manager to these services. The provider reads no `config` service, so the `config` panel is empty.
- **History.** The provider does not read the `history` keys. Request history works only in MVC.

### A Host with No Events Manager
Expand Down
69 changes: 69 additions & 0 deletions src/DebugBar/ADR/Bridge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

/**
* This file is part of the Phalcon Framework.
*
* (c) Phalcon Team <team@phalcon.io>
*
* For the full copyright and license information, please view the LICENSE.txt
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Phalcon\DebugBar\ADR;

use Phalcon\DebugBar\Collector\DatabaseCollector;
use Phalcon\DebugBar\DebugBar;
use Phalcon\Events\EventInterface;
use Phalcon\Events\ManagerInterface;

use function is_array;
use function is_string;

/**
* Feeds the collectors from the events of an ADR application. The event names are
* strings because `Phalcon\DataMapper\Pdo\Events` is not in versions before
* `v5.19.0`. A `before` listener must not return `false`: this cancels the operation.
*/
final class Bridge
{
private const AFTER_QUERY_EVENTS = ['dm:afterExec', 'dm:afterPerform', 'dm:afterQuery'];

private const BEFORE_QUERY_EVENTS = ['dm:beforeExec', 'dm:beforePerform', 'dm:beforeQuery'];

public function __construct(private readonly DebugBar $bar)
{
}

public function subscribe(ManagerInterface $eventsManager): void
{
$collector = $this->bar->getCollectors()[DatabaseCollector::NAME] ?? null;
if (!$collector instanceof DatabaseCollector) {
return;
}

foreach (self::BEFORE_QUERY_EVENTS as $eventName) {
$eventsManager->attach(
$eventName,
function () use ($collector): void {
$collector->startQuery();
}
);
}

foreach (self::AFTER_QUERY_EVENTS as $eventName) {
$eventsManager->attach(
$eventName,
function (EventInterface $event, mixed $connection, mixed $data) use ($collector): void {
if (!is_array($data) || !is_string($data['statement'] ?? null)) {
return;
}

$values = $data['values'] ?? [];
$collector->finishQuery($data['statement'], is_array($values) ? $values : []);
}
);
}
}
}
8 changes: 6 additions & 2 deletions src/DebugBar/ADR/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Phalcon\Contracts\Events\Manager as ManagerContract;
use Phalcon\Contracts\Http\AttributeRequest;
use Phalcon\DebugBar\Core;
use Phalcon\DebugBar\Debug;
use Phalcon\DebugBar\DebugBarTypes;
use Phalcon\DebugBar\Exceptions\CannotUseInProduction;
use Phalcon\Events\ManagerInterface;
Expand All @@ -27,7 +28,8 @@
* Boots the debug bar in an ADR application. Register it after all other
* providers: it resolves the events manager and the request, and the container
* cannot change a service after that. The ADR application emits the response
* after `application:afterHandle`, so the bar is injected on this event.
* after `application:afterHandle`, so the bar is injected on this event. `Bridge`
* feeds the collectors from the other events of the application.
*
* @phpstan-import-type provider_config from DebugBarTypes
*/
Expand Down Expand Up @@ -59,12 +61,14 @@ public function provide(Collection $services): void
$eventsManager,
$request instanceof RequestInterface ? $request : null
);
$bar = Debug::getBar();

if (null === $eventsManager || null === $listener) {
if (null === $eventsManager || null === $listener || null === $bar) {
return;
}

$eventsManager->attach('application:afterHandle', $listener);
(new Bridge($bar))->subscribe($eventsManager);
}

private function resolve(Collection $services, string $name): mixed
Expand Down
132 changes: 132 additions & 0 deletions tests/Unit/DebugBar/ADR/BridgeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

/**
* This file is part of the Phalcon Framework.
*
* (c) Phalcon Team <team@phalcon.io>
*
* For the full copyright and license information, please view the LICENSE.txt
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Phalcon\Tests\Unit\DebugBar\ADR;

use Closure;
use Phalcon\DataMapper\Pdo\Connection;
use Phalcon\DebugBar\ADR\Bridge;
use Phalcon\DebugBar\Collector\DatabaseCollector;
use Phalcon\DebugBar\DebugBar;
use Phalcon\Events\Manager;
use Phalcon\Events\ManagerInterface;
use Phalcon\Talon\PHPUnit\AbstractUnitTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;

#[RequiresPhpExtension('pdo_sqlite')]
final class BridgeTest extends AbstractUnitTestCase
{
/**
* @return array<string, array{Closure(Connection): mixed}>
*/
public static function operations(): array
{
return [
'exec' => [static fn (Connection $connection): mixed => $connection->exec('SELECT 1')],
'perform' => [static fn (Connection $connection): mixed => $connection->perform('SELECT 1')],
'query' => [static fn (Connection $connection): mixed => $connection->query('SELECT 1')],
];
}

public function testAfterEventWithInvalidValuesShowsOnlyTheStatement(): void
{
$manager = new Manager();
$collector = $this->collector($manager);

$manager->fire('dm:afterPerform', $this, ['statement' => 'SELECT 1', 'values' => 'invalid']);

$this->assertSame('SELECT 1', $collector->collect()['panel'][0]['message']);
}

public function testAfterEventWithNoStatementIsIgnored(): void
{
$manager = new Manager();
$collector = $this->collector($manager);

$manager->fire('dm:afterPerform', $this, ['values' => []]);
$manager->fire('dm:afterPerform', $this, ['statement' => 42]);
$manager->fire('dm:afterPerform', $this);

$this->assertSame(0, $collector->collect()['badge']);
}

public function testBarWithNoDatabaseCollectorAttachesNothing(): void
{
$manager = new Manager();

(new Bridge(new DebugBar()))->subscribe($manager);

foreach (['Exec', 'Perform', 'Query'] as $operation) {
$this->assertFalse($manager->hasListeners('dm:before' . $operation));
$this->assertFalse($manager->hasListeners('dm:after' . $operation));
}
}

public function testFetchAllRecordsOneQuery(): void
{
$manager = new Manager();
$collector = $this->collector($manager);

$this->connection($manager)->fetchAll('SELECT :value AS answer', ['value' => 42]);

$this->assertSame(1, $collector->collect()['badge']);
}

public function testPerformShowsTheStatementAndTheBindings(): void
{
$manager = new Manager();
$collector = $this->collector($manager);

$this->connection($manager)->perform('SELECT :value', ['value' => 42]);

$this->assertSame('SELECT :value {"value":42}', $collector->collect()['panel'][0]['message']);
}

/**
* With no `before` listener the time is the full clock value, not the time of
* the operation. Each case has a new collector, so no earlier start can hide this.
*
* @param Closure(Connection): mixed $operation
*/
#[DataProvider('operations')]
public function testRunsAndRecordsTheOperationWithItsTime(Closure $operation): void
{
$manager = new Manager();
$collector = $this->collector($manager);

$operation($this->connection($manager));

$row = $collector->collect()['panel'][0];

$this->assertSame('SELECT 1', $row['message']);
$this->assertLessThan(60000.0, (float) $row['label']);
}

private function collector(ManagerInterface $manager): DatabaseCollector
{
$collector = new DatabaseCollector();

(new Bridge((new DebugBar())->addCollector($collector)))->subscribe($manager);

return $collector;
}

private function connection(ManagerInterface $manager): Connection
{
$connection = new Connection('sqlite::memory:');
$connection->setEventsManager($manager);

return $connection;
}
}
20 changes: 20 additions & 0 deletions tests/Unit/DebugBar/ADR/ProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
use Phalcon\Talon\PHPUnit\AbstractUnitTestCase;
use Phalcon\Tests\Support\DebugBar\ADR\Action\Data\GetData;
use Phalcon\Tests\Support\DebugBar\ADR\Action\Page\GetPage;
use Phalcon\Tests\Support\DebugBar\ADR\Action\Queries\GetQueries;
use Phalcon\Tests\Support\DebugBar\ADR\Front;
use Phalcon\Tests\Support\DebugBar\ADR\RecordingEmitter;
use PHPUnit\Framework\Attributes\BackupGlobals;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;

use function is_array;

Expand Down Expand Up @@ -155,6 +157,24 @@ function () use ($manager): ManagerContract {
$this->assertInstanceOf(DebugBar::class, Debug::getBar());
}

#[RequiresPhpExtension('pdo_sqlite')]
public function testQueryReachesTheDatabasePanel(): void
{
$_ENV[self::ENV_VAR] = 'dev';

$response = $this->dispatch('/queries');

$database = $this->bootedBar()->collect()['data']['database'];
$panel = $database['panel'];
if (!is_array($panel) || !is_array($panel[0] ?? null)) {
$this->fail('The database panel should have a row.');
}

$this->assertSame(1, $database['badge']);
$this->assertSame(GetQueries::STATEMENT . ' {"value":42}', $panel[0]['message']);
$this->assertStringContainsString(GetQueries::STATEMENT, $response->getContent());
}

public function testRequestReachesTheCollector(): void
{
$_ENV[self::ENV_VAR] = 'dev';
Expand Down
43 changes: 43 additions & 0 deletions tests/support/DebugBar/ADR/Action/Queries/GetQueries.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/**
* This file is part of the Phalcon Framework.
*
* (c) Phalcon Team <team@phalcon.io>
*
* For the full copyright and license information, please view the LICENSE.txt
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Phalcon\Tests\Support\DebugBar\ADR\Action\Queries;

use Phalcon\Contracts\ADR\Action;
use Phalcon\Contracts\Http\AttributeRequest;
use Phalcon\DataMapper\Pdo\Connection;
use Phalcon\Http\Response;
use Phalcon\Http\ResponseInterface;

/**
* Runs a query and returns an HTML response for `GET /queries`.
*/
final class GetQueries implements Action
{
public const STATEMENT = 'SELECT :value AS answer';

public function __construct(private readonly Connection $connection)
{
}

public function __invoke(AttributeRequest $request): ResponseInterface
{
$this->connection->fetchAll(self::STATEMENT, ['value' => 42]);

$response = new Response();
$response->setContentType('text/html');
$response->setContent('<html><body>queries</body></html>');

return $response;
}
}
Loading
Loading