class: title # Atlas ORM ## A Persistence-Layer Data Mapper ![Atlas Logo](./images/atlas-128.png) ### Paul M. Jones ###
--- # 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 --- # Data Source Architecture .right-column[![DDD](./images/poeaa.jpg)] - 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 --- # Domain Logic .right-column[![DDD](./images/ddd.jpg)] - 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"](https://en.wikipedia.org/wiki/Object-relational_impedance_mismatch) --- # 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) --- # 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 --- # 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 --- # 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 --- # 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 --- # 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) --- # 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 --- # Mapper Object ``` 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'); } } ``` --- # Atlas 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(); ``` --- # 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."; } ``` --- # 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); ``` --- # Adding Simple Behaviors ``` first_name . ' ' . $this->last_name; } } ``` --- # Richer Domain Models ``` mapper = $mapper; } public function fetchThread($thread_id) { $record = $this->mapper->fetchRecord(...); return $this->newThread($record); } protected function newThread(ThreadRecord $record) { /* ??? */ } } ``` --- # Compose Persistence Into Domain ``` class ThreadRepository { protected function newThread(ThreadRecord $record) { return new ThreadAggregate($record); } } ``` --- # 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() ); } } ``` --- class: middle # Thanks! - Repo: https://github.com/atlasphp/Atlas.Orm - Docs: https://github.com/atlasphp/Atlas.Orm/blob/1.x/docs/index.md - @pmjones on Twitter and Gab - http://paul-m-jones.com