Atlas.Orm 2.0 Is Now Stable

I am very happy to announce that Atlas, a data-mapper for your persistence layer in PHP, is now stable for production use! There are no changes, other than documentation updates, since the beta release two weeks ago.

You can get Atlas from Packagist via Composer by adding …

    "require": {
        "atlas/orm": "~2.0"
    }

… to your composer.json file.

The updated documentation site is at atlasphp.io (with both 1.x and 2.x documentation).

Submit issues and pull requests as you see fit!

Atlas.Orm 2.0.0-beta1 Released

I am very happy to announce that I have just released 2.0.0-beta1 of Atlas, a data mapper for your SQL persistence model. You can get it via Composer as atlas/orm. This new major version requires PHP 7.1 and uses typehints, but best of all, it is backwards compatible with existing 1.x generated Atlas classes!

I.

The original motivation for starting the 2.x series was an edge-case bug. Atlas lets you open and manage transactions across multiple connections simultaneously; different table classes can point to different database connections. Atlas also lets you save an entire Record and all of its related fields with a single call to the persist() Mapper or Transaction method.

However, in 1.x, executing a Transaction::persist() call does not work properly when the relationships are mapped across connections that are not the same as the main Record. This is because the Transaction begins on the main Record connection, but does not have access to the connections for related records, and so cannot track them.

Admittedly, this is an uncommon operation, but ought to be supported properly. The only way to fix it was to break backwards compatibility on the Table and Transaction classes, both on their constructors and on their internal operations, by introducing a new ConnectionManager for them to use for connection and transaction management.

II.

As long as backwards compatibility breaks were going to happen anyway, that created the opportunity to upgrade the package to use PHP 7.1 and typehints. But even with that in mind …

  • You do not need to modify or regenerate any classes generated from 1.x (although if you have overridden class methods in custom classes, you may need to modify that code to add typehints).

  • Atlas 2.x continues to use Aura.Sql and Aura.SqlQuery 2.x, so you do not need to change any queries from Atlas 1.x.

  • You do not need to change any calls to AtlasContainer for setup.

So, the majority of existing code using Atlas 1.x should not have to change at all after upgrading to 2.x.

III.

There are some minor but breaking changes to the return values of some Atlas calls; these are a result of using typehints, especially nullable types.

First, the following methods now return null (instead of false) when they fail:

  • Atlas::fetchRecord()
  • Atlas::fetchRecordBy()
  • Mapper::fetchRecord()
  • Mapper::fetchRecordBy()
  • MapperSelect::fetchRecord()
  • RecordSet::getOneBy()
  • RecordSet::removeOneBy()
  • Table::fetchRow()
  • Table::updateRowPerform()
  • TableSelect::fetchOne()
  • TableSelect::fetchRow()

Checking for loosely false-ish return values will continue to work in 2.x as it did in 1.x, but if you were checking strictly for false you now need to check strictly for null – or switch to loose checking. (Note that values for a missing related record field are still false, not null. That is, a related field value of null still indicates “there was no attempt to fetch a related record,” while false still indicates “there was an attempt to fetch a related record, but it did not exist.”)

Second, the following methods will now always return a RecordSet, even when no records are found. (Previously, they would return an empty array when no records were found.)

  • Atlas::fetchRecordSet()
  • Atlas::fetchRecordSetBy()
  • Mapper::fetchRecordSet()
  • Mapper::fetchRecordSetBy()
  • MapperSelect::fetchRecordSet()

Whereas previously you would check for an empty array or loosely false-ish value as the return value, you should now call isEmpty() on the returned RecordSet.

That is the extent of user-facing backwards compatibility breaks.

IV.

There is one major new piece in 2.x: the table-level ConnectionManager. It is in charge of transaction management for table operations, and allows easy setting of read and write connections to be used for specific tables if desired.

It also allows for on-the-fly replacement of “read” connections with “write” connections. You can specify that this should…

  • never happen (the default)
  • always happen (which is useful in GET-after-POST situations when latency is high for master-slave replication), or
  • happen only when a table has started writing back to the database (which is useful for synchronizing reads with writes while in a transaction)

Other than that, you should not have to deal with the ConnectionManager directly, but it’s good to know what’s going on under the hood.

V.

Although this is a major release, the user-facing backwards-compatibility changes are very limited, so it should be an easy migration from 1.x. There is very little new functionality, so I’m releasing this as a beta instead of an alpha. I have yet to update the official documentation site, but the in-package docs are fully up-to-date.

I can think of no reason to expect significant API changes between now and a stable release, which should arrive in the next couple of weeks if there are no substantial bug reports.

Thanks for reading, and I hope that Atlas can help you out on your next project!

Atlas ORM 1.2.0 Released

The 1.2.0 release adds the ability to define WHERE conditions on relationships. (The 1.1.0 release added functionality to ignore foreign key string case when wiring up objects in memory, and 1.0.0 was released pretty quietly a couple of weeks ago.)

Try it out today, because you like keeping your persistence layer separate from your domain layer.

Now, read on for some history, if you care about that kind of thing.


Many years ago, we on the Solar project developed Solar_Sql_Model, an Active Record type of ORM. Overall I liked it well enough, though (as with anything) it had its strengths and weaknesses.

Since then, after extracting the Solar components to Aura libraries, I’ve mostly lived without ORMs. The majority of my legacy consulting work has not made use of them; where a legacy project did have an ORM of some sort, it was a custom in-house piece of work.

However, about three years ago, I hired on with a startup to build out their backend from scratch. At the time, I wanted to do “real” Domain-Driven Design, with entities and aggregates and value objects and everything else. That meant keeping the domain system separate from the persistence system, and that in turn meant Active Record was not an option. Doctrine, a domain model data mapper, was the next logical choice, but on review it was not to my liking. (The annotations, among other things about Doctrine, just rubbed me the wrong way.)

So instead of an Active Record implementation, or Doctrine, I figured that it would be enough to use a Table Data Gateway on top of Aura.Sql and Aura.SqlQuery to retrieve rows, then map the rows over to domain objects. This was fine, for as far as it went, but the problem was “relationships.” Oh dear Codd, the relationships.

Selecting one or many objects from a single table was no big deal, but doing multiple selections from multiple tables and building up the selection statements for those relationships was a tedious, tiresome, and time-consuming burden. (Wiring them up in memory once they’d been selected was not too bad, given Aura.Marshal, but it was still more effort than I’d’ve rather been expending.)

So it all “worked,” but it was just not satisfying at all. The DDD portions, for their part, worked out great, as did the separation of the domain layer from the persistence layer, so I was pretty sure I was on something like the right track.

Then I read this article from Mehdi Khalili. It’s fantastic and you should read the whole thing. In summary, Khalili points out that it is perfectly reasonable to use a persistence model in addition to a domain model. That is, you do not necessarily have to map directly from the database to domain objects; you can map from the database to persistence objects for the data, and then later compose or map the persistence model into the domain model for behaviors.

This was a revelation to me, something that I’d never considered, or even heard of before. It alleviates wide swaths of DDD-related burdens.

As a result of my relationship-related burdens at the startup, and after reading the Khalili article, I put together Atlas, a mapper for your persistence model. Like everything else I’ve been doing for the past several years, Atlas is built in discrete layers:

  • PDO at the very bottom;
  • Aura.Sql around that, to provide convenience methods for binding and fetching;
  • Aura.SqlQuery in parallel, to support query building;
  • all of those composed into a table data gateway system to emit Row objects from tables;
  • and finally a mapper system on top of that to emit Record objects composed of table Rows and their relationships

As such, each Record object is composed of a Row (from the “main” table) and its Related objects (themselves Records, each of which is composed of a Row and Relateds, and so on).

Atlas uses the term “Record” to indicate that the object is not a domain entity or aggregate. You can use Records directly for straightforward CRUD/BREAD operations, or you can map them over to your domain objects.

Fetching deep relationship structures is no big deal; see this article from Andrew Shell using 25 tables in different complex relationships. (Andrew’s project also shows how to keep the persistence and domain layers separate, and incorporates a debug bar implementation for Atlas.)

So, if you want a data mapper implementation that models your persistence layer, and the relationships therein, Atlas is just the thing for you. Try it out today!

Atlas: a persistence-model data mapper

Atlas is a data mapper implementation for your persistence model (not your domain model).

As such, Atlas uses the term “record” to indicate that its objects are not domain entities. Note that an Atlas record is a passive record, not an active record; it is disconnected from the database. Use Atlas records indirectly to populate your domain entities, or directly for simple data source interactions.

No migrations. No annotations. No lazy loading. No domain models. No data-type abstractions. No behaviors. No opinions. Just data source mapping.

Read more at the Github repository.