Datamapper pattern in PHP
The object-relational gap is a general problem in every OO programming language. Since PHP joined the OO club a while ago this problem also came along with it.
What I mean by object relational gap is the difference between a row in a relational database and an object in an OO language. Even though they have a lot in common, they aren’t the same thing and an OO programmer has to solve this by mapping rows to real objects. In an ideal world a programmer would only be concerned about objects and not database queries (and all the details that come with it) and many smart people have been trying to find the perfect solution to realize this. The result is many persistency frameworks that require the programmer to meta-tag class attributes to map them to database columns or write schemas to do the mapping. Some examples include JDO, JPA, (N)Hibernate, PDO and so on. In my opinion at this moment Microsoft offers the best solution with Linq, because it truly became part of the programming language and the programmer only has to deal with objects, while syntax checking happens at programming/compile time instead of runtime. Hopefully other languages (including PHP will follow), but for now we have to suffice with other solutions.
One of those solutions is the Datamapper design pattern which can be applied in every OO language. The UML schema (may) look like this:
In this example we have a class called UserMapper which has the responsibility to retrieve and store User objects. So basically it handles the mapping of a database row(s) to a User object. The different findBy* methods perform a SELECT query to find matching rows and then use the create(from the abstract superclass) and populate methods to create an object and fill it with the retrieved data.
The save method in the abstract superclass usually checks whether the id attribute of $obj has a value. If not it calls the private implementation of _insert and otherwise _update. Those specifics are up to you as the programmer/architect, but the main idea here is to keep the datamapping logic separated from the (business)logic, which is a good thing. If you want to get real fancy you can add an extra layer, often called the Gateway layer, which contains the actual database queries. You pass a gateway object (i.e. UserGateway) into the mapper class and just have the mapper class call the gateway instance to perform the database queries. In that case the schema would look more like this:
In this scenario you could switch one gateway layer for another, for instance if you want to support a different type of database.
Anyway the version without the extra gateway layer would like something like this in PHP (using Zend_Db for the database adapter):
MapperAbstract:
abstract class MapperAbstract { /** * @var Zend_Db_Adapter_Pdo_Mysql */ private $dbAdapter; /** * Create a new instance of the domain object. If the $data parameter is specified * then the object will be populated with it. * @param array $data * @return DomainObjectAbstract */ public function create(array $data = null) { $obj = $this->_create(); if ($data) { $obj = $this->populate($obj, $data); } return $obj; } /** * Save the domain object * * @param DomainObjectAbstract $obj */ public function save(Application_Model_Abstract $obj) { if (is_null($obj->getId())) { $this->_insert($obj); } else { $this->_update($obj); } } /** * Delete the domain object * @param DomainObjectAbstract $obj */ public function delete(Application_Model_Abstract $obj) { $this->_delete($obj); } /** * Populate the domain object with the values from the data array. * * @param DomainObjectAbstract $obj * @param array $data * @return DomainObjectAbstract */ abstract public function populate(Application_Model_Abstract $obj, array $data); /** * Create a new instance of a domain object * * @return DomainObjectAbstract */ abstract protected function _create(); /** * Insert the domain object into the database * * @param DomainObjectAbstract $obj */ abstract protected function _insert(Application_Model_Abstract $obj); /** * Update the domain object in persistent storage * * @param DomainObjectAbstract $obj */ abstract protected function _update(Application_Model_Abstract $obj); /** * Delete the domain object from peristent Storage * * @param DomainObjectAbstract $obj */ abstract protected function _delete(Application_Model_Abstract $obj); }
class UserMapper extends MapperAbstract { private $dbAdapter; public function __construct($dbAdapter) { $this->dbAdapter = $dbAdapter; } /** * Create a new User domainobject */ protected function _create() { return new User(); } /** * Insert the DomainObject in persistent storage * * @param DomainObjectAbstract $obj */ protected function _insert(DomainAbstract $obj) { // Insert User object into database /* @var $obj Application_Model_User */ $query = 'INSERT INTO users (username,password) VALUES (?,SHA1(?))'; $ret = $this->dbAdapter->query($query, array($obj->getUsername(), $obj->getPassword()) ); // Assign the new user id to the User object $obj->setId($this->dbAdapter->lastInsertId()); } /** * Update * * @param DomainObjectAbstract $obj */ protected function _update(DomainAbstract $obj) { // Update User object in database $query = 'UPDATE users SET username=?,password=SHA1(?) WHERE >; $ret = $this->dbAdapter->query($query, array($obj->getUsername(), $obj->getPassword(), $obj->getId() )); } /** * Delete the User from the database * * @param DomainObjectAbstract $obj */ protected function _delete(DomainAbstract $obj) { $query = 'DELETE FROM users WHERE >; $ret = $this->dbAdapter->query($query, array($obj->getId())); } /** * Populate the User (DomainObject) with the data array. * * @param DomainObjectAbstract $obj * @param array $data * @return User */ public function populate(DomainAbstract $obj, array $data) { if (isset($data['id'])) { $obj->setId($data['id']); } if (isset($data['username'])) { $obj->setUsername($data['username']); } if (isset($data['password'])) { $obj->setPassword($data['password']); } return $obj; } /** * Retrieves user by userid * * @param int $userId * @return User (null if not found) */ public function findById($userId) { $sql = "SELECT id,username,password FROM users WHERE >; $data = $this->dbAdapter->fetchRow($sql, array($userId), Zend_Db::FETCH_ASSOC); $user = null; if ($data != false) { $user = $this->create($data); } return $user; } /** * Retrieves user by username * * @param string $username * @return User (null if not found) */ public function findByUsername($username) { $sql = "SELECT id,username,password FROM users WHERE username=?"; $data = $this->dbAdapter->fetchRow($sql, array($username), Zend_Db::FETCH_ASSOC); $user = null; if ($data != false) { $user = $this->create(); } return $user; } }
How to use this? To retrieve a user in this setup you have to instantiate a UserMapper object and then ask it to retrieve and create the user domain object for you.
$userMapper = new UserMapper($dbAdapter); $user = $userMapper->findByUsername('ruben');
Notice how easy it becomes to retrieve a domain object this way. It keeps your business logic code from getting cluttered with dirty database and object mapping details. The same goes for storing the object into the database:
$userMapper = new UserMapper($dbAdapter); $user = $userMapper->findByUsername('ruben'); $user->setPassword('newpassword'); $userMapper->save($user);
The UserMapper class will figure out whether it has to perform an insert or an update query and do all the hard work.
2.4. Преобразователь Данных (Data Mapper)
Преобразователь Данных — это паттерн, который выступает в роли посредника для двунаправленной передачи данных между постоянным хранилищем данных (часто, реляционной базы данных) и представления данных в памяти (слой домена, то что уже загружено и используется для логической обработки). Цель паттерна в том, чтобы держать представление данных в памяти и постоянное хранилище данных независимыми друг от друга и от самого преобразователя данных. Слой состоит из одного или более mapper-а (или объектов доступа к данным), отвечающих за передачу данных. Реализации mapper-ов различаются по назначению. Общие mapper-ы могут обрабатывать всевозоможные типы сущностей доменов, а выделенные mapper-ы будет обрабатывать один или несколько конкретных типов.
Ключевым моментом этого паттерна, в отличие от Активной Записи (Active Records) является то, что модель данных следует Принципу Единой Обязанности SOLID.
2.4.2. Примеры
2.4.3. Диаграмма UML
2.4.4. Код
Вы можете найти этот код на GitHub
1 2 3declare(strict_types=1); 4 5namespace DesignPatterns\Structural\DataMapper; 6 7class User 8 9 public static function fromState(array $state): User 10 11 // validate state before accessing keys! 12 13 return new self( 14 $state['username'], 15 $state['email'] 16 ); 17 > 18 19 public function __construct(private string $username, private string $email) 20 21 > 22 23 public function getUsername(): string 24 25 return $this->username; 26 > 27 28 public function getEmail(): string 29 30 return $this->email; 31 > 32>
1 2 3declare(strict_types=1); 4 5namespace DesignPatterns\Structural\DataMapper; 6 7use InvalidArgumentException; 8 9class UserMapper 10 11 public function __construct(private StorageAdapter $adapter) 12 13 > 14 15 /** 16 * finds a user from storage based on ID and returns a User object located 17 * in memory. Normally this kind of logic will be implemented using the Repository pattern. 18 * However the important part is in mapRowToUser() below, that will create a business object from the 19 * data fetched from storage 20 */ 21 public function findById(int $id): User 22 23 $result = $this->adapter->find($id); 24 25 if ($result === null) 26 throw new InvalidArgumentException("User #$id not found"); 27 > 28 29 return $this->mapRowToUser($result); 30 > 31 32 private function mapRowToUser(array $row): User 33 34 return User::fromState($row); 35 > 36>
1 2 3declare(strict_types=1); 4 5namespace DesignPatterns\Structural\DataMapper; 6 7class StorageAdapter 8 9 public function __construct(private array $data) 10 11 > 12 13 /** 14 * @return array|null 15 */ 16 public function find(int $id) 17 18 if (isset($this->data[$id])) 19 return $this->data[$id]; 20 > 21 22 return null; 23 > 24>
2.4.5. Тест
1 2 3declare(strict_types=1); 4 5namespace DesignPatterns\Structural\DataMapper\Tests; 6 7use InvalidArgumentException; 8use DesignPatterns\Structural\DataMapper\StorageAdapter; 9use DesignPatterns\Structural\DataMapper\User; 10use DesignPatterns\Structural\DataMapper\UserMapper; 11use PHPUnit\Framework\TestCase; 12 13class DataMapperTest extends TestCase 14 15 public function testCanMapUserFromStorage() 16 17 $storage = new StorageAdapter([1 => ['username' => 'domnikl', 'email' => 'liebler.dominik@gmail.com']]); 18 $mapper = new UserMapper($storage); 19 20 $user = $mapper->findById(1); 21 22 $this->assertInstanceOf(User::class, $user); 23 > 24 25 public function testWillNotMapInvalidData() 26 27 $this->expectException(InvalidArgumentException::class); 28 29 $storage = new StorageAdapter([]); 30 $mapper = new UserMapper($storage); 31 32 $mapper->findById(1); 33 > 34>
© Copyright 2011-2020, Dominik Liebler and contributors. Ревизия 9e2553fe .