How Forge Kernel works under the hood.
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.
Router, View Engine, and Middleware are provided by installable capabilities (ForgeRouter, ForgeView), not built into the kernel.
These are not built into the kernel. Install them as capabilities when needed:
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.
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:
app/
modules/*/src/
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.
<?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;
}
}
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.
Attribute-based routing that auto-discovers routes from controllers. Provided by the ForgeRouter module.
<?php
use App\Modules\ForgeRouter\Routing\Route;
use App\Modules\ForgeRouter\Http\Request;
use App\Modules\ForgeRouter\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.
Filter HTTP requests before they hit your application.
ForgeRouter (an installable capability module) provides a comprehensive set of middleware organized into groups. The kernel itself does not include middleware.
SessionMiddleware
(order: 0)
RateLimitMiddleware
(order: 0)
CircuitBreakerMiddleware
(order: 1)
CorsMiddleware
(order: 2)
SanitizeInputMiddleware
(order: 3, enabled: false by
default)
CompressionMiddleware
(order: 4)
CsrfMiddleware
(order: 1)
RelaxSecurityHeadersMiddleware
(order: 3)
IpWhiteListMiddleware
(order: 0)
ApiKeyMiddleware
(order: 1)
ApiMiddleware
(order: 2)
CookieMiddleware
(order: 2)
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:
#[Middleware("group-name")]
on controllers or methods
<?php
use App\Modules\ForgeRouter\Http\Middleware;
use App\Modules\ForgeRouter\Http\Request;
use App\Modules\ForgeRouter\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);
}
}
<?php
use App\Modules\ForgeRouter\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.
PHP-first templating. Provided by the ForgeView module.
<!-- layouts/app.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><?= $title ?? 'My App' ?></title>
<?php use App\Modules\ForgeView\View; echo View::section('head'); ?>
</head>
<body>
<header>
<?php use App\Modules\ForgeView\View; echo View::section('header'); ?>
</header>
<main>
<?php use App\Modules\ForgeView\View; echo View::section('content'); ?>
</main>
<footer>
<?php use App\Modules\ForgeView\View; echo View::section('footer'); ?>
</footer>
</body>
</html>
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 App\Modules\ForgeView\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 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.
Forge resolutions components from several standard locations:
app/resources/components/
modules/ModuleName/src/Resources/components/
modules/ModuleName/src/Resources/views/
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>
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'
]);
?>
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>
Module:path syntax.
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
Forge provides three ways to work with data:
DatabaseConnectionInterface
(requires ForgeDatabaseSQL) or
QueryBuilderInterface
(requires ForgeSqlOrm) for direct database
access.
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);
}
}
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();
}
}
Both interfaces support database transactions. Here are examples of commit and rollback operations:
<?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'];
}
}
<?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'];
}
}
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__);
}
}
ForgeSqlOrm provides several traits to add common functionality to your models:
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;
}
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();
}
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();
<?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.
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.
{
"$schema": "./../../kernel/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"
}
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
}
}
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
}
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.
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
}
Understanding when and why to use queues, workers, and asynchronous processing.
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.
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.
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.
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).
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
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;
}
}
Commands are automatically discovered from:
app/Commands/
— Application-scoped commands
modules/ModuleName/src/Commands/
— Module-scoped commands
Commands must:
Command
base class
#[Cli]
attribute
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.
The kernel comes with a comprehensive set of built-in 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:link,
asset:unlink
storage:link,
storage:unlink
cache:flush
serve
key:generate
maintenance:up,
maintenance:down
help
stats
dev:registry:init,
dev:registry:list,
dev:registry:publish,
dev:registry:version
Capabilities (modules) can provide their own commands. These commands are automatically discovered and work just like kernel commands:
package:install-module,
package:remove-module,
package:list-modules,
package:install-project
migrate,
migrate:rollback,
seed,
seed:rollback
test
storage:*
commands
Module commands are placed in
modules/ModuleName/src/Commands/
and follow the same structure as app commands.
Commands can use various traits to add functionality:
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
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
#[Arg]
attributes to determine what to prompt
validate
parameter in
#[Arg]
<?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;
}
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
controllerPath(),
modelPath(),
migrationPath(), etc.
controllerNamespace(),
modelNamespace(), etc.
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
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)