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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ All notable changes to `phalcon/debugbar` are documented here. The format is bas
- `Logger\ExceptionAdapter` feeds the Exceptions panel from the logger in ADR applications. [#47](https://github.com/phalcon/debugbar/issues/47)
- `History\HistoryEndpoint` takes the request and the base URI with no DI container, and `Mvc\HistoryListener` selects the MVC controller. [#41](https://github.com/phalcon/debugbar/issues/41)
- `History\HistoryHandler` lists, reads and clears the request history with no MVC types. [#42](https://github.com/phalcon/debugbar/issues/42)
- Request history in ADR applications, with the `history.base_uri` option. [#43](https://github.com/phalcon/debugbar/issues/43)

### Fixed

Expand Down
26 changes: 24 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,33 @@ $container->set(

The closure calls `Debug::getBar()` when the application first uses the logger. This is after the provider boots the bar. The adapter does not change the response and does not keep the exception from the error responder. The panel also shows `RouteNotFound` for a URL with no route, because the error responder logs this too. If the error responder itself fails, the application returns a bare 500 response and logs nothing, so the panel does not show the exception.

The `history` keys turn on request history. You do not have to add a route. Set `history.base_uri` when the application is not at the root of the host, because an ADR application has no `url` service by default.

```php
<?php

use Phalcon\DebugBar\ADR\Provider as DebugBarProvider;

(new DebugBarProvider([
'history' => [
'enabled' => true,
'path' => dirname(__DIR__) . '/runtime/debugbar',
'base_uri' => '/',
],
]))->provide($container);
```

The provider decorates the router (`Phalcon\Contracts\ADR\Router\Router`) with `extend()`. The history URL goes to `Phalcon\DebugBar\ADR\HistoryAction`, and all other URLs go to the router of the application. The history URL replaces an application route with the same path. The global middleware of the application also runs for this Action. Route middleware does not run.

!!! warning "NOTE"

Do not resolve the router during registration. `extend()` on a resolved service throws `CannotExtendResolved`. In a blocked environment, the provider does not change the router.

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`, `view` and `cache` panels get data only when the application gives the events manager to these services. The `exceptions` panel gets data only when the application adds `ExceptionAdapter` to its logger. 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 Expand Up @@ -231,7 +252,7 @@ if (null !== $listener) {

## Configuration

The second argument to `Provider` is a nested array. Every key is optional. `Phalcon\DebugBar\ADR\Provider` and `Phalcon\DebugBar\Core` take the same array as their only argument. They do not read the `history` keys.
The second argument to `Provider` is a nested array. Every key is optional. `Phalcon\DebugBar\ADR\Provider` and `Phalcon\DebugBar\Core` take the same array as their only argument. `Core` does not read the `history` keys.

| Key | Type | Default | Purpose |
|--------------------|---------------------------|--------------------------|------------------------------------------------------------------|
Expand All @@ -244,6 +265,7 @@ The second argument to `Provider` is a nested array. Every key is optional. `Pha
| `env.strict` | `bool` | `false` | When `true`, `boot()` throws in a blocked/undefined environment instead of returning silently. |
| `env.var` | `string` | `APP_ENV` | Environment variable inspected by the gate. |
| `headers` | `bool` | `true` | Emit the `X-Debug-Bar` diagnostic header. |
| `history.base_uri` | `string` | `/` | Base URI of an ADR application. The MVC provider reads the `url` service. |
| `history.enabled` | `bool` | `false` | Store and browse recent requests for the active browser. |
| `history.url` | `string` | `/_debugbar/open` | Internal GET/DELETE endpoint path, relative to the application's base URI. |
| `history.path` | `string` | required when enabled | Absolute writable storage directory; keep it outside the document root. |
Expand Down
40 changes: 40 additions & 0 deletions src/DebugBar/ADR/HistoryAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?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\Contracts\ADR\Action;
use Phalcon\Contracts\Http\AttributeRequest;
use Phalcon\DebugBar\Contracts\History;
use Phalcon\DebugBar\History\HistoryHandler;
use Phalcon\DebugBar\Security\AccessGate;
use Phalcon\Http\Response;
use Phalcon\Http\ResponseInterface;

/**
* Serves the history endpoint in an ADR application. It creates its own response,
* so that it does not change a response of the application.
*/
final class HistoryAction implements Action
{
public function __construct(
private readonly History $history,
private readonly AccessGate $accessGate
) {
}

public function __invoke(AttributeRequest $request): ResponseInterface
{
return (new HistoryHandler($this->history, $this->accessGate, $request, new Response()))->open();
}
}
101 changes: 101 additions & 0 deletions src/DebugBar/ADR/HistoryRouter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?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\ADR\Router\RouterMatch;
use Phalcon\Contracts\ADR\ADRTypes;
use Phalcon\Contracts\ADR\Router\Router;
use Phalcon\Contracts\ADR\Router\RouterMatch as RouterMatchContract;
use Phalcon\DebugBar\History\HistoryEndpoint;
use Phalcon\Http\RequestInterface;

/**
* Sends the history URL to `HistoryAction` and all other requests to the router
* of the application. A listener cannot do this: `application:beforeHandle` cannot
* stop `handle()`, and `application:afterHandle` runs after the routing. The
* application router is `final`, so this class decorates it.
*
* @phpstan-import-type adr_middleware_map from ADRTypes
*/
final class HistoryRouter implements Router
{
public function __construct(
private readonly Router $router,
private readonly HistoryEndpoint $endpoint
) {
}

/**
* @return list<string>
*/
public function candidatesFor(string $method, string $path): array
{
return $this->router->candidatesFor($method, $path);
}

public function classFor(string $method, string $path): string
{
return $this->router->classFor($method, $path);
}

public function match(RequestInterface $request): ?RouterMatchContract
{
if ($this->endpoint->matches()) {
return new RouterMatch(HistoryAction::class);
}

return $this->router->match($request);
}

public function methodFor(string $className): ?string
{
return $this->router->methodFor($className);
}

public function pathFor(string $className): ?string
{
return $this->router->pathFor($className);
}

public function setActionDirectory(string $actionDirectory): Router
{
$this->router->setActionDirectory($actionDirectory);

return $this;
}

public function setBaseNamespace(string $baseNamespace): Router
{
$this->router->setBaseNamespace($baseNamespace);

return $this;
}

/**
* @phpstan-param adr_middleware_map $middlewareMap
*/
public function setMiddlewareMap(array $middlewareMap): Router
{
$this->router->setMiddlewareMap($middlewareMap);

return $this;
}

public function setWordSeparator(string $wordSeparator): Router
{
$this->router->setWordSeparator($wordSeparator);

return $this;
}
}
74 changes: 71 additions & 3 deletions src/DebugBar/ADR/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,21 @@

namespace Phalcon\DebugBar\ADR;

use Phalcon\Contracts\ADR\Router\Router;
use Phalcon\Contracts\Container\Service\Collection;
use Phalcon\Contracts\Container\Service\Provider as ProviderContract;
use Phalcon\Contracts\Events\Manager as ManagerContract;
use Phalcon\Contracts\Http\AttributeRequest;
use Phalcon\DebugBar\Collector\HistoryCollector;
use Phalcon\DebugBar\Contracts\History;
use Phalcon\DebugBar\Core;
use Phalcon\DebugBar\Debug;
use Phalcon\DebugBar\DebugBarTypes;
use Phalcon\DebugBar\Exceptions\CannotUseInProduction;
use Phalcon\DebugBar\History\FilesystemHistory;
use Phalcon\DebugBar\History\HistoryCookie;
use Phalcon\DebugBar\History\HistoryEndpoint;
use Phalcon\DebugBar\History\HistoryOptions;
use Phalcon\Events\ManagerInterface;
use Phalcon\Http\RequestInterface;

Expand All @@ -29,20 +36,29 @@
* 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. `Bridge`
* feeds the collectors from the other events of the application.
* feeds the collectors from the other events of the application. When request
* history is enabled, `HistoryRouter` sends the history URL to `HistoryAction`,
* and the application routes do not change.
*
* @phpstan-import-type provider_config from DebugBarTypes
*/
final class Provider implements ProviderContract
{
private Core $core;

private ?HistoryCookie $historyCookie = null;

private ?HistoryEndpoint $historyEndpoint = null;

private HistoryOptions $historyOptions;

/**
* @param provider_config $config
*/
public function __construct(array $config = [])
{
$this->core = new Core($config);
$this->core = new Core($config);
$this->historyOptions = HistoryOptions::fromConfig($config['history'] ?? []);
}

/**
Expand All @@ -57,9 +73,15 @@ public function provide(Collection $services): void
$eventsManager = $this->resolve($services, ManagerContract::class);
$eventsManager = $eventsManager instanceof ManagerInterface ? $eventsManager : null;
$request = $this->resolve($services, AttributeRequest::class);
$request = $request instanceof RequestInterface ? $request : null;
$history = $this->registerHistory($services, $eventsManager, $request);
$listener = $this->core->boot(
$eventsManager,
$request instanceof RequestInterface ? $request : null
$request,
null,
$history,
$this->historyEndpoint,
$this->historyCookie
);
$bar = Debug::getBar();

Expand All @@ -71,6 +93,52 @@ public function provide(Collection $services): void
(new Bridge($bar))->subscribe($eventsManager);
}

/**
* Registers the Action and decorates the router. The router must not be
* resolved yet: `extend()` throws for a resolved service.
*/
private function registerHistory(
Collection $services,
?ManagerInterface $eventsManager,
?RequestInterface $request
): ?History {
if (
!$this->historyOptions->enabled
|| !$this->core->isCollectorEnabled(HistoryCollector::NAME)
) {
return null;
}

$this->historyOptions->validate();

if (null === $eventsManager || !$services->has(Router::class)) {
return null;
}

$this->historyCookie = HistoryCookie::fromGlobals();
$history = new FilesystemHistory($this->historyOptions, null, $this->historyCookie);
$history->validateStorage();
$accessGate = $this->core->getAccessGate();
$baseUri = $this->historyOptions->baseUri;
$endpoint = new HistoryEndpoint(
$this->historyOptions,
fn (): ?RequestInterface => $request,
fn (): string => $baseUri
);
$this->historyEndpoint = $endpoint;

$services->set(
HistoryAction::class,
fn (): HistoryAction => new HistoryAction($history, $accessGate)
);
$services->extend(
Router::class,
fn (Router $router): Router => new HistoryRouter($router, $endpoint)
);

return $history;
}

private function resolve(Collection $services, string $name): mixed
{
return $services->has($name) ? $services->get($name) : null;
Expand Down
18 changes: 10 additions & 8 deletions src/DebugBar/DebugBarTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,23 @@
* @phpstan-type view_pending array{path: string, start: int|float}
* @phpstan-type view_render array{path: string, time: int|float}
* @phpstan-type request_context array{0: string|null, 1: bool}
* @phpstan-type history_config array{
* enabled?: bool,
* url?: string,
* path?: string,
* max_requests?: int,
* ttl_seconds?: int,
* max_browsers?: int,
* base_uri?: string
* }
* @phpstan-type provider_config array{
* env?: array{var?: string, blocked?: list<string>, strict?: bool},
* enabled?: bool,
* assets?: array{nonce?: string|null},
* access?: array{allow_ips?: list<string>, callback?: (\Closure(): bool)|null},
* collectors?: array<string, bool>,
* headers?: bool,
* history?: array{
* enabled?: bool,
* url?: string,
* path?: string,
* max_requests?: int,
* ttl_seconds?: int,
* max_browsers?: int
* },
* history?: history_config,
* redact?: array{mask?: list<string>, hidden?: list<string>}
* }
*/
Expand Down
Loading
Loading