preflow/skeleton

Starter template for Preflow applications. Provides a complete project scaffold with routing, components, authentication, i18n, and a blog admin demo.

Quick Start

composer create-project preflow/skeleton myapp
cd myapp
php preflow serve

Open http://localhost:8080. The installer handles .env, database, and demo content automatically.

Project Structure

app/
├── Components/    Reusable UI components (PHP + template + CSS/JS)
├── Controllers/   API and form controllers (#[Route] attributes)
├── Models/        Data models (#[Entity] attributes)
├── Providers/     Service providers
├── Seeds/         Demo data seeders
└── pages/         File-based routes (Twig templates)
config/            Framework configuration
lang/              Translation files (en/, de/)
migrations/        Database schema
public/            Web root (index.php, .htaccess)
storage/           SQLite database, cache, logs
tests/             PHPUnit tests

What's Included

Routing

File-based routes in app/pages/ map to URLs by directory structure. Dynamic segments use brackets: blog/[slug].twig matches /blog/hello-world.

Controllers use PHP attributes:

#[Route('/api')]
final class HealthController
{
    #[Get('/health')]
    public function health(ServerRequestInterface $request): ResponseInterface
    {
        return new Response(200, ['Content-Type' => 'application/json'],
            json_encode(['status' => 'ok']));
    }
}

Components

A component is a PHP class + Twig template + inline CSS/JS in one directory. Drop it in app/Components/, it auto-discovers.

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

    public function __construct(private readonly SessionInterface $session) {}

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

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

    public function actionIncrement(array $params = []): void
    {
        $this->count = (int) $this->session->get('example_counter', 0) + 1;
        $this->session->set('example_counter', $this->count);
    }
}

Use in templates: {{ component('ExampleCard', { title: 'Hello' }) }}

Authentication

Login, registration, and logout are included. Protect routes with middleware:

#[Route('/dashboard')]
#[Middleware(AuthMiddleware::class)]
final class DashboardController { /* ... */ }

Templates can check auth status:

{% if auth_check() %}
    Welcome, {{ auth_user().email }}
{% endif %}

Blog Admin

The skeleton includes a full blog admin at /admin (requires login). Create, edit, and delete posts through a form-based interface.

Default admin credentials:

  • Email: admin@preflow.dev
  • Password: password

The admin demonstrates the hybrid pattern: PostForm component handles form UI with co-located CSS, BlogAdminController handles CRUD logic with redirects and flash messages.

Internationalization

Translations live in lang/{locale}/{group}.php. Switch languages with the locale switcher in the header, or visit /de/... for German.

{{ t('blog.title') }}
{{ t('blog.published', { date: '2026-01-01' }) }}
{{ t('blog.post_count', {}, 5) }}

Data Layer

Models use PHP attributes for storage mapping:

#[Entity(table: 'posts', storage: 'default')]
final class Post extends Model
{
    #[Id] public string $uuid = '';
    #[Field(searchable: true)] public string $title = '';
    #[Field] public string $status = 'draft';
}

Query with the DataManager:

$posts = $dm->query(Post::class)
    ->where('status', 'published')
    ->orderBy('uuid', SortDirection::Desc)
    ->get();

HTMX

Components can define actions that handle HTMX requests. The ExampleCard counter persists in the session across page reloads -- no JavaScript needed.

Configuration

File Purpose
config/app.php App name, debug level, timezone, locale, template engine
config/auth.php Guards, user providers, session settings
config/data.php Storage drivers (SQLite, JSON, MySQL)
config/i18n.php Available locales, fallback, URL strategy
config/providers.php Service provider registration
.env Environment-specific overrides

CLI Commands

php preflow serve           # Start dev server (localhost:8080)
php preflow migrate         # Run pending migrations
php preflow db:seed         # Seed demo data
php preflow key:generate    # Generate APP_KEY
php preflow routes:list     # List all routes
php preflow cache:clear     # Clear cache

Web Server

Development: php preflow serve -- no configuration needed.

Apache: Point your document root to the public/ directory. The included .htaccess handles URL rewriting. Ensure mod_rewrite is enabled.

Nginx:

server {
    listen 80;
    server_name myapp.test;
    root /path/to/myapp/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Testing

./vendor/bin/phpunit

Tests live in tests/. The skeleton includes example tests for components and routing.