PHPデザインパターン: データマッパー

データマッパーって何?

オブジェクト指向において、オブジェクト間には単純な関係(オブジェクトが別のオブジェクトに関連する)だけでなく、継承構成集約などの複雑な関係性が一般的です。下に示すのは、商品との関係性を持つ販売で、これにより集約が形成されています。

この関係をデータベースに持っていくと、販売テーブルだけでなく販売アイテムテーブルもあるため、販売が記録される際には、販売アイテムも同時に記録されることが望ましいです。

Data Mapper

もしテーブルデータゲートウェイアクティブレコードのパターンをこの場合に適用させると、販売と販売アイテムを別々に記録する必要がありますが、データマッパーパターンはこの問題を解決するために登場します。それは、複雑なオブジェクトを受け取るか、複雑な関係を持つ「SaleMapper」といったクラスを持つ、永続化メソッド(永続性に関するメソッド)が含まれています。

データマッパーはパッケージ全体のオブジェクトの永続化を担当し、一度にすべてを記録、読み取り、または削除できるクラスです。上記の例でいうと、SaleMapperはSaleとその関係網全体を同時に記録します。この場合は販売アイテムもです。

以下にデータマッパーがどのように機能するかの例を示します。

ステップ1 - ディレクトリシステム:

📦Data_Mapper
 ┣ 📂classes
 ┃ ┣ 📜Product.php
 ┃ ┣ 📜Sale.php
 ┃ ┗ 📜SaleMapper.php
 ┣ 📂config
 ┃ ┗ 📜config.ini
 ┣ 📂database
 ┃ ┗ 📜DataMapper.db
 ┗ 📜index.php

ステップ2 - データベース設定ファイル:

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

ステップ3 - データベース:

  • 販売テーブル
CREATE TABLE sale(
  id INTEGER PRIMARY KEY NOT NULL,
  sale_date DATE
 );

  • 販売アイテムテーブル
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
);

ステップ4 - 商品クラス:

<?php

namespace classes;

class Product
{
    /**
     * 複数の型を取り得るため、
     * data変数はmixedとして定義されています。
     */
    private mixed $data;
}

  • __getと**__set**メソッド
    /**
     * プロパティ名を受け取り、キーとして
     * data属性に渡します。
     */
    public function __get(string $prop): mixed
    {
        return $this->data[$prop];
    }

    /**
     * それぞれプロパティ名と値を受け取ります。
     */
    public function __set(string $prop, mixed $value): void
    {
        $this->data[$prop] = $value;
    }

ステップ5 - 販売クラス:

<?php

namespace classes;

class Sale
{
    /**
     * 整数型のidプロパティ。
     */
    private int $id;

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

  • setIDgetIDメソッド
    /**
     * 販売IDの値を受け取ります。
     */
    public function setID(int $id): void
    {
        $this->id = $id;
    }

    /**
     * 販売IDを返します。
     */
    public function getID(): int
    {
        return $this->id;
    }

  • addItemgetItemsメソッド
    /**
     * 数量と製品のオブジェクトを受け取ります。
     */
    public function addItem(int $quantity, object $product): void
    {
        /*
         * 数量と製品データでitems配列を埋めます。
         */
        $this->items[] = [$quantity, $product];
    }

    /**
     * @return mixed[]
     */
    public function getItems(): array
    {
        return $this->items;
    }

ステップ6 - 販売Mapperクラス:

<?php

namespace classes;

class SaleMapper
{
    /**
     * connのプロパティは静的であり、値を維持するために、
     * 同じ接続を複数回開く必要はありません。
     */
    private static $conn;

    /**
     * データ変数は複数の型を取り得るため、
     * mixedとして定義されています。
     */
    private mixed $data;
}

  • setConnectionメソッド
    /**
     * setConnectionはパラメータとしてPDO接続を受け取り、
     * 静的属性connに格納します。
     */
    public static function setConnection(PDO $conn)
    {
        self::$conn = $conn;
    }

  • getLastIDメソッド
    private static function getLastID(): int
    {
        // max()関数を使用して最後に生成されたIDを取得
        $sql = 'SELECT max(id) as max FROM sale';
        // ステートメントを準備
        $result = self::$conn->prepare($sql);
        // ステートメントを実行
        $result->execute();
        // 次の行をフェッチし、オブジェクトとして返す
        $data = $result->fetchObject();

        // IDを返す
        return $data->max;
    }

  • saveメソッド
    // パラメータとして販売を受け取る
    public static function save(Sale $sale)
    {
        // 販売日のデータを生成
        $sale_date = date('Y-m-d');

        // 販売表に情報を挿入
        $sql = 'INSERT INTO sale (sale_date) VALUES (:sale_date)';
        // prepareメソッドを実行
        $result = self::$conn->prepare($sql);
        // パラメータを指定された変数名にバインド
        $result->bindParam(':sale_date', $sale_date);
        // SQLステートメントを実行
        $result->execute();

        // 販売IDを取得
        $id_sale = self::getLastID();

        // データベースに生成されたIDを販売オブジェクトのIDに保存
        $sale->setID($id_sale);

        // 販売アイテムをチェック
        foreach ($sale->getItems() as $items) {
            // 位置0には商品の数量がある
            $quantity = $items[0];
            // 位置1には商品がある
            $product = $items[1];
            // 商品IDを保存
            $id_product = $product->id;
            // 商品の価格を保存
            $price = $product->price;

            // sale_item表で挿入を実行
            $sql = 'INSERT INTO sale_item (id_product, id_sale, quantity, price)
                    VALUES (:id_product, :id_sale, :quantity, :price)';

            // prepareメソッドを実行
            $result = self::$conn->prepare($sql);
            // SQLステートメントを実行
            $result->execute([
                ':id_product' => $id_product,
                ':id_sale' => $id_sale,
                ':quantity' => $quantity,
                ':price' => $price,
            ]);
        }
    }

テスト

<?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インスタンス
    $p1 = new Product();
    // product1のID
    $p1->id = 1;
    // product1の価格
    $p1->price = 12;

    // product2インスタンス
    $p2 = new Product();
    // product2のID
    $p2->id = 2;
    // product2の価格
    $p2->price = 16;

    // 販売インスタンス
    $sale = new Sale();

    // 商品を追加(数量、製品)
    $sale->addItem(14, $p1);
    $sale->addItem(20, $p2);

    var_dump($sale);

    // 販売を保存
    SaleMapper::save($sale);
} catch (Exception $e) {
    echo $e->getMessage();
}

  • 販売オブジェクトのvar_dump
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

  • 販売テーブル

Table Sale

  • 販売アイテムテーブル

Table Sale_Item

これがデータマッパーパターンで、その目的は、単純でない関係を持つクラスのパッケージを保存するクラスを持つことです。

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/xxzeroxx/php-design-patterns-data-mapper-540l