Skip to content

Commit 3a236fe

Browse files
committed
RoutingPanel: added support for AI agents
1 parent 4473221 commit 3a236fe

5 files changed

Lines changed: 318 additions & 0 deletions

File tree

phpstan.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,10 @@ parameters:
211211
- offsetAccess.notFound
212212
- closure.unusedUse
213213
- identical.alwaysFalse
214+
215+
# Tracy routing agent panel: compiled Latte template with recursive block array (self-referencing $_blocks)
216+
-
217+
path: src/Bridges/ApplicationTracy/dist/panel.agent.phtml
218+
identifiers:
219+
- offsetAccess.notFound
220+
- closure.unusedUse

src/Bridges/ApplicationTracy/RoutingPanel.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,19 @@ public function getPanel(): string
6767
}
6868

6969

70+
public function getAgentInfo(): string
71+
{
72+
return Nette\Utils\Helpers::capture(function () {
73+
$matched = $this->matched;
74+
$routes = $this->routes;
75+
$source = $this->matched ? $this->findSource() : null;
76+
$url = $this->httpRequest->getUrl();
77+
$method = $this->httpRequest->getMethod();
78+
require __DIR__ . '/dist/panel.agent.phtml';
79+
});
80+
}
81+
82+
7083
/** @return array{path: string, domain: ?string, module: string, routes: array<mixed>} */
7184
private function analyse(Routing\RouteList $router, ?Nette\Http\IRequest $httpRequest): array
7285
{
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php declare(strict_types=1);
2+
3+
/** @var ?array<string,mixed> $matched */
4+
/** @var array{path:string,domain:?string,module:string,routes:array<mixed>} $routes */
5+
/** @var \ReflectionClass<Nette\Application\IPresenter>|\ReflectionMethod|string|null $source */
6+
/** @var Nette\Http\UrlScript $url */
7+
/** @var string $method */
8+
use Nette\Application\UI\Presenter;
9+
echo "\n";
10+
$_blocks['route'] = function ($route,$path) use (&$_blocks) {
11+
$mark = ['yes' => 'MATCHED', 'may' => 'MAY MATCH', 'no' => 'no match', 'oneway' => 'one-way', 'error' => 'ERROR'][$route->matched] ?? $route->matched /* pos 9:2 */;
12+
$mask = $route->mask !== null ? $path . $route->mask : $route->class /* pos 10:2 */;
13+
$defaults = [] /* pos 11:2 */;
14+
foreach ($route->defaults as $k => $v) /* pos 12:2 */ {
15+
$defaults[] = $k . '=' . (is_string($v) ? $v : (is_scalar($v) ? var_export($v, true) : get_debug_type($v))) /* pos 12:40 */;
16+
}
17+
18+
echo '- [';
19+
echo Tracy\Helpers::escapeMd($mark) /* pos 13:6 */;
20+
echo '] `';
21+
echo $mask /* pos 13:16 */;
22+
echo '`';
23+
if ($defaults) /* pos 13:33 */ {
24+
echo ' (';
25+
echo Tracy\Helpers::escapeMd(implode(', ', $defaults)) /* pos 13:49 */;
26+
echo ')';
27+
}
28+
if ($route->error) /* pos 13:82 */ {
29+
echo '';
30+
echo Tracy\Helpers::escapeMd($route->error->getMessage()) /* pos 13:105 */;
31+
}
32+
echo "\n";
33+
};
34+
echo "\n";
35+
$_blocks['routeList'] = function ($list,$path) use (&$_blocks) {
36+
if ($list['domain']) /* pos 17:2 */ {
37+
echo '- domain: `';
38+
echo Tracy\Helpers::escapeMd($list['domain']) /* pos 17:31 */;
39+
echo '`
40+
';
41+
}
42+
if ($list['module']) /* pos 19:2 */ {
43+
echo '- module: `';
44+
echo Tracy\Helpers::escapeMd($list['module']) /* pos 19:31 */;
45+
echo '`
46+
';
47+
}
48+
$path .= $list['path'] /* pos 21:2 */;
49+
foreach ($list['routes'] as $router) /* pos 22:2 */ {
50+
if (is_array($router)) /* pos 23:3 */ {
51+
$_blocks['routeList']($router,$path);
52+
} else /* pos 25:3 */ {
53+
$_blocks['route']($router,$path);
54+
}
55+
56+
}
57+
58+
};
59+
echo '
60+
## Routing
61+
62+
`';
63+
echo Tracy\Helpers::escapeMd($method) /* pos 33:2 */;
64+
echo ' ';
65+
echo Tracy\Helpers::escapeMd($url->getAbsoluteUrl()) /* pos 33:12 */;
66+
echo '`
67+
68+
';
69+
if ($matched === null) /* pos 35:1 */ {
70+
echo '**Matched:** no route
71+
';
72+
} else /* pos 37:1 */ {
73+
$target = ($matched[Presenter::PresenterKey] ?? '?') . ':' . ($matched[Presenter::ActionKey] ?? Presenter::DefaultAction) /* pos 38:2 */;
74+
echo '**Matched:** ';
75+
echo Tracy\Helpers::escapeMd($target) /* pos 39:15 */;
76+
if (isset($matched[Presenter::SignalKey])) /* pos 39:24 */ {
77+
echo Tracy\Helpers::escapeMd($matched[Presenter::SignalKey]) /* pos 39:66 */;
78+
echo '!';
79+
}
80+
echo "\n";
81+
if (is_string($source)) /* pos 40:2 */ {
82+
echo '**Handler:** ';
83+
echo Tracy\Helpers::escapeMd($source) /* pos 41:16 */;
84+
echo ' (class not found)
85+
';
86+
} elseif ($source instanceof \ReflectionMethod) /* pos 42:2 */ {
87+
echo '**Handler:** ';
88+
echo Tracy\Helpers::escapeMd($source->getDeclaringClass()->getName()) /* pos 43:16 */;
89+
echo '::';
90+
echo Tracy\Helpers::escapeMd($source->getName()) /* pos 43:59 */;
91+
echo '() in ';
92+
echo Tracy\Helpers::escapeMd($source->getFileName()) /* pos 43:85 */;
93+
echo ':';
94+
echo Tracy\Helpers::escapeMd($source->getStartLine()) /* pos 43:110 */;
95+
echo "\n";
96+
} elseif ($source instanceof \ReflectionClass) /* pos 44:2 */ {
97+
echo '**Handler:** ';
98+
echo Tracy\Helpers::escapeMd($source->getName()) /* pos 45:16 */;
99+
echo ' in ';
100+
echo Tracy\Helpers::escapeMd($source->getFileName()) /* pos 45:40 */;
101+
echo ':';
102+
echo Tracy\Helpers::escapeMd($source->getStartLine()) /* pos 45:65 */;
103+
echo "\n";
104+
}
105+
106+
107+
$skip = [Presenter::PresenterKey => 1, Presenter::ActionKey => 1, Presenter::SignalKey => 1] /* pos 47:2 */;
108+
$extras = array_diff_key($matched, $skip) /* pos 48:2 */;
109+
if ($extras) /* pos 49:2 */ {
110+
echo '**Parameters:**
111+
';
112+
foreach ($extras as $key => $value) /* pos 51:3 */ {
113+
echo '- ';
114+
echo Tracy\Helpers::escapeMd($key) /* pos 52:6 */;
115+
echo ' = ';
116+
echo Tracy\Helpers::escapeMd(is_string($value) ? $value : (is_scalar($value) ? var_export($value, true) : get_debug_type($value))) /* pos 52:15 */;
117+
echo "\n";
118+
119+
}
120+
121+
}
122+
}
123+
echo '
124+
### Routes
125+
';
126+
if (empty($routes['routes'])) /* pos 58:1 */ {
127+
echo '(none defined)
128+
';
129+
} else /* pos 60:1 */ {
130+
$_blocks['routeList']($routes,'');
131+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{varType ?array<string, mixed> $matched}
2+
{varType array{path: string, domain: ?string, module: string, routes: array<mixed>} $routes}
3+
{varType \ReflectionClass<Nette\Application\IPresenter>|\ReflectionMethod|string|null $source}
4+
{varType Nette\Http\UrlScript $url}
5+
{varType string $method}
6+
{use Nette\Application\UI\Presenter}
7+
8+
{define route $route, $path}
9+
{do $mark = [yes => 'MATCHED', may => 'MAY MATCH', no => 'no match', oneway => 'one-way', error => 'ERROR'][$route->matched] ?? $route->matched}
10+
{do $mask = $route->mask !== null ? $path . $route->mask : $route->class}
11+
{do $defaults = []}
12+
{foreach $route->defaults as $k => $v}{do $defaults[] = $k . '=' . (is_string($v) ? $v : (is_scalar($v) ? var_export($v, true) : get_debug_type($v)))}{/foreach}
13+
- [{$mark}] `{$mask|noescape}`{if $defaults} ({=implode(', ', $defaults)}){/if}{if $route->error}{$route->error->getMessage()}{/if}
14+
{/define}
15+
16+
{define routeList $list, $path}
17+
{if $list[domain]}- domain: `{$list[domain]}`
18+
{/if}
19+
{if $list[module]}- module: `{$list[module]}`
20+
{/if}
21+
{do $path .= $list[path]}
22+
{foreach $list[routes] as $router}
23+
{if is_array($router)}
24+
{include routeList $router, $path}
25+
{else}
26+
{include route $router, $path}
27+
{/if}
28+
{/foreach}
29+
{/define}
30+
31+
## Routing
32+
33+
`{$method} {$url->getAbsoluteUrl()}`
34+
35+
{if $matched === null}
36+
**Matched:** no route
37+
{else}
38+
{do $target = ($matched[Presenter::PresenterKey] ?? '?') . ':' . ($matched[Presenter::ActionKey] ?? Presenter::DefaultAction)}
39+
**Matched:** {$target}{if isset($matched[Presenter::SignalKey])}{$matched[Presenter::SignalKey]}!{/if}
40+
{if is_string($source)}
41+
**Handler:** {$source} (class not found)
42+
{elseif $source instanceof \ReflectionMethod}
43+
**Handler:** {$source->getDeclaringClass()->getName()}::{$source->getName()}() in {$source->getFileName()}:{$source->getStartLine()}
44+
{elseif $source instanceof \ReflectionClass}
45+
**Handler:** {$source->getName()} in {$source->getFileName()}:{$source->getStartLine()}
46+
{/if}
47+
{do $skip = [Presenter::PresenterKey => 1, Presenter::ActionKey => 1, Presenter::SignalKey => 1]}
48+
{do $extras = array_diff_key($matched, $skip)}
49+
{if $extras}
50+
**Parameters:**
51+
{foreach $extras as $key => $value}
52+
- {$key} = {is_string($value) ? $value : (is_scalar($value) ? var_export($value, true) : get_debug_type($value))}
53+
{/foreach}
54+
{/if}
55+
{/if}
56+
57+
### Routes
58+
{if empty($routes[routes])}
59+
(none defined)
60+
{else}
61+
{include routeList $routes, ''}
62+
{/if}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php declare(strict_types=1);
2+
3+
use Nette\Application\PresenterFactory;
4+
use Nette\Application\Routers\RouteList;
5+
use Nette\Bridges\ApplicationTracy\RoutingPanel;
6+
use Nette\Http;
7+
use Nette\Http\UrlScript;
8+
use Tester\Assert;
9+
10+
require __DIR__ . '/../bootstrap.php';
11+
12+
13+
$router = new RouteList;
14+
$router->addRoute('sign-in', 'Sign:in');
15+
$router->withPath('admin/')
16+
->addRoute('', 'Admin:Home:default')
17+
->addRoute('<presenter>/<action=default>', ['module' => 'Admin']);
18+
$router->addRoute('<presenter>/<action=default>');
19+
20+
21+
test('matched route', function () use ($router) {
22+
$httpRequest = new Http\Request(new UrlScript('http://example.com/admin', '/'));
23+
$panel = new RoutingPanel($router, $httpRequest, new PresenterFactory(function () {}));
24+
$panel->getTab();
25+
26+
Assert::match(
27+
<<<'XX'
28+
%A%
29+
## Routing
30+
31+
`GET http://example.com/admin`
32+
33+
**Matched:** Admin:Home:default
34+
%A%
35+
36+
### Routes
37+
- [no match] `sign-in` (presenter=Sign, action=in)
38+
- [MATCHED] `admin/` (presenter=Admin:Home, action=default)
39+
- [no match] `admin/<presenter>/<action=default>` (module=Admin, action=default)
40+
- [MAY MATCH] `<presenter>/<action=default>` (action=default)
41+
XX,
42+
$panel->getAgentInfo(),
43+
);
44+
});
45+
46+
47+
test('no matching route', function () {
48+
$router = new RouteList;
49+
$router->addRoute('sign-in', 'Sign:in');
50+
51+
$httpRequest = new Http\Request(new UrlScript('http://example.com/nowhere', '/'));
52+
$panel = new RoutingPanel($router, $httpRequest, new PresenterFactory(function () {}));
53+
$panel->getTab();
54+
55+
Assert::match(
56+
<<<'XX'
57+
%A%
58+
## Routing
59+
60+
`GET http://example.com/nowhere`
61+
62+
**Matched:** no route
63+
64+
### Routes
65+
- [no match] `sign-in` (presenter=Sign, action=in)
66+
XX,
67+
$panel->getAgentInfo(),
68+
);
69+
});
70+
71+
72+
test('no routes defined', function () {
73+
$router = new RouteList;
74+
75+
$httpRequest = new Http\Request(new UrlScript('http://example.com/', '/'));
76+
$panel = new RoutingPanel($router, $httpRequest, new PresenterFactory(function () {}));
77+
$panel->getTab();
78+
79+
Assert::match(
80+
<<<'XX'
81+
%A%
82+
## Routing
83+
84+
`GET http://example.com/`
85+
86+
**Matched:** no route
87+
88+
### Routes
89+
(none defined)
90+
XX,
91+
$panel->getAgentInfo(),
92+
);
93+
});
94+
95+
96+
test('matched with extra parameters', function () use ($router) {
97+
$httpRequest = new Http\Request(new UrlScript('http://example.com/article/detail?id=42', '/'));
98+
$panel = new RoutingPanel($router, $httpRequest, new PresenterFactory(function () {}));
99+
$panel->getTab();
100+
101+
$output = $panel->getAgentInfo();
102+
Assert::contains('**Matched:** Article:detail', $output);
103+
Assert::contains('**Parameters:**', $output);
104+
Assert::contains('- id = 42', $output);
105+
});

0 commit comments

Comments
 (0)