DALT.PHP
Framework Deep Dive3. Container & DB

Container Basics

Understanding dependency injection and the service container

The Toolbox Pattern

Imagine you're building furniture:

  • You need a hammer, screwdriver, saw
  • Instead of carrying them in your pockets, you use a toolbox
  • When you need a tool, you reach into the toolbox
  • Everyone shares the same toolbox

A dependency container is like that toolbox for your app:

  • It stores "services" (database, mailer, cache, etc.)
  • When you need something, you ask the container
  • The container knows how to create it
  • Everyone gets the same instance (usually)

What is Dependency Injection?

Before understanding the container, let's understand the problem it solves.

The Problem: Hard Dependencies

class PostController
{
    public function index()
    {
        $db = new Database([
            'driver' => 'sqlite',
            'database' => 'database/app.sqlite'
        ]);
        
        $posts = $db->query('SELECT * FROM posts')->get();
        // ...
    }
}

Issues:

  • ❌ Creates a new database connection every time
  • ❌ Hard to test (can't mock the database)
  • ❌ Tightly coupled (controller knows database details)
  • ❌ Can't reuse connections

The Solution: Inject Dependencies

class PostController
{
    private $db;
    
    public function __construct(Database $db)
    {
        $this->db = $db;
    }
    
    public function index()
    {
        $posts = $this->db->query('SELECT * FROM posts')->get();
        // ...
    }
}

Benefits:

  • ✅ Database is passed in (injected)
  • ✅ Easy to test (pass a mock)
  • ✅ Loosely coupled
  • ✅ Reuses the same connection

But now we have a new problem: who creates the database and passes it in?

That's where the container comes in.


The Container Class

DALT.PHP's container is beautifully simple:

class Container
{
    protected $bindings = [];
    protected $instances = [];

    public function bind($key, $resolver)
    {
        $this->bindings[$key] = $resolver;
    }

    public function resolve($key)
    {
        if (!array_key_exists($key, $this->bindings)) {
            throw new \Exception("No Matching Binding Found For {$key}");
        }

        if (!isset($this->instances[$key])) {
            $this->instances[$key] = call_user_func($this->bindings[$key]);
        }

        return $this->instances[$key];
    }
}

That's it! Just 20 lines of code.


How It Works

Step 1: Bind (Register a Recipe)

$container = new Container();

$container->bind('Core\Database', function () {
    return new Database([
        'driver' => 'sqlite',
        'database' => 'database/app.sqlite'
    ]);
});

What this does:

  • Key: 'Core\Database'
  • Value: A function that creates a Database

Think of it as: "When someone asks for Database, here's how to make one."

Step 2: Resolve (Get the Thing)

$db = $container->resolve('Core\Database');

What happens:

  1. Container looks up 'Core\Database' in $bindings
  2. Finds the function we registered
  3. Calls the function: call_user_func($resolver)
  4. Returns the result (a Database instance)

Breaking Down the Code

The Bindings Array

protected $bindings = [];

This is just a dictionary:

[
    'Core\Database' => function() { return new Database(...); },
    'Core\Mailer' => function() { return new Mailer(...); },
    'Core\Cache' => function() { return new Cache(...); },
]

The bind() Method

public function bind($key, $resolver)
{
    $this->bindings[$key] = $resolver;
}

Parameters:

  • $key - Usually a class name
  • $resolver - A function (closure) that creates the object

Why a function?

  • Lazy loading - only created when needed
  • Can use variables from outer scope
  • Can have complex creation logic

The resolve() Method

public function resolve($key)
{
    if (!array_key_exists($key, $this->bindings)) {
        throw new \Exception("No Matching Binding Found For {$key}");
    }

    $resolver = $this->bindings[$key];
    return call_user_func($resolver);
}

Step by step:

1. Check if binding exists

if (!array_key_exists($key, $this->bindings)) {
    throw new \Exception("No Matching Binding Found For {$key}");
}

If you ask for something that wasn't registered, throw an error.

2. Get the resolver function

$resolver = $this->bindings[$key];

3. Call it and cache the result

if (!isset($this->instances[$key])) {
    $this->instances[$key] = call_user_func($this->bindings[$key]);
}
return $this->instances[$key];

call_user_func() executes the function. We then save its return value into the $instances array (our singleton cache) so the next time someone asks for $key, we don't have to build it again!


Using Closures for Binding

The $resolver is a closure (anonymous function):

$container->bind('Core\Database', function () {
    return new Database([
        'driver' => 'sqlite',
        'database' => 'database/app.sqlite'
    ]);
});

Why Closures?

1. Lazy Evaluation

// This doesn't create the database yet
$container->bind('Core\Database', function () {
    return new Database(...);  // Not called yet
});

// Database is created only when you resolve it
$db = $container->resolve('Core\Database');  // Now it's created

2. Access to Outer Variables

$config = ['driver' => 'sqlite'];

$container->bind('Core\Database', function () use ($config) {
    return new Database($config);  // Can use $config
});

The use ($config) captures variables from the outer scope.

3. Complex Logic

$container->bind('Core\Database', function () {
    $config = loadConfig();
    $db = new Database($config);
    $db->connect();
    $db->setCharset('utf8mb4');
    return $db;
});

The App Facade

To make the container globally accessible, DALT.PHP uses an App class:

class App
{
    protected static $container;

    public static function setContainer($container)
    {
        static::$container = $container;
    }

    public static function container()
    {
        return static::$container;
    }

    public static function bind($key, $resolver)
    {
        static::container()->bind($key, $resolver);
    }

    public static function resolve($key)
    {
        return static::container()->resolve($key);
    }
}

What This Enables

Instead of:

global $container;
$db = $container->resolve('Core\Database');

You can:

$db = App::resolve(Database::class);

From anywhere in your code!


Real Usage in DALT.PHP

During Bootstrap

// framework/Core/bootstrap.php

$container = new Container();

$container->bind('Core\Database', function () use ($dbConfig) {
    return DatabaseManager::create($dbConfig['database']);
});

App::setContainer($container);

In Controllers

// app/Http/controllers/posts/index.php

$db = App::resolve(Database::class);

$posts = $db->query('SELECT * FROM posts')->get();

view('posts/index.view.php', ['posts' => $posts]);

In Other Classes

class Authenticator
{
    public function attempt($email, $password)
    {
        $user = App::resolve(Database::class)
            ->query('SELECT * FROM users WHERE email = :email', ['email' => $email])
            ->find();
        
        // ...
    }
}

Important: Singleton Behavior (Caching)

Notice something important in the resolve() method:

$db1 = App::resolve(Database::class);
$db2 = App::resolve(Database::class);

// Are these the same object?

Answer: YES!

Because of this block:

if (!isset($this->instances[$key])) {
    $this->instances[$key] = call_user_func($this->bindings[$key]);
}
return $this->instances[$key];

The first time you call resolve(), the container executes the closure to create the object and stores it in $instances. The next time you ask for the same class, it skips the heavy creation process and just returns the exact same object from its cache.

This is called the Singleton Pattern, and it is crucial for things like a Database connection so you don't exhaust your database connection pool by opening a new connection on every query!


Key Takeaways

  1. Container is a service registry - Stores how to create things
  2. Bind registers recipes - Functions that create objects
  3. Resolve gets instances - Builds them once, then returns the cached instance
  4. App is a global facade - Access container from anywhere
  5. Singleton by default - Resolving the same binding returns the exact same object

What's Good Here

✅ Clean, standard Dependency Injection implementation
✅ Built-in Singleton caching prevents massive resource leaks
✅ Easy to understand (just mapped arrays)
✅ Flexible (closures can do anything)
✅ Testable (can swap bindings for tests)
✅ Simple enough to understand completely

Design Note

Advanced DI features like auto-wiring and contextual binding are intentionally omitted. These use reflection and add significant complexity. DALT's container is simple and explicit - you can see exactly what's being created and when.


Next, explore how the database connection is created and managed.

On this page