PHPackages                             tcds-io/orm - 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. tcds-io/orm

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

tcds-io/orm
===========

Simple ORM based which avoid coupling entities to database models

1.0.0(1y ago)05[1 PRs](https://github.com/tcds-io/php-orm/pulls)MITPHPPHP &gt;=8.4CI passing

Since May 13Pushed 1y agoCompare

[ Source](https://github.com/tcds-io/php-orm)[ Packagist](https://packagist.org/packages/tcds-io/orm)[ RSS](/packages/tcds-io-orm/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (6)Versions (3)Used By (0)

PHP ORM for PHP 8.4
===================

[](#php-orm-for-php-84)

[![License: MIT](https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667)](LICENSE)

A lightweight, type-safe Object-Relational Mapper (ORM) for modern PHP (8.4+), designed to be expressive and easy to extend. It emphasizes strict typing and modern PHP features while providing a flexible mapping interface between database rows and PHP objects.

🚀 Installation
--------------

[](#-installation)

Install via [Composer](https://getcomposer.org):

```
composer require tcds-io/orm
```

🧠 Features
----------

[](#-features)

- Typed mapping of database rows to PHP objects
- Support for Records and Entities
- Fine-grained control over columns (e.g. enums, dates, nullable values)
- Lazy-loading support
- Extendable repository base classes
- Injectable mappers
- PHP 8.4+ only, leveraging modern language features

📦 Usage
-------

[](#-usage)

There are two main types of mappers:

- `RecordMapper` — for mapping simple, immutable data records
- `EntityRecordMapper` — for mapping richer domain entities, with support for repositories or lazy-loading

These mappers are designed to be injected wherever data transformation is needed, such as in services, repositories, or controllers.

### Record Mapper Example

[](#record-mapper-example)

```
/**
 * @extends RecordMapper
 */
final class AddressMapper extends RecordMapper
{
    private IntegerColumn $id;
    private StringColumn $street;
    private FloatColumn $number;
    private IntegerColumn $floor;
    private BoolColumn $active;
    private EnumColumn $type;
    private DateTimeColumn $createdAt;
    private DateTimeImmutableColumn $deletedAt;

    public function __construct()
    {
        $this->id = $this->integer('id', fn(Address $entry) => $entry->id);
        $this->street = $this->string('street', fn(Address $entity) => $entity->street);
        $this->number = $this->numeric('number', fn(Address $entity) => $entity->number);
        $this->floor = $this->integer('floor', fn(Address $entity) => $entity->floor);
        $this->active = $this->boolean('active', fn(Address $entity) => $entity->active);
        $this->type = $this->enum(AddressType::class, 'type', fn(Address $entity) => $entity->type);
        $this->createdAt = $this->datetime('created_at', fn(Address $entity) => $entity->createdAt);
        $this->deletedAt = $this->datetimeImmutable('deleted_at', fn(Address $entity) => $entity->deletedAt);
    }

    public function map(array $row): Address
    {
        return new Address(
            id: $this->id->value($row),
            street: $this->street->value($row),
            number: $this->number->value($row),
            floor: $this->floor->value($row),
            active: $this->active->value($row),
            type: $this->type->value($row),
            createdAt: $this->createdAt->value($row),
            deletedAt: $this->deletedAt->nullable($row),
        );
    }
}
```

### Entity Mapper Example

[](#entity-mapper-example)

```
/**
 * @extends EntityRecordMapper
 */
final class UserMapper extends EntityRecordMapper
{
    /** @var LazyBuffer */
    private LazyBuffer $addressLoader;

    public function __construct(
        private readonly AddressRepository $addressRepository,
    ) {
        parent::__construct($this->string('id', fn(User $entity) => $entity->id));

        $this->string('name', fn(User $entity) => $entity->name);
        $this->date('date_of_birth', fn(User $entity) => $entity->dateOfBirth);
        $this->integer('address_id', fn(User $entity) => $entity->address->id);

        $this->addressLoader = lazyBufferOf(Address::class, function (array $ids) {
            return listOf($this->addressRepository->loadAllByIds($ids))
                ->indexedBy(fn(Address $address) => $address->id)
                ->entries();
        });
    }

    #[Override] public function map(array $row): User
    {
        return new User(
            id: $row['id'],
            name: $row['name'],
            dateOfBirth: new DateTime($row['date_of_birth']),
            address: $this->addressLoader->lazyOf($row['address_id']),
        );
    }
}
```

### Nullable Support

[](#nullable-support)

For nullable fields, use the `->nullable(...)` method on column definitions. This allows you to gracefully handle `NULL`values in your database rows.

### Foreign keys and objects

[](#foreign-keys-and-objects)

The ORM does not resolve foreign keys and objects automatically. Instead, you must inject the object repository and load the object as needed:

```
return new User(
    ...,
    /** lazy load foreign object */
    address: lazyOf(Address::class, fn() => $addressRepository->loadById($row['address_id'])),
    /** eager load foreign object */
    address: $addressRepository->loadById($row['address_id']),
);
```

### Lazy loading

[](#lazy-loading)

Records can be lazy-loaded with the `lazyOf` function, which receives an initializer and loads the entry only when any of its properties are accessed:

```
/** lazy object */
$address = lazyOf(
    /** The class to be loaded */
    Address::class,
    /** The object initializer */
    fn() => $addressRepository->loadById($addressId),
);

/** loaded object */
$street = $address->street;
```

### Solving N+1 problems

[](#solving-n1-problems)

N+1 can be solved with the `lazyBufferOf` function, which manages buffered and loaded records. All buffered records are loaded at once when any of the entries are accessed, and all previously loaded records are returned immediately without additional loader calls.

```
$addressLoader = lazyBufferOf(
    /** The class to be loaded */
    Address::class,
    /** The object list loader */
    function (array $bufferedIds) {
        listOf($this->addressRepository->loadAllByIds($bufferedIds))
          ->indexedBy(fn(Address $address) => $address->id)
          ->entries();
    },
);

/** lazy object */
$address = $addressLoader->lazyOf($addressId);

/** loaded object */
$street = $address->street;
```

🗃️ Repositories
---------------

[](#️-repositories)

This library also provides base repository classes that you can extend to perform actual database operations.

### Record Repository Example

[](#record-repository-example)

```
/**
 * @extends RecordRepository
 */
class AddressRepository extends RecordRepository
{
    public function __construct(Connection $connection, RecordMapper $mapper)
    {
        parent::__construct($mapper, $connection, 'addresses');
    }

    public function loadById(int $id): Address
    {
        return $this->selectOneWhere(['id' => $id]) ?? throw new Exception('Address not found');
    }
}
```

### Entity Repository Example

[](#entity-repository-example)

```
/**
 * @extends EntityRecordRepository
 */
class UserRepository extends EntityRecordRepository
{
    public function __construct(Connection $connection, UserMapper $mapper)
    {
        parent::__construct($mapper, $connection, 'users');
    }

    public function loadById(string $id): User
    {
        return $this->selectEntityById($id) ?? throw new Exception('User not found');
    }
}
```

### 🔧 Repository Capabilities

[](#-repository-capabilities)

📘 `RecordRepository` Provides core operations for working with raw records:

```
insertOne($entry)
selectOneWhere(where: ['id' => 10])
selectOneByQuery(selectQuery: 'SELECT * FROM table where id = :id', bindings: ['id' => 10])
selectManyWhere([where: 'deleted_at' => null], limit: 10, offset: 100)
selectManyByQuery(selectQuery: 'SELECT * FROM table where deleted_at is null', bindings: [])
existsWhere(where: ['id' => 10])
deleteWhere(where: ['id' => 10])
updateWhere(values: ['name' => 'Arthur Dent', 'date_of_birth' => '1990-01-01'], where: ['id' => 10])
```

📙 `EntityRecordRepository` extends RecordRepository with additional features for managing entity lifecycles:

```
selectEntityById(id: 10)
updateOne(entity: $user)
updateMany($user1, $user2, $user3, ...)
deleteOne(entity: $user)
deleteMany($user1, $user2, $user3, ...)
```

🤝 Contributing
--------------

[](#-contributing)

Contributions are welcome! If you have ideas, find a bug, or want to improve the library, feel free to:

- Fork the repo
- Create a new branch
- Submit a pull request

Please follow PSR-12 coding standards and ensure tests pass before submitting changes.

🚀 Next steps
------------

[](#-next-steps)

- Query builder
- Extend where comparisons

📄 License
---------

[](#-license)

This project is open-sourced under the [MIT license](LICENSE).

---

Happy Mapping! 🎉

###  Health Score

31

—

LowBetter than 68% of packages

Maintenance50

Moderate activity, may be stable

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 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

Unknown

Total

1

Last Release

368d ago

### Community

Maintainers

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

---

Top Contributors

[![thiagocordeiro](https://avatars.githubusercontent.com/u/1073649?v=4)](https://github.com/thiagocordeiro "thiagocordeiro (15 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/tcds-io-orm/health.svg)

```
[![Health](https://phpackages.com/badges/tcds-io-orm/health.svg)](https://phpackages.com/packages/tcds-io-orm)
```

###  Alternatives

[doctrine/orm

Object-Relational-Mapper for PHP

10.2k285.3M6.2k](/packages/doctrine-orm)[jdorn/sql-formatter

a PHP SQL highlighting library

3.9k115.1M102](/packages/jdorn-sql-formatter)[illuminate/database

The Illuminate Database package.

2.8k52.4M9.4k](/packages/illuminate-database)[mongodb/mongodb

MongoDB driver library

1.6k64.0M546](/packages/mongodb-mongodb)[ramsey/uuid-doctrine

Use ramsey/uuid as a Doctrine field type.

90340.3M211](/packages/ramsey-uuid-doctrine)[reliese/laravel

Reliese Components for Laravel Framework code generation.

1.7k3.4M16](/packages/reliese-laravel)

PHPackages © 2026

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