...snip...
The Data\Mapper
class establishes a bridge between data objects and a data source. The purpose is to provide very light coupling from the data object to the mapper. In other words, the data object should not care too much about what mapper is used or where the data is coming from. However, there is usually very tight coupling from the data mapper to the data object. The mapper obviously needs to know a lot about the data source and, to a degree, also the type of data that it is loading.
Note that the mapper class is not intended to be a full Object Relationship Mapper (ORM) but it could be used to interface with established, third-party solutions that provide such features (Doctrine for example).
There are six public entry points to the mapper API.
The constructor takes no arguments so the developer is free to add additional arguments (probably relating to the type of data source).
The create
method is used to create new data. It expects a Data\Dumpable
object. This is an object that defines a dump
method and includes Data\Data
and Data\Set
. If a singular object, like Data\Data
is passed to the mapper, and instance of a singular object is expected to be returned. However, if an instance of a Data\Set
object is passed to the method, an instance of a Data\Set
will be returned.
The delete
method is used to remove data from the data source. It expects either a single object identifier (for example, the value of the primary key in a database table), or an array of object identifiers. Nothing is returned.
The find
method is used to search for and load data from the data source. It takes an optional where and sort criteria, a paging offset and an paging limit. It returns a Data\Set
of objects matching the criteria. If no criteria is supplied, it will return all results subject to the pagination values that are specified. The findOne
method works the same as find
but it only returns one (the first) result retrieved by find
.
The update
method is used to update data in the data source. It is otherwise identical to the create
method.
When extending the mapper, there are four abstract methods to implement and one protected method that can be optionally overriden.
The protected initialise
method is called in the Data\Mapper
constructor. It can be overriden to support any setup required by the developer.
The abstract doCreate
method must be implemented. It takes an array of dumped objects. The objects must be added to the data source, and should add any additional properties that are required (for example, time stamps). The method must return a Data\Set
containing the objects that were created in the data source, including additional data that may have been added by the data source (for example, setting the primary key or created time stamps).
The abstract doDelete
method must be implemented. It takes an array of unique object identifiers (such as primary keys in a database, cache identifiers, etc). The method must delete the corresponding objects in the data source. Any return value is ignored.
The abstract doFind
method must be implemented. It takes the same arguments as the find
method. The method must return a Data\Set
object regardless of whether any data was found to match the search criteria. If this method accidentally returns more data records than defined by $limit
, the calling find
method will truncate the data set to the pagination limit that was specificed.
The abstract doUpdate
method must be implemented. Like doCreate
, it takes an array of dumped objects that must be updated in the data source. The method must return a Data\Set
containing the objects that were updated in the data source, including additional data that may have been added by the data source (for example, modified time stamps).
The following basic example shows how a base mapper class could be devised to support database table operations (most DocBlocks are removed for brevity).
namespace Joomla\Database;
use Joomla\Data;
class TableMapper extends Data\Mapper
{
/**
* @var \Joomla\Database\Driver
*/
protected $db;
/**
* @var string
*/
protected $table;
/**
* @var string
*/
protected $tableKey;
/**
* @var array
*/
private $columns;
public function __construct(Driver $db, $table, $tableKey)
{
// As for JTable, set a database driver, the table name and the primary key.
$this->db = $db;
$this->table = $table;
$this->tableKey = $tableKey;
parent::__construct();
}
protected function doCreate(array $input)
{
$result = new Data\Set;
foreach ($input as $object)
{
// Ensure only the columns for this table are inserted.
$row = (object) array_intersect_key((array) $object, $this->columns);
$this->db->insertObject($this->table, $row, $this->tableKey);
$result[$row->{$this->tableKey}] = new Data\Data($row);
}
return $result;
}
protected function doDelete(array $input)
{
// Sanitise.
$input = array_map('intval', $input);
if (empty($input))
{
return;
}
$q = $this->db->getQuery(true);
$q->delete($q->qn($this->table))
->where($q->qn($this->tableKey) . ' IN (' . implode(',', $input). ')');
$this->db->setQuery($q)->execute();
}
protected function doFind($where = null, $sort = null, $offset = 0, $limit = 0)
{
$q = $this->db->getQuery(true);
$q->select('*')
->from($q->qn($this->table));
// A simple example of column-value conditions.
if (is_array($where) && !empty($where))
{
foreach ($where as $column => $value)
{
$q->where($q->qn($column) . '=' . $q->q($value));
}
}
// A simple example of column-direction pairs.
if (is_array($sort) && !empty($sort))
{
foreach ($sort as $column => $direction)
{
$q->where($q->qn($column) . ' ' . (strtoupper($direction == 'DESC') ? 'DESC' : 'ASC'));
}
}
return new Data\Set($this->db->setQuery($q)->loadObjectList($this->tableKey, 'JData'));
}
protected function doUpdate(array $input)
{
$result = new Data\Set;
foreach ($input as $object)
{
// Ensure only the columns for this table are updated.
$row = (object) array_intersect_key((array) $object, $this->columns);
$this->db->updateObject($this->table, $row, $this->tableKey);
$result[$row->{$this->tableKey}] = new Data\Data($row);
}
return $result;
}
protected function initialise()
{
// Stash the columns for this table.
$this->columns = $this->db->getTableColumns($this->table);
}
}
The core Data\Object
and Data\Set
classes are simple value objects and we don't recommend you mock them (the effort of mocking more or less produces the original classes anyway).
To mock a Data\Mapper
object you can use the Data\Tests\Mocker
class.
use Joomla\Data\Tests\Mocker as DataMocker;
class MyTest extends \PHPUnit_Framework_TestCase
{
private $instance;
protected function setUp()
{
parent::setUp();
// Create the mock input object.
$dataMocker = new DataMocker($this);
$mockMapper = $dataMocker->createMapper();
// Set up some test data for `find` and `findOne` to return.
$dataMocker->set = new Set(
array(new Object(array('foo' => 'bar')))
);
// Create the test instance injecting the mock dependency.
$this->instance = new MyClass($mockMapper);
}
}
The createMapper
method will return a mock with the following methods mocked to roughly simulate real behaviour albeit with reduced functionality:
find([$where, $sort, $offset, $limit])
findOne([$where, $sort])
The Data\Tests\Mocker
class supports a public set
property (shown in the example) for you to inject a Data\Set
object for the mock find
and findOne
methods to use.
Or, you can provide customised implementations these methods by creating the following methods in your test class respectively:
mockMapperFind
mockMapperFindOne