2.1.10. Other Topics

2.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();
    }
}

2.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.

2.1.10.3. 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.

2.1.10.4. Query Logging

To enable query logging, call the Atlas logQueries() method. Issue your queries, and then call getQueries() to get back the log entries.

// start logging
$atlas->logQueries();

// retrieve connections and issue queries, then:
$queries = $connectionLocator->getQueries();

// stop logging
$connectionLocator->logQueries(false);

Each query log entry will be an array with these keys:

  • connection: the name of the connection used for the query
  • start: when the query started
  • finish: when the query finished
  • duration: how long the query took
  • statement: the query statement string
  • values: the array of bound values
  • trace: an exception trace showing where the query was issued

You may wish to set a custom query logger for Atlas. To do so, call setQueryLogger() and pass a callable with the signature function (array $entry) : void.

class CustomDebugger
{
    public function __invoke(array $entry) : void
    {
        // call an injected logger to record the entry
    }
}

$customDebugger = new CustomDebugger();
$atlas->setQueryLogger($customDebugger);
$atlas->logQueries(true);

// now Atlas will send query log entries to the CustomDebugger

Note:

If you set a custom logger, the Atlas instance will no longer retain its own query log entries; they will all go to the custom logger. This means that getQueries() on the Atlas instance will not show any new entries.

2.1.10.5. Custom Factory Callable

The AtlasBuilder let you specify a custom factory callable to create the dependencies for each Table and Mapper instance. The default factory callable looks like this:

/**
 * @var string $class A fully-qualified class name.
 * @return object
 */
function (string $class) {
    return new $class();
}

Although this callable may in future be used for any kind of Table or Mapper dependency, in practice it is currently limited to Events classes.

If your Events instances need dependency injection, you can replace the default factory with your own callable; the AtlasBuilder will use it to create any new Events instances. This gives you full control over how the Events objects are instantiated.

Note:

The base TableEvents and MapperEvents classes have no constructors, so you are free to write your own in your generated Events classes.

For example, to use a PSR-11 container to create Events objects:

$atlasBuilder = new \Atlas\Orm\AtlasBuilder(...);

/** @var \Psr\Container\ContainerInterface $container */
$atlasBuilder->setFactory(function (string $class) use ($container) {
    return $container->get($class);
});

$atlas = $atlasBuilder->newAtlas();

// Atlas will now use $container to create
// TableEvents and MapperEvents instances.