1.1.10. Other Topics

1.1.10.1. Adding Custom Mapper Methods

Feel free to add custom methods to your Mapper classes, though do be sure that they are appropriate to a Mapper. For example, custom fetch*() methods are perfectly reasonable, so that you don't have to write the same queries over and over:

namespace App\DataSource\Content;

use Atlas\Mapper\Mapper;

class Content extends Mapper
{
    public function fetchLatestContent(int $count) : ContentRecordSet
    {
        return $this
            ->select()
            ->orderBy('publish_date DESC')
            ->limit($count)
            ->fetchRecordSet();
    }
}

Another example would be custom write behaviors, such as incrementing a value directly in the database (without going through any events) and modifying the appropriate Record in memory:

namespace App\DataSource\Content;

use Atlas\Mapper\Mapper;

class Content extends Mapper
{
    public function increment(ContentRecord $record, string $field)
    {
        $this->table
            ->update()
            ->set($field, "{$field} + 1")
            ->where("content_id = ", $record->content_id)
            ->perform();

        $record->$field = $this->table
            ->select($field)
            ->where("content_id = ", $record->content_id)
            ->fetchValue();
    }
}

1.1.10.2. Single Table Inheritance

Sometimes you will want to use one Mapper (and its underlying Table) to create more than one kind of Record. The Record type is generally specified by a column on the table, e.g. record_type. To do so, create Record classes that extend the Record for that Mapper in the same namespace as the Mapper, then override the Mapper getRecordClass() method to return the appropriate class name.

For example, given a Content mapper and ContentRecord ...

App\
    DataSource\
        Content\
            Content.php
            ContentEvents.php
            ContentRecord.php
            ContentRecordSet.php
            ContentRelationships.php
            ContentRow.php
            ContentSelect.php
            ContentTable.php
            ContentTableEvents.php
            ContentTableSelect.php

... , you might have the content types of "post", "page", "video", "wiki", and so on.

App\
    DataSource\
        Content\
            Content.php
            ContentEvents.php
            ContentRecord.php
            ContentRecordSet.php
            ContentRelationships.php
            ContentRow.php
            ContentSelect.php
            ContentTable.php
            ContentTableEvents.php
            ContentTableSelect.php
            PageContentRecord.php
            PostContentRecord.php
            VideoContentRecord.php
            WikiContentRecord.php

A WikiContentRecord might look like this ...

namespace App\DataSource\Content;

class WikiContentRecord extends ContentRecord
{
}

... and the Content getRecordClass() method would look like this:

namespace App\DataSource\Content;

use Atlas\Mapper\Mapper;
use Atlas\Table\Row;

class Content extends Mapper
{
    protected function getRecordClass(Row $row) : Record
    {
        switch ($row->type) {
            case 'page':
                return PageContentRecord::CLASS;
            case 'post':
                return PostContentRecord::CLASS;
            case 'video':
                return VideoContentRecord::CLASS;
            case 'Wiki':
                return PostContentRecord::CLASS;
            default:
                return ContentRecord::CLASS:
        }
    }
}

Note that you cannot define different relationships "per record." You can only define MapperRelationships for the mapper as whole, to cover all its record types.

Note also that there can only be one RecordSet class per Mapper, though it can contain any kind of Record.

1.1.10.3. Managing Many-To-Many Relateds

Given the typical example of a blog table, associated to tags, through a taggings table, here is how you would add a tag to a blog post:

// get a blog post, with taggings and tags
$blog = $atlas->fetchRecord(Blog::CLASS, $blog_id, [
    'taggings' => [
        'tags'
    ]
]);

// get all tags in the system
$tags = $atlas->fetchRecordSet(Tags::CLASS);

// create the new tagging association, with the related blog and tag objects
$tagging = $thread->taggings->appendNew([
    'blog' => $blog,
    'tag' => $tags->getOneBy(['name' => $tag_name])
]);

// persist the whole blog record, which will insert the tagging
$atlas->persist($tagging);

Similarly, here is how you would remove a tag:

// mark the Tagging association for deletion
$blog->taggings
    ->getOneBy(['name' => $tag_name)
    ->setDelete();

// persist the whole record with the tagging relateds,
// which will delete the tagging and detach the related record
$atlas->persist($thread);

1.1.10.4. Automated Validation

You will probably want to apply some sort of filtering (validation and sanitizing) to Row (and to a lesser extent Record) objects before they get written back to the database. To do so, implement or override the appropriate TableEvents (or MapperEvents) class methods for before or modify the insert or update event. Irrecoverable filtering failures should be thrown as exceptions to be caught by your surrounding application or domain logic.

For example, to check that a value is a valid email address:

namespace App\DataSource\Author;

use Atlas\Table\Row;
use Atlas\Table\Table;
use Atlas\Table\TableEvents;
use UnexpectedValueException;

class AuthorTableEvents extends TableEvents
{
    public function beforeInsert(Table $table, Row $row) : void
    {
        $this->assertValidEmail($row->email);
    }

    public function beforeUpdate(Table $table, Row $row) : void
    {
        $this->assertValidEmail($row->email);
    }

    protected function assertValidEmail($value)
    {
        if (! filter_var($value, FILTER_VALIDATE_EMAIL) {
            throw new UnexpectedValueException("The author email address is not valid.");
        }
    }
}

For detailed reporting of validation failures, consider writing your own extended exception class to retain a list of the fields and error messages, perhaps with the object being validated.