Skip to content

Static Facade

Muhammet Şafak edited this page May 24, 2026 · 1 revision

Static Facade (DB)

InitORM\Database\Facade\DB is a static, single-instance facade over a shared DatabaseInterface. Call any Database / QueryBuilder method on it; the call is forwarded to the underlying instance via __callStatic.

use InitORM\Database\Facade\DB;

DB::createImmutable($cfg);

// Anywhere afterwards:
$users = DB::read('users')->asAssoc()->rows();
DB::transaction(fn ($db) => $db->create('audit', [...]));

The facade is fully opt-in. If you prefer dependency-injected DatabaseInterface, skip it entirely and instantiate Database directly — see Multiple Connections.

Lifecycle

┌────────────────────────────┐
│ DB::createImmutable($cfg)  │   ← required, exactly once
└──────────────┬─────────────┘
               │
               ▼
┌────────────────────────────┐
│ DB::someMethod(...)        │   ← any DatabaseInterface / builder method
└────────────────────────────┘

If you call DB::someMethod() before DB::createImmutable(), you get a clear error:

DatabaseException: "No immutable Database instance is configured.
Call DB::createImmutable($connection) first."

createImmutable() is single-shot

createImmutable() only succeeds once. A second call throws:

DB::createImmutable($cfg);
DB::createImmutable($cfg); // DatabaseException: "An immutable Database instance has already been set."

This is intentional — silent overrides are a bug magnet. If you genuinely need to swap (typically in tests), use the explicit replaceImmutable():

DB::replaceImmutable($newCfg);             // credentials array
DB::replaceImmutable($connection);         // ConnectionInterface
DB::replaceImmutable($otherDatabase);      // DatabaseInterface
DB::replaceImmutable(null);                // clear the slot

The name says what it does.

connect() — non-binding builder

When you want a second database without touching the facade slot, use DB::connect():

DB::createImmutable($primaryCfg);

$replica = DB::connect($replicaCfg); // returns DatabaseInterface, doesn't change DB::getDatabase()

$primary = DB::read('users');         // hits primary
$rows    = $replica->read('users')->asAssoc()->rows(); // hits replica

DB::connect() is a thin alias of new Database($cfg). Pick whichever reads better in your codebase.

getDatabase() — escape hatch

When you need the underlying DatabaseInterface (e.g. to pass it into another component), call DB::getDatabase():

$db = DB::getDatabase();
$repository = new UserRepository($db);

Method forwarding

DB::__callStatic($name, $arguments) forwards to getDatabase()->$name(...$arguments). This means every method on DatabaseInterface + every method on QueryBuilderInterface is reachable on the facade. The DocBlock on DB enumerates them all so IDE autocomplete works out of the box:

DB::select('id', 'name')          // QueryBuilder method
  ->where('active', '=', 1)        // QueryBuilder method
  ->orderBy('id', 'DESC')          // QueryBuilder method
  ->limit(10)                      // QueryBuilder method
  ->read('users')                  // Database method
  ->asAssoc()                      // DataMapper method
  ->rows();                        // DataMapper method

Cannot be instantiated

The class is final and the constructor is private:

new DB(); // Error — Call to private InitORM\Database\Facade\DB::__construct()

This is intentional — the facade is pure static state. Anything else would mislead readers.

Testing with the facade

Two patterns work well.

Pattern A — fresh facade per test

final class UserServiceTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        DB::replaceImmutable(new Database([
            'driver' => 'sqlite', 'database' => ':memory:', 'charset' => '',
        ]));
        DB::query('CREATE TABLE users (...)');
    }

    protected function tearDown(): void
    {
        DB::replaceImmutable(null); // clear for the next test
        parent::tearDown();
    }
}

Pattern B — skip the facade entirely in tests

final class UserServiceTest extends TestCase
{
    private Database $db;

    protected function setUp(): void
    {
        $this->db = new Database([
            'driver' => 'sqlite', 'database' => ':memory:', 'charset' => '',
        ]);
    }
}

This is what the package's own tests do — see tests/AbstractDatabaseTestCase.php.

When NOT to use the facade

  • Libraries — never bind your library users to a particular facade; accept DatabaseInterface instead.
  • Multi-tenant apps where the "current" database depends on request context — thread the instance explicitly.
  • Applications with a DI container — your container is already a better place for the singleton.

For everything else (CLI scripts, single-DB web apps, prototypes), the facade is great.

See also

  • Multiple Connections — facade + secondary instances, sibling builders
  • Architecture — what __callStatic actually does
  • FAQ — "why is createImmutable named that way?", "how do I clear the facade in PHPUnit?"

Clone this wiki locally