What does Data Mapper mean?
In object orientation, it is common to have more complex relationships between objects, not simple relationships (object related to another), but relationships such as inheritance, composition, aggregation. Below we have sales that have a relationship with the product, thus forming an aggregation.
When this relationship goes to a database, we will have a sales table and also a sale item table, so when the sale is recorded it is preferable that the sale items are also recorded.
If the Table Data Gateway and Active Record patterns were applied, in this case we would have to record the sale and the sale items separately, so the Data Mapper pattern comes in to solve this problem. It consists of a class (SaleMapper
in this case) with persistence methods that receive a complex object or that has a complex relationship.
So Data Mapper is a class that takes care of the persistence of a package of objects and can record, read or delete everything at once, in the example above SaleMapper would record the Sale and its entire relationship web, in this case the sale items to same time.
Example
Next we will have an example of how Data Mapper works.
Step 1 - Directory System:
📦Data_Mapper
┣ 📂classes
┃ ┣ 📜Product.php
┃ ┣ 📜Sale.php
┃ ┗ 📜SaleMapper.php
┣ 📂config
┃ ┗ 📜config.ini
┣ 📂database
┃ ┗ 📜DataMapper.db
┗ 📜index.php
Step 2 - Database Config File:
host =
name = database/DataMapper.db
user =
pass =
type = sqlite
Step 3 - Database:
- Sale Table
CREATE TABLE sale(
id INTEGER PRIMARY KEY NOT NULL,
sale_date DATE
);
- SaleItem Table
CREATE TABLE sale_item(
id INTEGER PRIMARY KEY NOT NULL,
id_product INTEGER REFERENCES product(id),
id_sale INTEGER REFERENCES sale(id),
quantity float,
price float
);
Step 4 - Product Class:
<?php
namespace classes;
class Product
{
/**
* The data variable is defined as mixed,
* as it can take on several types.
* */
private mixed $data;
}
- __get and __set methods
/**
* receive the name of the property and pass it
* to the data attribute as the key.
* */
public function __get(string $prop): mixed
{
return $this->data[$prop];
}
/**
* receive the name and value
* of the property respectively.
* */
public function __set(string $prop, mixed $value): void
{
$this->data[$prop] = $value;
}
Step 5 - Sale Class:
<?php
namespace classes;
class Sale
{
/**
* id property of integer type.
*/
private int $id;
/**
* @var mixed[]
*/
private array $items;
}
- setID and getID methods
/**
* receives the value of the sale id.
*/
public function setID(int $id): void
{
$this->id = $id;
}
/**
* return the sale id.
*/
public function getID(): int
{
return $this->id;
}
- addItem and getItems methods
/**
* receives the quantity and an object of type product.
*/
public function addItem(int $quantity, object $product): void
{
/*
* fill the items array with quantity and product data
*/
$this->items[] = [$quantity, $product];
}
/**
* @return mixed[]
*/
public function getItems(): array
{
return $this->items;
}
Step 6 - SaleMapper Class:
<?php
namespace classes;
class SaleMapper
{
/**
* The conn property is static to maintain the value,
* so we don't need to open the same connection more than once.
*/
private static $conn;
/**
* The data variable is defined as mixed,
* as it can take on several types.
*/
private mixed $data;
}
- setConnection method
/**
* setConnection receives a PDO connection as a parameter
* and stores it in the static attribute conn.
*/
public static function setConnection(PDO $conn)
{
self::$conn = $conn;
}
- getLastID method
private static function getLastID(): int
{
// use the max() function to receive the last generated id
$sql = 'SELECT max(id) as max FROM sale';
// prepare the statment
$result = self::$conn->prepare($sql);
// execute the statment
$result->execute();
// Fetches the next row and returns it as an object
$data = $result->fetchObject();
// return the id
return $data->max;
}
- save method
// receives a sale as a parameter
public static function save(Sale $sale)
{
// generates the date of sale
$sale_date = date('Y-m-d');
// insert information into the sales table
$sql = 'INSERT INTO sale (sale_date) VALUES (:sale_date)';
// perform the prepare method
$result = self::$conn->prepare($sql);
// Binds a parameter to the specified variable name
$result->bindParam(':sale_date', $sale_date);
// execute sql statement
$result->execute();
// receive the sale id
$id_sale = self::getLastID();
// store the id generated in the database to the sales object
$sale->setID($id_sale);
// browse sale items
foreach ($sale->getItems() as $items) {
// in position 0 is the quantity of the product
$quantity = $items[0];
// in position 1 is the product
$product = $items[1];
// stored the product id
$id_product = $product->id;
// stored the price of the product
$price = $product->price;
// perform an insert in the table sale_item
$sql = 'INSERT INTO sale_item (id_product, id_sale, quantity, price)
VALUES (:id_product, :id_sale, :quantity, :price)';
// perform the prepare method
$result = self::$conn->prepare($sql);
// execute sql statement
$result->execute([
':id_product' => $id_product,
':id_sale' => $id_sale,
':quantity' => $quantity,
':price' => $price,
]);
}
}
Testing
<?php
require_once 'classes/Product.php';
require_once 'classes/Sale.php';
require_once 'classes/SaleMapper.php';
use classes\Product;
use classes\Sale;
use classes\SaleMapper;
try {
$ini = parse_ini_file('config/config.ini');
$name = $ini['name'];
$conn = new PDO('sqlite:' . $name);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
SaleMapper::setConnection($conn);
// product1 instance
$p1 = new Product();
// product1 id
$p1->id = 1;
// product1 price
$p1->price = 12;
// product2 instance
$p2 = new Product();
// product1 id
$p2->id = 2;
// product1 price
$p2->price = 16
// sale instance
$sale = new Sale();
// add product(quantity,product)
$sale->addItem(14, $p1);
$sale->addItem(20, $p2);
var_dump($sale);
// save sale
SaleMapper::save($sale);
} catch (Exception $e) {
echo $e->getMessage();
}
- var_dump of the sales object
object(classes\Sale)[4]
private int 'id' => *uninitialized*
private array 'items' =>
array (size=2)
0 =>
array (size=2)
0 => int 14
1 =>
object(classes\Product)[2]
private mixed 'data' =>
array (size=2)
'id' => int 1
'price' => int 12
1 =>
array (size=2)
0 => int 20
1 =>
object(classes\Product)[3]
private mixed 'data' =>
array (size=2)
'id' => int 2
'price' => int 16
- Sale Table
- Sale_Item Table
So this was the Data Mapper pattern whose objective is to have a class so that it saves a package of classes that have a non-trivial relationship.