Skip to content

Exceptions

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

Exceptions

The ORM layer raises a small hierarchy of typed exceptions. They are organised into two families: model errors (operation gates, configuration issues) and entity errors (magic-method misuse).

Hierarchy

\Exception
 ├── InitORM\ORM\Exceptions\ModelException
 │    ├── WritableException     ← create() / createBatch() with $writable = false
 │    ├── ReadableException     ← read() with $readable = false
 │    ├── UpdatableException    ← update() / updateBatch() with $updatable = false
 │    └── DeletableException    ← delete() with $deletable = false
 │
 └── InitORM\ORM\Exceptions\EntityException
                                 ← Entity::__call() with an unrecognised method name

The Model also propagates BadMethodCallException (from __call forwarding failures) and any exception raised by the layers below: DatabaseException, SQLExecuteException, ConnectionException, QueryBuilderException, DataMapperException.

When each fires

ModelException

The umbrella for all ORM-layer model errors. Direct instantiation is reserved for configuration problems detected at construction:

class Bad extends \InitORM\ORM\Model
{
    protected string $schema = 'x';
    protected bool   $useSoftDeletes = true;
    // $deletedField missing
}

new Bad();
// ModelException: "App\Model\Bad has $useSoftDeletes enabled but $deletedField is not configured."

Catch ModelException when you want to handle every ORM-layer "the model said no" outcome regardless of which specific gate / invariant tripped.

WritableException

class Configuration extends \InitORM\ORM\Model
{
    protected string $schema   = 'configuration';
    protected bool   $writable = false;
}

(new Configuration())->create(['k' => 'v']);
// WritableException: "App\Model\Configuration is not writable."

Fires from:

  • Model::create()
  • Model::createBatch()

Always before any SQL is built.

ReadableException

class AuditLog extends \InitORM\ORM\Model
{
    protected string $schema   = 'audit_log';
    protected bool   $readable = false;
}

(new AuditLog())->read();
// ReadableException: "App\Model\AuditLog is not readable."

Fires from Model::read().

UpdatableException

class Events extends \InitORM\ORM\Model
{
    protected string $schema    = 'events';
    protected bool   $updatable = false;
}

(new Events())->update([...]);
// UpdatableException

Fires from Model::update() and Model::updateBatch().

DeletableException

class ReadOnly extends \InitORM\ORM\Model
{
    protected string $schema    = 'configuration';
    protected bool   $deletable = false;
}

(new ReadOnly())->delete([...]);
// DeletableException

Fires from Model::delete().

EntityException

$entity = new \InitORM\ORM\Entity();
$entity->doSomething();
// EntityException: 'Unknown entity method "doSomething".'

Entity::__call provides a default implementation for the get{Column}Attribute / set{Column}Attribute family. Any other method name raises EntityException.

Catching patterns

Catch a single gate

use InitORM\ORM\Exceptions\WritableException;

try {
    $config->create([...]);
} catch (WritableException $e) {
    // Handle the specific "this model can't be written" case.
}

Catch any gate

use InitORM\ORM\Exceptions\ModelException;

try {
    $config->create([...]);
} catch (ModelException $e) {
    // Catches WritableException, ReadableException, UpdatableException,
    // DeletableException, and any direct ModelException (e.g. config invariants).
}

Catch ORM-layer errors only

use InitORM\ORM\Exceptions\ModelException;
use InitORM\ORM\Exceptions\EntityException;

try {
    // …
} catch (ModelException | EntityException $e) {
    // All ORM-layer failures, but not the underlying Database / DBAL / QB ones.
}

Catch forwarding errors

Model::__call raises BadMethodCallException when the requested method does not exist anywhere in the chain (model, Database, builder). The original DatabaseException is chained as the previous exception:

try {
    $posts->thisDoesNotExist();
} catch (\BadMethodCallException $e) {
    echo $e->getMessage();                  // 'Method "App\Model\Posts::thisDoesNotExist" does not exist.'
    echo $e->getPrevious()::class;          // 'InitORM\Database\Exceptions\DatabaseException'
}

Exceptions from lower layers

The model layer is a thin wrapper. Most "real" failures come from below:

Exception When
InitORM\Database\Exceptions\DatabaseException Database-layer configuration / state problems.
InitORM\Database\Exceptions\DatabaseInvalidArgumentException Invalid args passed to Database methods.
InitORM\DBAL\Connection\Exceptions\ConnectionException PDO cannot connect.
InitORM\DBAL\Connection\Exceptions\SQLExecuteException Prepare / execute fails.
InitORM\DBAL\DataMapper\Exceptions\DataMapperException DataMapper used before a statement is set.
InitORM\QueryBuilder\Exceptions\QueryBuilderException Builder used incorrectly (e.g. missing required clause).
InitORM\QueryBuilder\Exceptions\QueryBuilderInvalidArgumentException Builder called with an invalid logical operator etc.

These propagate unchanged through the model — they are documented in the corresponding sibling wiki.

Exception messages

Model exception messages always contain the fully qualified class name of the offending model:

App\Model\Configuration is not writable.
App\Model\AuditLog is not readable.
App\Model\Events is not updatable.
App\Model\ReadOnly is not deletable.
App\Model\Bad has $useSoftDeletes enabled but $deletedField is not configured.

This makes it trivial to grep logs and locate the misuse without consulting the stack trace.

Anti-patterns

❌ Catching \Exception

try {
    $config->create([...]);
} catch (\Exception $e) {
    // swallows everything — connection errors, syntax errors, …
}

Too broad. Pick the narrowest class that captures the error you actually want to handle.

❌ Catching exceptions to silence them

try {
    $posts->update([...]);
} catch (\Throwable) {
    // Pretend nothing happened.
}

If a gate refuses an operation or an SQL execute fails, the caller probably needs to know. Re-throw or log, do not silently drop.

Read also

Clone this wiki locally