I saw on Laravel news something about a new PHP framework called Tempest.
There are different things I like about the framework, and the most exciting feature for me is the lack of a predefined file structure.
In some of my previous posts I ranted a bit about the right file structure of a project. I prefer a modular or DDD file structure instead of an MVC file structure. So only for that reason I had to try it.
Installation and first impressions
Like any other framework a composer project is provided to install it, composer create-project tempest/app my-app --stability beta
.
In the app directory there is a HomeController.
use Tempest\Router\Get;
use Tempest\View\View;
use function Tempest\view;
final readonly class HomeController
{
#[Get('/')]
public function __invoke(): View
{
return view('home.view.php');
}
}
Using attributes to define the routes is familiar if you use Symfony. The view function is familiar if you use Laravel.
The home.view.php file content is;
<x-base>
<main class="w-screen h-screen overflow-hidden bg-sky-100/20">
<div class="mx-auto max-w-2xl py-32 sm:py-48 lg:py-56">
<div class="text-center">
<h1 class="text-balance text-5xl font-semibold tracking-tight text-gray-800 sm:text-7xl">Tempest</h1>
<p class="mt-8 text-pretty text-lg font-medium text-gray-500 sm:text-xl/8">The PHP framework that gets out of your way.</p>
</div>
</div>
</main>
</x-base>
I reduced the amount of HTML because most of it is not relevant for this post.
The template engine uses a web component syntax to define templates, people that use Blade components will be familiar with this. The template names starts with x- and end with .view.php, his is where the engine differs from Blade. The x-base.view.php template has following content.
<html lang="en">
<head>
<title :if="isset($title)">{{ $title }} | Tempest</title>
<title :else>Tempest</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png"/>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"/>
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"/>
<link rel="manifest" href="/favicon/site.webmanifest"/>
<x-slot name="styles"/>
</head>
<body>
<x-slot/>
</body>
</html>
The repetition of the title tag is a bit strange. It is a long time ago that I saw branches and loops as a tag attribute. But i saw it before so it is not that extreme.
To embed the content in between the template tags the engine uses the x-slot tag. It is possible to give the tag a name, this allows you to target it. All the rest of the content between the template tags will go in the nameless x-slot tag.
First change
I'm not a fan of using helpers in Laravel, so I'm not going to start in Tempest. Luckily the framework provides an alternative way.
use Tempest\Router\Get;
final readonly class HomeController
{
#[Get('/test')]
public function __invoke(): HomeView
{
return new HomeView();
}
}
use Tempest\View\View;
use Tempest\View\IsView;
class HomeView implements View
{
use IsView;
public function __construct(
) {
$this->path = __DIR__ . '/home.view.php';
}
}
The benefit of this option is that you can pass the data in the constructor, and in the template file $this
is the class instance.
I noticed that the framework uses traits, IsView
in the example, over abstract classes to make it easier to implement the interfaces. My object oriented programming heart bleeds a little.
A more challenging change
I'm used to a template function that uses the name of the route to output the url. When I looked at the http verb attributes I saw there is no option to add a name, only uri and middleware.
So I decided to create a template that accepts the class and method to output the uri. I saw that the artisan/console equivalent in Tempest had a routes command.
// `tempest routes` output
GET / ... App\HomeController::__invoke()
So I going to steal and manipulate that code to get what I want.
First the easy part, the template x-route.view.php.
<?php
use App\Common\Components\RouteMethodToUri;
use function Tempest\get;
?>
<a href="{{ get(RouteMethodToUri::class)->href($from) }}"><x-slot /></a>
I stole this code from the x-icon template.
The get
function takes care of the dependency injection.
use Tempest\Reflection\MethodReflector;
use Tempest\Router\RouteConfig;
use function Tempest\Support\Str\after_last;
use function Tempest\Support\Str\before_last;
class RouteMethodToUri
{
public function __construct(
private RouteConfig $routeConfig,
) {}
public function href(string $from = ''): string
{
if(strlen($from) == 0) {
return '/';
}
$routes = [];
foreach ($this->routeConfig->dynamicRoutes as $method => $routesForMethod) {
foreach ($routesForMethod as $route) {
$routes[$this->formatRouteHandler($route->handler)] = $route->uri;
}
}
foreach ($this->routeConfig->staticRoutes as $method => $routesForMethod) {
foreach ($routesForMethod as $route) {
$routes[$this->formatRouteHandler($route->handler)] = $route->uri;
}
}
if(!isset($routes[$from]))
{
return '/';
}
return $routes[$from];
}
private function formatRouteHandler(MethodReflector $handler): string
{
$namespace = before_last($handler->getDeclaringClass()->getName(), '\\');
$name = after_last($handler->getDeclaringClass()->getName(), '\\');
$method = $handler->getName();
return sprintf(
"%s\\%s::%s",
$namespace,
$name,
$method,
);
}
}
Most of this code is from the routes command. I changed the $routes
variable to hold the class and method concatination as the key and the uri as the value. And I made the formatRouteHandler
output simpler.
So now I can do <x-route from="app\Home\HomeController::__invoke">test</x-route>
and it returns a link with the uri.
Conclusion
Before I forget, yes moving all the files to directories of my choosing works as it says on the tin.
I focused on the templating but other things like classes as configuration and static pages are things that stand out for me.
For this first brief encounter my takeaway is that it is a framework to keep an eye on. At the moment it is in the beta fase.
Top comments (0)