A First Look at Atlas – the ORM That Delivers

Share this article

A First Look at Atlas – the ORM That Delivers

This article was peer reviewed by Paul M. Jones and Scott Molinari. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

AtlasORM is a data mapper implementation for your persistence model (not your domain model)”

By definition, a Data Mapper moves data between objects and a database and isolates them from one another. With a Data Mapper, the in memory objects don’t even need to know that a database exists. It does not need to know the SQL interface or database schema; it doesn’t even need the domain layer to know it exists!

This might lead us to thinking that, in Atlas, the persistence layer is totally disconnected from the database, but that is not quite what happens. Atlas uses the term Record to indicate that its objects are not domain entities. An Atlas Record is passive; not an active record. Unlike most ORMs, its objects represent the persistence model, not the domain model. Think of it as representing how the data is stored and not as real world representations.

Illustration of the god Atlas carrying a big globe

What’s the reasoning behind it?

The creator of Atlas, Paul Jones, based it on this article from Mehdi Khalili. The idea was to create an alternative to the Active Record pattern (an object that carries both data and behavior, and is intrinsically connected to the database).

Citing the author:

“I wanted an alternative to Active Record that would allow you to get started about as easily as [with] Active Record for your persistence model, and then refactor more easily towards a richer domain model as needed.”

Altas has its own set of characteristics that differentiates it from others.

  • No annotations – Under the premise that the code should be in the code and not in the comments.
  • No migrations or database modification logic – Since Atlas is decoupled from the database (not entirely, the package, as a whole, needs to interact with the database to be able to move data back and forth), it makes sense that it only acts as a model of the schema and not has a creator and manager of it.
  • No lazy-loading – The creator thought of lazy loading as being useful, but ultimately too much trouble for what it is worth.
  • No data type abstractions – The database types are exposed and available where possible.
  • Composite key support – Atlas supports both composite primary keys and composite foreign keys.

Now that we have a general idea of Atlas and what it stands for, let’s have a look at it in use.

Installation

Because Atlas is still in development at the time of this writing, we will install the latest, cutting edge version. Also, we will install the CLI package for ease of development. That leaves us with the following composer.json file:

{
    "require": {
        "atlas/orm": "@dev"
    },
    "require-dev": {
        "atlas/cli": "@dev"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/App/"
        }
    }
}

We also have some PSR-4 there for autoloading, which we will use further down the line.

Setting it up

Earlier on, we said that Atlas doesn’t act like a schema creator and manager. That means that we don’t have the tools for database creation. In this example, we are creating a new and simple database from scratch, but Atlas will work with any database you already have in place.

We will use a very simple database design. We will have products and categories in separate database tables. Using MySQL, let’s create our database:

CREATE DATABASE atlasorm;
USE DATABASE atlasORM;
CREATE TABLE `category` (`category_id` int(11) NOT NULL, `name` varchar(255) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `products` (`product_id` int(11) NOT NULL, `name` varchar(255) NOT NULL, `category_id` int(11) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `category` ADD PRIMARY KEY (`category_id`);
ALTER TABLE `products` ADD PRIMARY KEY (`product_id`);
ALTER TABLE `category` MODIFY `category_id` int(11) NOT NULL AUTO_INCREMENT;
ALTER TABLE `products` MODIFY `product_id` int(11) NOT NULL AUTO_INCREMENT;

After this, we add some values to both tables:

INSERT INTO `category` (`category_id`, `name`) VALUES (1, 'Steel'), (2, 'Wood'), (3, 'Leather');
INSERT INTO `products` (`product_id`, `name`, `category_id`) VALUES (1, 'Nails', 1), (2, 'Pipe', 1), (3, 'Chair', 2), (4, 'Screwdriver', 1);

With this information laid out, we can now set up our ORM. Atlas has very little code generation. To generate the skeleton classes (both the Table class and the Mapper class), we will first have to create a .php file where we will have our database connection information. This way, the generator will know what properties it should generate.

<?php
// connection.php
return ['mysql:dbname=testdb;host=localhost', 'username', 'password'];

Then, lets run the atlas-skeleton generator. The atlas-skeleton generator is a CLI tool that will use the database connection information on the connection.php file, the class name, and the given namespace to generate the skeleton classes. Please note that we have to run the generator for each individual table in the database.

./vendor/bin/atlas-skeleton.php --dir=./src/App/DataSource --conn=/path/to/connection.php --table=products App\\DataSource\\Products
./vendor/bin/atlas-skeleton.php --dir=./src/App/DataSource --conn=/path/to/connection.php --table=category App\\DataSource\\Category

After running the atlas-skeleton command, we can check the /src/App/Datasource folder and see that skeletons for both products and category were created. Both of these skeletons contain two files. A Mapper file, that’s almost empty and a Table class, which contains a description of the database table. This is all the setup we need.

Now, let’s finally see it in action!

CRUD Operations

With everything set up, lets focus on using our ORM. For this, we need an Atlas instance, and we create one using the AtlasContainer class.

<?php

require_once __DIR__ . '/vendor/autoload.php';

use Atlas\Orm\Mapper\Mapper;
use Atlas\Orm\AtlasContainer;
use App\DataSource\Category\CategoryMapper;
use App\DataSource\Products\ProductsMapper;

$atlasContainer = new AtlasContainer('mysql:host=host;dbname=atlasorm','username', 'password' );
$atlasContainer->setMappers([ProductsMapper::CLASS, CategoryMapper::CLASS]);

$atlas = $atlasContainer->getAtlas();

Nothing too fancy here, we autoload our vendor classes, include our mappers, and create an Atlas instance with both our Products and Category Mapper information.

Reading

To read a Record or a RecordSet from our database we can use the following:

$categoryRecord = $atlas->fetchRecord(CategoryMapper::CLASS, '2');

This will return a single Record, the one with ID 2. For a RecordSet:

$categoryRecordSet = $atlas
    ->select(CategoryMapper::CLASS)
    ->orderBy(['category_id DESC'])
    ->limit(10)
    ->fetchRecordSet();

This will fetch a RecordSet of the last 10 categories ordered by category_id.

Creating, updating and deleting

For creating, updating and deleting, Atlas uses the same principle. Obtaining a Record and manipulating it:

// start insert
$newCategory = $atlas->newRecord(CategoryMapper::CLASS);
$newCategory->name = "Bone";

// create a transaction
$transaction = $atlas->newTransaction();
$transaction->insert($newCategory);

// execute the transaction
$ok = $transaction->exec();
if ($ok) {
    echo "Transaction success.";
} else {
    echo "Transaction failure. ";
}

As we can see, creating is pretty straightforward. For updating and deleting we can use the same principle:

//Start updating
$newCategory = $atlas->fetchRecord(CategoryMapper::CLASS, '2');
$newCategory->name = "Wood";

// create a transaction
$transaction = $atlas->newTransaction();

// plan work for the transaction
$transaction->update($newCategory);
//$transaction->delete($newCategory);

// execute the transaction plan
$ok = $transaction->exec();
if ($ok) {
    echo "Transaction success.";
} else {
    echo "Transaction failure. ";
}
//End updating

In this case, we can use either update together with new table values to update a Record, or delete to delete this same Record.

A quick note on Relationships

Atlas supports all four kinds of relationships: OneToMany, ManyToOne, OneToOne and MantToMany. To add relationships, we have to add them in the Mapper classes.

In our particular case, let’s imagine that each category can have many products. A one to many relationship. Our CategoryMapper would look like this:

<?php

namespace App\DataSource\Category;

use Atlas\Orm\Mapper\AbstractMapper;
use App\DataSource\Products\ProductsMapper;

/**
 * @inheritdoc
 */
class CategoryMapper extends AbstractMapper
{
    /**
     * @inheritdoc
     */
    protected function setRelated()
    {
        $this->oneToMany('products', ProductsMapper::CLASS);
    }
}

By default, the relationship will take the primary key on the table of origin and map it to the corresponding column in the destination table. Of course, in the case where we have a many to one relationship this is not possible. In that case the reverse will be done. The primary key on the destination table will be mapped to the corresponding column on the table of origin.

So, if we want to fetch a RecordSet of our categories with their related product records, we can use the with() method, just like this:

$newRecord = $atlas
    ->select(CategoryMapper::CLASS)
    ->with([
        'products'
    ])
    ->fetchRecordSet();

With the knowledge of how the basic CRUD operations work, we can now take a look at some more practical examples of code using Atlas.

Some practical examples

It’s great to have a tool like Atlas and to see how it works. But ultimately, we want to see it being used in more practical scenarios. While we will not build a full fledged application, we will look at some possible uses.

There are some operations that we use every day without even noticing, like fetching information, inserting and deleting database records. How would those look using Atlas?

<?php

require_once __DIR__ . '/vendor/autoload.php';

use Atlas\Orm\Mapper\Mapper;
use Atlas\Orm\AtlasContainer;
use App\DataSource\Category\CategoryMapper;
use App\DataSource\Products\ProductsMapper;

/**
* This function will create and return our Atlas instance set up
* with both our Mappers
*
* @return $atlasContainer
*/
function getAtlasContainer(){

    $atlasContainer = new AtlasContainer('mysql:host=localhost;dbname=atlasorm','root', '' );
    $atlasContainer->setMappers([ProductsMapper::CLASS, CategoryMapper::CLASS]);

    return $atlasContainer->getAtlas();
}

/**
* This function will return a RecordSet of all our products
*
* @return RecordSet
*/
function getAllProducts(){

    $atlas = getAtlasContainer();

    $productsRecordSet = $atlas
            ->select(ProductsMapper::CLASS)
            ->fetchRecordSet();

    return $productsRecordSet;
}

/**
* This function will return a Record of the Product with the specified id
*
* @param int 
* @return Record 
*/
function getProductById( $id ){

    $atlas = getAtlasContainer();

    $productRecord = $atlas->fetchRecord(ProductsMapper::CLASS, $id);

    return $productRecord;
}

/**
* This function will insert a new product Record
*
* @param string $product_name
* @param int $category_name 
*/
function addProduct( $product_name, $category_id ){

    $atlas = getAtlasContainer();

    //First we check if the category exists
    $categoryRecord = $atlas->fetchRecord(CategoryMapper::CLASS, $category_id);

    //if our category exists we will insert our product
    if( $categoryRecord ){
        //Start insert
        $newProduct = $atlas->newRecord(ProductsMapper::CLASS);
        $newProduct->name = $product_name;
        $newProduct->category_id = $category_id;

        // create a transaction
        $transaction = $atlas->newTransaction();
        $transaction->insert($newProduct);

        // execute the transaction
        $ok = $transaction->exec();
        if ($ok) {
            echo "Transaction success.";
        } else {
            // get the exception that was thrown in the transaction
            $exception = $transaction->getException();
            $work = $transaction->getFailure();
            echo "Transaction failure. ";
            echo $work->getLabel() . ' threw ' . $exception->getMessage();
        }
        //End insert
    }else{
        echo 'The category is not valid.';
    }
}

/**
* This function will delete a product Record
*
* @param id $product_id
*/
function deleteProduct( $product_id ){

    $atlas = getAtlasContainer();

    //First, lets check if the product with $product_id exists
    $productRecord = $atlas->fetchRecord(ProductsMapper::CLASS, $product_id);

    if( $productRecord ){
        //Delete the product
        $transaction = $atlas->newTransaction();
        $transaction->delete($productRecord);

        // execute the transaction
        $ok = $transaction->exec();
        if ($ok) {
            echo "Transaction success.";
        } else {
            // get the exception that was thrown in the transaction
            $exception = $transaction->getException();
            $work = $transaction->getFailure();
            echo "Transaction failure. ";
            echo $work->getLabel() . ' threw ' . $exception->getMessage();
        }
    }else{
        echo 'The product you are trying to delete does not exist.';
    }
}

A very basic skeleton of operations using Atlas (void of OOP and modern programming concepts due this being just a demo).

Caveats

Finally, let’s look at two caveats.

  • Atlas uses code generation. If you change a database table that already had its code generated and you re-generate it, the mapper class will not be overridden. This is specially helpful if you wrote custom code in the mapper class.

  • Atlas is a work in progress, 1.0.0-alpha1 was very recently released. Be very careful when using it, especially if you try to use it in a production environment, as breaking changes may affect your projects.

Conclusion

The concept behind Atlas is original, and it’s easy to understand and use. The no annotations and very little code generation aspects are really strong selling points, especially if you, just like me, like to keep your code as decoupled and independent as possible.

What do you think of Atlas? Will you give it a spin?

Claudio RibeiroClaudio Ribeiro
View Author

Cláudio Ribeiro is a software developer, traveler, and writer from Lisbon. He's the author of the book An IDE Called Vim. When he is not developing some cool feature at Kununu he is probably backpacking somewhere in the world or messing with some obscure framework.

ActiveRecordatlasBrunoSdatabaseOOPHPormPHP
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week