The PHP framework
that embraces the browser.

Components. HTML over the wire.
No build step. No JS framework.

Preflow is a PHP framework built on a simple idea: the server renders HTML, the browser displays it. No JSON APIs to glue together, no client-side framework to learn. Components, templates, and styles live together — co-located, auto-discovered, and ready to ship.

Component Architecture

PHP class + template + CSS in one directory. Co-located, auto-discovered, self-contained. Every component is a unit.

HTML Over the Wire

The server renders HTML fragments, a hypermedia driver swaps them in. HTMX ships as the default — or bring Datastar, or build your own. The interface is open.

Zero External Requests

All CSS and JS are inlined in the HTML document. Hash-deduplicated. CSP nonces on every tag. No bundler, no build step.

Multi-Storage ORM

SQLite, JSON files, or MySQL — same query API. Each model picks its storage backend via a PHP attribute. Mix and match.

Template Engine Freedom

Twig by default. Blade as an alternative. Both sit behind the same interface — swap with one config change, no code rewrites.

Security First

HMAC-signed component tokens, CSRF protection, error boundaries, CSP nonces, session fixation prevention. Secure by default.

Up and running
in 30 seconds

Three commands. No configuration. No boilerplate.

1
Create your project
composer create-project preflow/skeleton myapp
2
Start the dev server
cd myapp && php preflow serve
3
Open in your browser
http://localhost:8080

Components as units

A Preflow component is a PHP class and a template in one directory. State, actions, and rendering — all in one place. No scattered files, no wiring.

ExampleCard.php php
<?php

final class ExampleCard extends Component
{
    public string $title = '';
    public int $count = 0;

    public function resolveState(): void
    {
        $this->title = $this->props['title'] ?? 'Hello';
        $this->count = (int) $this->session->get('counter', 0);
    }

    public function actions(): array
    {
        return ['increment'];
    }

    public function actionIncrement(): void
    {
        $this->count++;
        $this->session->set('counter', $this->count);
    }
}

Templates that own their style

CSS lives inside the template, right next to the markup it styles. No separate stylesheet, no build step. The asset collector deduplicates and inlines everything with CSP nonces.

ExampleCard.twig twig
<div class="example-card">
    <h2>{{ title }}</h2>
    <p>{{ message }}</p>
    <p>Count: {{ count }}</p>
    <button {{ hd.post('increment', componentClass, componentId, props) | raw }}>+1</button>
</div>

{% apply css %}
.example-card {
    padding: 2rem;
    border-radius: 0.5rem;
    background: #f8f9fa;
    max-width: 24rem;
    font-family: system-ui, sans-serif;
}

.example-card h2 {
    margin: 0 0 0.5rem;
    color: #333;
}

.example-card button:hover {
    background: #0052cc;
}
{% endapply %}

Models with attributes

Define your schema with PHP attributes. Pick a storage backend per model — SQLite, JSON files, or MySQL. The query API stays the same.

Post.php — Model php
<?php

#[Entity(table: 'posts', storage: 'sqlite')]
final class Post extends Model
{
    #[Id]
    public string $uuid = '';

    #[Field(searchable: true)]
    public string $title = '';

    #[Field]
    public string $slug = '';

    #[Field]
    public string $status = 'draft';

    #[Field]
    public ?string $created_at = null;
}

How it works

Every request flows through a clean, predictable pipeline.

Request
Middleware
Session, CSRF, i18n
Kernel
Component Mode
Pages & templates
or
Action Mode
Controllers & APIs
Response

13 packages, one framework

Every package has a single job. Require what you need, skip what you don't.

Explore the
documentation