Forge Engine

introduction

Forge is a simple PHP framework and module system I built to serve my own projects. It’s fully open source under the MIT license.

I didn’t create Forge to compete with anything. I created it because I wanted something I could fully understand, control, and evolve at my own pace. I believe frameworks should feel like tools, not black boxes — simple, readable, and flexible.

Modern frameworks often try to be everything for everyone, which usually means extra layers of abstraction and heavy dependencies. Forge goes in the opposite direction — it tries to stay small, focused, and honest about what it is.

philosophy

This isn’t a product. There’s no company behind it. No support SLA. No roadmap you should depend on. Forge exists because I like to understand and own my stack. If others find value in it — awesome. If not — that’s cool too.

I’m not chasing trends or buzzwords. Forge is built incrementally, based on real project needs. If I need something, I build it — and if you ever do too, maybe the pieces will already be there for you to use.

You won’t find magic here. Just PHP, well-structured, and thoughtfully organized.

this organization contains

want to build your own thing?

You can fork the entire framework and module ecosystem to start your own direction. Check out FORGING-YOUR-OWN.md in the main repo for notes on how everything is structured and how to get started.

I think every developer should, at some point, try building their own framework — not to replace existing tools, but to learn how things actually work under the hood. Forge is my version of that journey.

installation

First things first, you'll need PHP 8.2 or newer installed on your machine.

Forge is designed to be lightweight and quick to spin up. No complex scaffolding. No endless composer dependencies. Just you, your terminal, and a clean starting point to build on.

Ready? Here's the super simple way to get started:

  1. Grab the installer and run it:
bash <(curl -Ls https://raw.githubusercontent.com/forge-engine/installer/main/installer.sh)

That’s it! This little command will do all the heavy lifting for you — clone the starter project, set up the folder structure, and give you a working Forge app you can run immediately.

The default starter is intentionally minimal. No unnecessary files, no extra configs. Just the Forge Engine and a blank canvas for your ideas. Whether you’re building a personal site, a CMS, an API, or a full-on app — you can take it wherever you need.

If you’re curious about different project types or want something pre-wired with modules (like authentication or admin panels), check out the other starters listed in the docs or browse the examples directory.

Forge doesn’t assume how you want to work — it just tries to stay out of your way. If you like tinkering with your tools and enjoy building things from the ground up, you’ll probably feel right at home.

Happy coding! 😊

directory structure

Forge keeps things simple and predictable. The default project layout gives you a solid foundation, but you’re always free to organize it however you like. Nothing is locked in — use what makes sense for your project.

At the root of your Forge app, you’ll find the following:

The structure is modular on purpose. If you don’t need something — like services or tests — you can just delete those folders. Forge won’t complain. You can even skip the whole app/ folder if you're building your app entirely using modules.

The only requirement: if you’re using the Forge auto-discovery system, controllers must go in Controllers/, services in Services/, and migrations/seeders inside database/. Everything else is flexible.

Think of this structure as a starting point, not a rulebook. You’re in control.

framework structure

The Forge Engine is lightweight but powerful — built from the ground up to be understandable, flexible, and easy to extend. If you want to dive into the source code or even fork the engine and make it your own, this overview will help you get familiar with the layout.

The engine lives inside the engine/ directory and is structured like this:

Everything is built in plain PHP, without any dependencies — so you can dig into any part of the engine and understand exactly what’s going on. No magic, no black boxes.

Want to customize or even fork the engine? Go for it. It’s all MIT-licensed and designed to be yours.

Dependency Injection: My Way of Keeping Things Tidy

Okay, so let's talk about something called Dependency Injection (DI). For me, building Forge has been about keeping things simple and understandable, and DI is a big part of that philosophy. Think of it like this: instead of your code having to go out and grab all the tools it needs, I wanted a system where those tools are just handed to it.

It's like when you're building something, right? It's way smoother if you have all your parts laid out and ready instead of constantly searching for them. That's what DI does for your code. It helps keep everything organized and makes it easier to see what relies on what.

In Forge, the Container (that bit of code you shared) is the guy in charge of handing out these tools (we call them services). It knows how to create them and keep them ready when they're needed.

The Simple Breakdown (as I See It)


                    use Forge\Core\DI\Attributes\Service;

                    #[Service]
                    class MyAwesomeService
                    {
                    public function doSomething() {
                    // ...
                    }
                    }
                

Using register(): If you need more control, you can use the register() method. This is useful if you want to specify a different ID for the service.


                    use Forge\Core\DI\Container;

                    $container = Container::getInstance();
                    $container->register(MyAwesomeService::class); // Registers it with the class name as the ID

                    $container->register('my.awesome.service', MyAwesomeService::class); // Registers it with a custom ID
                

Binding with bind(): This is handy when you want to map an interface to a specific implementation, or if you want to provide a custom function (a Closure) to create the service.


                    use Forge\Core\DI\Container;
                    use App\Interfaces\MyServiceInterface;
                    use App\Services\MyServiceImpl;

                    $container = Container::getInstance();
                    $container->bind(MyServiceInterface::class, MyServiceImpl::class);

                    $container->bind('my.service', function(Container $c) {
                    return new MyServiceImpl($c->get('some.other.service')); // Injecting another service
                    });

                

Singletons with singleton(): If you only want one instance of a service, use singleton().


                    use Forge\Core\DI\Container;

                    $container = Container::getInstance();
                    $container->singleton('my.config', function() {
                    return ['api_key' => 'your_key', 'debug' => true];
                    });
                

Asking for the Tools: When a piece of your code needs a tool, it just asks the Container. Forge then makes sure it has one ready and passes it over. That's what the get() and make() methods are for.

get() (My Go-To): This is the most common way to get a service. It's simple and efficient.


                    use Forge\Core\DI\Container;

                    $container = Container::getInstance();
                    $service = $container->get(MyAwesomeService::class);
                    $service->doSomething();
                

make(): This is similar to get(), but it always creates a new instance of the service, even if it's registered as a singleton. I don't use this as often, but it's there if you need it.


                    use Forge\Core\DI\Container;

                    $container = Container::getInstance();
                    $service1 = $container->make(MyAwesomeService::class);
                    $service2 = $container->make(MyAwesomeService::class); // $service1 and $service2 are different instances
                

Forge Being Helpful (Auto-wiring): This is one of the cooler parts. Often, when your code asks for a tool, Forge can automatically figure out what other tools that tool needs to work! It looks at the constructor (that __construct() thing in your PHP classes) and sees what else is being requested. If Forge knows how to make those other things, it just does it – that's auto-wiring.

Making Sure There's Only One (Singletons): For some tools, you only ever need one in your whole project – like a settings manager. The singleton() method makes sure that Forge only creates one of those tools and gives you the same one every time you ask for it.

Using Attributes for Easy Setup: You might see things like #[Service] in the code. This is just a handy way to tell Forge, "Hey, this class is a service, so manage it for me!" It's like putting a label on a tool so the Container knows what it is without you having to spell it out somewhere else.

Why Go Through All This?

For me, using dependency injection just makes sense. It leads to code that's:

Usage Examples (Show Me the Code!)

Let's look at some real-world examples from Forge to see DI in action:

Example 1: Injecting a Service into a Controller

Here's how I inject the ForgeAuthService into a controller. The controller needs this service to handle user authentication. Because of DI, the controller doesn't have to worry about *how* to create the ForgeAuthService; it just gets it handed over.


                    use Forge\Core\DI\Attributes\Service;

                    #[Service]
                    class MyAwesomeService
                    {
                    public function doSomething() {
                    // ...
                    }
                    }
                

                    declare(strict_types=1);

                    namespace App\Controllers;

                    use App\Modules\ForgeAuth\Services\ForgeAuthService;
                    use Forge\Core\DI\Attributes\Service;
                    use Forge\Core\Http\Attributes\Middleware;
                    use Forge\Core\Http\Response;
                    use Forge\Core\Routing\Route;
                    use Forge\Traits\ControllerHelper;
                    use Forge\Traits\SecurityHelper;

                    #[Service] // Hey Forge, manage this controller!
                    #[Middleware('web')]
                    final class DashboardController
                    {
                    use ControllerHelper;
                    use SecurityHelper;

                    public function __construct(private ForgeAuthService $forgeAuthService) // Inject the auth service
                    {
                    }

                    #[Route("/dashboard")]
                    #[Middleware('App\Modules\ForgeAuth\Middlewares\AuthMiddleware')]
                    public function welcome(): Response
                    {
                    $user = $this->forgeAuthService->user() ?? [];

                    $data = [
                    "title" => "Welcome to Forge Framework",
                    "user" => $user
                    ];

                    return $this->view(view: "pages/dashboard/index", data: $data);
                    }

                    }
                

Notice how the ForgeAuthService is declared in the constructor? Forge's Container automatically provides an instance of that service when the DashboardController is created. Clean and simple!

Example 2: Using DI in an Event Listener

This example shows how a service (PageVisitLogger) is used to handle an event. The event listener class is itself a service, thanks to the #[Service] attribute.



                    declare(strict_types=1);

                    namespace App\Services;

                    use App\Events\TestPageVisitedEvent;
                    use App\Modules\ForgeEvents\Attributes\EventListener;
                    use Forge\Core\DI\Attributes\Service;

                    #[Service]
                    class PageVisitLogger
                    {
                    #[EventListener(TestPageVisitedEvent::class)]
                    public function handlePageVisit(TestPageVisitedEvent $event): void
                    {
                    // Implement your logging logic here
                    $logEntry = sprintf(
                    "User %d visited test page at %s",
                    $event->userId,
                    $event->visitedAt
                    );

                    // Example: Write to log file
                    file_put_contents(
                    BASE_PATH . '/storage/logs/page_visits.log',
                    $logEntry . PHP_EOL,
                    FILE_APPEND
                    );
                    }

                    }
                

In this case, if the PageVisitLogger had any dependencies, they would also be injected by the Container.

A Bit More on Using DI in Forge

Here are a few more things to keep in mind when working with DI in Forge:

Configuration:

Sometimes, services need configuration values (like API keys, database settings, etc.). You can use the setParameter() and getParameter() methods of the Container to manage these.


                    use Forge\Core\DI\Container;

                    $container = Container::getInstance();
                    $container->set('api.key', 'your_super_secret_key');

                    // ...

                    class MyService {
                    public function __construct(private string $apiKey) {} // Inject the parameter

                    public function doSomething() {
                    // Use $this->apiKey
                    }

                    }
                    $container->bind(MyService::class, function(Container $c) {
                    return new MyService($c->get('api.key'));
                    });
                

Tags: Forge also supports "tags". This lets you group related services together. For example, you might tag all your event listeners with an "event.listener" tag. Then, you can easily retrieve all of them from the Container.


                    use Forge\Core\DI\Container;
                    use Forge\Core\DI\Attributes\Service;
                    use Forge\Core\DI\Attributes\Tag;

                    #[Service]
                    #[Tag('event.listener')]
                    class MyEventListener {}

                    #[Service]
                    #[Tag('event.listener')]
                    class AnotherEventListener {}

                    // ...

                    $container = Container::getInstance();
                    $listeners = $container->tagged('event.listener'); // Get all services with the tag
                

Ultimately, Forge's dependency injection is about keeping things clean, manageable, and flexible – the way I like to build. It's there to help you, the builder, focus on the actual logic of your project without getting bogged down in the details of creating and managing dependencies.

Module System

Work in progress..

Configuration

Work in progress..

Database

Work in progress..

Routing

Work in progress..

Views

Work in progress..

Components

Work in progress..

CLI

Work in progress..

Middleware

Work in progress..

Traits

Work in progress..

Helpers

Work in progress..

Services

Work in progress..

Forge Package Manager

Work in progress..

Forge Error Handler

Work in progress..

Forge Events

Work in progress..

Forge Logger

Work in progress..

Forge Storage

Work in progress..

Forge Auth

Work in progress..

Core Components

Work in progress..