FW Framework: The Human Guide
Welcome to the FW Framework. FW is designed to be highly deterministic, strictly typed, and driven by a clean declarative design.
This guide walks you through the practical, day-to-day operation of an FW application.
Table of Contents
- Configuration (
.env) - CLI Commands Reference
- Routing & Handlers (Example Endpoint)
- Database & ORM Usage
- The Development Loop
- Rate Limiting
- The AI-First Task Scheduler
- Project Health & Analysis
1. Configuration (.env)
FW uses the modern standard .env configuration, powered by vlucas/phpdotenv.
Environment Loading Context: Because FW is designed for persistent async workers (Swoole/RoadRunner), the .env file is loaded exactly once when the server boots (public/server.php).
This means:
- Reading
$_ENV['APP_NAME']orgetenv('APP_NAME')inside your application logic takes 0 file-IO overhead. - If you change a value in your
.envfile, you must restart the worker (docker compose restart app).
# Example .env snippet
APP_ENV=local
DB_CONNECTION=sqlite
DB_DATABASE=database/database.sqlite2. CLI Commands Reference
FW is a compiled framework. You define structure via Data (YAML) and Code, then compile it.
In your application directory, run: php vendor/bin/fw <command>
build:api: Readsapi/*.yamland outputs strict DTOs, Handler classes, and the staticbuild/routes.phpmatrix.build:db: Readsdatabase/schema.yamland outputs native PHP Readonly Models, Repository Interfaces, and Base Repositories containing mapped SQL.build:migrations: Creates simpleCREATE TABLE.sqlscripts indatabase/migrations/.build:container: Scans your codebase using Reflection and generates a rawswitch/caseinstantiation matrix inbuild/container.php, eliminating runtime DI container reflection.
3. Routing & Handlers
FW uses Declarative APIs. You do not write routes imperative.
Step A: Define the YAML
Create api/post.yaml:
apiVersion: fw/v1
entity: Post
endpoints:
- method: GET
path: /posts/{id}
action: getStep B: Run the Compiler
Run php vendor/bin/fw build:api.
Step C: Write the Handler
FW automatically generated modules/Post/GetPostHandler.php. You just fill in the __invoke logic.
<?php
declare(strict_types=1);
namespace Modules\Post;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
class GetPostHandler
{
// FW's compile-time DI container will automatically inject this!
public function __construct(
private PostRepositoryInterface $postRepository
) {}
public function __invoke(Request $request): JsonResponse
{
// For dynamic params, you'd extract from the Request object.
$post = $this->postRepository->findById(1);
return new JsonResponse([
'data' => $post
]);
}
}4. Database & ORM Usage
FW leverages a Strict Compile-Time Data Mapper instead of the traditional Active Record pattern. This ensures typed data structures and explicit query building.
Step A: Define the Schema
Create database/schema.yaml:
models:
Post:
id: int @primary
title: string
content: stringStep B: Compile the ORM
Run php vendor/bin/fw build:db.
FW just generated:
PostModel: Areadonly classDTO containing only typed properties forid,title, andcontent.BasePostRepository: A class containing pre-written raw SQL mapping functions.PostRepository: Your concrete implementation extending the Base class.
Step C: Use the Database safely and asynchronously
<?php
// ... inside a Handler
public function __invoke() {
// findById was automatically generated!
// It checked out a PDO connection from the Swoole memory pool, executed the SQL asynchronously, and hydrated a strict DTO!
$postModel = $this->postRepository->findById(1);
// Auto-complete works perfectly. It is strictly typed.
echo $postModel->title;
// This will error! `$postModel->comments` does not exist on the DTO.
// You must explicitly define and call `$this->postRepository->findWithComments(1)`
}5. The Development Loop
Because FW is compiled and runs in persistent memory, the standard workflow is:
- Edit your YAML or PHP logic.
- Run the provided developer script:
./bin/dev.sh(This sequence compiles the APIs, the DB, the DI Container, runsfw analyze, and gracefully restarts the Swoole worker). - Your fast, strictly-typed application is live.
6. Rate Limiting
FW includes a high-performance, Redis-backed rate limiter that is purely declarative.
How to use:
In your api/*.yaml, add a ratelimit directive:
endpoints:
- path: /users
method: post
ratelimit: 5/60 # 5 requests per 60 secondsFW handles the rest:
- Injected
X-RateLimit-LimitandX-RateLimit-Remainingheaders. - Returns a clean
429 Too Many RequestsJSON response when exceeded. - Uses
Psr\SimpleCache\CacheInterface(Redis by default in the skeleton).
7. The AI-First Task Scheduler
Unlike traditional PHP frameworks that rely on external Cron jobs, FW runs its own persistent scheduler loop inside the Swoole worker memory.
How to schedule tasks:
In config/dependencies.php, you can define tasks on the Scheduler instance:
'Fw\Framework\Task\Scheduler' => [
'factory' => "function(\$container) {
\$scheduler = new \Fw\Framework\Task\Scheduler(\$container, \$container->get(\Psr\Log\LoggerInterface::class));
// Schedule a task every 60 seconds
\$scheduler->schedule('cleanup', function() {
// Your logic here
}, 60);
return \$scheduler;
}"
],Tasks are executed asynchronously and do not block the request loop.
8. Project Health & Analysis
Before deploying to production, always run the framework's self-analysis tool:
php vendor/bin/fw analyze
What it checks:
- Build Artifacts: Ensures DI container and routes are compiled (crucial for performance).
- Configuration: Verifies
.envpresence. - Static Analysis: Runs PHPStan (Level 5) on all modules to catch type-mismatch bugs early.
- Health Score: A benchmark of your project's technical debt and production readiness.