Skip to content

Commit 68dcd89

Browse files
committed
ten tips
1 parent 145920a commit 68dcd89

File tree

2 files changed

+337
-0
lines changed

2 files changed

+337
-0
lines changed
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
---
2+
title: Ten Tempest Tips
3+
description: "Ten things you might now know about Tempest"
4+
author: brent
5+
tag: Thoughts
6+
---
7+
8+
With the release of Tempest 1.0, many people wonder what the framework is about. There is so much to talk about, and I decided to highlight a couple of features in this blog post. I hope it might intrigue you to give Tempest a try, and discover even more!
9+
10+
## 1. Make it your own
11+
12+
Tempest is designed with the flexibility to structure your projects whatever way you want. You can choose a classic MVC project, a DDD-inspired approach, hexagonal design, or anything else that suits your needs, without any configuration or framework adjustments. It just works the way you want.
13+
14+
```txt
15+
. .
16+
└── src └── app
17+
├── Authors ├── Controllers
18+
│ ├── Author.php │ ├── AuthorController.php
19+
│ ├── AuthorController.php │ └── BookController.php
20+
│ └── authors.view.php ├── Models
21+
├── Books │ ├── Author.php
22+
│ ├── Book.php │ ├── Book.php
23+
│ ├── BookController.php │ └── Chapter.php
24+
│ ├── Chapter.php ├── Services
25+
│ └── books.view.php │ └── PublisherGateway.php
26+
├── Publishers └── Views
27+
│ └── PublisherGateway.php ├── authors.view.php
28+
└── Support ├── books.view.php
29+
└── x-base.view.php └── x-base.view.php
30+
```
31+
32+
## 2. Discovery
33+
34+
The mechanism that allows such a flexible project structure is called [Discovery](/blog/discovery-explained). With Discovery, Tempest will scan your whole project and infer an incredible amount of information by reading your code, so that you don't have to configure the framework manually. On top of that, Tempest's discovery is designed to be extensible for project developers and package authors.
35+
36+
For example, I built a small event-sourcing implementation to keep track of website analytics [on this website](https://github.com/tempestphp/tempest-docs/blob/main/src/StoredEvents/ProjectionDiscovery.php). For that, I wanted to discover event projections within the app. Instead of manually listing classes in a config file somewhere. So I hooked into Tempest's discovery flow, which only requires implementing a single interface:
37+
38+
```php
39+
final class ProjectionDiscovery implements Discovery
40+
{
41+
use IsDiscovery;
42+
43+
public function __construct(
44+
private readonly StoredEventConfig $config,
45+
) {}
46+
47+
public function discover(DiscoveryLocation $location, ClassReflector $class): void
48+
{
49+
if ($class->implements(Projector::class)) {
50+
$this->discoveryItems->add($location, $class->getName());
51+
}
52+
}
53+
54+
public function apply(): void
55+
{
56+
foreach ($this->discoveryItems as $className) {
57+
$this->config->projectors[] = $className;
58+
}
59+
}
60+
}
61+
```
62+
63+
Of course, Tempest comes with a bunch of discovery implementations built in: routes, console commands, middleware, view components, event and command handlers, migrations, other discovery classes, and more. You can [read more about discovery here](/blog/discovery-explained).
64+
65+
## 3. Config classes
66+
67+
[Configuration](/docs/essentials/configuration#configuration-files) in Tempest is handled via classes. Any component that needs configuration will have one or more config classes. Config classes are simple data objects and don't require any setup. They might look something like this:
68+
69+
```php
70+
71+
final class MysqlConfig implements DatabaseConfig
72+
{
73+
public string $dsn {
74+
get => sprintf(
75+
'mysql:host=%s:%s;dbname=%s',
76+
$this->host,
77+
$this->port,
78+
$this->database,
79+
);
80+
}
81+
82+
public DatabaseDialect $dialect {
83+
get => DatabaseDialect::MYSQL;
84+
}
85+
86+
public function __construct(
87+
#[SensitiveParameter]
88+
public string $host = 'localhost',
89+
#[SensitiveParameter]
90+
public string $port = '3306',
91+
#[SensitiveParameter]
92+
public string $username = 'root',
93+
#[SensitiveParameter]
94+
public string $password = '',
95+
#[SensitiveParameter]
96+
public string $database = 'app',
97+
// …
98+
) {}
99+
}
100+
```
101+
102+
The first benefit of config classes is that the configuration schema is defined with class properties, which means you'll have proper static insight when defining and using configuration within Tempest:
103+
104+
```php database.config.php
105+
use Tempest\Database\Config\MysqlConfig;
106+
use function Tempest\env;
107+
108+
return new MysqlConfig(
109+
host: env('DB_HOST'),
110+
post: env('DB_PORT'),
111+
username: env('DB_USERNAME'),
112+
password: env('DB_PASSWORD'),
113+
);
114+
```
115+
116+
The second benefit of config classes is that their instances are discovered and registered in the container. Whenever a file ends with `.config.php` and returns a new config object, then that config object will be available via autowiring throughout your code:
117+
118+
```php app/stored-events.config.php
119+
use App\StoredEvents\StoredEventConfig;
120+
121+
return new StoredEventConfig();
122+
```
123+
124+
```php app/StoredEvents/EventsReplayCommand.php
125+
use App\StoredEvents\StoredEventConfig;
126+
127+
final readonly class EventsReplayCommand
128+
{
129+
public function __construct(
130+
private StoredEventConfig $storedEventConfig,
131+
// …
132+
) {}
133+
}
134+
```
135+
136+
## 4. Static pages
137+
138+
Tempest has built-in support for generating [static websites](/blog/static-websites-with-tempest). The idea is simple: why boot the framework when all that's needed is the same HTML page for any request to a specific URI? All you need is to mark an existing controller with the `#[StaticPage]` attribute, optionally add a data provider for dynamic routes, and you're set:
139+
140+
```php
141+
use Tempest\Router\StaticPage;
142+
143+
final readonly class BlogController
144+
{
145+
// …
146+
147+
#[StaticPage(BlogDataProvider::class)]
148+
#[Get('/blog/{slug}')]
149+
public function show(string $slug, BlogRepository $repository): Response|View
150+
{
151+
// …
152+
}
153+
}
154+
```
155+
156+
Finally, all you need to do is run the `{console}static:generate` command, and your static website is ready:
157+
158+
```console
159+
~ tempest static:generate
160+
161+
- <u>/blog</u> > <u>/web/tempestphp.com/public/blog/index.html</u>
162+
- <u>/blog/exit-codes-fallacy</u> > <u>/web/tempestphp.com/public/blog/exit-codes-fallacy/index.html</u>
163+
- <u>/blog/unfair-advantage</u> > <u>/web/tempestphp.com/public/blog/unfair-advantage/index.html</u>
164+
- <u>/blog/alpha-2</u> > <u>/web/tempestphp.com/public/blog/alpha-2/index.html</u>
165+
<comment>…</comment>
166+
- <u>/blog/alpha-5</u> > <u>/web/tempestphp.com/public/blog/alpha-5/index.html</u>
167+
- <u>/blog/static-websites-with-tempest</u> > <u>/web/tempestphp.com/public/blog/static-websites-with-tempest/index.html</u>
168+
169+
<success>Done</success>
170+
```
171+
172+
## 5. Console arguments
173+
174+
Console commands in Tempest require as little configuration as possible, and will be defined by the handler method's signature. Once again thanks to discovery, Tempest will infer what kind of input a console command needs, based on the [method's argument list](/docs/essentials/console-commands#command-arguments):
175+
176+
```php
177+
final readonly class EventsReplayCommand
178+
{
179+
// …
180+
181+
#[ConsoleCommand]
182+
public function __invoke(?string $replay = null, bool $force = false): void
183+
{ /* … */ }
184+
}
185+
186+
// ./tempest events:replay PackageDownloadsPerDayProjector --force
187+
```
188+
189+
## 6. Response classes
190+
191+
While Tempest has a generic response class that can be returned from controller actions, you're encouraged to use one of the specific response implementations instead:
192+
193+
```php
194+
use Tempest\Http\Response;
195+
use Tempest\Http\Responses\Ok;
196+
use Tempest\Http\Responses\Download;
197+
198+
final class DownloadController
199+
{
200+
#[Get('/downloads')]
201+
public function index(): Response
202+
{
203+
// …
204+
205+
return new Ok(/* … */);
206+
}
207+
208+
#[Get('/downloads/{id}')]
209+
public function download(string $id): Response
210+
{
211+
// …
212+
213+
return new Download($path);
214+
}
215+
}
216+
```
217+
218+
Making your own response classes is trivial as well: you must implement the `Tempest\Http\Response` interface and you're ready to go. For convenience, there's also an `IsResponse` trait:
219+
220+
```php
221+
use Tempest\Http\Response
222+
use Tempest\Http\IsResponse;
223+
224+
final class BookCreated implements Response
225+
{
226+
use IsResponse;
227+
228+
public function __construct(Book $book)
229+
{
230+
$this->setStatus(\Tempest\Http\Status::CREATED);
231+
$this->addHeader('x-book-id', $book->id);
232+
}
233+
}
234+
```
235+
236+
## 7. SQL migrations
237+
238+
Tempest has a database migration builder to manage your database's schema:
239+
240+
```php
241+
use Tempest\Database\DatabaseMigration;
242+
use Tempest\Database\QueryStatement;
243+
use Tempest\Database\QueryStatements\CreateTableStatement;
244+
use Tempest\Database\QueryStatements\DropTableStatement;
245+
246+
final class CreateBookTable implements DatabaseMigration
247+
{
248+
public string $name = '2024-08-12_create_book_table';
249+
250+
public function up(): QueryStatement|null
251+
{
252+
return new CreateTableStatement('books')
253+
->primary()
254+
->text('title')
255+
->datetime('created_at')
256+
->datetime('published_at', nullable: true)
257+
->integer('author_id', unsigned: true)
258+
->belongsTo('books.author_id', 'authors.id');
259+
}
260+
261+
public function down(): QueryStatement|null
262+
{
263+
return new DropTableStatement('books');
264+
}
265+
}
266+
```
267+
268+
But did you know that Tempest also supports raw SQL migrations? Any `.sql` file within your application directory will be discovered automatically:
269+
270+
```sql app/Migrations/2025-01-01_create_publisher_table.sql
271+
CREATE TABLE Publisher
272+
(
273+
`id` INTEGER,
274+
`name` TEXT NOT NULL
275+
);
276+
```
277+
278+
## 8. Console middleware
279+
280+
You might know middleware as a concept for HTTP requests, but Tempest's console also supports middleware. This makes it easy to add reusable functionality to multiple console commands. For example, Tempest comes with a `CautionMiddleware` and `ForceMiddleware` built-in. These middlewares add an extra warning before executing the command in production, and an optional `--force` flag to skip these kinds of warnings.
281+
282+
```php
283+
use Tempest\Console\ConsoleCommand;
284+
use Tempest\Console\Middleware\ForceMiddleware;
285+
use Tempest\Console\Middleware\CautionMiddleware;
286+
287+
final readonly class EventsReplayCommand
288+
{
289+
#[ConsoleCommand(middleware: [ForceMiddleware::class, CautionMiddleware::class])]
290+
public function __invoke(?string $replay = null): void
291+
{ /* … */ }
292+
}
293+
```
294+
295+
You can also make your own console middleware, you can [find out how here](/docs/essentials/console-commands#middleware).
296+
297+
## 9. Interfaces everywhere
298+
299+
When you're diving into Tempest's internals, you'll notice how we prefer to use interfaces over abstract classes. The idea is simple: if there's something framework-related to hook into, you'll be able to implement an interface and register your own implementation in the container. Most of the time, you'll also find a default trait implementation. There's a good reason behind this design, and you can read all about it [here](https://stitcher.io/blog/extends-vs-implements).
300+
301+
## 10. Initializers
302+
303+
Finally, let's talk about [dependency initializers](/docs/essentials/container#dependency-initializers). Initializers are tasked with setting up one or more dependencies in the container. Whenever you need a complex dependency available everywhere, your best option is to make a dedicated initializer class for it. Here's an example: setting up a Markdown converter that can be used throughout your app:
304+
305+
```php
306+
use Tempest\Container\Container;
307+
use Tempest\Container\Initializer;
308+
309+
final readonly class MarkdownInitializer implements Initializer
310+
{
311+
public function initialize(Container $container): MarkdownConverter
312+
{
313+
$environment = new Environment();
314+
$highlighter = new Highlighter(new CssTheme());
315+
316+
$highlighter
317+
->addLanguage(new TempestViewLanguage())
318+
->addLanguage(new TempestConsoleWebLanguage())
319+
->addLanguage(new ExtendedJsonLanguage());
320+
321+
$environment
322+
->addExtension(new CommonMarkCoreExtension())
323+
->addExtension(new FrontMatterExtension())
324+
->addRenderer(FencedCode::class, new CodeBlockRenderer($highlighter))
325+
->addRenderer(Code::class, new InlineCodeBlockRenderer($highlighter));
326+
327+
return new MarkdownConverter($environment);
328+
}
329+
}
330+
```
331+
332+
As with most things-Tempest, they are discovered automatically. Creating an initializer class and setting the right return type for the `initialize()` method is enough for Tempest to pick it up and set it up within the container.
333+
334+
## There's a lot more!
335+
336+
To truly appreciate Tempest, you'll have to write code with it. To get started, head over to [the documentation](/docs/getting-started/installation) and [join our Discord server](/discord)!

src/Web/Documentation/ChapterController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
final readonly class ChapterController
2020
{
2121
#[Get('/docs/{path:.*}')]
22+
#[Get('/current/{path:.*}')]
2223
public function docsRedirect(string $path): Redirect
2324
{
2425
return new Redirect(sprintf('/%s/%s', Version::default()->value, $path));

0 commit comments

Comments
 (0)