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.
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. 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.
Filter HTTP requests before they hit your application.
The engine comes with a comprehensive set of middleware organized into groups:
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 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);
}
}
<?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.
PHP-first templating. No new syntax to learn.
<!-- 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>
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 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 QueryBuilderInterfaceForge 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 queryingload(string ...$relations) —
Lazy load relationships on an existing model instancerelation(string $name) — Get
a query builder for a relationshipdescribe(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": "./../../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"
}
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 commandsmodules/ModuleName/src/Commands/
— Module-scoped commandsCommands must:
Command base class
#[Cli] attributeexecute(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:moduleasset:link, asset:unlinkstorage:link, storage:unlinkcache:flushservekey:generatemaintenance:up, maintenance:downhelp
statsdev:registry:init, dev:registry:list, dev:registry:publish, dev:registry:versionCapabilities (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-projectmigrate, migrate:rollback, seed, seed:rollbacktest
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 messageserror($message) — Red error
messageswarning($message) — Yellow
warning messagessuccess($message) — Green
success messagescomment($message) — Yellow
comment messagesdebug($message) — Magenta
debug messagesline($message) — Simple line
outputlog($message, $context) —
Timestamped loggingtable($headers, $rows) —
Display tabular dataarray($data, $title) —
Display array dataclearScreen() — Clear
terminal screenProvides 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 promptvalidate
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 templatescontrollerPath(), modelPath(), migrationPath(), etc.controllerNamespace(), modelNamespace(), etc.Provides string transformation methods useful for code generation and formatting:
toCamelCase($string) —
Convert to camelCasetoPascalCase($string) —
Convert to PascalCasetoSnakeCase($string) —
Convert to snake_casetoKebabCase($string) —
Convert to kebab-casetoTitleCase($string) —
Convert to Title CaseisCamelCase($string), isPascalCase($string), etc. —
Validation methodstruncate($string, $length, $suffix)
— Truncate stringsslugify($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)