waffle-commons / skeleton
The official skeleton for Waffle Framework applications.
Requires
- php: ^8.5
- ext-yaml: *
- ext-zend-opcache: *
- waffle-commons/auth: 0.1.0-beta4
- waffle-commons/cache: 0.1.0-beta4
- waffle-commons/config: 0.1.0-beta4
- waffle-commons/console: 0.1.0-beta4
- waffle-commons/container: 0.1.0-beta4
- waffle-commons/contracts: 0.1.0-beta4
- waffle-commons/data: 0.1.0-beta4
- waffle-commons/error-handler: 0.1.0-beta4
- waffle-commons/event-dispatcher: 0.1.0-beta4
- waffle-commons/http: 0.1.0-beta4
- waffle-commons/http-client: 0.1.0-beta4
- waffle-commons/log: 0.1.0-beta4
- waffle-commons/pipeline: 0.1.0-beta4
- waffle-commons/routing: 0.1.0-beta4
- waffle-commons/runtime: 0.1.0-beta4
- waffle-commons/security: 0.1.0-beta4
- waffle-commons/utils: 0.1.0-beta4
- waffle-commons/waffle: 0.1.0-beta4
Requires (Dev)
- carthage-software/mago: ^1.29
- cyclonedx/cyclonedx-php-composer: ^6.2
- igor-php/igor-php: ^0.7.0
- php-mock/php-mock-phpunit: ^2.15
- phpunit/phpunit: ^12.5
- vimeo/psalm: ^6.16
This package is auto-updated.
Last update: 2026-06-14 07:49:32 UTC
README
๐ฆ Waffle Skeleton
The official starting point for building robust, secure, and high-performance applications with the Waffle Ecosystem.
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.AppKernelFactorymerges your.envwith the live process environment (Dockerenvironment:, Kubernetesenv:, shell exports, etc.) viaarray_merge((new DotEnv($root))->load(), getenv()). Becausearray_mergeis rightmost-wins on string keys, the OS value beats.envon collision. If you edit.envand the change doesn't take effect, check whether the same variable is exported by your shell ordocker-compose.ymlโ that export will silently override.env. This matches the Twelve-Factor convention. Seedocumentation/how-to/configuration.mdfor the merge rules and the type-normalization foot-gun aroundAPP_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.yamlalso wireslog(PSR-3 channel) andcache(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 HTTP403. 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-browserWAFFLE_SIDcookie issued byAnonymousSessionMiddleware, 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
authbridge 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.
