PHP Design Patterns: Data Mapper
Tags: php, designpatterns
Published: 2023-10-15 04:47:49

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fb2bd5k6gqilkuht652z.png)

If the [Table Data Gateway](https://dev.to/xxzeroxx/php-design-patterns-table-data-gateway-43jj) and [Active Record](https://dev.to/xxzeroxx/php-design-patterns-active-record-1i5) 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:**

```bash
host = 
name = database/DataMapper.db
user = 
pass = 
type = sqlite
```

**Step 3 - Database:**

- **Sale Table**

```sql
CREATE TABLE sale(
  id INTEGER PRIMARY KEY NOT NULL,
  sale_date DATE
 );
```

- **SaleItem Table**

```sql
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
<?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

```php
    /**
     * 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
<?php

namespace classes;

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

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

```

- **setID** and **getID** methods

```php
    /**
     * 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

```php
    /**
     * 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
<?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

```php
    /**
     * 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

```php
    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

```php
    // 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);
```

```php
        // 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
<?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);
```

```php
    // 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
```

```php
    // 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**

```php
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**

![Table Sale](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jx5ey53lu47g3bahiyef.png)

- **Sale_Item Table**

![Table Sale_Item](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r8fyfbv3qd2s6vqb441k.png)


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.