-
Notifications
You must be signed in to change notification settings - Fork 0
FAQ
The dependency chain — orm → database → dbal + query-builder — means each layer adds one concern. The model is the top of the chain; it does not own the connection, it composes one.
If you want raw query control, drop down: $model->getDatabase()->query(...). If you want connection control, drop further: $model->getDatabase()->getConnection()->getPDO(). You never lose access to the layer below.
Most methods are layered:
| Method | Layer |
|---|---|
create, read, update, delete, save
|
Model (this package) |
transaction, affectedRows, getQueryLogs, query
|
Database |
select, where, join, orderBy, limit, raw
|
QueryBuilder |
asAssoc, asObject, asClass, rows, row
|
DataMapper (DBAL) |
When you call them on a model, __call forwards everything down. The chain re-wraps at every boundary so fluent calls stay rooted in the model. See Architecture.
Relationships were an explicit non-goal. Use the query-builder's join() family directly — it composes cleanly with the model's __call forwarding:
$rows = $posts
->select('posts.*', 'users.name AS author_name')
->from('posts')
->innerJoin('users', 'posts.author_id = users.id')
->read()
->rows();For eager loading, do two queries and a manual key/group:
$posts = $postsModel->read()->rows();
$ids = array_map(fn ($p) => $p->author_id, $posts);
$authors = $authorsModel->whereIn('id', $ids)->read()->rows();
$authorById = [];
foreach ($authors as $a) {
$authorById[$a->id] = $a;
}Two SELECTs > one JOIN > N+1 — and the code is dialect-portable.
You wrote the mutator like this:
public function setTitleAttribute($value): void
{
$this->title = trim($value); // ❌
}From inside the class, $this->title = … creates a dynamic property and bypasses __set entirely. The value never reaches $attributes. PHP 8.2+ emits a "Creation of dynamic property" deprecation; a future PHP version will make this fatal.
The fix is $this->setAttribute('title', trim($value)). See Entities — this is the single biggest pitfall in the API.
You're on v1, which shipped a broken Helper::camelCaseToSnakeCase(). Either set $schema explicitly or upgrade to v2. See Migration from v1 to v2.
Set $schema explicitly. Auto-derivation only handles PascalCase / camelCase short names. Anything else (already-snake_case, kebab-case, plural pluralisation rules, table prefixes, …) needs an explicit value.
Yes. Leave $entity at its default (Entity::class), and read() will hydrate plain Entity instances. They behave the same as a subclass with no accessor/mutator methods.
class Tags extends \InitORM\ORM\Model
{
protected string $schema = 'tags';
// $entity defaults to Entity::class
}
foreach ((new Tags())->read()->rows() as $tag) {
echo $tag->label; // works fine
}No. Posts → posts, but Person → person (not people). For irregular plurals, set $schema explicitly.
Yes — they're plain public functions. The only thing to watch for is calling parent::method() to inherit the gate check, timestamp injection, and soft-delete predicates.
class Posts extends \InitORM\ORM\Model
{
public function delete(?array $conditions = null, bool $purge = false): bool
{
// Refuse purge entirely; only soft-deletes are allowed through the model.
return parent::delete($conditions, purge: false);
}
}By design — Model::update() lifts a non-empty $schemaId value out of $set into a WHERE clause, so you can never overwrite the primary key through the model. To explicitly update a row's id column (rare), use getDatabase()->update(...).
Three possibilities:
- The column is not in
$attributesbecause it was never written. Check$entity->getAttributes(). - You hydrated the entity via
PDO::FETCH_CLASSbut the column name in your SELECT doesn't match the property name.$entity->titlereads$attributes['title']; if your SELECT aliased the column ast(SELECT title AS t),$attributes['t']exists instead. - An accessor method returns the wrong thing — e.g. it expected
$valueto be a string but receivednull.
You're on v1, which called syncOriginal() before fill() — the original snapshot was always taken when $attributes was empty. v2 fixed this; see Migration from v1 to v2.
PHP doesn't let interfaces declare magic methods, so you can't enforce this at the type level. But because Entity::__call provides a sensible default for the get{Column}Attribute / set{Column}Attribute family, subclasses without any explicit accessor / mutator methods work fine — there's no "incomplete entity" failure mode.
$entity instanceof \JsonSerializable; // false — Entity doesn't implement it
echo json_encode($entity->toArray()); // works, no custom interface neededIf you want json_encode($entity) to work directly:
class JsonEntity extends \InitORM\ORM\Entity implements \JsonSerializable
{
public function jsonSerialize(): array
{
return $this->toArray();
}
}This is opt-in per subclass to avoid leaking values that an accessor might not transform.
By design — $useSoftDeletes = true adds deletedField IS NULL to every read. Use onlyDeleted() for soft-deleted rows specifically. See Soft Deletes.
$row = $model->selectCount('id', 'total')->read()->asAssoc()->row();
$total = (int) ($row['total'] ?? 0);There is no count() helper on the model — selectCount('*', 'alias') + asAssoc()->row() is the idiomatic form, and it composes with any pending WHERE chain.
$row = $model->read([], [$model->getSchemaId() => $id])->row();If $row instanceof YourEntity, the row was found; otherwise row() returned null.
You called update([...]) without conditions and without a pending WHERE chain. The model does not refuse to issue an unbounded UPDATE — it trusts the caller. Always pass a primary key (lifted into a WHERE) or explicit $conditions:
$model->update(['id' => 5, 'col' => 'x']); // safe
$model->update(['col' => 'x'], ['id' => 5]); // safe
$model->update(['col' => 'x']); // affects all rows — usually a bugFor "all rows" updates, make the intent explicit by setting a condition that matches everything ('1' => '1' etc.) — it's verbose, and that's the point.
Because PHP's \PDO::lastInsertId() returns a string. Cast it if you need an int:
$id = (int) $model->getDatabase()->insertId();You haven't called DB::createImmutable($credentials) yet. Do it once at boot. See Getting Started and Multiple Connections.
By design — silent reconfiguration of the application-wide connection is too easy to get wrong. To replace it explicitly:
DB::replaceImmutable($newDatabase);For one-off secondary connections that don't touch the facade, use DB::connect($credentials) or new Database($credentials).
Because the model resolves the connection in __construct() and there is no de-duplication at the model layer. For hot loops, hold one model instance:
// ❌ N connections
foreach ($jobs as $j) {
(new EventsModel())->create(['payload' => $j]);
}
// ✅ 1 connection
$events = new EventsModel();
foreach ($jobs as $j) {
$events->create(['payload' => $j]);
}For per-request reuse across many call sites, build a registry or DI container.
Either:
- You reused a
:memory:SQLite connection acrosssetUp()(each test should get a fresh one), or - You called
DB::createImmutable()once insetUpBeforeClass()(usereplaceImmutable()per test instead).
See Testing.
You instantiated an entity-less model ($entity = Entity::class) and then called ->asAssoc() or ->asObject() on the DataMapper, overriding FETCH_CLASS. Either:
- Set
$entityto your custom class and call->rows()(no override), or - Use
->asAssoc()->rows()to get arrays explicitly.
Five things:
-
Helper::camelCaseToSnakeCase()was broken and is now correct. Schemas auto-derived in v1 will change. -
Entity::__getnow passes the stored value to the accessor (get{Column}Attribute($value)). - Mutators MUST use
$this->setAttribute(...)— the previous$this->col = ...pattern was broken in PHP 8.2+. -
Model::onlyDeleted()is now a one-shot flag-based scope; the v1 implementation produced a contradictory WHERE. -
Entity::syncOriginal()is called afterfill()sogetOriginal()reflects construction-time data.
See Migration from v1 to v2 for diffs.
- Skim the Architecture page — most "why" questions have a structural answer.
- Check the issue tracker.
- Open a new issue with a minimal reproducible example.
InitORM ORM · MIT · maintained by Muhammet ŞAFAK · part of the InitORM stack
Getting Started
Models
Entities
Cross-Cutting
Reference
Upgrading
Project