PHPackages                             contenir/contenir-db-model - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. contenir/contenir-db-model

ActiveLibrary[Database &amp; ORM](/categories/database)

contenir/contenir-db-model
==========================

Contenir Db Model

v1.0.4.4(1y ago)05121BSD-3-ClausePHPPHP ^8.1.0

Since Apr 24Pushed 1mo agoCompare

[ Source](https://github.com/contenir/contenir-db-model)[ Packagist](https://packagist.org/packages/contenir/contenir-db-model)[ Docs](https://contenir.com.au)[ RSS](/packages/contenir-contenir-db-model/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (10)Dependencies (6)Versions (24)Used By (1)

contenir-db-model
=================

[](#contenir-db-model)

A small Laminas-flavoured data-mapper layer that pairs immutable-ish entities with table-backed repositories. Built on top of `laminas-db`, `laminas-hydrator` and `laminas-mvc`, it provides:

- A `Contenir\Db\Model\Entity\AbstractEntity` base class with column metadata, modification tracking, lazy relation loading via `laminas-eventmanager`, and array hydration.
- A `Contenir\Db\Model\Repository\AbstractRepository` table-gateway base class that wraps `laminas-db` `Sql` operations (`insert`, `update`, `delete`, `select`, `find`, `findByField`, `findOne`) and an opinionated `save()`helper that auto-detects insert vs. update from the entity's primary keys.
- A `Contenir\Db\Model\Hydrator\RelationsHydrator` that can hydrate single or many-related entities, optionally through an intermediate join table.
- Service-manager wiring (`ConfigProvider`, `Module`) so the package is usable out of the box from a `laminas-mvc` or Mezzio application.

Requirements
------------

[](#requirements)

- PHP `^8.1`
- `laminas/laminas-db` `^2.20`
- `laminas/laminas-hydrator` `^4.15`
- `laminas/laminas-mvc` `^3.0`
- `contenir/contenir-metadata` `^1.0`

Installation
------------

[](#installation)

```
composer require contenir/contenir-db-model
```

If you use the Laminas component installer the package will register itself automatically. Otherwise add the module to your application config:

```
// config/modules.config.php
return [
    // ...
    'Contenir\\Db\\Model',
];
```

The component exposes a configured database adapter alias under the `model.adapter` config key (defaulting to `Laminas\Db\Adapter\Adapter`). Override this in your application config if you use a custom adapter service.

```
// config/autoload/db.global.php
return [
    'model' => [
        'adapter' => 'My\\Custom\\Adapter',
        'map'     => [
            // Optional repository → entity mapping. Used by RepositoryFactory.
            App\Model\Repository\UserRepository::class => App\Model\Entity\UserEntity::class,
        ],
    ],
];
```

Usage
-----

[](#usage)

### 1. Define an entity

[](#1-define-an-entity)

Subclass `AbstractEntity` and declare column metadata. `primaryKeys`, `columns`, and `relations` are the three properties the base class reads.

```
use Contenir\Db\Model\Entity\AbstractEntity;

class UserEntity extends AbstractEntity
{
    protected array $primaryKeys = ['id'];

    protected array $columns = [
        'id',
        'email',
        'name',
        'created_at',
    ];

    protected array $relations = [
        'orders' => [
            'type'   => AbstractEntity::RELATION_MANY,
            'column' => 'id',
            'table'  => [
                'class'  => OrderRepository::class,
                'column' => 'user_id',
            ],
            'order'  => ['created_at DESC'],
        ],
    ];
}
```

Entities support array-style construction, isset/unset, modification tracking and PHP serialisation:

```
$user        = new UserEntity(['id' => 1, 'email' => 'a@example.com']);
$user->name  = 'Alice';

// Constructor populates via __set, so every supplied column is flagged
// modified — this is what tells save(MODE_INSERT) which columns to
// write. Repositories call markClean() (or synch()) after a successful
// load/save, so entities returned from find()/findOne() report only the
// caller's subsequent changes:
//   $user->getModifiedArrayCopy();
//   // => ['id' => 1, 'email' => 'a@example.com', 'name' => 'Alice']

$user->getArrayCopy();   // full row, including null columns
$user->getPrimaryKeys(); // ['id' => 1] — auto-increment PKs come back as null until saved
```

### 2. Define a repository

[](#2-define-a-repository)

Subclass `BaseRepository` (or `AbstractRepository` for full control) and set the table name. The factory wires the adapter, entity prototype and lookup service for you.

```
use Contenir\Db\Model\Repository\BaseRepository;
use Laminas\Db\Sql\TableIdentifier;

class UserRepository extends BaseRepository
{
    protected TableIdentifier|string|array|null $table = 'users';
}
```

Register the repository with `RepositoryFactory`:

```
// config/autoload/dependencies.global.php
use Contenir\Db\Model\Repository\Factory\RepositoryFactory;

return [
    'dependencies' => [
        'factories' => [
            App\Model\Repository\UserRepository::class => RepositoryFactory::class,
        ],
    ],
];
```

By default `RepositoryFactory` resolves the entity by anchoring the convention to the trailing class name and the `\Repository\` namespace segment, so `App\Model\Repository\UserRepository` resolves to `App\Model\Entity\UserEntity`. Unrelated occurrences of "Repository" elsewhere in the namespace are left alone. Override the convention with the `model.map` config when your naming differs.

### 3. Query and persist

[](#3-query-and-persist)

```
/** @var UserRepository $users */
$users = $container->get(UserRepository::class);

$user = $users->findOne(['email' => 'a@example.com']);

$user->name = 'Updated';
$users->save($user); // detects update vs insert from primary keys

$new = $users->create(['email' => 'b@example.com', 'name' => 'Bob']);
$users->save($new);  // mode auto → insert; primary key is back-filled

$users->delete(['id' => 42]);
```

`findByField($column, $value)` validates `$column` against the declared columns of the entity prototype and rejects unknown values, so caller-controlled column names cannot smuggle SQL into the predicate's left-hand side.

`$order` arguments to `find`, `findByField`, and `prepareSelect` are passed straight to `Laminas\Db\Sql\Select::order()`, which quotes identifiers. Pass either a string (`'name ASC'`), a list (`['name', 'created_at DESC']`), or an associative array (`['name' => 'ASC']`). For raw SQL fragments, pass a `Laminas\Db\Sql\Expression` instance explicitly.

`save()` accepts an explicit mode if you need to override the detection:

```
use Contenir\Db\Model\Repository\AbstractRepository;

$users->save($user, AbstractRepository::MODE_INSERT);
$users->save($user, AbstractRepository::MODE_UPDATE);
```

By default `save()` issues a single statement and applies the auto-generated PK locally — no post-write SELECT. Pass `refresh: true`when the entity needs to pick up DB-computed defaults, triggers or concurrent writes; the INSERT/UPDATE and refresh SELECT are then wrapped in a transaction:

```
$users->save($user, refresh: true);
```

For long-running scripts or when several statements need to be atomic, wrap them with `transactional()`. Calls are re-entrant — nested `transactional()` invocations join the outer transaction, and only the outermost frame commits or rolls back:

```
$users->transactional(function () use ($users, $orders, $user, $order) {
    $users->save($user);
    $orders->save($order);
});
```

### Optimistic locking

[](#optimistic-locking)

Declare a `versionColumn` on an entity to opt into optimistic concurrency control:

```
class WidgetEntity extends AbstractEntity
{
    protected array $columns       = ['id', 'name', 'version'];
    protected ?string $versionColumn = 'version';
}
```

`save()` then issues `UPDATE … WHERE pk = ? AND version = :loaded`, bumps the version through `nextVersion()`, and throws `Contenir\Db\Model\Exception\StaleEntityException` when the row no longer matches its loaded version (concurrently updated or deleted). Override `AbstractEntity::nextVersion()` for non-integer schemes (e.g. `microtime`-based timestamps).

### 4. Lazy-loaded relations

[](#4-lazy-loaded-relations)

Relations declared on the entity are fetched on first access. Internally the entity emits a `loadRelation` event; `RelationsHydrator` attaches a listener that pulls the related rows from the configured repository.

```
$user->orders; // triggers a SELECT on the orders repository
```

The hydrator caches identical FK lookups within its own lifetime, so iterating a result set in which many parent rows share the same FK target (e.g. fifty orders that all belong to one user) only issues one query per distinct target rather than one per row.

To avoid the classic N+1 pattern when each parent has a different FK, batch-load the relations up front with `preloadRelations()`:

```
$users = iterator_to_array($users->find());
$users->preloadRelations($users, ['orders', 'profile']);

foreach ($users as $user) {
    foreach ($user->orders as $order) {
        // already in memory, no query issued
    }
}
```

`preloadRelations()` issues one SELECT per relation with a `WHERE … IN (…)`clause over the parent foreign-key values and assigns the matching rows back onto each entity. It currently supports single-column relations without `via` join tables; for relations with `via` tables (many-to-many) fall back to the lazy-load path.

For many-to-many relationships, declare a `via` table:

```
'tags' => [
    'type'   => AbstractEntity::RELATION_MANY,
    'column' => 'id',
    'table'  => [
        'class'  => TagRepository::class,
        'column' => 'id',
    ],
    'via' => [
        'table'  => 'user_tag',
        'column' => 'user_id', // column on the join table matching the owning row's key
        'join'   => 'tag_id',  // column on the join table matching the related table's key
    ],
],
```

Exceptions
----------

[](#exceptions)

All exceptions thrown by the package implement `Contenir\Db\Model\Exception\ExceptionInterface`. Concrete classes extend the matching SPL exception:

- `Contenir\Db\Model\Exception\InvalidArgumentException`
- `Contenir\Db\Model\Exception\RuntimeException`

Development
-----------

[](#development)

```
composer install
composer test            # run the unit and integration tests
composer cs-check        # check coding standards
composer cs-fix          # apply coding standards fixes
composer test-coverage   # generate clover.xml coverage report (needs xdebug or pcov)
```

The integration tests live under `test/Integration/` and use an in-memory SQLite database (via `pdo_sqlite`) so they run without external setup. Unit tests live alongside the corresponding `src` directory under `test/`.

License
-------

[](#license)

Released under the [BSD-3-Clause](LICENSE) license.

###  Health Score

43

—

FairBetter than 90% of packages

Maintenance67

Regular maintenance activity

Popularity17

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity64

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~26 days

Recently: every ~2 days

Total

22

Last Release

606d ago

PHP version history (3 changes)v1.0.0PHP ^7.3 || ~8.0.0

v1.0.3PHP ^8.0.0

v1.0.4.1PHP ^8.1.0

### Community

Maintainers

![](https://www.gravatar.com/avatar/f377cb072a13f76c1b06d272c1f21f41903b3a90d9e1c5c6d928a955fe815441?d=identicon)[peptolab](/maintainers/peptolab)

---

Top Contributors

[![simon-mundy](https://avatars.githubusercontent.com/u/46739456?v=4)](https://github.com/simon-mundy "simon-mundy (27 commits)")

---

Tags

laminasmvccontenir

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/contenir-contenir-db-model/health.svg)

```
[![Health](https://phpackages.com/badges/contenir-contenir-db-model/health.svg)](https://phpackages.com/packages/contenir-contenir-db-model)
```

###  Alternatives

[doctrine/doctrine-orm-module

Laminas Module that provides Doctrine ORM functionality

4417.5M296](/packages/doctrine-doctrine-orm-module)[doctrine/doctrine-module

Laminas Module that provides Doctrine basic functionality required for ORM and ODM modules

3968.1M119](/packages/doctrine-doctrine-module)[doctrine/doctrine-mongo-odm-module

Laminas Module which provides Doctrine MongoDB ODM functionality

82685.4k35](/packages/doctrine-doctrine-mongo-odm-module)[doctrine/doctrine-laminas-hydrator

Doctrine hydrators for Laminas applications

373.0M23](/packages/doctrine-doctrine-laminas-hydrator)[samsonasik/error-hero-module

A Hero for your Laminas and Mezzio application to trap php errors &amp; exceptions

5133.6k1](/packages/samsonasik-error-hero-module)[laminas-api-tools/api-tools-doctrine

Laminas API Tools Doctrine module

10666.9k5](/packages/laminas-api-tools-api-tools-doctrine)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
