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.
PHP class + template + CSS in one directory. Co-located, auto-discovered, self-contained. Every component is a unit.
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.
All CSS and JS are inlined in the HTML document. Hash-deduplicated. CSP nonces on every tag. No bundler, no build step.
SQLite, JSON files, or MySQL — same query API. Each model picks its storage backend via a PHP attribute. Mix and match.
Twig by default. Blade as an alternative. Both sit behind the same interface — swap with one config change, no code rewrites.
HMAC-signed component tokens, CSRF protection, error boundaries, CSP nonces, session fixation prevention. Secure by default.
Three commands. No configuration. No boilerplate.
composer create-project preflow/skeleton myapp
cd myapp && php preflow serve
http://localhost:8080
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.
<?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);
}
}
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.
<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 %}
Define your schema with PHP attributes. Pick a storage backend per model — SQLite, JSON files, or MySQL. The query API stays the same.
<?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;
}
Every request flows through a clean, predictable pipeline.
Every package has a single job. Require what you need, skip what you don't.