数据映射模式

目的

数据映射器,是一个数据访问层,它在持久数据存储(通常是关系数据库)和内存数据(领域层)之间执行数据的双向传输。该模式的目标是使内存中的数据和持久层中的数据存储相互独立,并保持数据映射器本身的独立性。领域层由一个或多个映射器(或数据访问对象)组成,并执行数据传输。数据映射器的实现方式各不相同。通用映射器将处理许多不同领域的实体类型,专用映射器只处理一个或少量实体类型。

与活动记录(Active Record)模式不同,数据映射模式的关键点在于:数据模型遵循单一职责原则。

使用场景

  • 数据库对象关系映射(ORM):Doctrine2 使用名为 EntityRepository 的 DAO

UML 类图

数据映射模式类图

代码

User.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php declare(strict_types = 1);

namespace DesignPatterns\Extend\DataMapper;

class User
{
private string $username;
private string $email;

public static function fromState(array $state): User
{
// validate state before accessing keys!
return new self(
$state['username'],
$state['email']
);
}

public function __construct(string $username, string $email)
{
// validate parameters before setting them!
$this->username = $username;
$this->email = $email;
}

public function getUsername(): string
{
return $this->username;
}

public function getEmail(): string
{
return $this->email;
}
}

UserMapper.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php declare(strict_types = 1);

namespace DesignPatterns\Extend\DataMapper;

use InvalidArgumentException;

class UserMapper
{
private StorageAdapter $adapter;

public function __construct(StorageAdapter $storage)
{
$this->adapter = $storage;
}

/**
* finds a user from storage based on ID and returns a User object located
* in memory. Normally this kind of logic will be implemented using the Repository pattern.
* However the important part is in mapRowToUser() below, that will create a business object from the
* data fetched from storage
*/
public function findById(int $id): User
{
$result = $this->adapter->find($id);

if ($result === null) {
throw new InvalidArgumentException("User #$id not found");
}

return $this->mapRowToUser($result);
}

private function mapRowToUser(array $row): User
{
return User::fromState($row);
}
}

StorageAdapter.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php declare(strict_types = 1);

namespace DesignPatterns\Extend\DataMapper;

class StorageAdapter
{
private array $data = [];

public function __construct(array $data)
{
$this->data = $data;
}

/**
* @param int $id
*
* @return array|null
*/
public function find(int $id)
{
if (isset($this->data[$id])) {
return $this->data[$id];
}

return null;
}
}

测试

Tests/DataMapperTest.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php declare(strict_types = 1);

namespace DesignPatterns\Extend\DataMapper\Tests;

use InvalidArgumentException;
use DesignPatterns\Extend\DataMapper\StorageAdapter;
use DesignPatterns\Extend\DataMapper\User;
use DesignPatterns\Extend\DataMapper\UserMapper;
use PHPUnit\Framework\TestCase;

class DataMapperTest extends TestCase
{
public function testCanMapUserFromStorage()
{
$storage = new StorageAdapter([1 => ['username' => 'domnikl', 'email' => 'liebler.dominik@gmail.com']]);
$mapper = new UserMapper($storage);

$user = $mapper->findById(1);

$this->assertInstanceOf(User::class, $user);
}

public function testWillNotMapInvalidData()
{
$this->expectException(InvalidArgumentException::class);

$storage = new StorageAdapter([]);
$mapper = new UserMapper($storage);

$mapper->findById(1);
}
}