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
6 changes: 3 additions & 3 deletions src/Web/Blog/BlogPostTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ enum BlogPostTag: string
public function getStyle(): string
{
return match ($this) {
self::THOUGHTS => 'ring-amber-200 text-amber-400',
self::RELEASE => 'ring-blue-200 text-blue-400',
self::TUTORIAL => 'ring-teal-200 text-teal-400',
self::THOUGHTS => 'bg-yellow-400/20 dark:bg-yellow-400/10 text-yellow-700 dark:text-yellow-400',
self::RELEASE => 'bg-blue-400/20 dark:bg-blue-400/10 text-blue-700 dark:text-blue-400',
self::TUTORIAL => 'bg-teal-400/20 dark:bg-teal-400/10 text-teal-700 dark:text-teal-400',
};
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Request Objects in Tempest
title: Request objects in Tempest
description: Why Tempest requests are super intuitive
author: brent
tag: Tutorial
Expand Down
2 changes: 1 addition & 1 deletion src/Web/Blog/articles/2025-03-30-about-route-attributes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: About Route Attributes
title: About route attributes
description: Let's explore Tempest's route attributes in depth
author: brent
tag: Thoughts
Expand Down
2 changes: 1 addition & 1 deletion src/Web/Blog/articles/2025-05-08-beta-1.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Tempest is Beta
title: Tempest is beta
description: |
Today we release the first beta version of Tempest, the PHP framework for web and console apps that gets out of your way. It's one of the final steps towards a stable 1.0 release. We'll use this beta phase to fix bugs, and we're committed to not making any breaking changes anymore, apart from experimental features.
author: brent
Expand Down
2 changes: 1 addition & 1 deletion src/Web/Blog/articles/2025-05-26-tempests-vision.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Tempest's Vision
title: Tempest's vision
description: What sets Tempest apart as a framework for modern PHP development.
author: brent
tag: Thoughts
Expand Down
22 changes: 13 additions & 9 deletions src/Web/Blog/articles/2025-07-28-tempest-view-updates.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
title: Major updates to Tempest View
title: Major updates to Tempest views
description: Tempest 1.5 released with some major improvements to its templating engine
author: brent
tag: Release
---

Today we released Tempest version 1.5, which includes a bunch of improvements to [Tempest View](/docs/essentials/views), the templating engine that ships by default with the framework. Tempest also has support for Blade and Twig, but we designed Tempest View to take a unique approach to templating with PHP, and I must say: it looks excellent! (I might be biased.)

Designing a new language is hard, even if it's "only" a templating language, which is why we marked Tempest View as experimental when Tempest 1.0 released. This meant the package could still change over time, although we try to keep breaking changes at a minimum.
Designing a new language is hard, even if it's "only" a templating language, which is why we marked Tempest View as experimental when Tempest 1.0 released. This meant the package could still change over time, although we try to keep breaking changes at a minimum.

With the release of Tempest 1.5, we did have to make a handful of breaking changes, but overall they shouldn't have a big impact. And I believe both changes are moving the language forward in the right direction. In this post, I want to highlight the new Tempest View features and explain why they needed a breaking change or two.

Expand All @@ -34,11 +34,13 @@ And likewise, view components won't have access to variables from the outer scop
```html
<!-- $title will need to be passed in explicitly: -->

<x-post :title="$title"></x-post>
<x-post :title="$title"></x-post>
```

There's one exception to this rule: variables defined by the view itself are directly accessible from within view components. This can be useful when you're using view components that are tied to one specific view, but extracted to a component to avoid code repetition.

:::code-group

```html x-home-highlight.view.php
<div class="<!-- … -->">
{!! $highlights[$name] !!}
Expand All @@ -48,21 +50,23 @@ There's one exception to this rule: variables defined by the view itself are dir
<x-home-highlight name="orm" />
```

```php
```php app/HomeController.php
final class HomeController
{
#[Get('/')]
public function __invoke(HighlightRepository $highlightRepository): View
{
return view(
'home.view.php',
'./home.view.php',
highlights: $highlightRepository->all(),
);
}
}
```

Variable scoping now works by compiling view components to PHP closures instead of what we used to do: manage variable scope ourselves. Besides fixing some bugs, it also [simplified view component rendering significantly](https://github.com/tempestphp/tempest-framework/pull/1435), which is great!
:::

Variable scoping now works by compiling view components to PHP closures instead of what we used to do: manage variable scope ourselves. Besides fixing some bugs, it also [simplified view component rendering significantly](https://github.com/tempestphp/tempest-framework/pull/1435), which is great!

## Installable view components

Expand Down Expand Up @@ -126,7 +130,7 @@ $original = $session->getOriginalValueFor($name, $default);
</div>
```

While this style might require some getting used to for some people, I think it is the right decision to make: class-based view components had a lot of compiler edge cases that we had to take into account, and often lead to subtle bugs when building new components. I do plan on writing an in-depth post on how to build reusable view components with Tempest soon. Stay tuned for that!
While this style might require some getting used to for some people, I think it is the right decision to make: class-based view components had a lot of compiler edge cases that we had to take into account, and often lead to subtle bugs when building new components. I do plan on writing an in-depth post on how to build reusable view components with Tempest soon. Stay tuned for that!

## Work in progress IDE support

Expand All @@ -142,6 +146,6 @@ There is a lot of work to be done, but it's amazing to see this moving forward.

## What's next?

From the beginning I've said that IDE support is a must for any project to succeed. It now looks like there's a real chance of that happening, which is amazing. Besides IDE support, there are a couple of big features to tackle: I want Tempest to ship with some form of "standard component library" that people can use as a scaffold, we're looking into adding HTMX support (or something alike) to build async components, and we plan on making bridges for Laravel and Symfony so that you can use Tempest View in projects outside of Tempest as well.
From the beginning I've said that IDE support is a must for any project to succeed. It now looks like there's a real chance of that happening, which is amazing. Besides IDE support, there are a couple of big features to tackle: I want Tempest to ship with some form of "standard component library" that people can use as a scaffold, we're looking into adding HTMX support (or something alike) to build async components, and we plan on making bridges for Laravel and Symfony so that you can use Tempest View in projects outside of Tempest as well.

If you're inspired and interested to help out with any of these features, then you're more than welcome to [join the Tempest Discord](/discord) and take it from there.
If you're inspired and interested to help out with any of these features, then you're more than welcome to [join the Tempest Discord](/discord) and take it from there.
8 changes: 4 additions & 4 deletions src/Web/Blog/articles/2025-11-10-route-decorators.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: "Route Decorators in Tempest 2.8"
title: "Route decorators in Tempest 2.8"
description: Taking a deep dive in a new Tempest feature
author: brent
tag: release
Expand Down Expand Up @@ -70,7 +70,7 @@ While I really like attribute-based routing, grouping route behavior does feel
- Tempest's default route attributes are represented by HTTP verbs: `#[Get]`, `#[Post]`, etc. Making admin variants for each verb might be tedious, so in my previous example I decided to use one `#[AdminRoute]`, where the verb would be specified manually. There's nothing stopping me from adding `#[AdminGet]`, `#[AdminPost]`, etc; but it doesn't feel super clean.
- When you prefer to namespace admin-specific route attributes like `#[Admin\Get]`, and `#[Admin\Post]`, you end up with naming collisions between normal- and admin versions. I've always found those types of ambiguities to increase cognitive load while coding.
- This approach doesn't really scale: say there are two types of route groups that require a specific middleware (`AuthMiddleware`, for example), then you end up making two or more route attributes, duplicating that logic of adding `AuthMiddleware` to both.
- Say you want nested route groups: one for admin routes and then one for book routes (with a `/admin/books` prefix), you end up with yet another variant called `#[AdminBookRoute]` attribute, not ideal.
- Say you want nested route groups: one for admin routes and then one for book routes (with a `/admin/books` prefix), you end up with yet another variant called `#[AdminBookRoute]` attribute, not ideal.

So… what's the solution? I first looked at Symfony, which also uses attributes for routing:

Expand All @@ -95,7 +95,7 @@ class BookAdminController extends AbstractController
}
```

I think Symfony's approach gets us halfway there: it has the benefit of being able to define "shared route behavior" on the controller level, but not across controllers. You could create abstract controllers like `AdminController` and `AdminBookController`, which doesn't scale horizontally when you want to combine multiple route groups, because PHP doesn't have multi-inheritance. On top of that, I also like Tempest's design of using HTTP verbs to model route attributes like `#[Get]` and `#[Post]`, which is missing with Symfony. All of that to say, I like Symfony's approach, but I feel like there's room for improvement.
I think Symfony's approach gets us halfway there: it has the benefit of being able to define "shared route behavior" on the controller level, but not across controllers. You could create abstract controllers like `AdminController` and `AdminBookController`, which doesn't scale horizontally when you want to combine multiple route groups, because PHP doesn't have multi-inheritance. On top of that, I also like Tempest's design of using HTTP verbs to model route attributes like `#[Get]` and `#[Post]`, which is missing with Symfony. All of that to say, I like Symfony's approach, but I feel like there's room for improvement.

With the scene now being set, let's see the design we ended up with in Tempest.

Expand Down Expand Up @@ -227,4 +227,4 @@ On top of adding the {b`Tempest\Router\RouteDecorator`} interface, I've also add
- {b`Tempest\Router\WithoutMiddleware`}: which explicitely removes one or more middleware classes from the default middleware stack to all decorated routes.
- {b`Tempest\Router\Stateless`}: which will remove all session and cookie related middleware from the decorated routes.

I really like the solution we ended up with. I think it combines the best of both worlds. Maybe you have some thoughts about it as well? [Join the Tempest Discord](/discord) to let us know! You can also read all the details of route decorators [in the docs](/2.x/essentials/routing#route-decorators-route-groups).
I really like the solution we ended up with. I think it combines the best of both worlds. Maybe you have some thoughts about it as well? [Join the Tempest Discord](/discord) to let us know! You can also read all the details of route decorators [in the docs](/2.x/essentials/routing#route-decorators-route-groups).
60 changes: 31 additions & 29 deletions src/Web/Blog/index.view.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,46 @@

<x-base title="Blog">
<!-- Main container -->
<main class="container px-4 mx-auto xl:px-8 flex flex-col grow isolate">
<main class="isolate flex flex-col mx-auto px-4 xl:px-8 font-mono container grow">
<!-- Main content -->
<div class="grow px-2 w-full lg:pl-12 flex flex-col min-w-0 lg:mt-10">
<div class="flex flex-col lg:mt-10 px-2 lg:pl-12 w-full min-w-0 grow">
<!-- Header -->
<div class="flex flex-col pb-8 max-w-xl">
<h1 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl lg:text-5xl">Blog</h1>
<p class="mt-4 text-lg text-gray-500 dark:text-gray-400">
<h1 class="font-bold text-gray-900 dark:text-white text-3xl sm:text-4xl lg:text-5xl tracking-tight">Blog</h1>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-lg">
Read the latest news and announcements about Tempest, from framework updates to real-world applications and expert insights.
</p>
<div class="mt-2">
<a href="/rss" class="rounded font-semibold inline-flex items-center focus:outline-hidden disabled:cursor-not-allowed aria-disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:opacity-75 transition-colors px-2.5 py-1 text-sm gap-1.5 ring ring-inset ring-(--ui-border-accented) text-(--ui-text) bg-(--ui-bg) hover:bg-(--ui-bg-elevated) disabled:bg-(--ui-bg) aria-disabled:bg-(--ui-bg) focus-visible:ring-2 focus-visible:ring-(--ui-border-inverted)" rel="noopener noreferrer" target="_blank">
<x-icon name="tabler:rss" class="shrink-0 size-4" />
RSS
<div class="mt-4">
<a href="/rss" class="inline-flex items-center gap-1 bg-gray-300/20 dark:bg-gray-400/10 px-2.5 py-1 rounded font-medium text-gray-700 dark:text-gray-400 text-sm" rel="noopener noreferrer" target="_blank">
<x-icon name="tabler:rss" class="size-4 shrink-0" />
<span>RSS</span>
</a>
</div>
</div>
<!-- Articles -->
<ul class="grid md:grid-cols-2 lg:grid-cols-3 gap-4 lg:gap-8 mt-0 mb-8 lg:mt-4 2xl:mt-8">
<li :foreach="$posts as $post" class="flex flex-col justify-between relative border-dashed border border-(--ui-border-accented) hover:bg-(--ui-bg-elevated) rounded-lg p-4 transition">
<a class="absolute inset-0" :href="$post->uri"></a>
<div>
<span class="font-medium">{{ $post->title }}</span>
<p class="text-[15px] text-(--ui-text-muted) mt-1 line-clamp-2">{{ $post->description }}</p>
</div>
<div class="flex items-center mt-6 gap-x-4 justify-between">
<span
:if="$post->tag"
:class="$post->tag->getStyle()"
class="font-medium inline-flex items-center text-xs px-2 py-1 gap-1 rounded ring ring-inset"
>
{{ str($post->tag->value)->title() }}
</span>
<span :if="$post->author" class="text-(--ui-text-muted) text-sm">
by <span class="font-medium">{{ $post->author->getName() }}</span> on <span class="font-medium">{{ $post->createdAt->format('F d, Y') }}</span>
</span>
</div>
</li>
</ul>
<ul class="gap-4 lg:gap-6 grid md:grid-cols-2 lg:grid-cols-3 mt-0 lg:mt-4 2xl:mt-8 mb-8">
<li :foreach="$posts as $post" class="p-0.5 relative border border-(--ui-border) bg-(--ui-bg-elevated)/30 hover:bg-(--ui-bg-elevated)/75 rounded-lg transition">
<div class="h-full flex flex-col justify-between border border-dashed border-(--ui-border) p-4 rounded-md">
<a class="absolute inset-0" :href="$post->uri"></a>
<div>
<span class="font-medium">{{ $post->title }}</span>
<p class="text-(--ui-text-dimmed) mt-1 line-clamp-2">{{ $post->description }}</p>
</div>
<div class="flex justify-between items-center gap-x-2 mt-4">
<span
:if="$post->tag"
:class="$post->tag->getStyle()"
class="inline-flex items-center gap-1 px-2 py-1 rounded font-medium text-xs uppercase"
>
{{ str($post->tag->value)->title() }}
</span>
<span :if="$post->author" class="text-(--ui-text-muted) text-sm">
by <span class="font-semibold">{{ $post->author->getName() }}</span> on <span class="font-semibold">{{ $post->createdAt->format('F d, Y') }}</span>
</span>
</div>
</div>
</li>
</ul>
</div>
</main>
</x-base>
12 changes: 6 additions & 6 deletions src/Web/Blog/show.view.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,25 @@
:meta="$post->meta"
>
<!-- Main container -->
<main class="isolate relative flex flex-col items-center mx-auto px-4 xl:px-8 container grow">
<main class="isolate relative flex flex-col items-center mx-auto px-4 xl:px-8 font-mono container grow">
<!-- Main content -->
<article class="flex flex-col lg:mt-10 px-2 w-full md:w-auto min-w-0 max-w-3xl grow">
<!-- Breadcrumbs -->
<nav class="text-(--ui-text-dimmed) font-medium flex items-center mb-4 text-sm gap-x-1.5">
<nav class="text-(--ui-text-dimmed) font-medium flex items-center mb-6 text-sm gap-x-1.5">
<x-icon name="tabler:news" class="mr-1 size-5" />
<a :href="uri([BlogController::class, 'index'])" class="hover:text-(--ui-text) transition">Blog</a>
<span>/</span>
<span class="text-(--ui-primary)">{{ $post->title }}</span>
</nav>
<!-- Header -->
<div class="flex flex-col pb-8 border-b border-(--ui-border) max-w-[65ch]">
<h1 class="font-bold text-gray-900 dark:text-white text-3xl sm:text-4xl lg:text-5xl tracking-tight">
<div class="flex flex-col pb-8 border-b border-(--ui-border) w-full">
<h1 class="max-w-[65ch] font-bold text-3xl sm:text-4xl lg:text-5xl tracking-tight">
{{ $post->title }}
</h1>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-lg">
<p class="mt-4 text-lg text-(--ui-text-muted)">
{{ $post->description }}
</p>
<span :if="$post->author" class="text-(--ui-text-muted) text-sm mt-8">
<span :if="$post->author" class="text-(--ui-text-dimmed) text-sm mt-4">
by <span class="font-medium">{{ $post->author->getName() }}</span> on <span class="font-medium">{{ $post->createdAt->format('F d, Y') }}</span>
</span>
</div>
Expand Down
37 changes: 31 additions & 6 deletions src/Web/CommandPalette/CommandIndexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Web\CommandPalette;

use App\Web\Blog\BlogController;
use App\Web\Community\CommunityController;
use App\Web\Documentation\DocumentationController;
use App\Web\RedirectsController;
use Override;
Expand All @@ -30,6 +31,12 @@ public function index(): ImmutableArray
hierarchy: ['Commands', 'Blog'],
uri: uri([BlogController::class, 'index']),
),
new Command(
title: 'Check out community resources',
type: Type::URI,
hierarchy: ['Commands', 'Community'],
uri: uri([CommunityController::class, 'index']),
),
new Command(
title: 'See the code on GitHub',
type: Type::URI,
Expand All @@ -43,16 +50,34 @@ public function index(): ImmutableArray
uri: uri([RedirectsController::class, 'discord']),
),
new Command(
title: 'Follow me on Bluesky',
title: 'Follow Brent from the core team on Bluesky',
type: Type::URI,
hierarchy: ['Commands', 'Link'],
uri: uri([RedirectsController::class, 'bluesky']),
hierarchy: ['Commands', 'Link', 'Core team'],
uri: uri([RedirectsController::class, 'blueskyBrent']),
),
new Command(
title: 'Follow me on X',
title: 'Follow Brent from the core team on X',
type: Type::URI,
hierarchy: ['Commands', 'Link'],
uri: uri([RedirectsController::class, 'twitter']),
hierarchy: ['Commands', 'Link', 'Core team'],
uri: uri([RedirectsController::class, 'twitterBrent']),
),
new Command(
title: 'Follow Enzo from the core team on Bluesky',
type: Type::URI,
hierarchy: ['Commands', 'Link', 'Core team'],
uri: uri([RedirectsController::class, 'blueskyEnzo']),
),
new Command(
title: 'Follow Enzo from the core team on X',
type: Type::URI,
hierarchy: ['Commands', 'Link', 'Core team'],
uri: uri([RedirectsController::class, 'twitterEnzo']),
),
new Command(
title: 'Follow Aidan from the core team on X',
type: Type::URI,
hierarchy: ['Commands', 'Link', 'Core team'],
uri: uri([RedirectsController::class, 'twitterAidan']),
),
new Command(
title: 'Toggle dark mode',
Expand Down
Loading