Atlas.Query: Simple. Sensible. SQL.

I am happy to announce that Atlas.Query is now stable and ready for production
use! Installaton is as easy as composer require atlas/query ~1.0.

With Atlas.Query and any PDO instance, you can build and execute your queries in a single fluent series of method calls:

use Atlas\Query\Select;

$rows = Select::new($pdo)
    ->columns('*')
    ->from('posts')
    ->where('id IN ', $ids)
    ->fetchAll();

foreach ($rows as $row) {
    // ...
}

If you prefer, you can exercise fine control over your PDO connection, use a query factory, or build your queries in smaller steps:

use Atlas\Pdo\Connection;
use Atlas\Query\QueryFactory;

$connection = Connection::new(
    'mysql:host=localhost;dbname=testdb',
    'username',
    'password'
);

$queryFactory = new QueryFactory();

$select = $queryFactory->newSelect($connection);
$select->columns('*');
$select->from('posts');
$select->where('id = ', $id);

$row = $select->fetchOne();

Atlas.Query provides the full power of SQL at your fingertips …

$select
    ->columns(...)
    ->from(...)
    ->join(...)
    ->where(...)
    ->groupBy(...)
    ->having(...)
    ->orderBy(...)
    ->limit(...)
    ->offset(...);

… along with UNIONs, paging, sub-selects, inline value binding, and all sorts of fetch and yield styles.

Atlas.Query comes with INSERT, UPDATE, and DELETE builders as well:

use Atlas\Query\Insert;

$insert = Insert::new($pdo);

// insert a row ...
$insert->into('posts')
    ->columns([
        'title' => $title,
        'body' => $body,
    ])
    ->raw('created_at', 'NOW()')
    ->perform();

// ... and get back the autoincrement value:
$post_id = $insert->getLastInsertId();

Do you work on different project with different datbase backends? Atlas.Query lets you use the same interface for them all, while not restricting you to a common subset of functionality. MySQL, PostgreSQL, SQLite, and SQL Server are all supported explicitly.

And if you discover you need more than just a query system, you’ll have a clear refactoring path towards Atlas.Orm. If you are looking for a modern, stable, easy-to-use query system, try Atlas.Query in your project!


You can read the Reddit commentary on this post here.

Atlas 3.x (“Cassini”) and PHPStorm Completion

I’m proud to announce the release of Atlas.Orm 3.0.0-beta1, along with releases of the supporting Mapper, Table, Query, Cli, and Pdo packages. (Atlas is a data-mapper for your persistence model, not your domain model, in PHP.)

The goal for this release round was “better IDE return typehinting support” and I am happy to say that it has been a great success, though it did take some substantial renaming of classes. Unfortunately, this results in a big break from the prior alpha release; if you already have alpha-based data source skeleton classes, you will need to regenerate them with the new class names. Barring the unforeseen, this marks the first, last, and only time that regeneration will be necessary.

I.

I am not a heavy user of IDEs; I lean more toward text editors like Sublime. However, I work with people who use the PHPStorm IDE extensively, and I have come to appreciate some of its features.

One of those features is code autocompletion. For example, if you type $foo = new Foo(), and then refer to $foo later, the IDE will pop up a list of methods and properties on that object.

This is very convenient, except that the IDE has to know what class is being referenced, in order to figure out what the hinting should be. If you are using a non- or loosely-return-typed factory/locator/container, which is what everything in PHP land does, the IDE cannot know by default how to map the requested name to an actual class. In this example …

class Factory
{
    public static function new($class)
    {
        return new $class();
    }
}

$foo = Factory::new(Foo::CLASS);

… the IDE has no idea what the return from the new() method is, or ought to be, so it cannot give you autocompletion hints on $foo.

That idiom is exactly what Atlas MapperLocator::get() and TableLocator::get() use: the get() param is a class name, and the locator then retains and returns an instance of that class. Likewise, the overarching Atlas class methods all take a mapper class name as their first param, which Atlas uses to figure out which mapper to use for that method call.

You can typehint those methods to abstract classes or interfaces, but then the IDE will not recognize any custom extensions or overrides on the returned concrete classes. What is needed is a way to determine the return type from the get() param, rather than from the method’s return typehint.

II.

Lucky for us, the PHPStorm IDE allows for a .phpstorm.meta.php file (or a collection of files under a .phpstorm.meta.php/ directory) where you can map the factory method inputs to their return types. (See here for the documentation.)

On working with it, though, I found it to be a little inflexible. I expected to be able to map a string directly to a literal class name, but that didn’t really work. The IDE never seemed to pick up the return typehinting. Further, with class names the way they are in the 1.x, 2.x, and 3.x-alpha releases, I would need to add a series of param-to-type mappings for every single data source class (i.e., about 10 entries for each data source type). I imagined that would become cumbersome.

To adapt to this, I decided to modify the Atlas class naming with PHPStorm’s ‘@’ metadata token in mind. The ‘@’ token, per the above documentation, gets replaced with the factory method parameter value, making it perfect as a class name prefix. However, you can’t do any string manipulations on it; you cannot, say, call substr('@', 0, -6) . 'Record' and give it “FooMapper” to get back “FooRecord”. It would have to be ‘@Record’ or nothing.

This leads to the biggest change in Atlas since its inception: the data source mapper classes are no longer suffixed with “Mapper”. Previously, if you had a foo table, you would expect a mapper class name like App\DataSource\Foo\FooMapper. With this naming change, the mapper class name is now App\DataSource\Foo\Foo. That makes the data source mapper class name a good base as a prefix for all the other data source type classes, which in turn means the ‘@’ token can be used without need for string manipulation, and it works with only a very few lines of PHPStorm metadata code.

You can see the resulting .phpstorm.meta.php resource bundled with Atlas 3.x here. Put that at the root of your project, and PHPStorm will now successfully typehint all the Atlas method returns. (You might have to restart PHPStorm for it to take full effect.)

III.

There was still another category of problems, though. While the overarching Atlas class now had IDE completion, the underlying Mapper and Table packages were missing some hints on their classes and methods.

For example, the base Mapper::fetchRecord() method is typehinted to return a Record, but the child FooMapper::fetchRecord() actually returns a FooRecord. The properties on the FooRecord are specific to the underlying FooRow from the table and any FooRelationships on the mapper, and that needs to be indicated by the IDE.

Likewise, the base Mapper::select() class is typehinted to return the base MapperSelect class; in turn, the MapperSelect::fetchRecord() method is typehinted to return a Record. But a $fooMapper->select()->fetchRecord() call actually returns a FooRecord. Those too need to be indicated by the IDE.

I ended up solving this in two steps:

  1. Each data source type now gets its own type-specific select class; whereas FooMapper::select() used to return a general-purpose Select, it now returns a FooSelect instance extended from the general-purpose select. This type-specific select class is generated by Atlas.Cli tooling.

  2. On the type-specific mapper and select classes, the Atlas.Cli tooling now generates a docblock of @method declarations with overriding type-specific returns. (You can see an example of this in the Atlas.Testing package Author and AuthorSelect classes.) PHPStorm picks up those docblocks and uses them to provide return typehints when those classes are used.

With those two additions to the Atlas.Cli skeleton generator, autocompletion in the PHPStorm IDE appears to be complete.

IV.

Of course, “there are no solutions, there are only tradeoffs.” This situation is no different. Yes, Atlas now has a relatively straightforward IDE completion approach, but at these costs:

  • If IDE completion is not important to you, these very significant changes from the alpha version to the beta version will feel useless or even counter-productive.

  • There are two more classes per data-source type: one type-specific MapperSelect, and one type-specific TableSelect (to typehint the Row returns).

  • Not having a “Mapper” suffix on the actual data-source mapper class may feel wrong or counter-intuitive.

For some, these tradeoffs will not be worth the effort to transition from the alpha release to the beta. However, after working with the beta for a while, I think they end up being a net benefit overall.

Atlas.ORM “Cassini” (v3) Early-Access Alpha Release

For those of you who don’t know, Atlas is an ORM for your persistence model, not your domain model. Atlas 1 “Albers” (for PHP 5.6) was released in April 2017. Atlas 2 “Boggs” (for PHP 7.1) came out in October 2017.

And now, in April 2018, we have an early-access release of Atlas 3 “Cassini”, the product of several lessons from a couple of years of use.

Architecture and Composition

One big architectural change is that Atlas 3 no longer uses the older Aura packages for SQL connections and queries; instead, Atlas has adopted these packages as its own, and upgraded them for PHP 7.1 using strict typing and nullable returns. Another big architectural change is that the table data gateway and data mapper implementations are now available as packages in their own right.

The end result is a mini-framework built from a stack of packages, where any “lower” package can be used indepdendently of the the ones “above” it. The package hierarchy, from bottom to top, looks like this:

  • Atlas.Pdo: Descended from Aura.Sql, this provides a database Connection object and a ConnectionLocator. If all you need is convenience wrapper around PDO with fetch and yield methods, this is the package for you.

  • Atlas.Query: Descended from Aura.SqlQuery, this is a query builder that wraps an Atlas.Pdo Connection. If you just want to build and perform SQL queries using an object-oriented approach, the Atlas query objects can handle that.

  • Atlas.Table: Extracted from Atlas 2, this is a table data gateway implementation that uses Atlas.Query under the hood. If you don’t need a full data mapper system and only want to interact with individual tables and their row objects, this will do the trick.

  • Atlas.Mapper: Also extracted from Atlas 2, this is a data mapper implementation that models the relationships between tables. It allows you to build Record and RecordSet objects whose Row objects map naturally back to Table objects; you can write them back to the database one by one, or persist an entire Record graph back to the database in one go.

  • Atlas.Orm: Finally, at the top of the hierarchy, the overarching ORM package provides a convenience wrapper around the Mapper system, and provides several strategies for transaction management.

There are also two “side” packages:

  • Atlas.Info: Descended from Aura.SqlSchema, this inspects the database to get information about tables, columns, and sequences.

  • Atlas.Cli: This command-line interface package uses Atlas.Info to examine the database, then create skeleton classes for your persistence model from the database tables.

Separation, Completion, and Hierarchy

One goal for Atlas 3 was to split up the Table and Mapper subsystems into their own packages, so they could be used in place of the full transactional ORM if needed. Along with that, I wanted better IDE autocompletion, especially at the query-building level, particularly in support of different SQL dialects (e.g., PostgreSQL has a RETURNING clause, but nothing else does).

The first idea long these lines was to have parallel packages, one for each SQL driver: Atlas.Query-Mysql, Atlas.Query-Pgsql, Atlas.Query-Sqlite, etc. However, I shortly realized that typehinting at higher levels would have been a problem. If a generic Table class returns a TableSelect that extends a generic Select object, then providing a driver-speific MysqlSelect had to return a MysqlTableSelect for a MysqlTable. That in turn would have meant parallel packages for each driver all the way up the hierarchy: Table, Mapper, and Orm. Generics at the language level might have solved this, but PHP doesn’t have them, so that idea was out.

Then the idea was to have a single “master” ORM package with all the subsytems included in it as virtual packages, and template methods where driver-specific implementations could be filled in. However, that ended up being an all-or-nothing approach, where the “lower” level packages could not be used independently of the “higher” level ones.

I could think of only one other alternative that would enable IDE autocompletion for driver-specific functionality, and keep the packages well-separated. That was to make Atlas.Query itself more general-purpose. As a result, the returning() method is available, even if the particular backend database will not recognize it. I’m not especially happy about this, since I’d rather the classes expose only driver-specific functionality, but the tradeoff is that you get full IDE completion. (I rationalize this by saying that the query objects are not there to prevent you from writing incorrect SQL, just to let you write SQL with object methods; you could just as well send a RETURNING clause to MySQL by putting it in a query string. Again, generics at the PHP level would help here.)

Further, on the query objects, I found myself wanting to perform the query represented by the object directly from that object, rather than manually passing it through PDO each time. As such, the query objects now take a PDO instance (which gets decorated by an Atlas.Pdo Connection automatically) so you can do things like this:

$result = Select::new($pdo)
    ->columns('foo', 'bar')
    ->from('table_name')
    ->where('baz = ', $baz)
    ->fetchAll();

Gateways, Mappers, and Relationships

With that in place, the next step was to extract the table data gateway subsystem to its own separate package. The new Atlas.Table library is not too different from the Atlas 2 version. The biggest single change is that the identity map functionality has been moved “up” one layer to the Mapper system. This keeps the package more in line with expectations for table data gateway implementations.

That, in turn, led to extracting the data mapper subsystem to its own package as well. Atlas.Mapper is now in charge of identity mapping the Rows that serve as the core for each Record, but the transaction management work has been moved up one layer to the overarching ORM package.

Of particular note, the new Atlas.Mapper package does away with the idea of a
“many-to-many” relationship as a first-class element. It turned out that managing many-to-many relateds was both counterintuitive and counterproductive in subtle but significant ways, not least of which was having to keep track of new or deleted records in two places (the “through” association and the “foreign” far side of the association). Under the hood, Atlas 2 had to load up the assocation mapping records anyway, so forcing an explicit with() call when fetching a many-to-many relationship through the association mapping seems like a reasonable approach. With that, you only need to manage the association mapping to add or remove the “foreign” records on the far side of the relationship.

Also in the relationship department, the “ManyToOneByReference” relationship has been renamed to “ManyToOneVariant”. (I think that name flows better.)

Finally, 1:1 and 1:M relationships now support different kinds of cascading delete functionality. These methods on the relationship definition will have these effects on the foreign record when the native record is deleted:

  • onDeleteSetNull(): Sets $foreignRecord keys to null.

  • onDeleteSetDelete(): Calls $foreignRecord::setDelete() to mark it for deletion.

  • onDeleteCascade(): Calls $foreignMapper::delete($foreignRecord) to delete it right away.

  • onDeleteInitDeleted(): Presumes that the database has deleted the foreign record via trigger, and re-initializes the foreign record row to DELETED.

Further, the 1:M relationship will detach deleted records from foreign RecordSets, and the 1:1 relationship will set the foreign record value to false when deleted. This helps with managing the in-memory objects, so you don’t have to detach or unset deleted records yourself.

ORM Transaction Management

At the very top of the framework hierarchy, then, we have the Atlas.Orm package. Almost all functionality has been extracted to the underlying packages; the only parts remaining are the overarching Atlas object with its convenience methods, and the transaction management system.

Whereas Atlas 2 provided something similar to a Unit of Work implementation, I have come around to the idea that Unit of Work is a domain-layer pattern, not a persistence-layer pattern. It’s about tracking changes on domain objects and writing them back to the database approriately: “A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you’re done, it figures out everything that needs to be done to alter the database as a result of your work.” (cf. POEAA: UnitOf Work).

With that in mind, Atlas 3 provides more typical transaction management strategies, though with some automation if you feel you need it:

  • The default AutoCommit strategy starts in “autocommit” mode, which means that each interaction with the database is its own micro-transaction (cf. https://secure.php.net/manual/en/pdo.transactions.php). You can, of course, manually begin/commit/rollback transactions as you wish.

  • The AutoTransact strategy will automatically begin a transaction when you perform a write operation, then automatically commit that operation, or roll it back on exception. (This was the default for Atlas 2.)

  • The BeginOnWrite strategy will automatically begin a transaction when you perform a write operation. It will not commit or roll back; you will need to do so yourself. Once you do, the next time you perform a write operation, Atlas will begin another transaction.

  • Finally, the BeginOnRead strategy will automatically begin a transaction when you perform a write operation or a read operation. It will not commit or roll back; you will need to do so yourself. Once you do, the next time you perform a write or read operation, Atlas will begin another transaction.

Style and Approach

Even though I was the lead on PSR-1 and PSR-2, I find myself beginning to drift from them somewhat as I use PHP 7 more. In particular, I find things line up more nicely when you put the opening brace on the next line, for a method with a typed return and an argument list that spreads across multiple lines. That is, instead of this …

    public function foo(
        LongClassName $foo,
        LongerClassName $bar
    ) : ?VeryLongClassName {
        // ...
    }

… I’m leaning toward this:

    public function foo(
        LongClassName $foo,
        LongerClassName $bar
    ) : ?VeryLongClassName
    {
        // ...
    }

The extra bit of whitespace in this situation gives a nice visual separation.

I’m also beginning to drift a little from some of my devotion to interfaces. One of the problems with exposing interfaces is that strict backwards compatibility becomes harder to maintain without also bumping major versions; if you find you need even a single new feature, you can’t add it to the interface without breaking BC. As such, to maintain both Semantic Versioning and to avoid adding off-interface methods in sub-major releases, Atlas 3 avoids interfaces entirely in favor of abstract class implementations. Further, these abstract classes do not use the “Abstract” prefix, as I have done in the past. This means Atlas can typehint to Abstract classes without it looking ugly, and can avoid technical BC breaks when interfaces are added to. This has its own problems too; it is not a solution so much as a tradeoff.

I’m also trying to avoid docblocks as much as possible in favor of typehinting everthing possible.

Upgrading from Atlas 2 to Atlas 3

Whereas the upgrade path from Atlas 1 to Atlas 2 was relatively straightforward, the jump from Atlas 2 to Atlas 3 is a much bigger one. The most significant changes are in the method names and signatures at the Connection and Query levels; the Table classes now each have their own Row instead of using an all-purpose generic row; the Mapper classes now separate the relationship-definition logic to its own class as well. I will be preparing an upgrade document to help ease the transition.

But if you’re just getting started with Atlas, you’ll want to try v3 “Cassini” first.

Atlas 2.1.0 Released with “Polymorphic Association” Support

I’m happy to announce that I released Atlas 2.1.0 late yesterday. (Atlas is a data mapper for your persistence model in PHP – not your domain model.)

In addition to some minor added informational and convenience functionality, the big addition in this release is support for many-to-one relationships by reference (aka “polymorphic association”). You can see the documentation for it here.

I.

Atlas uses SQL terms for relationships instead of OOP ones (e.g., “many-to-one” instead of “has one”). As such, the OOP term “polymorphic assocation” just wasn’t a good name for the feature.

However, some research revealed that PostgresANSI SQL has a constraint type named REFERENCES that supports the feature natively:

After trying out several alternative names, “many-to-one by reference” was a much better fit than “polymorphic association.”

II.

Because Atlas is for the persistence model, and not for the domain model, I had to wonder if this kind of behavior belongs in the database work at all. Should it happen in the domain instead?

After working through the problem, the answer turned out to be that it has to go in the database work. You simply don’t know which foreign tables to select from in the first place, without that information being represented in a relationship description. The reference column determines what the foreign table should be. If there are different values in the reference column, then you have to select from different tables to get the related rows. That can’t happen once you’re in the domain layer; it must happen in the persistence layer.

III.

Relationships-by-reference may not be a good data design choice if you are starting from scratch. See this 2009 presentation from Bill Karwin for some other alternatives:

https://www.slideshare.net/billkarwin/practical-object-oriented-models-in-sql/22

These each have different tradeoffs, and in one case require that your database system supports parent tables.

Of course, if you already have a database design that uses many-to-one relationships by reference, then you’re probably stuck with it. Atlas can now help you out in this situation.

UPDATE: This Reddit comment leads me to understand that I read the REFERENCES Postgres doc too hastily. In context of the linked mailing list message, I understood “refcolumn” to be on the native table, not the foreign one. So it’s a standard foreign key constraint, not a specialized/extended form provided by Postgres; I confess I find it easy to believe that Postgres often supports things that other databases do not.


You can read the Reddit commentary on this post here.

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.