Skip to content

Commit 2479148

Browse files
authored
feat(view): support relative view paths (#953)
1 parent dff166b commit 2479148

File tree

9 files changed

+107
-11
lines changed

9 files changed

+107
-11
lines changed

src/Tempest/Framework/Testing/IntegrationTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,19 @@ protected function tearDown(): void
8787
{
8888
parent::tearDown();
8989

90+
/** @phpstan-ignore-next-line */
9091
unset($this->root);
92+
/** @phpstan-ignore-next-line */
9193
unset($this->discoveryLocations);
94+
/** @phpstan-ignore-next-line */
9295
unset($this->appConfig);
96+
/** @phpstan-ignore-next-line */
9397
unset($this->kernel);
98+
/** @phpstan-ignore-next-line */
9499
unset($this->container);
100+
/** @phpstan-ignore-next-line */
95101
unset($this->console);
102+
/** @phpstan-ignore-next-line */
96103
unset($this->http);
97104
}
98105
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tempest\Mapper\Exceptions;
4+
5+
use Exception;
6+
7+
final class ViewNotFound extends Exception
8+
{
9+
public function __construct(string $path)
10+
{
11+
parent::__construct("View {$path} not found");
12+
}
13+
}

src/Tempest/View/src/IsView.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44

55
namespace Tempest\View;
66

7+
use function Tempest\path;
8+
79
/** @phpstan-require-implements \Tempest\View\View */
810
trait IsView
911
{
1012
public string $path;
1113

14+
public ?string $relativeRootPath = null;
15+
1216
public array $data = [];
1317

1418
public function __construct(
@@ -17,6 +21,14 @@ public function __construct(
1721
) {
1822
$this->path = $path;
1923
$this->data = $data;
24+
25+
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
26+
27+
if (str_ends_with($trace[0]['file'], 'Tempest/View/src/functions.php')) {
28+
$this->relativeRootPath = path($trace[1]['file'])->dirname();
29+
} else {
30+
$this->relativeRootPath = path($trace[0]['file'])->dirname();
31+
}
2032
}
2133

2234
public function get(string $key): mixed

src/Tempest/View/src/Renderers/TempestViewCompiler.php

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
use Dom\NodeList;
99
use Exception;
1010
use Tempest\Core\Kernel;
11+
use Tempest\Discovery\DiscoveryLocation;
12+
use Tempest\Mapper\Exceptions\ViewNotFound;
1113
use Tempest\View\Attributes\AttributeFactory;
1214
use Tempest\View\Element;
1315
use Tempest\View\Elements\ElementFactory;
16+
use Tempest\View\View;
1417
use function Tempest\path;
18+
use function Tempest\Support\arr;
1519
use function Tempest\Support\str;
1620
use const Dom\HTML_NO_DEFAULT_NS;
1721

@@ -35,12 +39,12 @@ public function __construct(
3539
private Kernel $kernel,
3640
) {}
3741

38-
public function compile(string $path): string
42+
public function compile(string|View $view): string
3943
{
4044
$this->elementFactory->setViewCompiler($this);
4145

4246
// 1. Retrieve template
43-
$template = $this->retrieveTemplate($path);
47+
$template = $this->retrieveTemplate($view);
4448

4549
// 2. Parse as DOM
4650
$dom = $this->parseDom($template);
@@ -57,23 +61,38 @@ public function compile(string $path): string
5761
return $compiled;
5862
}
5963

60-
private function retrieveTemplate(string $path): string
64+
private function retrieveTemplate(string|View $view): string
6165
{
66+
$path = $view instanceof View ? $view->path : $view;
67+
6268
if (! str_ends_with($path, '.php')) {
6369
return $path;
6470
}
6571

66-
$discoveryLocations = $this->kernel->discoveryLocations;
72+
$searchPathOptions = [
73+
$path
74+
];
6775

68-
$searchPath = $path;
76+
if ($view instanceof View && $view->relativeRootPath !== null) {
77+
$searchPathOptions[] = path($view->relativeRootPath, $path)->toString();
78+
}
6979

70-
while (! file_exists($searchPath) && $location = current($discoveryLocations)) {
71-
$searchPath = path($location->path, $path)->toString();
72-
next($discoveryLocations);
80+
$searchPathOptions = [
81+
...$searchPathOptions,
82+
...arr($this->kernel->discoveryLocations)
83+
->map(fn (DiscoveryLocation $discoveryLocation) => path($discoveryLocation->path, $path)->toString())
84+
->toArray()
85+
];
86+
87+
foreach ($searchPathOptions as $searchPath)
88+
{
89+
if (file_exists($searchPath)) {
90+
break;
91+
}
7392
}
7493

7594
if (! file_exists($searchPath)) {
76-
throw new Exception("View {$searchPath} not found");
95+
throw new ViewNotFound($path);
7796
}
7897

7998
return file_get_contents($searchPath);

src/Tempest/View/src/Renderers/TempestViewRenderer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function render(string|View $view): string
4343

4444
$path = $this->viewCache->getCachedViewPath(
4545
path: $view->path,
46-
compiledView: fn () => $this->cleanupCompiled($this->compiler->compile($view->path)),
46+
compiledView: fn () => $this->cleanupCompiled($this->compiler->compile($view)),
4747
);
4848

4949
return $this->renderCompiled($view, $path);

src/Tempest/View/src/View.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ interface View
1010
get;
1111
}
1212

13+
public ?string $relativeRootPath {
14+
get;
15+
}
16+
1317
public array $data {
1418
get;
1519
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Fixtures\Controllers;
4+
5+
use Tempest\Router\Get;
6+
use Tempest\View\GenericView;
7+
use Tempest\View\View;
8+
use function Tempest\view;
9+
10+
final class RelativeViewController
11+
{
12+
#[Get('/relative-view-controller-with-view-function')]
13+
public function asFunction(): View
14+
{
15+
return view('./relative-view.view.php');
16+
}
17+
18+
#[Get('/relative-view-controller-with-view-object')]
19+
public function asObject(): View
20+
{
21+
return new GenericView('./relative-view.view.php');
22+
}
23+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Yes!

tests/Integration/View/TempestViewRendererTest.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
use Tempest\Support\HtmlString;
88
use Tempest\View\Exceptions\InvalidElement;
99
use Tempest\View\ViewCache;
10+
use Tests\Tempest\Fixtures\Controllers\RelativeViewController;
1011
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
12+
use function Tempest\uri;
1113
use function Tempest\view;
1214

1315
/**
@@ -45,6 +47,21 @@ public function test_view_renderer(): void
4547
);
4648
}
4749

50+
public function test_relative_view_path_rendering(): void
51+
{
52+
if (PHP_OS_FAMILY === 'Windows') {
53+
$this->markTestSkipped('Relative paths not supported on Windows');
54+
}
55+
56+
$this->http->get(uri([RelativeViewController::class, 'asFunction']))
57+
->assertOk()
58+
->assertSee('Yes!');
59+
60+
$this->http->get(uri([RelativeViewController::class, 'asObject']))
61+
->assertOk()
62+
->assertSee('Yes!');
63+
}
64+
4865
public function test_if_attribute(): void
4966
{
5067
$this->assertSame(
@@ -496,4 +513,4 @@ public function test_html_tags(): void
496513
$this->assertStringContainsString('<body', $html);
497514
$this->assertStringContainsString('<!-- test comment -->', $html);
498515
}
499-
}
516+
}

0 commit comments

Comments
 (0)