https://a.storyblok.com/f/270183/1368x684/f8bd632cac/26nov-php_frameworks_r1.jpg

PHP Frameworks You May Not Have Heard Of

Published on November 10, 2026

Time to read: 12 minutes

Anyone can write a listicle, but it's far more interesting to have a look at what I've never seen before and play with it a bit rather than just telling everyone what's out there and listing out features.

The majority of content I write centres around Laravel and Symfony, a logical pairing considering those are two of the largest communities out there in PHP land, bar WordPress and Drupal. Many, many developers have trod down the path with these names on, so why not look at something different?

One thing I noticed during this is that several new platforms that are getting traction are based entirely around performance.

Slow speed is a criticism often aimed at PHP that I don't see as particular relevant anymore, and perhaps these frameworks are a testament to that, especially with PHP 8.6 shipping with the new Polling API.

So, armed with some projects that I have heard of, and some that I have not, I dug out the Vonage PHP SDK and threw it at some frameworks to see what would happen.

TempestPHP

A Stormy lighthouse at sea, used as a visual metaphor for TempestPHP's turbulent but powerful design.Caption: A new framework has got a storm brewingI've decided to kick off with TempestPHP, given that I recently wrote about JetBrains' PHPVerse event, which had Brent Roose as one half of the hosting duties.

It was some time ago now that I saw him launch TempestPHP on his blog, stitcher.io.

The "why to use a framework" is always important. There is a reason for everything. Laravel, for instance, came about because of dissatisfaction using CodeIgniter, and inspiration from Ruby on Rails. Symfony was born out of frustrations with Zend, and sometimes it's essentially a developer who has worked many, many hours on a particular framework and has the experience to say, "maybe I'll just do it my way?"

Tempest is that.

Using Composer's create-project to scaffold a new project:

composer create-project app

What's Out of the Box

Everything in Tempest has been written from the ground up. That even includes a new templating engine. You've got a service container, views, database, and routing.

Looking at the internal API level, you've got everything new and shiny – property hooks, attributes for routing (Symfony and API Platform both use similar patterns), and proxy objects (something new that I'm not that familiar with).

Using Tempest to Send a Vonage RCS Message

I'm going for the basics here, to cover a smaller amount of ground. We're going to have a route to present a form, the user adds a phone number in and message, hits send, the code sends the message (this is also under the requirement that you have set up your sandbox for RCS messaging).

Install the Vonage PHP SDK

First up, let's bring in the Vonage PHP SDK:

composer require vonage/client-core

You'll then need your credentials by creating a new application in the Vonage Dashboard or with the Vonage CLI, resulting in an Application ID and private.key if successful.

Open up your .env file and add your credentials:

VONAGE_APPLICATION_ID=your-application-id
VONAGE_PRIVATE_KEY_PATH="./private.key"

Wire up the Vonage Client

Tempest has quite an interesting design, in that it doesn't give you a set pattern with which to follow; it's the antithesis of the "opinionated framework".

I find this approach fairly similar to SlimPHP, although really Tempest is designed to make the attributes/property hooks/proxies speak as the identifiers for your structure. It's certainly a departure from what I'm used to.

For the Vonage client, we're going to create a Service which will use Tempest's discovery to be injected to a controller.

namespace App;

use Vonage\Client;
use Vonage\Client\Credentials\Keypair;

final class VonageClientFactory
{
    public function create(): Client
    {
        return new Client(
            new Keypair(
                $_ENV['VONAGE_APPLICATION_ID'],
                $_ENV['VONAGE_PRIVATE_KEY'],
            )
        );
    }
}

Creating the Form

There are two parts to this: a Tempest request object which can handle things like validation, and the form itself, which is a view.

Something I quite like is the use of traits to identify your code for high-level discovery. Here is the request object:

<?php

namespace App\Messaging;

use Tempest\Http\Request;
use Tempest\Router\IsRequest;
use Tempest\Validation\Rules\Length;

final class SendRcsMessageRequest implements Request
{
    use IsRequest;

    #[Length(min: 10, max: 20)]
    public string $phoneNumber;

    #[Length(min: 1, max: 3072)]
    public string $message;
}

I've just gone for string type and length, but you can also add custom rules. The Vonage PHP SDK has, for instance, a isValidE164() method built into it which you could bake into your validation rules.

Now for the form:

<form method="post" action="/messages">
    <div>
        <label for="phoneNumber">Phone number</label>
        <input id="phoneNumber" name="phoneNumber" type="tel" placeholder="447700900123" required>
    </div>
    <div>
        <label for="message">Message</label>
        <textarea id="message" name="message" rows="6" required></textarea>
    </div>
    <button type="submit">Send message</button>
</form>

You can see that the form's action is a POST to /messages, so the final wire-up is a controller. Tempest lets you choose your own structure, so traditionally in frameworks I've used, it has the route → controller → view pattern. With attributes and discovery, though, you don't have to have a separate route file if you don't want one.

So I create a controller which handles the RCS sending logic, and we're done:

<?php

namespace App\Messaging;

use Tempest\Http\Redirect;
use Tempest\Http\Session\Session;
use Tempest\Router\Post;
use Vonage\Client;
use Vonage\Messages\Channel\Rcs\RcsText;

final readonly class RcsController
{
    public function __construct(
        private Client $vonage,
        private Session $session,
    ) {}

    #[Post('/messages')]
    public function send(
        SendRcsMessageRequest $request,
    ): Redirect {
        $message = new RcsText(
            $request->phoneNumber,
            $_ENV['VONAGE_RCS_SENDER_ID'],
            $request->message,
        );

        $response = $this->vonage
            ->messages()
            ->send($message);

        $this->session->flash(
            'success',
            'RCS message sent'
        );

        return new Redirect('/messages');
    }
}

Similar to how other PHP frameworks work, it makes use of the session()->flash() functionality, so now all that's left to do is add the flash rendering below the form view:

<?php if ($success = session()->get('success')): ?>
    <div class="success">
        <?= $success ?>
    </div>
<?php endif; ?>

You'll notice the PHP tags, but don't be alarmed! We've seen Laravel Livewire components used in this way, which is essentially a close template pattern to how VueJS works: the logic section comes first, the HTML template comes second.

Tempest has given me a pretty straight-up way to wire in Vonage, no big gotchas to mull over. The experience feels pretty smooth to me, especially with having Laravel and Symfony experience, but also absolutely worth having a play with as the autowiring discovery is slightly more novel (you can form your own opinion on whether more or less "magic" is a good thing).

Phalcon

A juvenile peregrine falcon perched beside a weathered branch, looking back over its shoulder at the camera.Phalcon is a name I have heard bandied about for quite some time, and even back when I did hear about it in around 2016, it was the performance that was getting people's attention. This framework is like nothing I've seen before: it's actually a C extension that exposes the framework elements for PHP developers.

Getting Started (Throwing the Rulebook Out the Window)

I am immediately flummoxed. Such is the prevalence of creating a new framework installation for an app through either composer create-project or a CLI-wrapped implementation that uses it under the hood; I am immediately thrown outside of my comfort zone.

It doesn't use Composer; it's a PHP extension. The installation instructions are long; they depend on your operating system, and there are warnings and gotchas everywhere. The framework itself is all compiled and held in memory. This is going to be a bit left-field for me.

After following a mass of work, which honestly felt like a bit of harkening back to the old days of manually wiring Apache, MySQL, and php-fpm together, I can actually throw some code at it. It's not a Packagist project, so this is about as manual as it comes:

composer init --no-interaction

This sets up your project in the current directory, and then next we need Vonage:

composer require vonage/client

I've used the client library here because it will also bundle in Guzzle, something that Phalcon won't have shipped out of the box. It's a very different approach.

As we did in the previous section with TempestPHP, you'll need an Application ID and a private.key file. Put the key file in the root of your project, but rather than the Application ID going into an .env file, think of how this would work more at a runtime level. Instead of an .env file, the Application ID goes in via a terminal:

export VONAGE_APPLICATION_ID=xxx-xxx-xxx-xxx

A bit different, right?

Boot the Phalcon Application

That's right, nothing comes out of the box as there's no Composer. Instead, Phalcon is now (as a PHP C extension) available as "part of the core". So, we build our own bootstrap file in its own space, public/index.php:

<?php

use Phalcon\Autoload\Loader;
use Phalcon\Di\FactoryDefault;
use Phalcon\Mvc\Application;
use Phalcon\Mvc\View;

require_once __DIR__ . '/../vendor/autoload.php';

// Register controllers directory with Phalcon's autoloader
$loader = new Loader();
$loader->setDirectories([
    __DIR__ . '/../app/controllers/',
]);
$loader->register();

// Create the Dependency Injection container
$di = new FactoryDefault();

// Register the View service
$di->set('view', function () {
    $view = new View();
    $view->setViewsDir(__DIR__ . '/../app/views/');
    return $view;
});

// Handle the request
$app = new Application($di);

try {
    $response = $app->handle($_SERVER['REQUEST_URI']);
    $response->send();
} catch (\Exception $e) {
    echo "Exception: " . $e->getMessage();
}

We've set the folders for controllers and views, so we can create the view. Phalcon uses a basic view system with .phtml, but I'd recommend later on actually using a templating engine like Blade or Twig.

We've registered our views directory as app/views, so this view goes under app/views/index/index.phtml:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vonage RCS Messenger</title>
    <style>
        body { font-family: sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; }
        label { display: block; margin-top: 16px; font-weight: bold; }
        input, textarea { width: 100%; padding: 8px; margin-top: 4px; box-sizing: border-box; }
        button { margin-top: 20px; padding: 10px 24px; background: #871fff; color: white; border: none; cursor: pointer; border-radius: 4px; }
        .result { margin-top: 20px; padding: 12px; border-radius: 4px; }
        .success { background: #d4edda; color: #155724; }
        .error { background: #f8d7da; color: #721c24; }
    </style>
</head>
<body>
    <h1>Send an RCS Message</h1>
    <p>Send a Rich Communication Services (RCS) message via the Vonage Messages API.</p>
    <?php if ($result): ?>
        <div class="result <?= $result['success'] ? 'success' : 'error' ?>">
            <?= htmlspecialchars($result['message']) ?>
        </div>
    <?php endif; ?>
    <form action="/send" method="POST">
        <label for="to_number">To Number (E.164 format, e.g. 447700900000)</label>
        <input type="text" id="to_number" name="to_number" required placeholder="447700900000">
        <label for="sender_id">RCS Sender ID</label>
        <input type="text" id="sender_id" name="sender_id" required placeholder="Your RCS Sender ID">
        <label for="message">Message</label>
        <textarea id="message" name="message" rows="4" required placeholder="Type your RCS message here..."></textarea>
        <button type="submit">Send RCS Message</button>
    </form>
</body>
</html>

Ah, yes. Hence why I suggested using a templating engine. Out of the box, Phalcon has good old-fashioned PHP logic mixed into an HTML form. I guess you could say that was similar to Tempest, but Tempest definitely has a separation of concerns to make the logic and the template different parts that make a component up.

The last part is the controller, which comes under app/controllers/IndexController.php:

<?php

use Phalcon\Mvc\Controller;

class IndexController extends Controller
{
    /**
     * Display the RCS send form.
     */
    public function indexAction(): void
    {
        // Pass an empty result to the view by default
        $this->view->result = null;
    }

    /**
     * Process the form submission and send an RCS message via Vonage.
     */
    public function sendAction(): void
    {
        // Only accept POST requests
        if (!$this->request->isPost()) {
            return $this->response->redirect('/');
        }

        $toNumber    = $this->request->getPost('to_number', 'string');
        $senderId    = $this->request->getPost('sender_id', 'string');
        $messageText = $this->request->getPost('message', 'string');

        // Authenticate using your Application ID and private key
        $keypair = new \Vonage\Client\Credentials\Keypair(
            file_get_contents(__DIR__ . '/../../private.key'),
            getenv('VONAGE_APPLICATION_ID')
        );

        $client = new \Vonage\Client($keypair);

        try {
            // Build and send the RCS text message
            $rcsText = new \Vonage\Messages\Channel\RCS\RcsText(
                $toNumber,
                $senderId,
                $messageText
            );

            $client->messages()->send($rcsText);

            $this->view->result = [
                'success' => true,
                'message' => "RCS message successfully sent to {$toNumber}!",
            ];
        } catch (\Exception $e) {
            $this->view->result = [
                'success' => false,
                'message' => "Error sending message: " . $e->getMessage(),
            ];
        }

        // Render the index view with the result
        $this->view->pick('index/index');
    }
}

Remarkably, the code still looks like a mixture of plain PHP and a little bit of framework code. The routing got to me a little bit, though, as I didn't define a route or indeed used any of the routing or dispatching system within Phalcon. So, because index is the controller namespace, I've ended up with what I want (i.e. GET / and POST /) but the reason the home will land is that 'index' is the default route name.

To serve the app, it's easiest to use the PHP built-in web server:

cd public
php -S localhost:8080

The Verdict

Phew, done. I don't think I've ever really seen a pattern and structure like it in a framework, but given there are some nice features in here – a 'Micro' application structure you can use, for instance – I can absolutely see the value in using this as a microservices or serverless backend.

Better still, pairing the speed of the native C extension with an event loop like ReactPHP would really put benchmarks against other performance-focused languages, let alone frameworks.

LeafPHP: The Pivot to AI

Autumn maple leaves representing LeafPHP's lightweight, seasonal growth philosophyBack into more conventional territory (to an extent), we leave behind the mad world of standalone C extensions and enter back into Web Application Development, but with an "AI-first" mantra.

It's another example, like Slim and Phalcon, where you really aren't given much out of the box. Just routing and the request/response objects. You can then build out as much as you want (and the list of packages is pretty extensive).

Because it's been designed to make sure the internal API is all chainable among the packages, it means a better experience for AI agents building out. Rather than just performance speed, they want (similar in a Laravel vein of thinking) to be fast at developing. From brain to shipped, something I am admittedly terrible at (but that's what the AI era is for).

Installing Leaf

Like Symfony and Laravel, Leaf comes with its own CLI to help things along. First, use Composer to install it globally on your machine:

composer global require leafs/cli

Next, create a new LeafPHP application and install the Vonage PHP SDK:

leaf create vonage-leaf
cd vonage-leaf
composer require vonage/client-core

We're going back to the more traditional usage of an .env file, although the internal syntax to call an env variable is somewhat different from what I've been used to. Add the Application ID to the .env file, and the private.key to the project root. Calling the env variable looks like this:

<?php
$applicationId = _env(VONAGE_APPLICATION_ID);

Unlike Phalcon, Leaf has a configuration system for env out of the box, which makes it quite a nice comparable developer experience to other frameworks (though there is a good reason that Phalcon doesn't – its use case is somewhat different).

Setting Up the Vonage Service

Like Tempest, Leaf ships with a Service container, which is a binding application pattern for Dependency Injection. Like other PHP frameworks like Symfony and Laravel, you can type-hint the Service into a Controller's constructor, and it becomes available (and you can create mock objects during testing to replace it).

<?php

namespace App\Services;

use Vonage\Client;
use Vonage\Client\Credentials\Keypair;
use Vonage\Messages\Channel\RCS\RcsText;

class VonageService
{
    private Client $client;

    public function __construct()
    {
        $keypair = new Keypair(
            file_get_contents(_env('VONAGE_PRIVATE_KEY_PATH')),
            _env('VONAGE_APPLICATION_ID')
        );

        $this->client = new Client($keypair);
    }

    public function sendRcs(
        string $to,
        string $message
    ): void {
        $rcsText = new RcsText(
            $to,
            'vonage',
            $message
        );

        $this->client->messages()->send($rcsText);
    }
}

You can see that the Service is actually just a PHP class, rather than extending anything. This is because Leaf acts in the more traditional framework style of registering a service provider that needs to be registered.

I could have also exposed the Client in a public method which would then use the Messages API Client (inside the Vonage Client) to use the send() method, but to mix it up a bit, the VonageService instead has a wrapper method named sendRcs() that handles this for you.

Next up, we need a controller that a route will eventually be wired to:

<?php

namespace App\Controllers;

use App\Services\VonageService;

class MessageController extends Controller
{
    public function __construct(
        private VonageService $vonage
    ) {}

    public function create()
    {
        return response()->view('message');
    }

    public function send()
    {
        $data    = request()->body();
        $to      = $data['to'] ?? null;
        $message = $data['message'] ?? null;

        if (!$to || !$message) {
            return response()->view('message', [
                'error'   => 'Both a phone number and message are required.',
                'to'      => $to,
                'message' => $message,
            ]);
        }

        try {
            $this->vonage->sendRcs($to, $message);
            return response()->view('message', [
                'success' => 'Message sent successfully.',
            ]);
        } catch (\Throwable $exception) {
            return response()->view('message', [
                'error'   => 'Unable to send the message.',
                'to'      => $to,
                'message' => $message,
            ]);
        }
    }
}

The VonageService is injected automatically, and we can use the sendRcs method after some basic validation. You'll note that the GET request is tied to a view named message, which we've not created yet (or routed to the create() method), so we now need to make the form.

Create it under app/views/message.blade.php – those Laravel developers among us might notice that Leaf ships with the Blade templating engine out of the box, which will be familiar.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Send an RCS message</title>
    <style>
        body { font-family: sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
        label { display: block; margin-top: 20px; margin-bottom: 5px; }
        input, textarea { width: 100%; padding: 10px; box-sizing: border-box; }
        textarea { min-height: 150px; }
        button { margin-top: 20px; padding: 10px 20px; }
        .success { padding: 10px; background: #dff0d8; margin-bottom: 20px; }
        .error { padding: 10px; background: #f2dede; margin-bottom: 20px; }
    </style>
</head>
<body>
    <h1>Send an RCS message</h1>

    @if (!empty($success))
        <div class="success">{{ $success }}</div>
    @endif

    @if (!empty($error))
        <div class="error">{{ $error }}</div>
    @endif

    <form method="POST" action="/message">
        <label for="to">Phone number</label>
        <input type="text" id="to" name="to" value="{{ $to ?? '' }}" placeholder="447700900000" required>

        <label for="message">Message</label>
        <textarea id="message" name="message" required>{{ $message ?? '' }}</textarea>

        <button type="submit">Send RCS message</button>
    </form>
</body>
</html>

There's the form with the action POSTing to the /message endpoint, so now we need to register the route. This is done in a similar bootstrap approach to something like Slim. Create your entry point under /public/index.php:

<?php

require __DIR__ . '/../vendor/autoload.php';

use App\Controllers\MessageController;
use App\Services\VonageService;
use Leaf\App;

$app = new App();

$app->register(
    'vonage',
    fn () => new VonageService()
);

$controller = new MessageController(
    $app->vonage
);

$app->get('/message', [$controller, 'create']);
$app->post('/message', [$controller, 'send']);

$app->run();

And there we have it: an RCS service, controller, and view.

Conclusion

So, a quick summary of our three frameworks:

Framework

Pros

Cons

Use Case Examples

TempestPHP

Brand new, modern PHP source code; Automatic framework discovery feature; An integrated Reflection API, brilliant for testing

Not production-ready yet; Documentation still an evolving work-in-progress; Small community due to lack of maturity

The rapid speed of development, plus less boilerplate in Tempest makes it an ideal candidate for building a custom SaaS Dashboard

Phalcon

Remarkable speed; Low memory usage and requirements; Adaptable to either a full MVC stack or a micro instance

DevOps pipelines and infrastructure are significantly more complex to handle, due to it being a C extension; Community is relatively niche

A REST (or gRPC) platform that requires high performance benchmarks for high throughput

LeafPHP

The bonus of features from something big, like Laravel, but far smaller and modular; Guardrails for rapid AI Development

Less production-ready than options like Slim, Symfony, or Laravel; Small community due to maturity

Due to the smaller footprint, Leaf makes an excellent choice for prototyping or developing an MVP at something like a Hackathon

The joy of using PHP is the vast amount of options you have. Symfony, Laravel, WordPress, and Drupal all have large, mature communities supporting them. However, so does TYPO3, and both CakePHP and CraftCMS boast an impressive array of clients that use their technology.

AI might be able to drive forward features and code at a lightning pace in the new world of software development, but the choice of stack that powers what you build is still something only you, an architect, can investigate and pick accordingly.

Want to learn, stay connected, or share what you're building?

Keep learning

Stay connected

Share:

https://a.storyblok.com/f/270183/400x385/12b3020c69/james-seconde.png
James SecondeSenior PHP Developer Advocate

A trained actor with a dissertation on standup comedy, I came into PHP development via the meetup scene. You can find me speaking and writing on tech, or playing/buying odd records from my vinyl collection.