PHPデザインパターン: データマッパー
データマッパーって何?
オブジェクト指向において、オブジェクト間には単純な関係(オブジェクトが別のオブジェクトに関連する)だけでなく、継承、構成、集約などの複雑な関係性が一般的です。下に示すのは、商品との関係性を持つ販売で、これにより集約が形成されています。
この関係をデータベースに持っていくと、販売テーブルだけでなく販売アイテムテーブルもあるため、販売が記録される際には、販売アイテムも同時に記録されることが望ましいです。
もしテーブルデータゲートウェイやアクティブレコードのパターンをこの場合に適用させると、販売と販売アイテムを別々に記録する必要がありますが、データマッパーパターンはこの問題を解決するために登場します。それは、複雑なオブジェクトを受け取るか、複雑な関係を持つ「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;
}
- setIDとgetIDメソッド
/**
* 販売IDの値を受け取ります。
*/
public function setID(int $id): void
{
$this->id = $id;
}
/**
* 販売IDを返します。
*/
public function getID(): int
{
return $this->id;
}
- addItemとgetItemsメソッド
/**
* 数量と製品のオブジェクトを受け取ります。
*/
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
- 販売テーブル
- 販売アイテムテーブル
これがデータマッパーパターンで、その目的は、単純でない関係を持つクラスのパッケージを保存するクラスを持つことです。
こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/xxzeroxx/php-design-patterns-data-mapper-540l