Core Concepts

How Forge Kernel works under the hood.

Architecture

Forge Kernel uses a modular architecture with dependency injection. It's built to be simple and flexible. Think of it like a foundation with utilities — you get the foundation, plumbing, electrical, and basic structure. You decide what to build on top.

Kernel Components (Built-in)

  • Kernel Core
  • Router
  • View Engine
  • DI Container
  • Capability Loader
  • CLI Kernel
  • Bootstrap
  • Config Manager

Capabilities (Pluggable)

These are not built into the kernel. Install them as capabilities when needed:

  • Database (ForgeDatabaseSQL)
  • ORM (ForgeSqlOrm)
  • Authentication (ForgeAuth)
  • Storage (ForgeStorage)
  • And more...

Dependency Injection

Forge's DI container is like a smart warehouse manager. It knows where everything is stored, gets you what you need when you ask, and manages dependencies automatically. It resolves dependencies using PHP attributes, keeping code testable and clean.

Service Discovery

Services are automatically discovered from any folder in your application or modules when they have the #[Service] or #[Discoverable] attribute. The framework recursively scans all directories, so you can organize your code however you prefer.

Attributes:

  • #[Service] - Register a class as a service in the dependency injection container
  • #[Discoverable] - Semantically marks a class as discoverable (same behavior as #[Service], useful for non-service classes that need DI)

Discovery Scope: Services are discovered from:

  • All directories under app/
  • All directories under modules/*/src/
  • Engine core directories

Service discovery happens once at bootstrap and uses an incremental class map cache for performance. The cache automatically updates when files change. If you add a new service and it's not being discovered, clear the cache with php forge.php cache:flush.

Service Registration

<?php

use Forge\Core\DI\Attributes\Service;

#[Service]
class UserService
{
    public function __construct(
        private UserRepository $repository,
        private EmailService $emailService
    ) {}
    
    public function createUser(array $data): User
    {
        $user = $this->repository->create($data);
        $this->emailService->sendWelcomeEmail($user);
        return $user;
    }
}

#[Service(singleton: true)] // Only one instance throughout the application
class CacheService
{
    private array $cache = [];
    
    public function get(string $key): mixed
    {
        return $this->cache[$key] ?? null;
    }
}

Interface Binding

You can bind interfaces to implementations using the container's bind() method. In modules, this is typically done in the module's register() method.

<?php
// In a module's register() method
namespace App\Modules\ForgeAuth;

use Forge\Core\DI\Container;
use App\Modules\ForgeAuth\Contracts\ForgeAuthInterface;
use App\Modules\ForgeAuth\Services\ForgeAuthService;
use App\Modules\ForgeAuth\Contracts\UserRepositoryInterface;
use App\Modules\ForgeAuth\Repositories\UserRepository;

public function register(Container $container): void
{
    // Bind interface to implementation
    $container->bind(ForgeAuthInterface::class, ForgeAuthService::class);
    
    // Bind with closure for complex dependencies
    $container->bind(UserRepositoryInterface::class, function ($container) {
        return new UserRepository($container->get(QueryCache::class));
    });
}

Note: There is no #[Bind] attribute. Interface binding is done programmatically in the container, typically in module registration or service providers.

Routing System

Attribute-based routing that auto-discovers routes from controllers. No separate route files needed.

<?php

use Forge\Core\Routing\Route;
use Forge\Core\Http\Request;
use Forge\Core\Http\Response;

class ApiController
{
    #[Route("/api/users")]
    public function listUsers(): Response
    {
        return $this->json(User::query()->get());
    }
    
    #[Route("/api/users/{id}", method: "GET")]
    public function getUser(Request $request, int $id): Response
    {
        $user = User::query()->id($id)->first();
        return $this->json($user);
    }
    
    #[Route("/api/users", method: "POST")]
    public function createUser(Request $request): Response
    {
        $data = $request->json();
        $user = User::create($data);
        return $this->json($user, 201);
    }
    
    #[Route("/api/users/{id}", method: "PUT")]
    public function updateUser(Request $request, int $id): Response
    {
        $user = User::query()->id($id)->first();
        $user->update($request->json());
        return $this->json($user);
    }
}

Note: Use curly braces for route parameters. Names must match method params.

Middleware

Filter HTTP requests before they hit your application.

Built-in Middleware

The engine comes with a comprehensive set of middleware organized into groups:

  • Global group:
    • SessionMiddleware (order: 0)
    • RateLimitMiddleware (order: 0)
    • CircuitBreakerMiddleware (order: 1)
    • CorsMiddleware (order: 2)
    • SanitizeInputMiddleware (order: 3, enabled: false by default)
    • CompressionMiddleware (order: 4)
  • Web group:
    • CsrfMiddleware (order: 1)
    • RelaxSecurityHeadersMiddleware (order: 3)
  • API group:
    • IpWhiteListMiddleware (order: 0)
    • ApiKeyMiddleware (order: 1)
    • ApiMiddleware (order: 2)
    • CookieMiddleware (order: 2)

Middleware Configuration

Configure middleware groups, order, and overrides in config/middleware.php:

<?php
// config/middleware.php
return [
    'global' => [
        // Add your own middleware to global group
        \App\Middlewares\CustomMiddleware::class,
    ],
    'web' => [
        // Override order or add middleware to web group
        \App\Middlewares\CustomWebMiddleware::class,
    ],
    'api' => [
        // Create custom API middleware groups
        \App\Middlewares\ApiAuthMiddleware::class,
    ],
    'api-auth' => [
        // Create new middleware groups
        App\Modules\ForgeAuth\Middlewares\ApiJwtMiddleware::class,
    ]
];

You can:

  • Create new middleware groups: Add new keys to the array
  • Override order: Change the order of built-in middleware by adjusting their position in the array
  • Override built-in middleware: If you create a middleware with the same class name as a built-in one, your custom version will be used instead
  • Apply groups: Use #[Middleware("group-name")] on controllers or methods

Creating Middleware

<?php

use Forge\Core\Http\Middleware;
use Forge\Core\Http\Request;
use Forge\Core\Http\Response;

class AuthMiddleware extends Middleware
{
    public function handle(Request $request, callable $next): Response
    {
        if (!$request->hasHeader('Authorization')) {
            return new Response('Unauthorized', 401);
        }
        
        // Continue to next middleware or controller
        return $next($request);
    }
}

Applying Middleware

<?php

use Forge\Core\Http\Attributes\Middleware;

// Apply middleware to entire controller
#[Middleware("auth")]
class DashboardController
{
    #[Route("/dashboard")]
    public function index(): Response
    {
        return $this->view('dashboard/index');
    }
    
    #[Route("/dashboard/settings")]
    public function settings(): Response
    {
        return $this->view('dashboard/settings');
    }
}

// Apply middleware to specific methods
class UserController
{
    #[Middleware("App\Modules\ForgeAuth\Middlewares\AuthMiddleware")]
    #[Route("/profile")]
    public function profile(): Response
    {
        return $this->view('user/profile');
    }
    
    // Multiple middleware using repeatable attribute
    #[Middleware("web")]
    #[Middleware("auth")]
    #[Route("/admin")]
    public function admin(): Response
    {
        return $this->view('admin/dashboard');
    }
}

Use #[Middleware] for controllers/methods, or middlewares in #[Route] for routes.

View Engine

PHP-first templating. No new syntax to learn.

Template Inheritance

<!-- layouts/app.php -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title><?= $title ?? 'My App' ?></title>
    <?php use Forge\Core\View\View; echo View::section('head'); ?>
</head>
<body>
    <header>
        <?php use Forge\Core\View\View; echo View::section('header'); ?>
    </header>
    
    <main>
        <?php use Forge\Core\View\View; echo View::section('content'); ?>
    </main>
    
    <footer>
        <?php use Forge\Core\View\View; echo View::section('footer'); ?>
    </footer>
</body>
</html>

Child Templates

The loadFromModule parameter is optional and defaults to false. When false (default), the layout must exist in app/resources/views/layouts/. When true, the layout is loaded from modules/ModuleName/src/Resources/views/layouts/.

<!-- pages/home.php -->
<?php 
use Forge\Core\View\View; 

// Default (loadFromModule: false) - layout must be in app/resources/views/layouts/
View::layout(name: "layouts/app");

// Or explicitly specify
View::layout(name: "layouts/app", loadFromModule: false);

// Load from module
View::layout(name: "layouts/app", loadFromModule: true);
?>

<?php View::startSection('head'); ?>
    <link rel="stylesheet" href="/css/home.css">
<?php View::endSection(); ?>

<?php View::startSection('content'); ?>
    <div class="hero">
        <h1>Welcome to <?= $appName ?></h1>
        <p><?= $description ?></p>
    </div>
<?php View::endSection(); ?>

Components

Components in Forge are simple, reusable UI pieces. Unlike other frameworks that force you into complex class structures, Forge components are just standard PHP templates. They are lightning-fast to render and easy to organize.

Component Locations

Forge resolutions components from several standard locations:

  • App Scope: app/resources/components/
  • Module Scope (Standard): modules/ModuleName/src/Resources/components/
  • Module Scope (Views): modules/ModuleName/src/Resources/views/

Creating a Component

A component is just a PHP file. Variables passed as props are automatically extracted for use in the template.

<!-- app/resources/components/ui/alert.php -->
<div class="alert alert-<?= $type ?? 'info' ?>">
    <?= $message ?>
</div>

Using Components

Use the global component() helper to render your UI pieces anywhere.

<?php
// Render an app-scope component
echo component('ui/alert', [
    'type' => 'success',
    'message' => 'Operation successful!'
]);

// Render a module-scope component using ":" syntax
echo component('ForgeNexus:sidebar/item', [
    'label' => 'Dashboard',
    'icon' => 'fa-home'
]);
?>

Automatic Data Extraction

Forge's view engine is smart. If you pass an array, it extracts its keys. If you pass an object, it automatically extracts all its public properties into local variables. This is perfect for passing DTOs or reactive state objects directly to components.

<?php
// Passing an object as props
$user = new UserDto(name: "John Doe", email: "john@example.com");

echo component('user/card', $user);
?>

<!-- Inside user/card.php, $name and $email are available! -->
<div class="card">
    <h3><?= $name ?></h3>
    <p><?= $email ?></p>
</div>

Key Benefits

  • Pure PHP: No new syntax to learn, full IDE support.
  • No Overheard: No base component classes or complex lifecycles for simple UI.
  • Flexible: Pass arrays or objects seamlessly.
  • Namespaced: Easily share components across modules using the Module:path syntax.

Database & ORM (Capabilities)

Database and ORM are not built into the kernel. They're capabilities you install when you need them. The kernel provides contracts (interfaces) for database operations, but these contracts must be implemented by a module.

Important: The kernel provides DatabaseConnectionInterface and QueryBuilderInterface contracts, but they won't work unless you install a module that implements them. For example:

  • ForgeDatabaseSQL implements DatabaseConnectionInterface
  • ForgeSqlOrm implements QueryBuilderInterface
  • You can also create your own module that implements these contracts

Forge provides three ways to work with data:

  1. Raw SQL Queries: Use DatabaseConnectionInterface (requires ForgeDatabaseSQL) or QueryBuilderInterface (requires ForgeSqlOrm) for direct database access.
  2. Query Builder: Use the fluent query builder interface for type-safe database operations (requires ForgeSqlOrm).
  3. ORM: Use the ForgeSqlOrm capability for attribute-based models and relationships.

Raw SQL Queries with DatabaseConnectionInterface

When using DatabaseConnectionInterface, you have direct access to PDO methods. This requires the ForgeDatabaseSQL module to be installed.

<?php

use Forge\Core\Contracts\Database\DatabaseConnectionInterface;

class MyController
{
    public function __construct(
        private readonly DatabaseConnectionInterface $connection
    ) {}

    // Execute raw SQL (DDL statements)
    public function createTable(): void
    {
        $this->connection->exec(
            "CREATE TABLE IF NOT EXISTS example_table (id INTEGER PRIMARY KEY, name TEXT)"
        );
    }

    // Query with prepared statements
    public function findUser(int $id): array
    {
        $stmt = $this->connection->prepare("SELECT * FROM users WHERE id = :id");
        $stmt->execute([':id' => $id]);
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }

    // Simple query (no parameters)
    public function getAllUsers(): array
    {
        $stmt = $this->connection->query("SELECT * FROM users LIMIT 5");
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }
}

Raw SQL Queries with QueryBuilderInterface

When using QueryBuilderInterface, you can use raw SQL methods or combine them with the query builder. This requires the ForgeSqlOrm module to be installed.

<?php

use App\Modules\ForgeSqlOrm\ORM\QueryBuilder;

class MyController
{
    public function __construct(
        private readonly QueryBuilder $builder
    ) {}

    // Raw SQL query
    public function rawQuery(): array
    {
        return $this->builder->raw(
            "SELECT * FROM users WHERE status = :status",
            [':status' => 'active']
        );
    }

    // WhereRaw with query builder
    public function whereRawExample(): array
    {
        return $this->builder
            ->table('users')
            ->whereRaw('status = :status', [':status' => 'active'])
            ->get();
    }

    // Combined: query builder + raw SQL
    public function combinedExample(): array
    {
        return $this->builder
            ->table('users')
            ->select('id', 'email', 'identifier')
            ->where('status', '=', 'active')
            ->whereRaw('identifier IS NOT NULL', [])
            ->orderBy('created_at', 'DESC')
            ->limit(10)
            ->get();
    }
}

Transactions

Both interfaces support database transactions. Here are examples of commit and rollback operations:

Transactions with DatabaseConnectionInterface

<?php

// Transaction with commit
public function transactionCommit(): array
{
    $this->connection->beginTransaction();
    try {
        $stmt = $this->connection->prepare("INSERT INTO example_table (name) VALUES (:name)");
        $stmt->execute([':name' => 'transaction_test_commit']);
        $this->connection->commit();
        return ['status' => 'committed', 'message' => 'Transaction committed successfully'];
    } catch (\Exception $e) {
        $this->connection->rollBack();
        return ['status' => 'error', 'message' => $e->getMessage()];
    }
}

// Transaction with rollback
public function transactionRollback(): array
{
    $this->connection->beginTransaction();
    try {
        $stmt = $this->connection->prepare("INSERT INTO example_table (name) VALUES (:name)");
        $stmt->execute([':name' => 'transaction_test_rollback']);
        throw new \Exception('Simulated error to trigger rollback');
    } catch (\Exception $e) {
        $this->connection->rollBack();
        return ['status' => 'rolled_back', 'message' => 'Transaction rolled back successfully'];
    }
}

Transactions with QueryBuilderInterface

<?php

// Transaction with commit
public function transactionCommit(): array
{
    try {
        $this->builder->beginTransaction();
        $id = $this->builder->table('example_table')->insert(['name' => 'orm_transaction_commit']);
        $this->builder->commit();
        return ['status' => 'committed', 'inserted_id' => $id];
    } catch (\Exception $e) {
        $this->builder->rollback();
        return ['status' => 'error', 'message' => $e->getMessage()];
    }
}

// Transaction with rollback
public function transactionRollback(): array
{
    try {
        $this->builder->beginTransaction();
        $this->builder->table('example_table')->insert(['name' => 'orm_transaction_rollback']);
        throw new \Exception('Simulated error to trigger rollback');
        $this->builder->commit();
    } catch (\Exception $e) {
        $this->builder->rollback();
        return ['status' => 'rolled_back', 'message' => 'Transaction rolled back successfully'];
    }
}

Model Definition

Models extend the Model base class and use attributes to define table structure, columns, and relationships. You can also use traits to add common functionality.

<?php

use App\Modules\ForgeSqlOrm\ORM\Model;
use App\Modules\ForgeSqlOrm\ORM\Attributes\Table;
use App\Modules\ForgeSqlOrm\ORM\Attributes\Column;
use App\Modules\ForgeSqlOrm\ORM\Attributes\ProtectedFields;
use App\Modules\ForgeSqlOrm\ORM\Values\Cast;
use App\Modules\ForgeSqlOrm\ORM\Values\Relate;
use App\Modules\ForgeSqlOrm\ORM\Values\Relation;
use App\Modules\ForgeSqlOrm\ORM\Values\RelationKind;
use App\Modules\ForgeSqlOrm\Traits\HasTimeStamps;
use App\Modules\ForgeSqlOrm\Traits\HasMetaData;
use App\Modules\ForgeSqlOrm\ORM\CanLoadRelations;

#[Table('users')]
#[ProtectedFields(['password'])]
class User extends Model
{
    use HasTimeStamps;  // Adds created_at and updated_at columns
    use CanLoadRelations;  // Enables relationship loading methods
    use HasMetaData;  // Adds metadata column for JSON data

    #[Column(primary: true, cast: Cast::INT)]
    public int $id;

    #[Column(cast: Cast::STRING)]
    public string $status;

    #[Column(cast: Cast::STRING)]
    public string $identifier;

    #[Column(cast: Cast::STRING)]
    public string $email;

    #[Column(cast: Cast::STRING)]
    public string $password;

    #[Column(cast: Cast::JSON)]
    public ?UserMetadataDto $metadata;  // Uses HasMetaData trait

    // Relationships use #[Relate] attribute
    #[Relate(RelationKind::HasOne, Profile::class, "user_id")]
    public function profile(): Relation
    {
        return self::describe(__FUNCTION__);
    }
}

Available Traits

ForgeSqlOrm provides several traits to add common functionality to your models:

HasTimeStamps

Automatically adds created_at and updated_at timestamp columns to your model. These are automatically managed by the ORM when creating or updating records.

<?php

use App\Modules\ForgeSqlOrm\Traits\HasTimeStamps;

class User extends Model
{
    use HasTimeStamps;
    
    // Automatically adds:
    // public ?DateTimeImmutable $created_at = null;
    // public ?DateTimeImmutable $updated_at = null;
}
HasMetaData

Adds a metadata column for storing JSON data. Useful for flexible, schema-less data that doesn't need its own table.

<?php

use App\Modules\ForgeSqlOrm\Traits\HasMetaData;

class User extends Model
{
    use HasMetaData;
    
    // Automatically adds:
    // public ?array $metadata = null;
    
    // Usage:
    // $user->metadata = ['preferences' => ['theme' => 'dark']];
    // $user->save();
}
CanLoadRelations

Provides methods for loading and working with relationships. This trait is already included in the Model base class, but you can use it explicitly if needed.

Key methods:

  • with(string ...$paths) — Eager load relationships when querying
  • load(string ...$relations) — Lazy load relationships on an existing model instance
  • relation(string $name) — Get a query builder for a relationship
  • describe(string $method) — Get relationship metadata
<?php

// Eager loading relationships
$user = User::with('profile')->id(1)->first();

// Lazy loading relationships
$user = User::query()->id(1)->first();
$user->load('profile');

// Using relation() to build queries
$posts = $user->relation('posts')->where('status', '=', 'published')->get();

Query Builder

<?php

// Get all records
$users = User::query()->get();

// Find by ID
$user = User::query()->id(1)->first();

// Where queries
$activeUsers = User::query()
    ->where('status', '=', 'active')
    ->get();

// Advanced queries
$users = User::query()
    ->where('created_at', '>', '2024-01-01')
    ->get();

// Eager load relationships (using with() static method)
$user = User::with('profile', 'posts')->id(1)->first();

// Or using query builder with with()
$user = User::query()
    ->with('profile', 'posts')
    ->id(1)
    ->first();

// Lazy load relationships on existing instance
$user = User::query()->id(1)->first();
$user->load('profile', 'posts');

// Access relationship using relation() method
$posts = $user->relation('posts')->where('status', '=', 'published')->get();

Note: Forge uses a query builder pattern. Always start with Model::query() to build queries. There are no static methods like all() or find() directly on the Model class. Use with() for eager loading relationships, or load() for lazy loading on existing instances.

Capability System

Extend Forge Kernel with self-contained capability modules. Think of capabilities like modular additions to a house. The kernel gives you the foundation. Capabilities add what you need: database, ORM, authentication, storage — all optional, all pluggable.

Need a database? Install a database capability. Need an ORM? Install an ORM capability. Don't need authentication? Don't install it. The kernel stays lean. You stay in control.

Capability Structure

{
  "$schema": "./../../engine/Core/Schema/module-schema.json",
  "name": "forge-my-module",
  "version": "1.0.0",
  "description": "A custom module for my application",
  "type": "generic",
  "order": 100,
  "author": "Your Name",
  "license": "MIT"
}

Capability Class

The #[Module] attribute supports several options to control module behavior:

  • core: true — Module won't be auto-loaded. You must wire it manually in your bootstrap or service providers.
  • isCli: true — Module won't be loaded in web context. Only loaded when running CLI commands.
<?php

namespace App\Modules\MyModule;

use Forge\Core\DI\Container;
use Forge\Core\Module\Attributes\Compatibility;
use Forge\Core\Module\Attributes\Module;
use Forge\Core\Module\Attributes\Repository;
use Forge\Core\Module\Attributes\ConfigDefaults;
use Forge\Core\Module\Attributes\PostInstall;
use Forge\Core\Module\Attributes\PostUninstall;
use Forge\Core\DI\Attributes\Service;
use Forge\Core\Module\Attributes\LifecycleHook;
use Forge\Core\Module\LifecycleHookName;
use App\Modules\MyModule\Contracts\MyModuleInterface;
use App\Modules\MyModule\Services\MyModuleService;

#[Module(
    name: 'MyModule',
    description: 'A custom module for my application',
    order: 100,
    core: false,  // Set to true to disable auto-loading
    isCli: false  // Set to true to only load in CLI context
)]
#[Service]
#[Compatibility(kernel: '>=0.1.0', php: '>=8.3')]
#[Repository(type: 'git', url: 'https://github.com/your-repo/modules')]
#[ConfigDefaults(defaults: [
    'my_module' => [
        'enabled' => true,
        'default_option' => 'value'
    ]
])]
#[PostInstall(command: 'migrate', args: ['--type=module', '--module=my-module'])]
#[PostUninstall(command: 'migrate:rollback', args: ['--type=module', '--module=my-module'])]
final class MyModule
{
    public function register(Container $container): void
    {
        // Register services
        $container->bind(MyModuleInterface::class, MyModuleService::class);
    }

    #[LifecycleHook(hook: LifecycleHookName::AFTER_MODULE_REGISTER)]
    public function onAfterModuleRegister(): void
    {
        // Boot logic after module registration
    }
}

Configuration Defaults

The #[ConfigDefaults] attribute allows you to define default configuration values directly in your module class, eliminating the need for a separate config/ folder and config file.

You can still override these defaults by creating a config file in /config/ (e.g., config/my_module.php). When loading configuration in your module, you need to explicitly define that the config can be overridden from the /config/ directory.

<?php

#[ConfigDefaults(defaults: [
    'my_module' => [
        'enabled' => true,
        'default_option' => 'value',
        'nested' => [
            'setting' => 'default'
        ]
    ]
])]
class MyModule
{
    // Config defaults are automatically available
    // Can be overridden in config/my_module.php
}

Post-Install and Post-Uninstall Hooks

Use #[PostInstall] and #[PostUninstall] attributes to automatically run CLI commands after a module is installed or removed. This is useful for running migrations, linking assets, seeding data, or performing other setup/cleanup tasks.

<?php

#[PostInstall(command: 'migrate', args: ['--type=module', '--module=my-module'])]
#[PostInstall(command: 'asset:link', args: ['--type=module', '--module=my-module'])]
#[PostUninstall(command: 'migrate:rollback', args: ['--type=module', '--module=my-module'])]
#[PostUninstall(command: 'asset:unlink', args: ['--type=module', '--module=my-module'])]
class MyModule
{
    // Commands run automatically after install/uninstall
}

You can specify multiple #[PostInstall] or #[PostUninstall] attributes. Commands are executed in the order they appear on the class.

Configuration Management

Environment-based configuration that's secure and flexible.

<?php

use Forge\Core\Config\Config;
use Forge\Core\Config\Environment;

// Using helper functions (recommended)
$dbHost = env('DB_HOST', 'localhost');
$debug = env('APP_DEBUG', false);
$appName = config('app.name', 'Forge App');

// Using Config class directly
$config = Config::get('database.connections.mysql');
Config::set('cache.driver', 'redis');

// Using Environment class directly  
$env = Environment::getInstance();
$port = $env->get('APP_PORT', 8000);
$isDev = $env->isDevelopment();
$debugEnabled = $env->isDebugEnabled();

// Checking if configuration exists
if (config('services.stripe.key')) {
    // Stripe is configured
}

Asynchronous Processing & Queues

Understanding when and why to use queues, workers, and asynchronous processing.

The Cashier & Warehouse Analogy

Imagine a store with a cashier taking orders. Most orders are simple — "I want a sheet of paper" — and the cashier handles them quickly because the paper is right next to them. Everyone in line moves smoothly, no one notices any slowdown.

But sometimes, someone needs something from the warehouse — like a screw that's 3 feet away. Now the cashier has to walk, pick up the screw, come back, and deliver both items. The person behind has to wait. This is called latency — the delay caused by the slower operation.

If it's just one person, nothing bad happens. But what if 4 people in a row each need something from the warehouse? Or what if someone needs 2 shovels from deep in the warehouse, and the cashier doesn't know where they are? Now the whole process stops for everyone.

The Solution: Workers

To solve this, you hire a warehouse worker. The cashier handles simple orders (synchronous — fast). The warehouse worker handles complex orders (asynchronous — doesn't block the cashier). When an order needs something from the warehouse, the cashier writes it down and puts it in a queue. The warehouse worker picks up multiple orders at once (batch processing), goes to the warehouse, brings back all the items, and delivers them.

If you need more capacity, you hire more workers. You can plan ahead — before a big sale or holiday, you allocate more workers a few hours before the rush. Everything feels smooth.

In Your Application

Synchronous (Cashier): Simple operations that are fast — rendering a view, returning JSON, simple database queries. These happen immediately and don't need queues.

Asynchronous (Warehouse Worker): Complex operations that take time — sending emails, processing images, generating reports, calling external APIs. These go into a queue and are processed by workers.

Batch Processing: Workers can process multiple jobs at once, like the warehouse worker bringing back multiple items in one trip.

Scaling: Add more workers before peak times (holidays, promotions, scheduled events) to handle increased load smoothly.

Key Takeaway: Not every task needs to go to a queue or be processed by a worker. Use queues for operations that would block or slow down your main application flow. Keep simple operations synchronous.

CLI Kernel

The kernel provides a powerful CLI system for creating custom commands and automating tasks. Commands can be created in your application or provided by capabilities (modules).

Interactive Command Browser

Forge includes a retro-styled interactive command browser. Simply run php forge.php without arguments to access it. The browser features a splash screen, multi-column command listings, category-based browsing, and allows you to execute commands or view help directly. Use arrow keys (↑↓←→) to navigate and Esc to exit.

# Launch interactive browser
php forge.php

# Skip splash screen
php forge.php --no-splash

# Show traditional command list
php forge.php --list

Creating Custom Commands

Commands use the #[Cli] attribute to define the command name, description, usage, and examples. Arguments are defined using the #[Arg] attribute on class properties.

<?php

declare(strict_types=1);

namespace App\Commands;

use Forge\CLI\Attributes\Arg;
use Forge\CLI\Attributes\Cli;
use Forge\CLI\Command;
use Forge\CLI\Traits\CliGenerator;
use Forge\Traits\StringHelper;

#[Cli(
    command: 'my:command',
    description: 'A custom command example',
    usage: 'my:command [--type=app|module] [--module=ModuleName] [--name=Example]',
    examples: [
        'my:command --type=app --name=Example',
        'my:command --type=module --module=Blog --name=Example',
        'my:command   (starts wizard)',
    ]
)]
final class MyCommand extends Command
{
    use StringHelper;
    use CliGenerator;  // Optional: for code generation commands

    #[Arg(name: 'type', description: 'app or module', default: 'app', validate: 'app|module')]
    private string $type = 'app';

    #[Arg(name: 'module', description: 'Module name when type=module', required: false)]
    private ?string $module = null;

    #[Arg(name: 'name', description: 'Name parameter')]
    private string $name = '';

    public function execute(array $args): int
    {
        $this->wizard($args);  // Interactive prompts if args missing

        if ($this->type === 'module' && !$this->module) {
            $this->error('--module=Name required when --type=module');
            return 1;
        }

        $this->info("Processing: {$this->name}");
        $this->success('Command completed successfully!');
        
        return 0;
    }
}

Command Discovery

Commands are automatically discovered from:

  • app/Commands/ — Application-scoped commands
  • modules/ModuleName/src/Commands/ — Module-scoped commands

Commands must:

  • Extend the Command base class
  • Use the #[Cli] attribute
  • Implement the execute(array $args): int method

Commands are cached for performance. If you add a new command and it's not discovered, clear the cache with php forge.php cache:flush.

Kernel-Provided Commands

The kernel comes with a comprehensive set of built-in commands:

  • Generate commands: generate:controller, generate:model, generate:migration, generate:service, generate:component, generate:command, generate:test, generate:trait, generate:enum, generate:dto, generate:event, generate:middleware, generate:seeder, generate:module
  • Asset commands: asset:link, asset:unlink
  • Storage commands: storage:link, storage:unlink
  • Cache commands: cache:flush
  • Server commands: serve
  • Key commands: key:generate
  • Maintenance commands: maintenance:up, maintenance:down
  • Help commands: help
  • Stats commands: stats
  • Registry commands (developer mode): dev:registry:init, dev:registry:list, dev:registry:publish, dev:registry:version

Module-Provided Commands

Capabilities (modules) can provide their own commands. These commands are automatically discovered and work just like kernel commands:

  • ForgePackageManager: package:install-module, package:remove-module, package:list-modules, package:install-project
  • ForgeDatabaseSQL: migrate, migrate:rollback, seed, seed:rollback
  • ForgeTesting: test
  • ForgeStorage: storage:* commands

Module commands are placed in modules/ModuleName/src/Commands/ and follow the same structure as app commands.

Available Traits

Commands can use various traits to add functionality:

OutputHelper

Included in the Command base class. Provides colored output methods:

  • info($message) — Blue informational messages
  • error($message) — Red error messages
  • warning($message) — Yellow warning messages
  • success($message) — Green success messages
  • comment($message) — Yellow comment messages
  • debug($message) — Magenta debug messages
  • line($message) — Simple line output
  • log($message, $context) — Timestamped logging
  • table($headers, $rows) — Display tabular data
  • array($data, $title) — Display array data
  • clearScreen() — Clear terminal screen

Wizard

Provides interactive prompts for missing required arguments. Automatically prompts users when arguments are not provided via command line:

  • wizard($args) — Automatically prompts for missing required arguments
  • Uses #[Arg] attributes to determine what to prompt
  • Validates input based on validate parameter in #[Arg]
  • Supports default values — if a default is provided and user presses Enter, the default is used
<?php
// In your execute() method
public function execute(array $args): int
{
    $this->wizard($args);  // Prompts for any missing required arguments
    
    // Your command logic here
    return 0;
}

CliGenerator

Used by code generation commands (all generate:* commands). Provides methods for generating files from stub templates:

  • generateFromStub($stub, $targetPath, $tokens, $force) — Generate files from stub templates
  • Helper methods for resolving paths: controllerPath(), modelPath(), migrationPath(), etc.
  • Helper methods for resolving namespaces: controllerNamespace(), modelNamespace(), etc.
  • Automatically handles app vs module scope
  • Creates parent directories if they don't exist

StringHelper

Provides string transformation methods useful for code generation and formatting:

  • toCamelCase($string) — Convert to camelCase
  • toPascalCase($string) — Convert to PascalCase
  • toSnakeCase($string) — Convert to snake_case
  • toKebabCase($string) — Convert to kebab-case
  • toTitleCase($string) — Convert to Title Case
  • isCamelCase($string), isPascalCase($string), etc. — Validation methods
  • truncate($string, $length, $suffix) — Truncate strings
  • slugify($string) — Create URL-friendly slugs

CommandOptionTrait

Included in the Command base class. Provides methods for parsing command-line arguments:

  • option($name, $args) — Get option value (e.g., --option=value)
  • flag($name, $args) — Check if flag is present (e.g., --flag)
  • argument($name, $args) — Get argument value (legacy method, prefer #[Arg] attributes)