Skip to content

Commit 2f5a3bc

Browse files
authored
feat(view): standalone TempestViewRenderer support (#1686)
1 parent 745ff7b commit 2f5a3bc

15 files changed

+361
-37
lines changed

docs/1-essentials/02-views.md

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,83 @@ Tempest views are always compiled to plain PHP code before being rendered. Durin
622622

623623
During deployments, that cache must be cleared in order to not serve outdated views to users. You may do that by running `tempest view:clear` on every deploy.
624624

625+
## Tempest View as a standalone engine
626+
627+
Tempest View is also designed to be used as a standalone engine in whatever PHP project you want. Start by requiring `tempest/view`:
628+
629+
```sh
630+
composer require tempest/view
631+
```
632+
633+
As a bare minimum setup, you can create an instance of the renderer by calling `TempestViewRenderer::make()`:
634+
635+
```php
636+
use Tempest\View\Renderers\TempestViewRenderer;
637+
use function Tempest\view;
638+
639+
$renderer = TempestViewRenderer::make();
640+
641+
$html = $renderer->render(view('home.view.php', name: 'Brent'));
642+
```
643+
644+
If, however, you want view component support, you will need to provide a `ViewConfig` object as well:
645+
646+
```php
647+
use Tempest\View\Renderers\TempestViewRenderer;
648+
use Tempest\View\ViewConfig;
649+
650+
$config = new ViewConfig()->addViewComponents(
651+
__DIR__ . '/components/x-base.view.php',
652+
__DIR__ . '/components/x-footer.view.php',
653+
__DIR__ . '/components/x-header.view.php',
654+
);
655+
656+
$renderer = TempestViewRenderer::make($config);
657+
```
658+
659+
If you want to rely on Tempest's discovery to find view components, you can boot a minimal version of Tempest, and resolve the view renderer from the container:
660+
661+
```php
662+
use Tempest\Core\Tempest;
663+
use Tempest\View\ViewRenderer;
664+
665+
$container = Tempest::boot(__DIR__);
666+
667+
$html = $container->get(ViewRenderer::class)->render(
668+
view('home.view.php', name: 'Brent')
669+
);
670+
```
671+
672+
You can choose whichever way you prefer. Chances are that, if you use the minimal setup without booting Tempest, you'll want to add a custom view component loader. That's up to you to implement then.
673+
674+
### A note on caching
675+
676+
When you're using the minimal setup, view caching can be enabled by passing in a `$viewCache` paremeter into `TempestViewRenderer::make()`:
677+
678+
```php
679+
use Tempest\View\Renderers\TempestViewRenderer;
680+
use Tempest\View\ViewCache;
681+
682+
$renderer = TempestViewRenderer::make(
683+
cache: ViewCache::enabled(),
684+
);
685+
```
686+
687+
It's recommended to turn view caching on in production environments. To clear the view cache, you can call the `clear()` method on the `ViewCache` object:
688+
689+
```php
690+
use Tempest\View\Renderers\TempestViewRenderer;
691+
use Tempest\View\ViewCache;
692+
693+
$viewCache = ViewCache::enabled();
694+
695+
$viewCache->clear();
696+
697+
$renderer = TempestViewRenderer::make(
698+
cache: $viewCache,
699+
);
700+
```
701+
625702
## Separate view directories
626703

627704
View files can live in any directory that is discoverable by Tempest. That means: a directory with a PSR-4 namespace associated with it. If you want your view files to live outside of `src` or `app`, you can add a namespace for it in composer.json:
@@ -637,7 +714,7 @@ View files can live in any directory that is discoverable by Tempest. That means
637714

638715
Don't forget to run `composer up` after making changes to your composer.json file.
639716

640-
Note that view files themselves don't need a namespace; this namespace is only here to tell Tempest that `views/` is a directory it should scan. If you want to add a class in the `Views` namespace (like, for example, a [custom view object](/2.x/essentials/views#using-dedicated-view-objects)), then that is possible as well.
717+
Note that view files themselves don't need a namespace; this namespace is only here to tell Tempest that `views/` is a directory it should scan. If you want to add a class in the `Views` namespace (like, for example, a [custom view object](/2.x/essentials/views#using-dedicated-view-objects)), then that is possible as well.
641718

642719
## Using other engines
643720

docs/5-extra-topics/02-standalone-components.md

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -97,25 +97,7 @@ ld($variable);
9797

9898
## `tempest/view`
9999

100-
Tempest View can be used as a standalone package:
101-
102-
```
103-
composer require tempest/view
104-
```
105-
106-
```php
107-
$container = Tempest::boot(__DIR__);
108-
109-
$view = view(__DIR__ . '/src/b.view.php');
110-
111-
echo $container->get(ViewRenderer::class)->render($view);
112-
```
113-
114-
There are a couple of notes to make when running Tempest View as a standalone component:
115-
116-
- Any view files and components will be discovered and must be in a directory with a valid PSR-4 namespace. View files themselves don't need to have a namespace, though.
117-
- View files are compiled and cached. You can manually enable or disable this cache by setting the `{env}{:hl-keyword:VIEW_CACHE:}` environment variable to `true` or `false`. By default, the view cache is disabled.
118-
- Optionally, you can require `tempest/console`, which will provide you with the `vendor/bin/tempest view:clear` command to clear view caches. If you don't install `tempest/console`, you'll have to manually clear view caches on deployment by removing the `.tempest/cache/views` directory.
100+
Tempest View can be used as a standalone package. You can read about how to use it [here](/2.x/essentials/views#tempest-view-as-a-standalone-engine).
119101

120102
## `tempest/event-bus`
121103

packages/view/src/Elements/ElementFactory.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
namespace Tempest\View\Elements;
66

7-
use Tempest\Container\Container;
8-
use Tempest\Core\AppConfig;
7+
use Tempest\Core\Environment;
98
use Tempest\View\Attributes\PhpAttribute;
109
use Tempest\View\Element;
1110
use Tempest\View\Parser\TempestViewCompiler;
@@ -18,14 +17,13 @@ final class ElementFactory
1817
{
1918
private TempestViewCompiler $compiler;
2019

20+
private(set) bool $isHtml = false;
21+
2122
public function __construct(
22-
private readonly AppConfig $appConfig,
2323
private readonly ViewConfig $viewConfig,
24-
private readonly Container $container,
24+
private readonly Environment $environment,
2525
) {}
2626

27-
private(set) bool $isHtml = false;
28-
2927
public function setViewCompiler(TempestViewCompiler $compiler): self
3028
{
3129
$this->compiler = $compiler;
@@ -93,7 +91,7 @@ private function makeElement(Token $token, ?Element $parent): ?Element
9391
if ($viewComponentClass = $this->viewConfig->viewComponents[$token->tag] ?? null) {
9492
$element = new ViewComponentElement(
9593
token: $token,
96-
environment: $this->appConfig->environment,
94+
environment: $this->environment,
9795
compiler: $this->compiler,
9896
viewComponent: $viewComponentClass,
9997
attributes: $attributes,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tempest\View\Exceptions;
4+
5+
use Exception;
6+
7+
final class ViewComponentPathWasInvalid extends Exception
8+
{
9+
public function __construct(string $fileName)
10+
{
11+
parent::__construct("View component file names must start with `x-` and end with `.view.php`, instead got `{$fileName}`.");
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tempest\View\Exceptions;
4+
5+
use Exception;
6+
7+
final class ViewComponentPathWasNotFound extends Exception
8+
{
9+
public function __construct(string $fileName)
10+
{
11+
parent::__construct("There's no view component file at `{$fileName}`.");
12+
}
13+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Tempest\View\Initializers;
4+
5+
use Tempest\Container\Container;
6+
use Tempest\Container\Initializer;
7+
use Tempest\Container\Singleton;
8+
use Tempest\Core\AppConfig;
9+
use Tempest\View\Elements\ElementFactory;
10+
use Tempest\View\ViewConfig;
11+
12+
final class ElementFactoryInitializer implements Initializer
13+
{
14+
#[Singleton]
15+
public function initialize(Container $container): ElementFactory
16+
{
17+
return new ElementFactory(
18+
$container->get(ViewConfig::class),
19+
$container->get(AppConfig::class)->environment,
20+
);
21+
}
22+
}

packages/view/src/ViewCacheInitializer.php renamed to packages/view/src/Initializers/ViewCacheInitializer.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
<?php
22

3-
namespace Tempest\View;
3+
namespace Tempest\View\Initializers;
44

55
use Tempest\Container\Container;
66
use Tempest\Container\Initializer;
77
use Tempest\Container\Singleton;
88
use Tempest\Core\AppConfig;
9+
use Tempest\View\ViewCache;
910

1011
use function Tempest\env;
1112

packages/view/src/Parser/TempestViewCompiler.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace Tempest\View\Parser;
66

7-
use Tempest\Core\Kernel;
87
use Tempest\Discovery\DiscoveryLocation;
98
use Tempest\Support\Filesystem;
109
use Tempest\View\Attribute;
@@ -30,7 +29,8 @@
3029
public function __construct(
3130
private ElementFactory $elementFactory,
3231
private AttributeFactory $attributeFactory,
33-
private Kernel $kernel,
32+
/** @var DiscoveryLocation[] */
33+
private array $discoveryLocations = [],
3434
) {}
3535

3636
public function compile(string|View $view): string
@@ -86,7 +86,7 @@ private function retrieveTemplate(string|View $view): string
8686

8787
$searchPathOptions = [
8888
...$searchPathOptions,
89-
...arr($this->kernel->discoveryLocations)
89+
...arr($this->discoveryLocations)
9090
->map(fn (DiscoveryLocation $discoveryLocation) => path($discoveryLocation->path, $path)->toString())
9191
->toArray(),
9292
];

packages/view/src/Renderers/TempestViewRenderer.php

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66

77
use Stringable;
88
use Tempest\Container\Container;
9+
use Tempest\Core\Environment;
910
use Tempest\Support\Filesystem;
1011
use Tempest\Support\Html\HtmlString;
12+
use Tempest\View\Attributes\AttributeFactory;
13+
use Tempest\View\Elements\ElementFactory;
1114
use Tempest\View\Exceptions\ViewCompilationFailed;
1215
use Tempest\View\Exceptions\ViewVariableWasReserved;
1316
use Tempest\View\GenericView;
@@ -26,9 +29,38 @@ public function __construct(
2629
private readonly TempestViewCompiler $compiler,
2730
private readonly ViewCache $viewCache,
2831
private readonly ViewConfig $viewConfig,
29-
private readonly Container $container,
32+
private readonly ?Container $container,
3033
) {}
3134

35+
public static function make(
36+
?ViewConfig $viewConfig = null,
37+
?ViewCache $viewCache = null,
38+
Environment $environment = Environment::PRODUCTION,
39+
): self {
40+
$viewConfig ??= new ViewConfig();
41+
42+
$elementFactory = new ElementFactory(
43+
$viewConfig,
44+
$environment,
45+
);
46+
47+
$compiler = new TempestViewCompiler(
48+
elementFactory: $elementFactory,
49+
attributeFactory: new AttributeFactory(),
50+
);
51+
52+
$elementFactory->setViewCompiler($compiler);
53+
54+
$viewCache ??= ViewCache::disabled();
55+
56+
return new self(
57+
compiler: $compiler,
58+
viewCache: $viewCache,
59+
viewConfig: $viewConfig,
60+
container: null,
61+
);
62+
}
63+
3264
public function __get(string $name): mixed
3365
{
3466
return $this->currentView?->get($name);
@@ -58,8 +90,12 @@ public function render(string|View $view): string
5890
private function processView(View $view): View
5991
{
6092
foreach ($this->viewConfig->viewProcessors as $viewProcessorClass) {
61-
/** @var \Tempest\View\ViewProcessor $viewProcessor */
62-
$viewProcessor = $this->container->get($viewProcessorClass);
93+
if ($this->container) {
94+
/** @var \Tempest\View\ViewProcessor $viewProcessor */
95+
$viewProcessor = $this->container->get($viewProcessorClass);
96+
} else {
97+
$viewProcessor = new $viewProcessorClass();
98+
}
6399

64100
$view = $viewProcessor->process($view);
65101
}

packages/view/src/ViewCache.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ public function __construct(
2020
);
2121
}
2222

23+
public static function enabled(?string $path = null): self
24+
{
25+
return new self(
26+
enabled: true,
27+
pool: new ViewCachePool($path ?? __DIR__ . '/../.tempest/cache'),
28+
);
29+
}
30+
31+
public static function disabled(?string $path = null): self
32+
{
33+
return new self(
34+
enabled: false,
35+
pool: new ViewCachePool($path ?? __DIR__ . '/../.tempest/cache'),
36+
);
37+
}
38+
2339
public function clear(): void
2440
{
2541
$this->pool->clear();

0 commit comments

Comments
 (0)