Хотя по-прежнему существует огромное количество случаев, когда доменные модели считаются избыточным «корпоративным» решением, которое не сочетается с естественным прагматизмом, распространяющимся по всему миру PHP, они постоянно нарушают умы многих разработчиков, даже тех, кто которые цепляются за парадигму модели базы данных, как последний спасательный жилет тонущего корабля.
Есть несколько причин, которые во многом оправдывают такую реакцию. В конце концов, построение даже самой простой предметной модели требует определения ограничений, правил и отношений между ее строительными объектами, как они будут вести себя в данном контексте, и какой тип данных они будут нести в течение своего жизненного цикла. Кроме того, процесс передачи данных модели в хранилище и из него, вероятно, в какой-то момент потребует отбрасывания набора картографических данных , и этот факт подчеркивает, почему доменные модели часто окружены облаком издевательств.
Однако нетерпеливые предрассудки вводят в заблуждение. Кости богатой доменной модели, безусловно, будут более комфортно размещаться в границах большого приложения, но их можно уменьшить и получить максимальную отдачу от них в небольших средах. Чтобы продемонстрировать это, в моей предыдущей статье я показал вам, как реализовать простую модель домена блога, состоящую из нескольких постов, комментариев и пользовательских объектов.
В предыдущей статье не было настоящего счастливого конца; он просто показал механику модели, а не то, как заставить ее работать синхронно с «настоящим» постоянным слоем. Поэтому, прежде чем вы бросите меня во львов за такое невежливое отношение, в этом продолжении мы разработаем базовый модуль отображения, который позволит вам легко перемещать данные между моделью блога и базой данных MySQL, сохраняя их аккуратно изолированы друг от друга.
Создание Naive DAL (или почему мой адаптер PDO лучше вашего)
Я знаю, что эта фраза может звучать как дешевое клише, но мне не особо интересно заново изобретать колесо каждый раз, когда я решаю проблему с программным обеспечением (если, конечно, мне не нужно более красивое и быстрое колесо). В этом случае ситуация требует дополнительных усилий, учитывая, что мы попытаемся подключить группу классов сопоставления к модели домена блога. Учитывая масштаб усилий, идея состоит в том, чтобы с нуля настроить базовый уровень доступа к данным (DAL), чтобы объекты домена можно было легко сохранить в базе данных MySQL и, в свою очередь, получать по запросу через некоторые общие средства поиска.
Рассматриваемый DAL будет состоять всего из нескольких компонентов: первым будет простой интерфейс адаптера базы данных, контракт которого выглядит следующим образом:
<?php namespace LibraryDatabase; interface DatabaseAdapterInterface { public function connect(); public function disconnect(); public function prepare($sql, array $options = array()); public function execute(array $parameters = array()); public function fetch($fetchStyle = null, $cursorOrientation = null, $cursorOffset = null); public function fetchAll($fetchStyle = null, $column = 0); public function select($table, array $bind, $boolOperator = "AND"); public function insert($table, array $bind); public function update($table, array $bind, $where = ""); public function delete($table, $where = ""); }
Несомненно, выше DatabaseAdapterInterface
является укротимым существом. Его контракт позволяет нам создавать различные адаптеры базы данных во время выполнения и выполнять несколько общих задач, таких как подключение к базе данных и выполнение операций CRUD без особых хлопот.
Теперь нам нужен по крайней мере один разработчик интерфейса, который делает все эти крутые вещи. Гордым кавалером, который возьмет на себя эту ответственность, станет неканонический адаптер PDO, который выглядит следующим образом:
<?php namespace LibraryDatabase; class PdoAdapter implements DatabaseAdapterInterface { protected $config = array(); protected $connection; protected $statement; protected $fetchMode = PDO::FETCH_ASSOC; public function __construct($dsn, $username = null, $password = null, array $driverOptions = array()) { $this->config = compact("dsn", "username", "password", "driverOptions"); } public function getStatement() { if ($this->statement === null) { throw new PDOException( "There is no PDOStatement object for use."); } return $this->statement; } public function connect() { // if there is a PDO object already, return early if ($this->connection) { return; } try { $this->connection = new PDO( $this->config["dsn"], $this->config["username"], $this->config["password"], $this->config["driverOptions"]); $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->connection->setAttribute( PDO::ATTR_EMULATE_PREPARES, false); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function disconnect() { $this->connection = null; } public function prepare($sql, array $options = array() { $this->connect(); try { $this->statement = $this->connection->prepare($sql, $options); return $this; } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function execute(array $parameters = array()) { try { $this->getStatement()->execute($parameters); return $this; } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function countAffectedRows() { try { return $this->getStatement()->rowCount(); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function getLastInsertId($name = null) { $this->connect(); return $this->connection->lastInsertId($name); } public function fetch($fetchStyle = null, $cursorOrientation = null, $cursorOffset = null) { if ($fetchStyle === null) { $fetchStyle = $this->fetchMode; } try { return $this->getStatement()->fetch($fetchStyle, $cursorOrientation, $cursorOffset); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function fetchAll($fetchStyle = null, $column = 0) { if ($fetchStyle === null) { $fetchStyle = $this->fetchMode; } try { return $fetchStyle === PDO::FETCH_COLUMN ? $this->getStatement()->fetchAll($fetchStyle, $column) : $this->getStatement()->fetchAll($fetchStyle); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function select($table, array $bind = array(), $boolOperator = "AND") { if ($bind) { $where = array(); foreach ($bind as $col => $value) { unset($bind[$col]); $bind[":" . $col] = $value; $where[] = $col . " = :" . $col; } } $sql = "SELECT * FROM " . $table . (($bind) ? " WHERE " . implode(" " . $boolOperator . " ", $where) : " "); $this->prepare($sql) ->execute($bind); return $this; } public function insert($table, array $bind) { $cols = implode(", ", array_keys($bind)); $values = implode(", :", array_keys($bind)); foreach ($bind as $col => $value) { unset($bind[$col]); $bind[":" . $col] = $value; } $sql = "INSERT INTO " . $table . " (" . $cols . ") VALUES (:" . $values . ")"; return (int) $this->prepare($sql) ->execute($bind) ->getLastInsertId(); } public function update($table, array $bind, $where = "") { $set = array(); foreach ($bind as $col => $value) { unset($bind[$col]); $bind[":" . $col] = $value; $set[] = $col . " = :" . $col; } $sql = "UPDATE " . $table . " SET " . implode(", ", $set) . (($where) ? " WHERE " . $where : " "); return $this->prepare($sql) ->execute($bind) ->countAffectedRows(); } public function delete($table, $where = "") { $sql = "DELETE FROM " . $table . (($where) ? " WHERE " . $where : " "); return $this->prepare($sql) ->execute() ->countAffectedRows(); } }
с<?php namespace LibraryDatabase; class PdoAdapter implements DatabaseAdapterInterface { protected $config = array(); protected $connection; protected $statement; protected $fetchMode = PDO::FETCH_ASSOC; public function __construct($dsn, $username = null, $password = null, array $driverOptions = array()) { $this->config = compact("dsn", "username", "password", "driverOptions"); } public function getStatement() { if ($this->statement === null) { throw new PDOException( "There is no PDOStatement object for use."); } return $this->statement; } public function connect() { // if there is a PDO object already, return early if ($this->connection) { return; } try { $this->connection = new PDO( $this->config["dsn"], $this->config["username"], $this->config["password"], $this->config["driverOptions"]); $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->connection->setAttribute( PDO::ATTR_EMULATE_PREPARES, false); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function disconnect() { $this->connection = null; } public function prepare($sql, array $options = array() { $this->connect(); try { $this->statement = $this->connection->prepare($sql, $options); return $this; } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function execute(array $parameters = array()) { try { $this->getStatement()->execute($parameters); return $this; } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function countAffectedRows() { try { return $this->getStatement()->rowCount(); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function getLastInsertId($name = null) { $this->connect(); return $this->connection->lastInsertId($name); } public function fetch($fetchStyle = null, $cursorOrientation = null, $cursorOffset = null) { if ($fetchStyle === null) { $fetchStyle = $this->fetchMode; } try { return $this->getStatement()->fetch($fetchStyle, $cursorOrientation, $cursorOffset); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function fetchAll($fetchStyle = null, $column = 0) { if ($fetchStyle === null) { $fetchStyle = $this->fetchMode; } try { return $fetchStyle === PDO::FETCH_COLUMN ? $this->getStatement()->fetchAll($fetchStyle, $column) : $this->getStatement()->fetchAll($fetchStyle); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function select($table, array $bind = array(), $boolOperator = "AND") { if ($bind) { $where = array(); foreach ($bind as $col => $value) { unset($bind[$col]); $bind[":" . $col] = $value; $where[] = $col . " = :" . $col; } } $sql = "SELECT * FROM " . $table . (($bind) ? " WHERE " . implode(" " . $boolOperator . " ", $where) : " "); $this->prepare($sql) ->execute($bind); return $this; } public function insert($table, array $bind) { $cols = implode(", ", array_keys($bind)); $values = implode(", :", array_keys($bind)); foreach ($bind as $col => $value) { unset($bind[$col]); $bind[":" . $col] = $value; } $sql = "INSERT INTO " . $table . " (" . $cols . ") VALUES (:" . $values . ")"; return (int) $this->prepare($sql) ->execute($bind) ->getLastInsertId(); } public function update($table, array $bind, $where = "") { $set = array(); foreach ($bind as $col => $value) { unset($bind[$col]); $bind[":" . $col] = $value; $set[] = $col . " = :" . $col; } $sql = "UPDATE " . $table . " SET " . implode(", ", $set) . (($where) ? " WHERE " . $where : " "); return $this->prepare($sql) ->execute($bind) ->countAffectedRows(); } public function delete($table, $where = "") { $sql = "DELETE FROM " . $table . (($where) ? " WHERE " . $where : " "); return $this->prepare($sql) ->execute() ->countAffectedRows(); } }
с<?php namespace LibraryDatabase; class PdoAdapter implements DatabaseAdapterInterface { protected $config = array(); protected $connection; protected $statement; protected $fetchMode = PDO::FETCH_ASSOC; public function __construct($dsn, $username = null, $password = null, array $driverOptions = array()) { $this->config = compact("dsn", "username", "password", "driverOptions"); } public function getStatement() { if ($this->statement === null) { throw new PDOException( "There is no PDOStatement object for use."); } return $this->statement; } public function connect() { // if there is a PDO object already, return early if ($this->connection) { return; } try { $this->connection = new PDO( $this->config["dsn"], $this->config["username"], $this->config["password"], $this->config["driverOptions"]); $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->connection->setAttribute( PDO::ATTR_EMULATE_PREPARES, false); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function disconnect() { $this->connection = null; } public function prepare($sql, array $options = array() { $this->connect(); try { $this->statement = $this->connection->prepare($sql, $options); return $this; } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function execute(array $parameters = array()) { try { $this->getStatement()->execute($parameters); return $this; } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function countAffectedRows() { try { return $this->getStatement()->rowCount(); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function getLastInsertId($name = null) { $this->connect(); return $this->connection->lastInsertId($name); } public function fetch($fetchStyle = null, $cursorOrientation = null, $cursorOffset = null) { if ($fetchStyle === null) { $fetchStyle = $this->fetchMode; } try { return $this->getStatement()->fetch($fetchStyle, $cursorOrientation, $cursorOffset); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function fetchAll($fetchStyle = null, $column = 0) { if ($fetchStyle === null) { $fetchStyle = $this->fetchMode; } try { return $fetchStyle === PDO::FETCH_COLUMN ? $this->getStatement()->fetchAll($fetchStyle, $column) : $this->getStatement()->fetchAll($fetchStyle); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function select($table, array $bind = array(), $boolOperator = "AND") { if ($bind) { $where = array(); foreach ($bind as $col => $value) { unset($bind[$col]); $bind[":" . $col] = $value; $where[] = $col . " = :" . $col; } } $sql = "SELECT * FROM " . $table . (($bind) ? " WHERE " . implode(" " . $boolOperator . " ", $where) : " "); $this->prepare($sql) ->execute($bind); return $this; } public function insert($table, array $bind) { $cols = implode(", ", array_keys($bind)); $values = implode(", :", array_keys($bind)); foreach ($bind as $col => $value) { unset($bind[$col]); $bind[":" . $col] = $value; } $sql = "INSERT INTO " . $table . " (" . $cols . ") VALUES (:" . $values . ")"; return (int) $this->prepare($sql) ->execute($bind) ->getLastInsertId(); } public function update($table, array $bind, $where = "") { $set = array(); foreach ($bind as $col => $value) { unset($bind[$col]); $bind[":" . $col] = $value; $set[] = $col . " = :" . $col; } $sql = "UPDATE " . $table . " SET " . implode(", ", $set) . (($where) ? " WHERE " . $where : " "); return $this->prepare($sql) ->execute($bind) ->countAffectedRows(); } public function delete($table, $where = "") { $sql = "DELETE FROM " . $table . (($where) ? " WHERE " . $where : " "); return $this->prepare($sql) ->execute() ->countAffectedRows(); } }
с<?php namespace LibraryDatabase; class PdoAdapter implements DatabaseAdapterInterface { protected $config = array(); protected $connection; protected $statement; protected $fetchMode = PDO::FETCH_ASSOC; public function __construct($dsn, $username = null, $password = null, array $driverOptions = array()) { $this->config = compact("dsn", "username", "password", "driverOptions"); } public function getStatement() { if ($this->statement === null) { throw new PDOException( "There is no PDOStatement object for use."); } return $this->statement; } public function connect() { // if there is a PDO object already, return early if ($this->connection) { return; } try { $this->connection = new PDO( $this->config["dsn"], $this->config["username"], $this->config["password"], $this->config["driverOptions"]); $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->connection->setAttribute( PDO::ATTR_EMULATE_PREPARES, false); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function disconnect() { $this->connection = null; } public function prepare($sql, array $options = array() { $this->connect(); try { $this->statement = $this->connection->prepare($sql, $options); return $this; } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function execute(array $parameters = array()) { try { $this->getStatement()->execute($parameters); return $this; } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function countAffectedRows() { try { return $this->getStatement()->rowCount(); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function getLastInsertId($name = null) { $this->connect(); return $this->connection->lastInsertId($name); } public function fetch($fetchStyle = null, $cursorOrientation = null, $cursorOffset = null) { if ($fetchStyle === null) { $fetchStyle = $this->fetchMode; } try { return $this->getStatement()->fetch($fetchStyle, $cursorOrientation, $cursorOffset); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function fetchAll($fetchStyle = null, $column = 0) { if ($fetchStyle === null) { $fetchStyle = $this->fetchMode; } try { return $fetchStyle === PDO::FETCH_COLUMN ? $this->getStatement()->fetchAll($fetchStyle, $column) : $this->getStatement()->fetchAll($fetchStyle); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function select($table, array $bind = array(), $boolOperator = "AND") { if ($bind) { $where = array(); foreach ($bind as $col => $value) { unset($bind[$col]); $bind[":" . $col] = $value; $where[] = $col . " = :" . $col; } } $sql = "SELECT * FROM " . $table . (($bind) ? " WHERE " . implode(" " . $boolOperator . " ", $where) : " "); $this->prepare($sql) ->execute($bind); return $this; } public function insert($table, array $bind) { $cols = implode(", ", array_keys($bind)); $values = implode(", :", array_keys($bind)); foreach ($bind as $col => $value) { unset($bind[$col]); $bind[":" . $col] = $value; } $sql = "INSERT INTO " . $table . " (" . $cols . ") VALUES (:" . $values . ")"; return (int) $this->prepare($sql) ->execute($bind) ->getLastInsertId(); } public function update($table, array $bind, $where = "") { $set = array(); foreach ($bind as $col => $value) { unset($bind[$col]); $bind[":" . $col] = $value; $set[] = $col . " = :" . $col; } $sql = "UPDATE " . $table . " SET " . implode(", ", $set) . (($where) ? " WHERE " . $where : " "); return $this->prepare($sql) ->execute($bind) ->countAffectedRows(); } public function delete($table, $where = "") { $sql = "DELETE FROM " . $table . (($where) ? " WHERE " . $where : " "); return $this->prepare($sql) ->execute() ->countAffectedRows(); } }
Не стесняйтесь проклинать меня за то, что я бросил в вас такой жирный код, но это было неизбежное зло. Более того, даже несмотря на то, что класс PdoAdapter
выглядит несколько запутанным, на самом деле это простая оболочка, которая использует большую часть функциональности, которую PDO предлагает сразу, не подвергая клиентский код многословному API.
Теперь, когда PdoAdapter
выполняет за нас грязную базу данных, давайте создадим несколько таблиц MySQL, в которых будут храниться данные, соответствующие сообщениям в блогах, комментариям и пользователям:
CREATE TABLE posts ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(100) DEFAULT NULL, content TEXT, PRIMARY KEY (id) ); CREATE TABLE users ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(45) DEFAULT NULL, email VARCHAR(45) DEFAULT NULL, PRIMARY KEY (id) ); CREATE TABLE comments ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, content TEXT, user_id INTEGER DEFAULT NULL, post_id INTEGER DEFAULT NULL, PRIMARY KEY (id), FOREIGN KEY (user_id) REFERENCES users(id), FOREGIN KEY (post_id) REFERENCES posts(id) );
Приведенная выше схема базы данных определяет отношение «один ко многим» между сообщениями и комментариями и отношение «один к одному» между комментариями и пользователями (комментаторами блога). Если вы чувствуете себя авантюрным, вы можете настроить схему по своему усмотрению. Однако для краткости я сохранил это так просто.
К этому моменту мы реализовали простой DAL, который мы можем использовать для сохранения модели домена блога в MySQL без чрезмерного потоотделения во время процесса. Теперь нам нужно добавить промежуточных людей к изображению, то есть вышеупомянутым картографам данных, чтобы любые несоответствия импеданса можно было спокойно обработать за кулисами.
Реализация слоя двунаправленного отображения
Конечно, это зависит от контекста, но большую часть времени построение слоя отображения (и, в частности, двунаправленного реляционного) довольно далеко от тривиальности. Процесс не сводится к тому, чтобы просто сказать… эй, я включу эти реляционные картографы во время перерыва на кофе. Вот почему библиотеки ORM, такие как Doctrine, в конце концов живут и дышат. В этом случае, однако, мы хотим использовать смелый кодер, живущий внутри нас, и создать наш собственный набор картографов, чтобы помассировать доменные объекты блога, не сталкиваясь с кривой обучения стороннего пакета.
Давайте начнем с инкапсуляции максимально возможной логики отображения в абстрактный класс , как показано ниже:
<?php namespace ModelMapper; use LibraryDatabaseDatabaseAdapterInterface; abstract class AbstractDataMapper { protected $adapter; protected $entityTable; public function __construct(DatabaseAdapterInterface $adapter) { $this->adapter = $adapter; } public function getAdapter() { return $this->adapter; } public function findById($id) { $this->adapter->select($this->entityTable, array('id' => $id)); if (!$row = $this->adapter->fetch()) { return null; } return $this->createEntity($row); } public function findAll(array $conditions = array()) { $entities = array(); $this->adapter->select($this->entityTable, $conditions); $rows = $this->adapter->fetchAll(); if ($rows) { foreach ($rows as $row) { $entities[] = $this->createEntity($row); } } return $entities; } // Create an entity (implementation delegated to concrete mappers) abstract protected function createEntity(array $row); }
Класс выделяет за парой общих искателей всю логику, необходимую для извлечения данных из указанной таблицы, которая затем используется для восстановления объектов домена в допустимом состоянии. Поскольку воссоздания должны быть делегированы по иерархии уточненным реализациям, метод createEntity()
был объявлен абстрактным.
Давайте теперь определим набор конкретных картографов, которые будут работать с сообщениями в блогах, комментариями и пользователями. Вот первый, наряду с интерфейсом, который это реализует:
<?php namespace ModelMapper; use ModelPostInterface; interface PostMapperInterface { public function findById($id); public function findAll(array $conditions = array()); public function insert(PostInterface $post); public function delete($id); }
<?php namespace ModelMapper; use LibraryDatabaseDatabaseAdapterInterface, ModelPostInterface, ModelPost; class PostMapper extends AbstractDataMapper implements PostMapperInterface { protected $commentMapper; protected $entityTable = "posts"; public function __construct(DatabaseAdapterInterface $adapter, CommentMapperInterface $commenMapper) { $this->commentMapper = $commenMapper; parent::__construct($adapter); } public function insert(PostInterface $post) { $post->id = $this->adapter->insert($this->entityTable, array("title" => $post->title, "content" => $post->content)); return $post->id; } public function delete($id) { if ($id instanceof PostInterface) { $id = $id->id; } $this->adapter->delete($this->entityTable, "id = $id"); return $this->commentMapper->delete("post_id = $id"); } protected function createEntity(array $row) { $comments = $this->commentMapper->findAll( array("post_id" => $row["id"])); return new Post($row["title"], $row["content"], $comments); } }
Обратите внимание, что реализация PostMapper
идет по довольно логичному пути. Проще говоря, он не только расширяет своего абстрактного родителя, но и внедряет в конструктор средство отображения комментариев (все еще неопределенное), чтобы обрабатывать синхронно как посты, так и комментарии, не раскрывая внешнему миру сложности создания целого графа объектов. , Конечно, мы не должны отказывать себе в радости от того, как выглядит все еще завуалированный механизм отображения комментариев, поэтому вот его исходный код, связанный с соответствующим интерфейсом:
<?php namespace ModelMapper; use ModelCommentInterface; interface CommentMapperInterface { public function findById($id); public function findAll(array $conditions = array()); public function insert(CommentInterface $comment, $postId, $userId); public function delete($id); }
<?php namespace ModelMapper; use LibraryDatabaseDatabaseAdapterInterface, ModelCommentInterface, ModelComment; class CommentMapper extends AbstractDataMapper implements CommentMapperInterface { protected $userMapper; protected $entityTable = "comments"; public function __construct(DatabaseAdapterInterface $adapter, UserMapperInterface $userMapper) { $this->userMapper = $userMapper; parent::__construct($adapter); } public function insert(CommentInterface $comment, $postId, $userId) { $comment->id = $this->adapter->insert($this->entityTable, array("content" => $comment->content, "post_id" => $postId, "user_id" => $userId)); return $comment->id; } public function delete($id) { if ($id instanceof CommentInterface) { $id = $id->id; } return $this->adapter->delete($this->entityTable, "id = $id"); } protected function createEntity(array $row) { $user = $this->userMapper->findById($row["user_id"]); return new Comment($row["content"], $user); } }
Класс CommentMapper
ведет себя очень похоже на своего родного PostMapper
. Короче говоря, он запрашивает отображение пользователя в конструкторе, так что конкретный комментарий может быть привязан к соответствующему комментарию. Учитывая непринужденную природу CommentMapper
, давайте сделаем последнее усилие и определим другой, который будет обрабатывать пользователей:
<?php namespace ModelMapper; use ModelUserInterface; interface UserMapperInterface { public function findById($id); public function findAll(array $conditions = array()); public function insert(UserInterface $user); public function delete($id); }
<?php namespace ModelMapper; use ModelUserInterface, ModelUser; class UserMapper extends AbstractDataMapper implements UserMapperInterface { protected $entityTable = "users"; public function insert(UserInterface $user) { $user->id = $this->adapter->insert($this->entityTable, array("name" => $user->name, "email" => $user->email)); return $user->id; } public function delete($id) { if ($id instanceof UserInterface) { $id = $id->id; } return $this->adapter->delete($this->entityTable, array("id = $id")); } protected function createEntity(array $row) { return new User($row["name"], $row["email"]); } }
Теперь, UserMapper
класс UserMapper
установлен, мы, наконец, достигли цели, к которой мы UserMapper
с самого начала: создать с нуля удобный для отображения слой отображения, способный перемещать данные назад и вперед между упрощенной моделью домена блога. и MySQL. Но давайте пока не будем поглаживать себя по спине, так как лучший способ увидеть, являются ли картографы такими же функциональными, как они выглядят на первый взгляд, на примере.
Отображение доменных объектов блога в и из DAL
Как и следовало ожидать, эффективное использование модели домена блога довольно просто, поскольку API-интерфейсы картографов выполняют тяжелую работу и скрывают базу данных от самой модели. Эта способность, однако, лучше всего оценивается с точки зрения прикладного уровня. Давайте соединим все графы картографов вместе:
<?php use LibraryLoaderAutoloader; require_once __DIR__ . "/Autoloader.php"; $autoloader = new Autoloader(); $autoloader->register(); // create a PDO adapter $adapter = new PdoAdapter("mysql:dbname=blog", "myfancyusername", "myhardtoguesspassword"); // create the mappers $userMapper = new UserMapper($adapter); $commentMapper = new CommentMapper($adapter, $userMapper); $postMapper = new PostMapper($adapter, $commentMapper);
Все идет нормально. На этом этапе преобразователи были инициализированы путем помещения их коллабораторов в соответствующие конструкторы. Учитывая, что они готовы к каким-то реальным действиям, теперь давайте используем постмаппер и вставим несколько банальных постов в базу данных:
<?php $postMapper->insert( new Post( "Welcome to SitePoint", "To become yourself a true PHP master, you must first master PHP.")); $postMapper->insert( new Post( "Welcome to SitePoint (Reprise)", "To become yourself a PHP Master, you must first master... Wait! Did I post that already?"));
Если все работает должным образом, таблица posts
должна быть хорошо заполнена предыдущими записями. Но это только я или они выглядят немного одинокими? Давайте исправим это, добавив несколько комментариев:
<?php $user = new User("Everchanging Joe", "[email protected]"); $userMapper->insert($user); // Joe's comments for the first post (post ID = 1, user ID = 1) $commentMapper->insert( new Comment( "I just love this post! Looking forward to seeing more of this stuff.", $user), 1, $user->id); $commentMapper->insert( new Comment( "I just changed my mind and dislike this post! Hope not seeing more of this stuff.", $user), 1, $user->id); // Joe's comment for the second post (post ID = 2, user ID = 1) $commentMapper->insert( new Comment( "Not quite sure if I like this post or not, so I cannot say anything for now.", $user), 2, $user->id);
Благодаря замечательному красноречию Джо, первое сообщение в блоге теперь должно иметь два комментария, а второе — одно. Обратите внимание, что внешние ключи, используемые для поддержания границы между комментариями и пользователями, были просто подобраны во время выполнения. Однако в производстве их, скорее всего, следует собирать внутри пользовательского интерфейса.
Теперь, когда база данных блога была окончательно заполнена парой постов, комментариев и информации болтливого пользователя, последнее, что нам нужно сделать, это собрать все данные и вывести их на экран. Вот так:
<?php $posts = $postMapper->findAll();
Даже когда эта однострочная… ну, просто однострочная, на самом деле это рабочая лошадка, которая создает графы объектов домена блога по запросу из базы данных и помещает их в память для дальнейшей обработки. С другой стороны, рассматриваемые графики могут быть легко разложены обратно через скелетное представление следующим образом:
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Building a Domain Model in PHP</title> </head> <body> <header> <h1>SitePoint.com</h1> </header> <section> <ul> <?php foreach ($posts as $post) { ?> <li> <h2><?php echo $post->title;?></h2> <p><?php echo $post->content;?></p> <?php if ($post->comments) { ?> <ul> <?php foreach ($post->comments as $comment) { ?> <li> <h3><?php echo $comment->user->name;?> says:</h3> <p><?php echo $comment->content;?></p> </li> <?php } ?> </ul> <?php } ?> </li> <?php } ?> </ul> </section> </body> </html>
Действительно, просмотр нескольких постов, комментариев и пользователей — скучная задача, которая не заслуживает дальнейшего объяснения. Конечно, если вы проницательный наблюдатель «ястребиного глаза», вы могли заметить, что этот, казалось бы, безобидный вид на самом деле является пожирателем свиней, который нагло тянет всю базу данных и бросает ее в сердце пользовательского интерфейса. Давайте не будем спешить с суждениями, поскольку есть несколько распространенных приемов, которые можно использовать для решения этой проблемы, включая кэширование (в любой из его многочисленных форм), ленивую загрузку и так далее.
Приспособление картографов данных с некоторыми из этих положительных героев останется для вас домашней работой, что наверняка будет держать вас в течение достаточно долгого времени.
Заключительные замечания
Очень немногие не согласятся с тем, что реализация богатой модели предметной области далека от того, чтобы быть заданием, похожим на старшую школу, за одну ночь, даже при использовании простого языка, такого как PHP. Хотя можно с уверенностью сказать, что пересылка данных модели в и из DAL во многих случаях может быть делегирована под ключ в библиотеке или среде отображения (при условии, что такая вещь существует), определяя отношения между объектами домена, а также их собственные правила, данные и поведение зависит от разработчика.
Несмотря на это, дополнительные усилия в целом вызывают полезный волновой эффект, так как они помогают активно продвигать использование многоуровневого проектирования наряду с хорошими практиками ООП, в которых вовлеченные объекты имеют только несколько четко определенных обязанностей , и модель не загрязняет свою нетронутую экосистему логикой базы данных. Добавьте к этому, что перенос модели из одной инфраструктуры в другую можно выполнить довольно безболезненно, и вы поймете, почему этот подход очень привлекателен при разработке приложений, которые должны хорошо масштабироваться.
Изображение через kentoh / Shutterstock