This guide will help you get Forge up and running. It's pretty straightforward.
Note: Forge doesn't require Composer. The kernel is dependency-free. Database and ORM are capabilities you install when needed, not built-in requirements.
A few ways to get Forge running.
# Quick install with one command
bash <(curl -Ls https://raw.githubusercontent.com/forge-engine/installer/main/installer.sh)
That's it! This command will clone the starter project, set up the folder structure, and give you a working Forge app you can run immediately.
If you prefer to set things up manually:
git clone https://github.com/forge-engine/forge-starter
cd forge-starter
cp env-example .env
php install.php
php forge.php key:generate
php forge.php package:install-project
Forge uses environment variables for configuration. Edit your .env file:
# Application Settings
APP_NAME="My Forge App"
APP_ENV=development
APP_DEBUG=true
APP_KEY=your-generated-key
FORGE_DEVELOPER_MODE=false
# Database Configuration (only if you've installed a database capability)
DB_DRIVER=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=forge_app
DB_USER=root
DB_PASS=password
SQLITE_PATH=/storage/database
SQLITE_DB=/database.sqlite
# Cache Configuration
CACHE_DRIVER=file
# Session Configuration
SESSION_DRIVER=file
SESSION_LIFETIME=1440
Start the development server to test your installation:
php forge.php serve
Visit http://localhost:8000 to see your application running!
Most Forge commands use interactive wizards by default. Let's create a simple controller to handle HTTP requests:
Run the command without arguments to start the interactive wizard:
# Run wizard (interactive)
php forge.php generate:controller
# The wizard will ask:
# Type (app or module): app
# Name (Controller class name without suffix): Welcome
# Path (Optional subfolder inside Controllers): [press Enter for none]
You can skip the wizard by providing options directly:
# Skip wizard for app scope
php forge.php generate:controller --type=app --name=Welcome
# Skip wizard for module scope
php forge.php generate:controller --type=module --module=Blog --name=Post
# With optional subfolder path
php forge.php generate:controller --type=app --name=User --path=Admin
This creates a controller in app/Controllers/WelcomeController.php:
<?php
declare(strict_types=1);
namespace App\Controllers;
use Forge\Core\DI\Attributes\Service;
use Forge\Core\Http\Attributes\Middleware;
use Forge\Core\Http\Response;
use Forge\Core\Routing\Route;
use Forge\Core\Http\Request;
use Forge\Traits\ControllerHelper;
#[Service]
#[Middleware('web')]
final class WelcomeController
{
use ControllerHelper;
#[Route("/welcome")]
public function index(Request $request): Response
{
$data = [
"title" => "Welcome to Forge",
"message" => "Hello from your first controller!"
];
return $this->view(view: "pages/welcome/index", data: $data);
}
}
Forge uses attributes for dependency injection. Services are automatically discovered from any folder in your application or modules when they have the #[Service] or #[Discoverable] attribute.
The framework automatically scans all directories recursively, so you can organize your code however you prefer. No specific folder structure is required.
Attributes:
#[Service] - Register a class as a service in the dependency injection container#[Discoverable] - Semantically marks a class as discoverable (same behavior as #[Service])Discovery Scope: Services are discovered from all directories under:
app/ - Any folder structuremodules/*/src/ - Any folder structure within module source directories
Note: While you can use any folder structure, traditional structures like app/Services/ or app/Repositories/ still work perfectly. The framework discovers services based on attributes, not folder location.
<?php
namespace App\Services;
use Forge\Core\DI\Attributes\Service;
#[Service]
class UserService
{
public function findUser(int $id)
{
// Your logic here
// Note: This example assumes you have a User model from an ORM capability
// return User::query()->id($id)->first();
}
}
Use services in controllers by adding them to the constructor:
<?php
namespace App\Controllers;
use App\Services\UserService;
use Forge\Core\DI\Attributes\Service;
use Forge\Core\Http\Response;
use Forge\Core\Routing\Route;
#[Service]
class UserController
{
public function __construct(
private readonly UserService $userService
) {}
#[Route('/users/{id}')]
public function show(string $id): Response
{
$user = $this->userService->findUser((int)$id);
return $this->view('users/show', ['user' => $user]);
}
}
Service discovery happens once at bootstrap and uses a class map cache for performance. The cache is automatically updated when files change. If you add a new service with #[Service] and it's not being discovered, clear the cache:
php forge.php cache:flush
Note: Services are automatically resolved by the container. Just add them to your constructor and they'll be injected. The discovery process has no performance trade-off since it uses class mapping and only updates entries when files change.
Forge uses attribute-based routing. Define routes directly on your controller methods:
<?php
#[Route("/")]
public function home(): Response
{
return $this->view("pages/home");
}
#[Route("/api/posts", method: "POST")]
public function createPost(Request $request): Response
{
// Handle POST request
return $this->json(["status" => "created"]);
}
// Using Middleware attribute on controller/method (recommended)
use Forge\Core\Http\Attributes\Middleware;
#[Middleware("web")]
class WebController
{
#[Middleware("auth")]
#[Route("/profile")]
public function profile(): Response
{
return $this->view("profile/index");
}
}
Routes support parameters that are automatically extracted and type-cast based on your method signature:
<?php
// Basic parameter - automatically cast to int
#[Route("/users/{id}")]
public function showUser(Request $request, int $id): Response
{
// $id is automatically cast to int
return $this->json(["user_id" => $id]);
}
// Multiple parameters
#[Route("/posts/{year}/{month}/{slug}")]
public function showPost(int $year, int $month, string $slug): Response
{
return $this->json([
"year" => $year,
"month" => $month,
"slug" => $slug
]);
}
// Complex parameter with constraint - matches anything including slashes
#[Route("/files/{path:.+}")]
public function serveFile(string $path): Response
{
// {path:.+} matches any path including slashes (e.g., /files/docs/user-guide.pdf)
return $this->file($path);
}
// Mix Request object with route parameters
#[Route("/users/{id}/posts/{postId}")]
public function showUserPost(Request $request, int $id, int $postId): Response
{
// Both $id and $postId are extracted from the route
// $request is injected automatically
return $this->json([
"user_id" => $id,
"post_id" => $postId,
"query" => $request->query("filter")
]);
}
Parameter Constraints: Use {param:.+} to match any path including slashes. Route parameters are automatically type-cast (int, float, bool) based on your method signature types.
Note: The method parameter accepts a single HTTP method as a string (e.g., "GET", "POST", "PUT", "DELETE"), not an array.
Routes are automatically discovered from your controllers. No need for separate route files! Use #[Middleware] attribute for cleaner middleware application on controllers/methods.
Forge uses PHP-first templating. Views can be created in your app or within modules (capabilities).
app/resources/views/modules/ModuleName/src/Resources/views/Layouts are expected to be in:
app/resources/views/layouts/modules/ModuleName/src/Resources/views/layouts/<!-- app/resources/views/pages/welcome/index.php -->
<?php
use Forge\Core\View\View;
// Load layout from app
View::layout(name: "layouts/app", loadFromModule: false);
?>
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold text-gray-900 mb-4">
<?= $title ?>
</h1>
<p class="text-xl text-gray-600">
<?= $message ?>
</p>
</div>
To load a layout from a module (like ForgeUi):
<?php
use Forge\Core\View\View;
// Load layout from module
View::layout(name: "layouts/app", loadFromModule: true, moduleName: "ForgeUi");
Create a layout file in your app:
<!-- app/resources/views/layouts/app.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Forge App</title>
</head>
<body>
<?php
use Forge\Core\View\View;
echo View::section('content');
?>
</body>
</html>
Note: See the ForgeUi module for reference on module folder structure and component organization.
Database and ORM are not built into the kernel. They're capabilities you install when you need them.
Install a database capability first:
# Install a database capability (with wizard)
php forge.php package:install-module
# Or skip wizard and specify directly
php forge.php package:install-module --module=ForgeDatabaseSQL
# Or install an ORM capability
php forge.php package:install-module --module=ForgeSqlOrm
Once installed, you can create migrations and models:
# Create a migration
php forge.php generate:migration CreateUsersTable
# Run migrations
php forge.php db:migrate
Note: Database and ORM functionality requires installing the appropriate capability module first. The kernel doesn't include these by default.
Developer mode unlocks advanced CLI commands for working with registries and publishing kernel/capability versions. You only need this if you're building your own kernel or publishing capabilities.
Set FORGE_DEVELOPER_MODE=true in your .env file:
FORGE_DEVELOPER_MODE=true
Or set it in config/app.php:
<?php
return [
'developer_mode' => true,
// ... other config
];
When developer mode is enabled, these commands become available:
# Framework/Kernel registry commands
php forge.php dev:framework:list # List available kernel versions
php forge.php dev:framework:publish # Publish kernel registry changes
php forge.php dev:framework:version # Create new kernel version
# Module/Capability registry commands
php forge.php dev:module:list # List available capability versions
php forge.php dev:module:publish # Publish capability registry changes
php forge.php dev:module:version # Create new capability version
# Registry management
php forge.php dev:registry:config # Configure registry settings
php forge.php dev:registry:init # Initialize new registry
When to enable: Only enable developer mode if you're building your own kernel fork or publishing capabilities to registries. For regular application development, you don't need it.
Most commands use interactive wizards by default. You can skip wizards by providing options with --option=value flags.
Forge includes a retro-styled interactive command browser that makes it easy to discover and execute commands. Simply run php forge.php without any arguments to access it.
Features: The interactive browser includes a splash screen (skippable with --no-splash), multi-column command listings that adapt to your terminal width, category-based browsing, and the ability to execute commands or view help directly from the interface. Use arrow keys to navigate and Esc to exit.
# Launch interactive command browser
php forge.php
# Skip splash screen
php forge.php --no-splash
# Show traditional command list
php forge.php --list
# Launch interactive browser directly
php forge.php --interactive
The interactive browser allows you to:
# Generate files (wizards ask for type: app/module, name, etc.)
php forge.php generate:controller
php forge.php generate:middleware
php forge.php generate:migration
php forge.php generate:model
php forge.php generate:service
php forge.php generate:component
# Skip wizards with options
php forge.php generate:controller --type=app --name=User
php forge.php generate:controller --type=module --module=Blog --name=Post
# Generate test (wizard can ask for type: app/module/engine)
php forge.php generate:test
php forge.php generate:test --type=app --group=example
php forge.php generate:test --type=module --module=Blog --group=example
# Note: engine scope is for testing the kernel itself
php forge.php db:migrate
php forge.php db:migrate:rollback
php forge.php serve
php forge.php cache:clear
php forge.php cache:flush # Flush service discovery cache
php forge.php down
php forge.php up
Package management commands use wizards by default. Use --module= to skip the wizard:
# With wizard (interactive)
php forge.php package:install-module
php forge.php package:remove-module
# Skip wizard
php forge.php package:install-module --module=ForgeAuth
php forge.php package:install-module --module=ForgeAuth@1.2.0
php forge.php package:remove-module --module=ForgeAuth
php forge.php package:list-modules
# Generate application key
php forge.php key:generate
# See all available commands (traditional list)
php forge.php help
# Launch interactive command browser
php forge.php
What you might want to look at next: