Tempest, the PHP framework that gets out of your way

3 min read Original article ↗

With zero configuration and zero boilerplate, Tempest gives you the architectural freedom to focus entirely on your business logic.

Zero-configuration with code discovery

Tempest scans your code and instantly registers routes, view components, console commands, middleware and more. It doesn’t need hand-holding; it just works.

src/Books/BookController.php

final readonly class BookController
{
    #[Post('/books')]
    public function store(CreateBookRequest $request): Response
    {
        $book = map($request)->to(Book::class)->save();

        return new Redirect(uri([self::class, 'show'], book: $book->id));
    }
}

src/Books/x-book.view.php

<article>
  <h1>{{ $book->title }}</h1>
  {!! $book->body !!}
</article>

src/Books/BookObserver.php

final readonly class BookObserver
{
    #[EventHandler]
    public function onBookPublished(BookPublished $event): void
    {
        // …
    }
}

A refreshing new template engine

Tempest reimagines templating in PHP with a clean front-end engine, inspired by modern front-end frameworks.

Whether you love our modern syntax or prefer the battle-tested reliability of Blade and Twig, Tempest has you covered.

src/x-base.view.php

<!DOCTYPE html>
<html lang="en" class="h-dvh flex flex-col">
  <head>
    <!-- Conditional elements -->
    <title :if="isset($title)">{{ $title }} — Books</title>
    <title :else>Books</title>
    <!-- Built-in Vite integration -->
    <x-vite-tags />
  </head>
  <body class="antialiased flex flex-col grow">
    <x-slot /> <!-- Main slot -->
    <x-slot name="scripts" /> <!-- Named slot -->
  </body>
</html>

src/Books/index.view.php

<x-base :title="$this->seo->title">
  <ul>
    <li :foreach="$this->books as $book">
      <!-- Title -->
      <span>{{ $book->title }}</span>

      <!-- Metadata -->
      <span :if="$this->showDate($book)">
        <x-badge variant="outline">
          {{ $book->publishedAt }}
        </x-badge>
      </span>
    </li>
  </ul>
</x-base>

A truly decoupled ORM

Models in Tempest embrace modern PHP and are designed to be decoupled from the database; they don’t even have to persist to the database and can be mapped to any kind of data source.

src/Books/Book.php

final class Book
{
    #[Length(min: 1, max: 120)]
    public string $title;

    public ?Author $author = null;

    /** @var \App\Books\Chapter[] */
    public array $chapters = [];
}
$book = query(Book::class)
    ->select()
    ->where('title', 'Timeline Taxi')
    ->first();

// …

$json = map($book)->toJson();

Console applications reimagined

Console commands are automatically discovered and use PHP’s type system to define arguments and flags.

No need to search the documentation to remember the syntax, just write PHP.

src/Books/FetchBookCommand.php

final readonly class FetchBookCommand
{
    public function __construct(
        private BookRepository $repository,
        private Isbn $isbn,
        private Console $console,
    ) {}
    
    #[ConsoleCommand(description: 'Synchronize a book from ISBN by its title')]
    public function __invoke(string $title, bool $force = false): void 
    {
        $data = $this->isbn->findByTitle($title);

        if (! $data) {
            $this->console->error("No book found matching that title.");
            return;
        }

        $book = map($data)->to(Book::class);

        if ($this->repository->exists($book->isbn13) && ! $force) {
            $this->console->info("Book already exists.");
            return;
        }

        $this->repository->save($book);
        $this->console->success("Synchronized {$book->title}.");
    }
}

And much, much more.

Configuration objects for easy autocompletion and injection, data mapping, a powerful dependency container with autowiring. Tempest is designed to be frictionless.

src/sqlite.config.php

return new SQLiteConfig(
    path: env('DB_PATH', __DIR__ . '/../database.sqlite'),
);

src/Blog/MarkdownInitializer.php

final readonly class MarkdownInitializer implements Initializer
{
    #[Singleton]
    public function initialize(Container $container): MarkdownConverter
    {
        $highlighter = new Highlighter(new CssTheme())
            ->addLanguage(new TempestViewLanguage());
        
        $environment = new Environment()
            ->addRenderer(Code::class, new CodeBlockRenderer($highlighter));

        return new MarkdownConverter($environment);
    }
}

>_ ./tempest static:generate

/framework/01-getting-started .. /public/framework/01-getting-started/index.html
/framework/02-the-container ...... /public/framework/02-the-container/index.html
/framework/03-controllers .......... /public/framework/03-controllers/index.html
/framework/04-views ...................... /public/framework/04-views/index.html
/framework/05-models .................... /public/framework/05-models/index.html