TempestTempest

Tempest Alpha 3

Written by Brent on October 31, 2024 — Back

It's been a month since the previous alpha release of Tempest. Since then, we've merged over 60 pull requests, created by 13 contributors and our Discord server now has over 200 members.

I have to admit: I never imagined so many people would be interested in trying out and contributing to Tempest so early in the project's lifetime. A big thank you to everyone who's contributing — either by trying out Tempest, making issues, or submitting PRs — you're awesome!

There's a lot of work to be done still, and today I'm happy to announce we've tagged the next alpha release. Let's take a look at what's new!

composer require tempest/framework:1.0-alpha.3

Refactored Tempest View

One of the most significant refactors I've worked on since the dawn of Tempest: large parts of Tempest View have been rewritten. View files are now compiled and cached, lots of bugs have been fixed, and we've also added a dedicated landing page for it.

<x-base title="Home">
    <x-post :foreach="$this->posts as $post">
        {!! $post->title !!}

        <span :if="$this->showDate($post)">
            {{ $post->date }}
        </span>
        <span :else>
            -
        </span>
    </x-post>
    <div :forelse>
        <p>It's quite empty here…</p>
    </div>
    
    <x-footer />
</x-base>

One of our most important TODOs now is IDE support. If you're reading this blog post and have experience with writing LSPs or IntelliJ language plugins, feel free to contact me via email or Discord.

ArrayHelper and StringHelper additions

During October, a handful of people have pitched in and added a lot of new functions to our StringHelper and ArrayHelper classes. The docs for these classes are still work in progress, but we've been using them all over the place, and they are really helpful.

use function Tempest\Support\str;

$excerpt = str($content)
    ->excerpt(
        $previous->getLine() - 5,
        $previous->getLine() + 5,
        asArray: true,
    )
    ->map(function (string $line, int $number) use ($previous) {
        return sprintf(
            "%s%s | %s",
            $number === $previous->getLine() ? '> ' : '  ',
            $number,
            $line
        );
    })
    ->implode(PHP_EOL);

Special thanks to Enzo, Yassine, and Guillaume for all the contributions!

Custom route param regex

Tempest's router now supports regex parameters, giving you even more flexibility for route matching. Thanks to Sergiu for the PR!

#[Get(uri: '/blog/{category}/{type:article|news}')]
public function category(string $category, string $type): Response
{
    // …
}

We're also still working on making the router even more performant (even though it already is pretty fast).

Defer Helper

Inspired by Laravel, we added a defer() helper: any closure passed to it will be executed after the response has been sent to the client. This is especially useful for tasks that take a little bit more time and don't affect the response: analytics tracking, email sending, caching, …

use function Tempest\defer;

final readonly class PageVisitedMiddleware implements HttpMiddleware
{
    public function __invoke(Request $request, callable $next): Response
    {
        defer(fn () => event(new PageVisited($request->getUri())));

        return $next($request);
    }
}

We still plan on adding asynchronous commands as well for even more complex background tasks, that's planned for the next alpha release.

Initializers for built-in types

Vincent added support for tagged built-in types in the container. This feature can come in handy when you want to, for example, inject an array of grouped dependencies.

final readonly class BookValidatorsInitializer implements Initializer
{
    #[Singleton(tag: 'book-validators')]
    public function initialize(Container $container): array
    {
        return [
            $container->get(HeaderValidator::class),
            $container->get(BodyValidator::class),
            $container->get(FooterValidator::class),
        ];
    }
}
final readonly class BookService
{
    public function __construct(
        #[Tag('book-validators')] private array $validators,
    ) {}
}

Closure-based event listeners

Enzo added support for closure-based event listeners. These are useful to create local scoped event listeners that shouldn't be discovered globally.

#[ConsoleCommand(name: 'migrate:down')]
public function __invoke(): void
{
	$this->eventBus->listen(MigrationFailed::class, function (MigrationFailed $event) {
		$this->console->error($event->exception->getMessage());
	});

	$this->migrationManager->up();
}

ClassGenerator

Enzo also created a wrapper for nette/php-generator, which opens the door for "make commands" and installers.

use Tempest\Generation\ClassManipulator;

(new ClassManipulator(PackageMigration::class))
    ->removeClassAttribute(DoNotDiscover::class)
    ->setNamespace('App\\Migrations')
    ->print();

Installers

A pretty neat new feature in Tempest are installers: these are classes that know how to install a package or framework component. They are discovered automatically, and Tempest provides a CLI interface for them:

./tempest install auth

Running the `auth` installer, continue? [yes/no] 

app/User.php already exists. Do you want to overwrite it? [yes/no] 
app/User.php created 

app/UserMigration.php already exists. Do you want to overwrite it? [yes/no] 

app/Permission.php already exists. Do you want to overwrite it? [yes/no]
 
app/PermissionMigration.php already exists. Do you want to overwrite it? [yes/no] 
app/PermissionMigration.php created 

app/UserPermission.php already exists Do you want to overwrite it? [yes/no]
Done 

We're still fine-tuning the API, but here's what an installer looks like currently:

use Tempest\Core\Installer;
use Tempest\Core\PublishesFiles;
use function Tempest\src_path;

final readonly class AuthInstaller implements Installer
{
    use PublishesFiles;

    public function getName(): string
    {
        return 'auth';
    }

    public function install(): void
    {
        $publishFiles = [
            __DIR__ . '/User.php' => src_path('User.php'),
            __DIR__ . '/UserMigration.php' => src_path('UserMigration.php'),
            __DIR__ . '/Permission.php' => src_path('Permission.php'),
            __DIR__ . '/PermissionMigration.php' => src_path('PermissionMigration.php'),
            __DIR__ . '/UserPermission.php' => src_path('UserPermission.php'),
            __DIR__ . '/UserPermissionMigration.php' => src_path('UserPermissionMigration.php'),
        ];

        foreach ($publishFiles as $source => $destination) {
            $this->publish(
                source: $source,
                destination: $destination,
            );
        }
        
        $this->publishImports();
    }
}

You can read more about installers here.

Cache improvements

Finally, we've integrated the previously added cache component within several parts of the framework: discovery, config, and view compiling. We also added support for environment-based cache toggling.

./tempest cache:status

Tempest\Core\DiscoveryCache enabled
Tempest\Core\ConfigCache enabled
Tempest\Cache\ProjectCache disabled
Tempest\View\ViewCache disabled

You can read more about caching here.

Up next

I am amazed by how much the community got done in a single month's time. Like I said at the start of this post: I didn't expect so many people to pitch in so early, and it's really encouraging to see.

That being said, there's still a lot of work to be done before a stable 1.0 release. We plan for the next alpha release to be available end of November, right after the PHP 8.4 release. These are the things we want to solve by then:

If you want to help out with Tempest, the best starting point is to join our Discord server.

Until next time!

Tempest