Tempest is still a work in progress. Visit our GitHub or Discord

Getting Started

Tempest is a PHP framework that gets out of your way. Its design philosophy is that developers should write as little framework-related code as possible, so that they can focus on application code instead.

Tempest embraces modern PHP syntax, covers a wide range of features: routing, MVC, ORM and database, rich console applications, events and commands, logging, a modern view engine, and unique capabilities such as discovery to improve developer experience.

Tempest can be installed as a standalone PHP project, as well as a package within existing projects. The framework modules — like, for example, tempest/console or tempest/event-bus — can also be installed individually, including in projects built on other frameworks.

Since code says more than words, here's a Tempest controller:

// app/BookController.php

use Tempest\Http\Get;
use Tempest\Http\Post;
use Tempest\Http\Response;
use Tempest\Http\Responses\Ok;
use Tempest\Http\Responses\Redirect;

final readonly class BookController
{
    #[Get('/books/{book}')]
    public function show(Book $book): Response
    {
        return new Ok($book);
    }

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

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

And here's a Tempest console command:

// app/MigrateUpCommand.php

use Tempest\Console\Console;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\Middleware\ForceMiddleware;
use Tempest\Console\Middleware\CautionMiddleware;
use Tempest\EventBus\EventHandler;

final readonly class MigrateUpCommand
{
    public function __construct(
        private Console $console,
        private MigrationManager $migrationManager,
    ) {}

    #[ConsoleCommand(
        name: 'migrate:up',
        description: 'Run all new migrations',
        middleware: [ForceMiddleware::class, CautionMiddleware::class],
    )]
    public function __invoke(): void
    {
        $this->migrationManager->up();

        $this->console->success("Everything migrated");
    }

    #[EventHandler]
    public function onMigrationMigrated(MigrationMigrated $migrationMigrated): void
    {
        $this->console->writeln("- {$migrationMigrated->name}");
    }
}

Ready to give it a try? Keep on reading, give Tempest a star️ on GitHub. If you want to be part of the community, you can join our Discord server, and you can check out our contributing guide!

Installation

You can install Tempest in two ways: as a web app with a basic frontend bootstrap, or by requiring the framework as a package in any project you'd like — these can be projects built on top of other frameworks.

A standalone Tempest app

If you want to start a new Tempest project, you can use tempest/app as the starting point. Use composer create-project to start:

composer create-project tempest/app my-app --stability alpha
cd my-app

The project scaffold includes a basic frontend setup including tailwind:

npm run dev

You can access your app by using PHP's built-in server.

./tempest serve
PHP 8.3.3 Development Server (http://localhost:8000) started

Tempest as a package

If you don't need an app scaffold, you can opt to install tempest/framework as a standalone package. You could do this in any project; it could already contain code, or it could be an empty project.

composer require tempest/framework:1.0-alpha.4

Installing Tempest this way will give you access to the tempest console as a composer binary:

./vendor/bin/tempest

Optionally, you can choose to install Tempest's entry points in your project:

./vendor/bin/tempest install framework

Installing Tempest into a project means copying one or more of these files into that project:

  • public/index.php — the web application entry point
  • tempest – the console application entry point
  • .env.example – a clean example of a .env file
  • .env – the real environment file for your local installation

You can choose which files you want to install, and you can always rerun the install command at a later point in time.

Project structure

Tempest won't impose any file structure on you: one of its core features is that it will scan all project and package code for you, and it will automatically discover any files the framework needs to know about. For example: Tempest is able to differentiate between a controller method and a console command by looking at the code, instead of relying on naming conventions or verbose configuration files. This concept is called discovery, and it's one of Tempest's most powerful features.

For example, you can make a project that looks like this:

app
├── Console
│   └── RssSyncCommand.php
├── Controllers
│   ├── BlogPostController.php
│   └── HomeController.php
└── Views
    ├── blog.view.php
    └── home.view.php

Or a project that looks like this:

app
├── Blog
│   ├── BlogPostController.php
│   ├── RssSyncCommand.php
│   └── blog.view.php
└── Home
    ├── HomeController.php
    └── home.view.php

From Tempest's perspective, it's all the same.

Discovery works by scanning you project code, and looking at each file and method individually to determine what that code does. For production apps, Tempest will cache the discovery process, so there's no performance overhead that comes with it.

As an example, Tempest is able to determine which methods are controller methods based on their route attributes:

// app/BlogPostController.php

use Tempest\Http\Get;
use Tempest\Http\Response;
use Tempest\View\View;

final readonly class BlogPostController
{
    #[Get('/blog')]
    public function index(): View
    { /* … */ }
    
    #[Get('/blog/{post}')]
    public function show(Post $post): Response
    { /* … */ }
}

And likewise, it's able to detect console commands based on their console command attribute:

// src/RssSyncCommand.php

use Tempest\Console\HasConsole;
use Tempest\Console\ConsoleCommand;

final readonly class RssSyncCommand
{
    use HasConsole;

    #[ConsoleCommand('rss:sync')]
    public function __invoke(bool $force = false): void  
    { /* … */ }
}

Discovery in production

While discovery is a really powerful feature, it also comes with some performance considerations. In production environments, you want to make sure that the discovery workflow is cached. That's done like this:

# .env
DISCOVERY_CACHE=true

What's important though, is that production discovery cache will also need to be pre-generated. You can do this by running the discovery:generate command:

~ ./tempest discovery:generate
Clearing existing discovery cache…
Discovery cached has been cleared
Generating new discovery cache… (cache strategy used: all)
Done 1114 items cached

In other words: it's best that you include the discovery:generate command in your deployment pipeline. Make sure to run it before you run any other Tempest commands.

Discovery for local development

By default, discovery cache will be disabled in local development. Depending on your local setup, it's likely that you won't run into noticeable slowdowns. However, for larger projects, you might benefit from enabling partial discovery cache:

# .env
DISCOVERY_CACHE=partial

This caching strategy will only cache discovery for vendor files. Keep in mind that you will also have to generate the discovery cache:

~ ./tempest discovery:generate
Clearing existing discovery cache…
Discovery cached has been cleared
Generating new discovery cache… (cache strategy used: partial)
Done 111 items cached

If you're using partial discovery cache, it is recommended to automatically run discovery:generate after every composer update:

// …

"scripts": {
    "post-package-update": [
        "php tempest discovery:generate"
    ]
}

Note that, if you've created your project using tempest/app, you'll have the post-package-update script already included. If you want to, you can read the internal documentation about discovery to learn more.