PHPackages                             matthiasnoback/talis-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. matthiasnoback/talis-orm

ActiveLibrary

matthiasnoback/talis-orm
========================

v0.2.1(5y ago)1671.1k↓50%10[7 issues](https://github.com/matthiasnoback/TalisORM/issues)MITPHPPHP &gt;=7.2CI passing

Since Oct 18Pushed 3mo ago19 watchersCompare

[ Source](https://github.com/matthiasnoback/TalisORM)[ Packagist](https://packagist.org/packages/matthiasnoback/talis-orm)[ RSS](/packages/matthiasnoback-talis-orm/feed)WikiDiscussions master Synced 2mo ago

READMEChangelog (2)Dependencies (5)Versions (10)Used By (0)

[![Build Status](https://camo.githubusercontent.com/aff1f67d35f534f8e237b396df873f490f1ebdae4acff7cd225105ae57e5053f/68747470733a2f2f7472617669732d63692e6f72672f6d617474686961736e6f6261636b2f54616c69734f524d2e7376673f6272616e63683d6d6173746572)](https://travis-ci.org/matthiasnoback/TalisORM) [![Code Coverage](https://camo.githubusercontent.com/f642fcb59e79cee4709d0f78a293062e91df47958b2dd292faf01e4ae9be088e/68747470733a2f2f7363727574696e697a65722d63692e636f6d2f672f6d617474686961736e6f6261636b2f54616c69734f524d2f6261646765732f636f7665726167652e706e673f623d6d6173746572)](https://scrutinizer-ci.com/g/matthiasnoback/TalisORM/?branch=master)

About TalisORM
==============

[](#about-talisorm)

A good design starts with some limitations. You can start simple and keep building until you have a large ORM like Doctrine. Or you can choose not to support a mapping configuration, table inheritance, combined write/read models, navigable object graphs, lazy-loading, etc. That's what I'm looking for with TalisOrm. The rules are:

- You model a persistable domain object as an *Aggregate*: one (root) *Entity*, and optionally some *Child entities*.
- The child entities themselves have no children.
- You use the ORM for your *write model* only. That is, you don't need to fetch hundreds of these aggregates to show them to the user.
- Your aggregate internally records domain events, which will automatically be released and dispatched after saving changes to the aggregate.

Furthermore:

- You're going to write your own mapping code, which converts your values or *Value objects* to and from column values.

I explain more about the motivation for doing this in ["ORMless; a Memento-like pattern for object persistence"](https://matthiasnoback.nl/2018/03/ormless-a-memento-like-pattern-for-object-persistence).

You can find some examples of how to use this library in [test/TalisOrm/AggregateRepositoryTest/](test/TalisOrm/AggregateRepositoryTest/).

Recording and dispatching domain events
---------------------------------------

[](#recording-and-dispatching-domain-events)

A domain event is a simple object indicating that something has happened inside an aggregate (usually this just means that something has changed). You can use the [`EventRecordingCapabilities`](src/TalisOrm/DomainEvents/EventRecordingCapabilities.php) trait to save yourself from rewriting a couple of simple lines over and over again.

Immediately after saving an aggregate, the [`AggregateRepository`](src/TalisOrm/AggregateRepository.php) will call the aggregate's `releaseEvents()` method, which returns previously recorded domain events. It dispatches these events to an object that implements [`EventDispatcher`](src/TalisOrm/DomainEvents/EventDispatcher.php). As a user of this library you have to provide your own implementation of this interface, which is very simple. Maybe you just want to forward the call to your favorite event dispatcher, or the one that ships with your framework.

Managing the database schema
----------------------------

[](#managing-the-database-schema)

Aggregates can implement `SpecifiesSchema` and, yes, specify their own schema. This can be useful if you want to use a tool to synchronize your current database schema with the schema that your aggregates expect, e.g. the Doctrine DBAL's own `SingleDatabaseSynchronizer`:

```
use Doctrine\DBAL\Schema\Synchronizer\SingleDatabaseSynchronizer;
use TalisOrm\Schema\AggregateSchemaProvider;

// set up or reuse a Doctrine\DBAL\Connection instance
$connection = ...;

$schemaProvider = new AggregateSchemaProvider($connection, [
    // list all the aggregate class names of your application, e.g.
    User::class,
    Order::class
]);
$synchronizer = new SingleDatabaseSynchronizer($connection);
$synchronizer->createSchema($schemaProvider->createSchema());
```

You could also use [Doctrine Migrations](https://github.com/doctrine/migrations/) to automatically generate migrations based on schema changes. It may need a bit of setup, but once you have it working, you'll notice that this tool needs a `SchemaProviderInterface` instance (note: this interface is only available in recent versions of `doctrine/migrations`, which requires PHP 7). You can easily set up an adapter for `AggregateSchemaProvider`. For example:

```
final class AggregateMigrationsSchemaProvider implements SchemaProviderInterface
{
    /**
     * @var AggregateSchemaProvider
     */
    private $aggregateSchemaProvider;

    public function __construct(AggregateSchemaProvider $aggregateSchemaProvider)
    {
        $this->aggregateSchemaProvider = $aggregateSchemaProvider;
    }

    public function createSchema(): Schema
    {
        return $this->aggregateSchemaProvider->createSchema();
    }
}
```

Protecting against concurrent updates
-------------------------------------

[](#protecting-against-concurrent-updates)

Traditionally, we PHP developers aren't used to protect our aggregates against concurrent updates. Concurrent updates after all are a matter of chance. Maybe there aren't that many users who are working on the same aggregate in your project. But if you're worried that it might happen, there's an easy solution built-in to TalisORM: optimistic concurrency locking.

You need to take the following steps to make it work:

Make sure the table definition for your aggregate has an `Aggregate::VERSION_COLUMN` column, and that your `fromState()` and `state()` methods are aware of it. For example:

```
final class Order implements Aggregate, SpecifiesSchema
{
    /**
     * @var int
     */
    private $aggregateVersion;

    public function state(): array
    {
        // N.B. It's important to increment the version manually every time state() gets called!
        $this->aggregateVersion++;

        return [
            // ...
            Aggregate::VERSION_COLUMN => $this->aggregateVersion
        ];
    }

    public static function fromState(array $aggregateState, array $childEntityStatesByType): Aggregate
    {
        $order = new self();

        // ...

        $order->aggregateVersion = $aggregateState[Aggregate::VERSION_COLUMN];

        return $order;
    }

    /**
     * Only if your aggregate implements SpecifiesSchema:
     */
    public static function specifySchema(Schema $schema): void
    {
        $table = $schema->createTable('orders');

        // ...

        $table->addColumn(Aggregate::VERSION_COLUMN, 'integer');
    }
}
```

The above setup will protect your aggregate against concurrent updates between retrieving the aggregate from the database and saving it again. However, you may want to warn a user who's working with the aggregate's data in the user interface that once they store the object, someone else has modified it. To do this, you need to remember the version of the aggregate the user is looking at in the user's session. An outline of this solution:

```
final class Order implements Aggregate, SpecifiesSchema
{
    // ...

    public function setAggregateVersion(int $version): void
    {
        $this->aggregateVersion = $version;
    }

    public function aggregateVersion(): int
    {
        return $this->aggregateVersion;
    }
}

/*
 * Inside the controller which (for instance) renders a form, allowing the
 * user to modify some aspect of the aggregate:
 */
$order = $repository->getById($orderId);
$session->set('aggregate_version', $order->aggregateVersion());
// show form

/*
 * Inside the controller which modifies the aggregate based on the data the
 * user provided:
 */
$order = $repository->getById($orderId);
$order->setAggregateVersion($session->get('aggregate_version');

$order->makeSomeChange();

// This will compare the provided version to the version in the database:
$repository->save($order);
```

###  Health Score

42

—

FairBetter than 90% of packages

Maintenance53

Moderate activity, may be stable

Popularity32

Limited adoption so far

Community23

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 85.7% 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 ~342 days

Total

3

Last Release

1974d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1193078?v=4)[Matthias Noback](/maintainers/matthiasnoback)[@matthiasnoback](https://github.com/matthiasnoback)

---

Top Contributors

[![matthiasnoback](https://avatars.githubusercontent.com/u/1193078?v=4)](https://github.com/matthiasnoback "matthiasnoback (66 commits)")[![fransik](https://avatars.githubusercontent.com/u/1881022?v=4)](https://github.com/fransik "fransik (4 commits)")[![kix](https://avatars.githubusercontent.com/u/345754?v=4)](https://github.com/kix "kix (1 commits)")[![rpkamp](https://avatars.githubusercontent.com/u/1059790?v=4)](https://github.com/rpkamp "rpkamp (1 commits)")[![ruudk](https://avatars.githubusercontent.com/u/104180?v=4)](https://github.com/ruudk "ruudk (1 commits)")[![viniciusss](https://avatars.githubusercontent.com/u/1016025?v=4)](https://github.com/viniciusss "viniciusss (1 commits)")[![arturpanteleev](https://avatars.githubusercontent.com/u/7831658?v=4)](https://github.com/arturpanteleev "arturpanteleev (1 commits)")[![yourwebmaker](https://avatars.githubusercontent.com/u/812971?v=4)](https://github.com/yourwebmaker "yourwebmaker (1 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/matthiasnoback-talis-orm/health.svg)

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

###  Alternatives

[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.4k5.6M648](/packages/sylius-sylius)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.3M152](/packages/sulu-sulu)[veewee/xml

XML without worries

1835.9M29](/packages/veewee-xml)[oat-sa/tao-core

TAO core extension

66136.7k74](/packages/oat-sa-tao-core)[php-soap/wsdl-reader

A WSDL reader in PHP

212.3M9](/packages/php-soap-wsdl-reader)[phpro/http-tools

HTTP tools for developing more consistent HTTP implementations.

28137.8k](/packages/phpro-http-tools)

PHPackages © 2026

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