Skip to content

Architecture

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

Architecture

A walk through the package — what classes exist, why they exist, and how a single $db->query(...) call flows through them.

Layered view

┌────────────────────────────────────────────────────────────┐
│ your code                                                  │
└──────────────┬─────────────────────────────────────────────┘
               │
               ▼
┌────────────────────────────────────────────────────────────┐
│ Connection                       ←─ logging, credentials,  │
│   ├── DsnBuilder                    DSN building, lifecycle│
│   ├── QueryLogger                                          │
│   └── Logger          (PSR-3 / callable / file / duck)     │
└──────────────┬─────────────────────────────────────────────┘
               │ prepare($sql, $options)
               ▼
┌────────────────────────────────────────────────────────────┐
│ PDO                                                        │
└──────────────┬─────────────────────────────────────────────┘
               │ returns PDOStatement
               ▼
┌────────────────────────────────────────────────────────────┐
│ DataMapperFactory ──► DataMapper (wraps PDOStatement)      │
│                          ├── bind / bindValue / bindValues │
│                          └── asAssoc / asObject / asClass /│
│                              asLazy / asArray / row / rows │
└────────────────────────────────────────────────────────────┘

Directory layout

src/
├── Connection/
│   ├── Connection.php
│   ├── ConnectionFactory.php
│   ├── Exceptions/
│   │   ├── ConnectionException.php
│   │   ├── ConnectionInvalidArgumentException.php
│   │   ├── SQLExecuteException.php
│   │   ├── ConnectionAlreadyEstablishedException.php
│   │   └── ValidConnectionAvailableException.php   [deprecated alias]
│   ├── Interfaces/
│   │   ├── ConnectionInterface.php                 [aggregate]
│   │   ├── ConfigurableConnectionInterface.php
│   │   ├── LoggableConnectionInterface.php
│   │   └── ConnectionFactoryInterface.php
│   └── Support/
│       ├── DsnBuilder.php          ← driver-aware DSN composer
│       ├── QueryLogger.php         ← in-memory query log buffer
│       └── Logger.php              ← critical-log dispatcher
│
└── DataMapper/
    ├── DataMapper.php
    ├── DataMapperFactory.php
    ├── Exceptions/
    │   ├── DataMapperException.php
    │   └── DataMapperInvalidArgumentException.php
    └── Interfaces/
        ├── DataMapperInterface.php
        └── DataMapperFactoryInterface.php

Interface segregation

ConnectionInterface is the public face, but it deliberately extends two narrower contracts so callers can depend on only what they use:

Interface Responsibility
ConfigurableConnectionInterface Credentials accessors (setHost, setDatabase, …)
LoggableConnectionInterface Query log + critical log surface
ConnectionInterface Lifecycle (getPDO, connect, disconnect) + query()

A consumer that only needs to run queries can type-hint ConnectionInterface. A consumer that only configures connections can type-hint ConfigurableConnectionInterface and never see the query surface.

The __call chain

Both Connection and DataMapper use __call to forward unknown methods. The rule is the same in both:

public function __call(string $name, array $arguments)
{
    $result = $this->target->{$name}(...$arguments);

    return $result instanceof TargetClass ? $this : $result;
}

Where TargetClass is PDO for Connection, and PDOStatement for DataMapper. This rewrite is important — it keeps fluent chains working across the wrapper boundary:

$db->beginTransaction()   // PDO returns $this (PDO) → we re-wrap to Connection
   ->lastInsertId();      // OK to call on Connection

Without the rewrite, the second call would land on the bare PDO instance and break the wrapper's logging / lifecycle hooks.

Lifecycle of a query() call

$db->query('SELECT id FROM users WHERE id = :id', ['id' => 1]);

  ├─ microtime(true)                           ← startTime
  ├─ $options + $queryOptions                  ← merge with +, not array_merge
  ├─ getPDO()                                  ← connects lazily on first call
  │     ├─ new PDO($dsn, $u, $p, $opts)
  │     ├─ applyCharsetAndCollation()          ← MySQL only
  │     └─ fill missing 'driver' from PDO
  │
  ├─ $pdo->prepare($sql, $options)             ← may throw PDOException
  ├─ dataMapperFactory->createDataMapper($stmt)
  ├─ $mapper->bindValues($params)              ← per-value type via bind()
  ├─ $mapper->execute()                        ← may throw PDOException
  │
  ├─ queryLogger->add($sql, $params, startTime)
  └─ return $mapper

  catch Throwable:
       queryLogger->add(...)
       logger->write(failure message, $params)
       throw original exception

Key points:

  • Lazy connect. No PDO is created until the first call that needs it. Constructing a Connection is essentially free.
  • Logging on both paths. A successful query and a failing query both produce a log entry (when enabled).
  • Exception passthrough. The library does not transform PDO exceptions on the success path. They reach your catch block in their original form.

Defaults in one place

Concern Default Where
Driver mysql Connection::$credentials
Host / port 127.0.0.1 / 3306 Connection::$credentials
Charset utf8mb4 Connection::$credentials
ATTR_EMULATE_PREPARES false Connection::getOptions()
ATTR_PERSISTENT false Connection::getOptions()
ATTR_ERRMODE PDO::ERRMODE_EXCEPTION Connection::getOptions()
ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC Connection::getOptions()

Design choices, briefly

  • No query builder. That is a separate package (initorm/query-builder). DBAL only runs strings.
  • No connection pool. PHP's lifecycle is per-request; pooling belongs to a proxy (ProxySQL, PgBouncer) or a persistent runtime (Swoole) — not to a library.
  • No reflection-driven hydration. asClass() uses native PDO::FETCH_CLASS. If you need richer hydration, drop down to getStatement() and use any mapper you like.
  • Driver-aware but driver-agnostic on the surface. DsnBuilder knows MySQL, PostgreSQL, and SQLite; the public API does not change per driver.

Where to look in the code

What's next

Clone this wiki locally