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();
}
}
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.
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.
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 querystart
: when the query startedfinish
: when the query finishedduration
: how long the query tookstatement
: the query statement stringvalues
: the array of bound valuestrace
: an exception trace showing where the query was issuedYou 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.
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.