+ - 0:00:00
Notes for current slide
Notes for next slide

Atlas ORM

A Persistence-Layer Data Mapper

Atlas Logo

Paul M. Jones

https://github.com/atlasphp/Atlas.Orm

1 / 20

Objects and Tables

  • Moving complex data from SQL to PHP is troublesome

  • OOP fundamentally different from relational algebra

  • Evolved data-source patterns to deal with this

2 / 20

Data Source Architecture

DDD

  • Row Data Gateway: row data and peristence logic

  • Active Record: row data, persistence logic, and domain logic

  • Table Data Gateway: all table rows, and persistence logic

  • Data Mapper: persistence logic for a disconnected domain object

3 / 20

Domain Logic

DDD

  • Domain "should not" be modeled on database structure

  • Keep domain objects separated from database connections

  • Converting between database and domain is very difficult

  • "Object-Relational Impedance Mismatch"

4 / 20

ORMs

  • Ease conversion of relational rows to domain objects

  • Generate SQL, retrieve/save data, map data to objects

  • Active Record (persistence combined with domain logic)

  • Data Mapper (persistence separated from domain objects)

5 / 20

Tradeoffs (Active Record)

  • Easy to start with for CRUD/BREAD

  • Easy to add simple domain logic

  • As complexity increases, need separate domain layer

  • Hard to extract domain behavior from perisistence

  • Harder to maintain and refactor

6 / 20

Tradeoffs (Data Mapper)

  • Clear separation between persistence and domain

  • Easier to maintain as complexity increases

  • Harder to get started with

  • Presumes rich domain model and mapping expertise

  • Too much for early CRUD/BREAD operations

7 / 20

The Underlying Problem

  • All systems start simple

  • Some systems become complex

  • Can't tell in advance

  • Want a low-cost path in case of complexity

8 / 20

Desiderata

  • Easy to get started with

  • Clear refactoring path if complexity increases

  • Amenable to CRUD/BREAD in early stages

  • Ability to add simple behaviors

  • Strategy to convert from ORM to Domain Model proper

  • Maintain separation of persistence from data

9 / 20

Persistence Model, not Domain Model

  • Use the Data Mapper approach for separation

  • Instead of mapping to domain model entities (Aggregates) ...

  • ... map to the persistence model rows and relationships (Records)

10 / 20

Atlas

  • A data mapper for your persistence model

  • Build a series of Table Data Gateway

  • Relate them to each other via Mappers

  • Retrieve and save Record objects through the Mapper objects

11 / 20

Mapper Object

<?php
namespace App\DataSource\Thread;
use App\DataSource\Author\AuthorMapper;
use App\DataSource\Summary\SummaryMapper;
use App\DataSource\Reply\ReplyMapper;
use App\DataSource\Tagging\TaggingMapper;
use App\DataSource\Tag\TagMapper;
use Atlas\Orm\Mapper\AbstractMapper;
class ThreadMapper extends AbstractMapper
{
protected function setRelated()
{
$this->manyToOne('author', AuthorMapper::CLASS);
$this->oneToOne('summary', SummaryMapper::CLASS);
$this->oneToMany('replies', ReplyMapper::CLASS);
$this->oneToMany('taggings', TaggingMapper::CLASS);
$this->manyToMany('tags', TagMapper::CLASS, 'taggings');
}
}
12 / 20

Atlas Container

<?php
$container = new \Atlas\Orm\AtlasContainer(
'mysql:dbname=testdb;host=localhost',
'username',
'password'
);
$container->setMappers(
\App\DataSource\Author\AuthorMapper::CLASS,
\App\DataSource\Summary\SummaryMapper::CLASS,
\App\DataSource\Reply\ReplyMapper::CLASS,
\App\DataSource\Thread\ThreadMapper::CLASS,
\App\DataSource\Tagging\TaggingMapper::CLASS,
\App\DataSource\Tag\TagMapper::CLASS,
);
$atlas = $container->newAtlas();
13 / 20

CRUD/BREAD Retrieval with Relatonships

$threads = $atlas->select(ThreadMapper::CLASS)
->limit(10)
->orderBy(['date DESC'])
->with([
'author',
'replies' => function ($replies) {
$replies
->orderBy(['date ASC'])
->with(['author']);
};
'taggings',
'tags',
])
->fetchRecordSet();
foreach ($threads as $thread) {
echo "{$thread->title} by {$thread->author->first_name} "
. "has " count($thread->replies) . " replies.";
}
14 / 20

CRUD/BREAD Saving

$thread = $atlas->newRecord(ThreadMapper::CLASS);
$thread->title = 'New Title';
$atlas->insert($thread);
echo $thread->thread_id;
$thread = $atlas->fetchRecord(ThreadMapper::CLASS, $threadId);
$thread->title = 'Changed Title';
$atlas->update($thread);
$thread = $atlas->fetchRecord(ThreadMapper::CLASS, $threadId);
$atlas->delete($thread);
15 / 20

Adding Simple Behaviors

<?php
namespace App\DataSource\Author;
use Atlas\Orm\Mapper\Record;
class AuthorRecord extends Record
{
public function getFullName()
{
return $this->first_name . ' ' . $this->last_name;
}
}
16 / 20

Richer Domain Models

<?php
namespace App\Domain\Thread;
use App\DataSource\Thread\ThreadMapper;
class ThreadRepository
{
protected $mapper;
public function __construct(ThreadMapper $mapper)
{
$this->mapper = $mapper;
}
public function fetchThread($thread_id)
{
$record = $this->mapper->fetchRecord(...);
return $this->newThread($record);
}
protected function newThread(ThreadRecord $record)
{
/* ??? */
}
}
17 / 20

Compose Persistence Into Domain

class ThreadRepository
{
protected function newThread(ThreadRecord $record)
{
return new ThreadAggregate($record);
}
}
18 / 20

Map From Persistence To Domain

class ThreadRepository
{
protected function newThread(ThreadRecord $record)
{
return new ThreadAggregate(
$record->thread_id,
$record->title,
$record->body,
$record->date_published,
$record->author->author_id,
$record->author->name,
$record->tags->getArrayCopy(),
$record->replies->getArrayCopy()
);
}
}
19 / 20

Objects and Tables

  • Moving complex data from SQL to PHP is troublesome

  • OOP fundamentally different from relational algebra

  • Evolved data-source patterns to deal with this

2 / 20
Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
Esc Back to slideshow