Tempest is Beta
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.
by Brent on May 08, 2025Two years ago, Tempest started out as an educational project during one of my livestreams. Since then, we've had 56 people contribute to the framework, merged 591 pull requests, resolved 455 issues, and have written around 50k lines of code. Two contributors joined the core team and dedicated a lot of their time to help make Tempest into something real. And today, we're tagging Tempest as beta.
We have to be real though: we won't get it perfect from the start. Tempest is now in beta, which means we don't plan any breaking changes to stable components anymore, but it also means we expect there to be bugs. And this puts us in an underdog position: why would anyone want to use a framework that has fewer features and likely more bugs than other frameworks?
It turns out, people do see value in Tempest. It's the only reason I decided to work on it in the first place: there is a group of people who want to use it, even when they are aware of its current shortcomings. There is interest in a framework that embraces modern PHP without 10 to 20 years of legacy to carry with it. There is interest in a project that dares to rethink what we've gotten used to over the years. There already is a dedicated community. People already are building with Tempest. Several core members have real use cases for Tempest and are working hard to be able to use it in their own projects as soon as possible. So while Tempest is the underdog, there already seems enough reason for people to use it today.
And I don't want Tempest to remain the underdog. Getting closer to that goal requires getting more people involved. We need hackers to build websites and console applications with Tempest, we need them to run into bugs and edge cases that we haven't thought of. We need entrepreneurs to look into third-party packages, we need to learn what should be improved on our side from their experience. We need you to be involved. That's the next step for Tempest.
Our commitment to you is that we're doing all we can to make Tempest the best developer experience possible. Tempest is and must stay the framework that truly gets out of your way. You need to focus on your code, not on hand-holding and guiding the framework. We're still uncertain about a handful of features and have clearly marked them as experimental, with tried and tested alternatives in place. We're committed to a period of bug fixing to make sure Tempest can be trusted when we release the 1.0 version.
We're committed, and I hope you're intrigued to give Tempest a go.
composer create-project tempest/app:1.0-beta.1 <name>
All of that being said, let's look at what's new in this first beta release!
A truly decoupled ORM
A long-standing issue within Tempest was our ORM: the goal of our model classes was to be truly disconnected from the database, but they weren't really. That's changed in beta.1, where we removed the DatabaseModel
interface. Any object with typed public properties can now be considered "a model class" by the ORM:
use Tempest\Validation\Rules\Length; use App\Author; final class Book { #[Length(min: 1, max: 120)] public string $title; public ?Author $author = null; /** @var \App\Chapter[] */ public array $chapters = []; }
Now that these model objects aren't tied to the database, they can receive and persistent their data from anywhere, not just a database:
use function Tempest\map; $books = map($json)->collection()->to(Book::class); $json = map($books)->toJson();
We did decide to keep the IsDatabaseModel
trait still, because we reckon database persistence is a very common use case:
use Tempest\Database\IsDatabaseModel; final class Book { use IsDatabaseModel; // … } $book = Book::create( title: 'Timeline Taxi', author: $author, chapters: [ new Chapter(index: 1, contents: '…'), new Chapter(index: 2, contents: '…'), new Chapter(index: 3, contents: '…'), ], ); $books = Book::select() ->where('publishedAt > ?', new DateTimeImmutable()) ->orderBy('title DESC') ->limit(10) ->with('author') ->all(); $books[0]->chapters[2]->delete();
However, we also added a new query()
helper function that can be used instead of the IsDatabaseModel
trait.
$data = query(Book::class) ->select('title', 'index') ->where('title = ?', 'Timeline Taxi') ->andWhere('index <> ?', '1') ->orderBy('index ASC') ->all();
We've managed to truly decouple model classes from the persistence layer, while still making them really convenient to use. This is a great example of how Tempest gets out of your way.
An important note to make here is that our ORM is one of the few experimental components within Tempest. We acknowledge that there's more work to be done to make it even better, and there might be some future breaking changes still. It's one of the prime examples where we need the community to help us learn what should be improved, and how.
tempest/view changes
We've added support for dynamic view components, which allows you to render view components based on runtime data:
<!-- $name = 'x-post' --> <x-component :is="$name" :title="$title" />
We've improved boolean attributes, they now also work for truthy and falsy values, as well as for custom expression attributes:
<div :data-active="{$isActive}"></div> <!-- <div></div> when $isActive is falsy --> <!-- <div data-active></div> when $isActive is truthy -->
Finally, we switched from PHP's built-in DOM parser to our custom implementation. We realized that trying to parse tempest/view syntax according to the official HTML spec added more problems than it solved. After all, tempest/view syntax is a superset of HTML: it compiles to spec-compliant HTML, but in itself it is not spec-compliant.
Moving to a custom parser written in PHP comes with a small performance price to pay, but our implementation is slightly more performant than masterminds/html5, the most popular PHP-based DOM parser, and everything our parser does is cached as well. You can check out the implementation here.
Container features
We've added a new interface called HasTag
, which allows any object to manually specify its container tag. This feature is especially useful combined with config files, and allows you to define multiple config files for multiple occasions. For example, to define multiple database connections:
return new PostgresConfig( tag: 'backup', // … );
use Tempest\Database\Database; use Tempest\Container\Tag; final readonly class BackupService { public function __construct( #[Tag('backup')] private Database $database, ) {} // … }
We also added support for proxy dependencies, using PHP 8.4's new object proxies. Any dependency that might be expensive to construct, but not often used, can be injected as a proxy. As a proxy, the dependency will only get resolved when actually needed:
use Tempest\Container\Proxy; final readonly class BookController { public function __construct( #[Proxy] private VerySlowClass $verySlowClass ) { /* … */ } }
Middleware discovery
One thing that has felt icky for a long time was that middleware classes could not be discovered (this was the case for all HTTP, console, event bus and command bus middleware). The reason for this restriction was that in some cases, it's important to ensure middleware order: some middleware must come before other, and discovery doesn't guarantee that order. This restriction doesn't match our Tempest mindset, though: we forced all middleware to be manually configured, even though only a small number of middleware classes actually needed that flexibility.
So, as of beta.1, we've added middleware discovery to make the most common case very developer-friendly, and we added the tools necessary to make sure the edge cases are covered as well.
First, you can skip discovery for middleware classes entirely when needed:
use Tempest\Discovery\SkipDiscovery; use Tempest\Router\HttpMiddleware; #[SkipDiscovery] final readonly class ValidateWebhook implements HttpMiddleware { public function __invoke(Request $request, HttpMiddlewareCallable $next): Response { // … } }
And, second, you can define middleware priority for specific classes to ensure the right order when needed:
use Tempest\Core\Priority; #[Priority(Priority::HIGHEST)] final readonly class OverviewMiddleware implements ConsoleMiddleware { public function __invoke(Invocation $invocation, ConsoleMiddlewareCallable $next): ExitCode|int { // … } }
Smaller features
Finishing with a couple of smaller changes, but it's these kinds of small details that make the difference in the long run. So thanks to everyone who contributed:
- We've added a couple of new commands:
make:migration
andcontainer:show
- We've added testing utilities for our event bus
- There's a new
Back
response class to redirect to the previous page - We now allow controllers to also return strings and arrays directly
- We've added a new storage component, which is a slim wrapper around Flysystem
- And, a lot more
In closing
It's amazing to see what we've achieved in a little less than two years. Tempest has grown from being a dummy project used during livestreams, to a real framework.
There's a long way to go still, but I'm confident when I see how many people are contributing to and excited about Tempest. You can follow along the beta progress on GitHub; and you can be part of the journey as well: give Tempest a try and join our Discord server.
See you soon!
