waffle-commons/skeleton

The official skeleton for Waffle Framework applications.

Maintainers

Package info

github.com/waffle-commons/skeleton

Type:project

pkg:composer/waffle-commons/skeleton

Statistics

Installs: 10

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

0.1.0-beta4 2026-06-14 06:08 UTC

README

Waffle Ecosystem Logo

๐Ÿฆ Waffle Skeleton

The official starting point for building robust, secure, and high-performance applications with the Waffle Ecosystem.

Minimum PHP Version Waffle Ecosystem License

Release: 0.1.0-beta4 ย |ย  PHP 8.5+ ยท FrankenPHP worker mode

Welcome to the Waffle Skeleton, the official starting point for building robust, secure, and high-performance applications with the Waffle Ecosystem.

This skeleton is not just a folder structure; it is a Production-Grade Boilerplate pre-configured with:

  • FrankenPHP (Caddy): Modern application server with native HTTP/3 and Early Hints support.

  • Docker Multi-Stage: Optimized images for Development (with Xdebug) and Production (Immutable, <100MB).

  • Hardened Security Defaults: Auto-generated secrets, fail-closed CORS (SEC-04), default-on SSRF protection on outbound calls (SEC-02), stateless HMAC CSRF (SEC-01), and the Universal Authentication Bridge (RFC-021).

  • Strict Standards: PHP 8.5+, Typed Properties, and Read-Only classes.

๐Ÿš€ Installation

Prerequisites

  • Docker & Docker Compose (Required)

  • PHP 8.5+ & Composer (Optional, for local commands)

Create a New Project

Use Composer to create your project. This will automatically trigger the setup scripts to generate secure keys and initialize the directory structure.

composer create-project waffle-commons/skeleton my-app
cd my-app

Note: If you don't have PHP installed locally, you can use the Docker setup immediately after cloning the repository manually.

๐Ÿณ Docker Environment

Waffle is Cloud-Native by design. We provide two distinct environments managed via a single Dockerfile.

1. Development Mode (dev)

Optimized for Developer Experience (DX).

  • Hot Reload: Code is mounted via volumes. Changes are reflected instantly.

  • Debugging: Xdebug is installed and configured.

  • Tooling: Composer is available inside the container.

Start the Dev Server:

docker compose up --build -d

Your application is now available at: ๐Ÿ‘‰ https://localhost (Accept the self-signed certificate auto-generated by Caddy).

2. Production Mode (prod)

Optimized for Performance and Security.

  • Immutable: No source code volumes. The code is baked into the image.

  • Fast: Opcache validation is disabled, Preloading is enabled.

  • Secure: Dev tools (Composer, Xdebug) are removed. Rootless execution.

Test the Production Build locally:

docker compose -f docker-compose.prod.yml up --build -d

๐Ÿ“‚ Directory Structure

A Waffle application follows a strict but simple structure:

.
โ”œโ”€โ”€ config/                       # โš™๏ธ Configuration
โ”‚   โ”œโ”€โ”€ app.yaml                  # Main Waffle Configuration
โ”‚   โ””โ”€โ”€ preload.php               # Opcache Preloading Script
โ”œโ”€โ”€ docker/                       # ๐Ÿณ Infrastructure as Code (Dockerfile, Caddyfile, PHP config)
โ”œโ”€โ”€ migrations/                   # ๐Ÿ—ƒ๏ธ Versioned SQL migration scripts (bin/waffle db:migrate)
โ”œโ”€โ”€ public/                       # ๐ŸŒ Web Entry Point (index.php)
โ”œโ”€โ”€ scripts/                      # ๐Ÿ› ๏ธ Composer Lifecycle Scripts
โ”œโ”€โ”€ src/                          # ๐Ÿง  Your Application Logic (Namespace: App\)
โ”‚   โ”œโ”€โ”€ Controller/               # HTTP Entry points
โ”‚   โ”œโ”€โ”€ Factory/                  # Application Factory
โ”‚   โ”‚  โ”œโ”€โ”€ AppKernelFactory.php   # The Kernel Factory (Dependencies)
โ”‚   โ”œโ”€โ”€ Service/                  # Business Logic
โ”‚   โ””โ”€โ”€ Kernel.php                # The Application Core (Configuration & Boot)
โ”œโ”€โ”€ tests/                        # ๐Ÿงช PHPUnit Test Suite
โ””โ”€โ”€ var/                          # ๐Ÿ“ฆ Temporary files (Cache, Logs, Exports, etc) - Ignored by Git

๐Ÿ› ๏ธ Configuration

Environment Variables (.env)

Waffle ships a native DotEnv parser (no third-party dependency). When you run create-project, a .env file is automatically created from .env.example with a generated APP_SECRET.

Variable Description
APP_ENV dev (debug enabled) or prod (optimized).
APP_DEBUG true displays detailed stack traces. false renders JSON errors.
APP_SECRET 32-byte Hex string used for cryptographic operations.
WAFFLE_CSRF_SECRET 32+ byte signing secret for stateless HMAC CSRF tokens (Beta-1 / SEC-01). Bound at boot by AppKernelFactory::resolveCsrfSecret(). In prod, a missing or short value aborts boot; non-prod falls back to a per-process random secret.
WAFFLE_AUTH_SECRET 32+ byte shared HMAC-SHA256 secret for the Universal Authentication Bridge (RFC-021). Signs X-Wfl-Assert-User identity assertions and validates the demo HS256 JWTs. In prod, a missing or short value aborts boot (fail-closed).
SERVER_NAME The domain name used by Caddy (e.g., example.com or localhost).
DB_HOST / DB_PORT Database host and port consumed by waffle.database.* (RFC-022).
DB_NAME Database / schema name.
DB_USER / DB_PASSWORD Database credentials. Override from your orchestrator in production.

โš  Precedence: OS env wins over .env. AppKernelFactory merges your .env with the live process environment (Docker environment:, Kubernetes env:, shell exports, etc.) via array_merge((new DotEnv($root))->load(), getenv()). Because array_merge is rightmost-wins on string keys, the OS value beats .env on collision. If you edit .env and the change doesn't take effect, check whether the same variable is exported by your shell or docker-compose.yml โ€” that export will silently override .env. This matches the Twelve-Factor convention. See documentation/how-to/configuration.md for the merge rules and the type-normalization foot-gun around APP_DEBUG/DEBUG.

Framework Config (config/app.yaml)

Waffle uses native YAML parsing (via PECL extension) for blazing-fast configuration loading.

# Main application configuration for the Waffle Framework
waffle:
  env: '%env(APP_ENV)%'
  debug: '%env(APP_DEBUG)%'
  # Host-header allowlist (anti-poisoning). REQUIRED in production.
  trusted_hosts:
    - localhost
  security:
    level: 10
    csrf:
      # SEC-01: stateless HMAC CSRF secret. Prod refuses to boot without a 32+ byte value.
      secret: '%env(WAFFLE_CSRF_SECRET)%'
    # SEC-04: fail-closed CORS. Empty โ‡’ every cross-origin request is rejected.
    # Add exact origins (scheme://host[:port]); never '*' with credentials.
    cors:
      allowed_origins: []
    # SEC-02: the outbound HTTP client resolves โ†’ validates โ†’ pins every host,
    # refusing private/loopback/reserved IPs. allowed_hosts whitelists trusted
    # internal backends (exact host or CIDR) โ€” keep it tight.
    ssrf:
      allowed_hosts: []
  # Universal Authentication Bridge (RFC-021). The secret aborts boot in prod if absent.
  auth:
    secret: '%env(WAFFLE_AUTH_SECRET)%'
    tenant: 'skeleton'
    jwt:
      issuer: 'https://waffle-dev.local'
      audience: 'waffle-skeleton'
  paths:
    controllers: 'src/Controller'
    services: 'src/Service'
  # Database & migrations (RFC-022). Credentials resolved from DB_* env vars.
  database:
    driver: 'mysql'
    host: '%env(DB_HOST)%'
    port: '%env(DB_PORT)%'
    database: '%env(DB_NAME)%'
    username: '%env(DB_USER)%'
    password: '%env(DB_PASSWORD)%'
    charset: 'utf8mb4'
    migrations_path: 'migrations'

The shipped config/app.yaml also wires log (PSR-3 channel) and cache (PSR-16 adapter) โ€” see the file for the full, commented set.

Security defaults (Beta-4)

The skeleton ships the canonical Beta-4 middleware pipeline out of the box:

ErrorHandler โ†’ TrustedHost โ†’ CORS โ†’ AnonymousSession โ†’ Authentication โ†’ Routing โ†’ CSRF โ†’ Security โ†’ SecureHeaders โ†’ Dispatcher

This means every controller action you write is, by default, subject to:

  • Fail-closed ABAC โ€” an action without #[Voter] returns HTTP 403. Tag explicitly public actions with #[\Waffle\Commons\Contracts\Security\Attribute\PublicAccess].
  • Stateless HMAC CSRF on mutating routes (SEC-01) โ€” opt actions in with #[RequiresCsrfToken]. Tokens are HMAC-bound to the per-browser WAFFLE_SID cookie issued by AnonymousSessionMiddleware, which rotates the SID on privilege change.
  • Fail-closed CORS (SEC-04) โ€” cross-origin requests are rejected unless their origin is listed in waffle.security.cors.allowed_origins; wildcard-with-credentials is refused at construction.
  • Universal Authentication (RFC-021) โ€” the auth bridge verifies inbound credentials (JWT / OAuth2-OIDC / HMAC assertion / API key / Basic) fail-closed and publishes the identity for ABAC.

The outbound HTTP client is hardened too: default-on SSRF protection (SEC-02) resolves โ†’ validates โ†’ pins every target host and refuses private/reserved addresses, with waffle.security.ssrf.allowed_hosts whitelisting trusted internal backends.

See the framework docs at waffle-commons/documentation for the full design rationale.

๐Ÿ—ƒ๏ธ Database & Migrations

Waffle ships a lightweight, forward-only SQL migration runner (RFC-022, via waffle-commons/data). Database access is configured under waffle.database in config/app.yaml (credentials from the DB_* env vars), and the connection pool + runner are wired in src/Factory/AppKernelFactory.php and registered as db:migrate in bin/waffle.

Write versioned SQL scripts in migrations/, named Version<YYYYMMDDNN>_<Description>.sql, then apply the pending ones from the project root:

php bin/waffle db:migrate

Each migration runs in its own transaction and is recorded in a waffle_migrations table, so re-runs skip already-applied scripts. The skeleton ships a sample migrations/Version2026053101_CreateUsersTable.sql to get you started. See the Database Migrations how-to for the full workflow and the MySQL transactional-DDL caveat.

๐Ÿ‘ฉโ€๐Ÿ’ป Usage Example

1. Create a Service

Create src/Service/Greeter.php:

<?php

namespace App\Service;

readonly class Greeter
{
    public function sayHello(string $name): string
    {
        return "Welcome to Waffle, {$name}!";
    }
}

2. Create a Controller

Create src/Controller/HelloController.php. Waffle uses Attributes for routing.

<?php

namespace App\Controller;

use App\Service\Greeter;
use Psr\Http\Message\ResponseInterface;
use Waffle\Commons\Routing\Attribute\Argument;
use Waffle\Commons\Routing\Attribute\Route;
use Waffle\Core\BaseController;

class HelloController extends BaseController
{
    // Services are automatically injected (Autowiring)
    public function __construct(
        private Greeter $greeter
    ) {}

    #[Route(path: '/greet/{name}', method: 'GET')]
    public function index(string $name): ResponseInterface
    {
        $message = $this->greeter->sayHello($name);

        return $this->jsonResponse(data: [
            'message' => $message,
            'status' => 'success'
        ]);
    }
}

Go to https://localhost/greet/Developer. You should see your JSON response!

๐Ÿงช Running Tests

The skeleton comes with PHPUnit pre-configured.

# Run tests inside the Docker container
vendor/bin/phpunit

๐Ÿค Contributing

We welcome contributions! Please see CONTRIBUTING.md for details.

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE.md file for details.