PHP Design Patterns: Data Mapper

Antonio Silva - Oct 15 '23 - - Dev Community

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.

Data Mapper

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
Enter fullscreen mode Exit fullscreen mode

Step 2 - Database Config File:

host = 
name = database/DataMapper.db
user = 
pass = 
type = sqlite
Enter fullscreen mode Exit fullscreen mode

Step 3 - Database:

  • Sale Table
CREATE TABLE sale(
  id INTEGER PRIMARY KEY NOT NULL,
  sale_date DATE
 );
Enter fullscreen mode Exit fullscreen mode
  • 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
);
Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode
  • __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;
    }
Enter fullscreen mode Exit fullscreen mode

Step 5 - Sale Class:

<?php

namespace classes;

class Sale
{
    /**
     * id property of integer type.
     */
    private int $id;

    /**
     * @var mixed[]
     */
    private array $items;
}

Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
Enter fullscreen mode Exit fullscreen mode
  • 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);
Enter fullscreen mode Exit fullscreen mode
        // 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,
            ]);
        }
    }
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
    // 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
Enter fullscreen mode Exit fullscreen mode
    // 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();
}
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • Sale Table

Table Sale

  • Sale_Item Table

Table Sale_Item

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .