我目前对模型使用自制的ORM方法,即对象的属性映射到表的字段。这似乎是一种非常可行和直观的方式来处理简单的网站。
...然而
随着复杂性的增加,当我想从连接表中获取数据时,这种方法变得很麻烦。我的数据库抽象仅针对单个表,并且使用连接进行查询并不会自然而然地落实到位。
我一直在调查DMM,特别是在https://github.com/codeinthehole/domain-model-mapper,但我在细节中某处迷路了。如果我能弄清楚我的问题是什么,我需要回答一些更大的问题:/
首先,MVC有一个很好的描述结构:模型,视图和控制器。您可以分离目录结构以分离关注点。但是,在我学习的过程中,模型并不是一个单一的类。您希望将存储与业务逻辑分开。
第二个问题与实际的数据库访问有关。根据DMM模式,您可以通过调用->save()
方法来保存内存中的对象。此内存中对象不一定与任何数据库表1:1映射。这就是我迷路的地方...... save
方法可能会将内存中的对象注入到数据访问对象中,从而将对象保留在数据库中。
通过数据库抽象,我可以使用我自己的find
,findall
,insert
,update
,delete
等方法创建一个类。可以为每个数据库表扩展。但DMM似乎是一个完全不同的范例。是否有DAO的抽象,还是必须为每个应用程序定制设计?
对于这两个问题,我意识到它们是抽象的问题。我不是要求别人为我调试代码;我想要理解它背后的理论。因此,很难提出易于回答问题的问题。但我欢迎任何部分答案,只要它能让社区有机会与我一起学习。
答案 0 :(得分:1)
从一般角度来看,基于MVC概念的Web应用程序由两层组成:模型层和表示层。他们的实施实现了 - 关注点分离的目标。
model layer 由三个子图层组成:
表示层包含:
请注意,我没有完成此图层的说明。我是故意这样做的,因为我认为你更好地遵循这个链接,以便对这个主题有正确的看法:
关于第二个问题:实际上,ORM会自动化域层和数据库之间的映射。它们很有用,但也有缺点,因为它们迫使您在业务逻辑PLUS数据库结构方面进行思考。 " 每个表一个类"如(Model-View-Confusion Series)," protected $ tableName; "如在Table Data Gateway的父类Mapper中," 类用户扩展ActiveRecord "如DMM等,是灵活性限制的迹象。例如,正如我在DMM代码中看到的那样,它会强制您在Mapper构造函数中提供$tableName
和$identityFields
。这是一个很大的限制。
无论如何,如果你想在涉及(复杂)查询数据库的任务中非常灵活,那么保持简单:
tableName
之类的内容不应该出现在那里。稍后您也想创建存储库和服务。
所以,关闭你的第一个问题:有一个非常好的解释文章系列,关于你感兴趣的内容。在你阅读它们之后,你将毫不怀疑所有模型层组件如何协同工作。注意:您会在那里看到与$tableName
相同的属性,但现在您知道从哪个角度考虑它。所以:
这是一个映射器的版本,灵感来自上述文章。请注意,父/抽象类没有继承。要找出原因,请阅读An Introduction to Services中的答案。
<?php
/*
* User mapper.
*
* Copyright © 2017 SitePoint
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
namespace App\Modules\Connects\Models\Mappers;
use App\Modules\Connects\Models\Models\User;
use App\Modules\Connects\Models\Models\UserInterface;
use App\Modules\Connects\Models\Mappers\UserMapperInterface;
use App\Modules\Connects\Models\Collections\UserCollectionInterface;
use App\Core\Model\Storage\Adapter\Database\DatabaseAdapterInterface;
/**
* User mapper.
*/
class UserMapper implements UserMapperInterface {
/**
* Adapter.
*
* @var DatabaseAdapterInterface
*/
private $adapter;
/**
* User collection.
*
* @var UserCollectionInterface
*/
private $userCollection;
/**
*
* @param DatabaseAdapterInterface $adapter Adapter.
* @param UserCollectionInterface $userCollection User collection.
*/
public function __construct(DatabaseAdapterInterface $adapter, UserCollectionInterface $userCollection) {
$this
->setAdapter($adapter)
->setUserCollection($userCollection)
;
}
/**
* Find user by id.
*
* @param int $id User id.
* @return UserInterface User.
*/
public function findById($id) {
$sql = "SELECT * FROM users WHERE id=:id";
$bindings = [
'id' => $id
];
$row = $this->getAdapter()->selectOne($sql, $bindings);
return $this->createUser($row);
}
/**
* Find users by criteria.
*
* @param array $filter [optional] WHERE conditions.
* @return UserCollectionInterface User collection.
*/
public function find(array $filter = array()) {
$conditions = array();
foreach ($filter as $key => $value) {
$conditions[] = $key . '=:' . $key;
}
$whereClause = implode(' AND ', $conditions);
$sql = sprintf('SELECT * FROM users %s'
, !empty($filter) ? 'WHERE ' . $whereClause : ''
);
$bindings = $filter;
$rows = $this->getAdapter()->select($sql, $bindings);
return $this->createUserCollection($rows);
}
/**
* Insert user.
*
* @param UserInterface $user User.
* @return UserInterface Inserted user (saved data may differ from initial user data).
*/
public function insert(UserInterface $user) {
$properties = get_object_vars($user);
$columnsClause = implode(',', array_keys($properties));
$values = array();
foreach (array_keys($properties) as $column) {
$values[] = ':' . $column;
}
$valuesClause = implode(',', $values);
$sql = sprintf('INSERT INTO users (%s) VALUES (%s)'
, $columnsClause
, $valuesClause
);
$bindings = $properties;
$this->getAdapter()->insert($sql, $bindings);
$lastInsertId = $this->getAdapter()->getLastInsertId();
return $this->findById($lastInsertId);
}
/**
* Update user.
*
* @param UserInterface $user User.
* @return UserInterface Updated user (saved data may differ from initial user data).
*/
public function update(UserInterface $user) {
$properties = get_object_vars($user);
$columns = array();
foreach (array_keys($properties) as $column) {
if ($column !== 'id') {
$columns[] = $column . '=:' . $column;
}
}
$columnsClause = implode(',', $columns);
$sql = sprintf('UPDATE users SET %s WHERE id = :id'
, $columnsClause
);
$bindings = $properties;
$this->getAdapter()->update($sql, $bindings);
return $this->findById($user->id);
}
/**
* Delete user.
*
* @param UserInterface $user User.
* @return bool TRUE if user successfully deleted, FALSE otherwise.
*/
public function delete(UserInterface $user) {
$sql = 'DELETE FROM users WHERE id=:id';
$bindings = array(
'id' => $user->id
);
$rowCount = $this->getAdapter()->delete($sql, $bindings);
return $rowCount > 0;
}
/**
* Create user.
*
* @param array $row Table row.
* @return UserInterface User.
*/
public function createUser(array $row) {
$user = new User();
foreach ($row as $key => $value) {
$user->$key = $value;
}
return $user;
}
/**
* Create user collection.
*
* @param array $rows Table rows.
* @return UserCollectionInterface User collection.
*/
public function createUserCollection(array $rows) {
$this->getUserCollection()->clear();
foreach ($rows as $row) {
$user = $this->createUser($row);
$this->getUserCollection()->add($user);
}
return $this->getUserCollection()->toArray();
}
/**
* Get adapter.
*
* @return DatabaseAdapterInterface
*/
public function getAdapter() {
return $this->adapter;
}
/**
* Set adapter.
*
* @param DatabaseAdapterInterface $adapter Adapter.
* @return $this
*/
public function setAdapter(DatabaseAdapterInterface $adapter) {
$this->adapter = $adapter;
return $this;
}
/**
* Get user collection.
*
* @return UserCollectionInterface
*/
public function getUserCollection() {
return $this->userCollection;
}
/**
* Set user collection.
*
* @param UserCollectionInterface $userCollection User collection.
* @return $this
*/
public function setUserCollection(UserCollectionInterface $userCollection) {
$this->userCollection = $userCollection;
return $this;
}
}
<?php
/*
* User mapper interface.
*/
namespace App\Modules\Connects\Models\Mappers;
use App\Modules\Connects\Models\Models\UserInterface;
/**
* User mapper interface.
*/
interface UserMapperInterface {
/**
* Find user by id.
*
* @param int $id User id.
* @return UserInterface User.
*/
public function findById($id);
/**
* Find users by criteria.
*
* @param array $filter [optional] WHERE conditions.
* @param string $operator [optional] WHERE conditions concatenation operator.
* @return UserCollectionInterface User collection.
*/
public function find(array $filter = array(), $operator = 'AND');
/**
* Insert user.
*
* @param UserInterface $user User.
* @return UserInterface Inserted user (saved data may differ from initial user data).
*/
public function insert(UserInterface $user);
/**
* Update user.
*
* @param UserInterface $user User.
* @return UserInterface Updated user (saved data may differ from initial user data).
*/
public function update(UserInterface $user);
/**
* Delete user.
*
* @param UserInterface $user User.
* @return bool TRUE if user successfully deleted, FALSE otherwise.
*/
public function delete(UserInterface $user);
/**
* Create user.
*
* @param array $row Table row.
* @return UserInterface User.
*/
public function createUser(array $row);
/**
* Create user collection.
*
* @param array $rows Table rows.
* @return UserCollectionInterface User collection.
*/
public function createUserCollection(array $rows);
}
<?php
namespace App\Core\Model\Storage\Adapter\Database\Pdo;
use PDO;
use PDOStatement;
use PDOException as Php_PDOException;
use App\Core\Exception\PDO\PDOException;
use App\Core\Exception\SPL\UnexpectedValueException;
use App\Core\Model\Storage\Adapter\Database\DatabaseAdapterInterface;
abstract class AbstractPdoAdapter implements DatabaseAdapterInterface {
/**
* Database connection.
*
* @var PDO
*/
private $connection;
/**
* Fetch mode for a PDO statement. Must be one of the PDO::FETCH_* constants.
*
* @var int
*/
private $fetchMode = PDO::FETCH_ASSOC;
/**
* Fetch argument for a PDO statement.
*
* @var mixed
*/
private $fetchArgument = NULL;
/**
* Constructor arguments for a PDO statement when fetch mode is PDO::FETCH_CLASS.
*
* @var array
*/
private $fetchConstructorArguments = array();
/**
* For a PDOStatement object representing a scrollable cursor, this value determines<br/>
* which row will be returned to the caller.
*
* @var int
*/
private $fetchCursorOrientation = PDO::FETCH_ORI_NEXT;
/**
* The absolute number of the row in the result set, or the row relative to the cursor<br/>
* position before PDOStatement::fetch() was called.
*
* @var int
*/
private $fetchCursorOffset = 0;
/**
* @param PDO $connection Database connection.
*/
public function __construct(PDO $connection) {
$this->setConnection($connection);
}
/**
* Fetch data by executing a SELECT sql statement.
*
* @param string $sql Sql statement.
* @param array $bindings [optional] Input parameters.
* @return array An array containing the rows in the result set, or FALSE on failure.
*/
public function select($sql, array $bindings = array()) {
$statement = $this->execute($sql, $bindings);
$fetchArgument = $this->getFetchArgument();
if (isset($fetchArgument)) {
return $statement->fetchAll(
$this->getFetchMode()
, $fetchArgument
, $this->getFetchConstructorArguments()
);
}
return $statement->fetchAll($this->getFetchMode());
}
/**
* Fetch the next row from the result set by executing a SELECT sql statement.<br/>
* The fetch mode property determines how PDO returns the row.
*
* @param string $sql Sql statement.
* @param array $bindings [optional] Input parameters.
* @return array An array containing the rows in the result set, or FALSE on failure.
*/
public function selectOne($sql, array $bindings = array()) {
$statement = $this->execute($sql, $bindings);
return $statement->fetch(
$this->getFetchMode()
, $this->getFetchCursorOrientation()
, $this->getFetchCursorOffset()
);
}
/**
* Store data by executing an INSERT sql statement.
*
* @param string $sql Sql statement.
* @param array $bindings [optional] Input parameters.
* @return int The number of the affected records.
*/
public function insert($sql, array $bindings = array()) {
$statement = $this->execute($sql, $bindings);
return $statement->rowCount();
}
/**
* Update data by executing an UPDATE sql statement.
*
* @param string $sql Sql statement.
* @param array $bindings [optional] Input parameters.
* @return int The number of the affected records.
*/
public function update($sql, array $bindings = array()) {
$statement = $this->execute($sql, $bindings);
return $statement->rowCount();
}
/**
* Delete data by executing a DELETE sql statement.
*
* @param string $sql Sql statement.
* @param array $bindings [optional] Input parameters.
* @return int The number of the affected records.
*/
public function delete($sql, array $bindings = array()) {
$statement = $this->execute($sql, $bindings);
return $statement->rowCount();
}
/**
* Prepare and execute an sql statement.
*
* @todo I want to re-use the statement to execute several queries with the same SQL statement
* only with different parameters. So make a statement field and prepare only once!
* See: https://www.sitepoint.com/integrating-the-data-mappers/
*
* @param string $sql Sql statement.
* @param array $bindings [optional] Input parameters.
* @return PDOStatement The PDO statement after execution.
*/
protected function execute($sql, array $bindings = array()) {
// Prepare sql statement.
$statement = $this->prepareStatement($sql);
// Bind input parameters.
$this->bindInputParameters($statement, $bindings);
// Execute prepared sql statement.
$this->executePreparedStatement($statement);
return $statement;
}
/**
* Prepare and validate an sql statement.<br/>
*
* ---------------------------------------------------------------------------------
* If the database server cannot successfully prepare the statement,
* PDO::prepare() returns FALSE or emits PDOException (depending on error handling).
* ---------------------------------------------------------------------------------
*
* @param string $sql Sql statement.
* @return PDOStatement If the database server successfully prepares the statement,
* return a PDOStatement object. Otherwise return FALSE or emit PDOException
* (depending on error handling).
* @throws Php_PDOException
* @throws PDOException
*/
private function prepareStatement($sql) {
try {
$statement = $this->getConnection()->prepare($sql);
if (!$statement) {
throw new PDOException('The sql statement can not be prepared!');
}
} catch (Php_PDOException $exc) {
throw new PDOException('The sql statement can not be prepared!', 0, $exc);
}
return $statement;
}
/**
* Bind the input parameters to a prepared PDO statement.
*
* @param PDOStatement $statement PDO statement.
* @param array $bindings Input parameters.
* @return $this
*/
private function bindInputParameters($statement, $bindings) {
foreach ($bindings as $key => $value) {
$statement->bindValue(
$this->getInputParameterName($key)
, $value
, $this->getInputParameterDataType($value)
);
}
return $this;
}
/**
* Get the name of an input parameter by its key in the bindings array.
*
* @param int|string $key The key of the input parameter in the bindings array.
* @return int|string The name of the input parameter.
*/
private function getInputParameterName($key) {
return is_int($key) ? ($key + 1) : (':' . ltrim($key, ':'));
}
/**
* Get the PDO::PARAM_* constant, e.g the data type of an input parameter, by its value.
*
* @param mixed $value Value of the input parameter.
* @return int The PDO::PARAM_* constant.
*/
private function getInputParameterDataType($value) {
$dataType = PDO::PARAM_STR;
if (is_int($value)) {
$dataType = PDO::PARAM_INT;
} elseif (is_bool($value)) {
$dataType = PDO::PARAM_BOOL;
}
return $dataType;
}
/**
* Execute a prepared PDO statement.
*
* @param PDOStatement $statement PDO statement.
* @return $this
* @throws UnexpectedValueException
*/
private function executePreparedStatement($statement) {
if (!$statement->execute()) {
throw new UnexpectedValueException('The statement can not be executed!');
}
return $this;
}
/**
* Get the ID of the last inserted row or of the sequence value.
*
* @param string $sequenceObjectName [optional] Name of the sequence object<br/>
* from which the ID should be returned.
* @return string The ID of the last row, or the last value retrieved from the specified<br/>
* sequence object, or an error IM001 SQLSTATE If the PDO driver does not support this.
*/
public function getLastInsertId($sequenceObjectName = NULL) {
return $this->getConnection()->lastInsertId($sequenceObjectName);
}
public function getConnection() {
return $this->connection;
}
public function setConnection(PDO $connection) {
$this->connection = $connection;
return $this;
}
public function getFetchMode() {
return $this->fetchMode;
}
public function setFetchMode($fetchMode) {
$this->fetchMode = $fetchMode;
return $this;
}
public function getFetchArgument() {
return $this->fetchArgument;
}
public function setFetchArgument($fetchArgument) {
$this->fetchArgument = $fetchArgument;
return $this;
}
public function getFetchConstructorArguments() {
return $this->fetchConstructorArguments;
}
public function setFetchConstructorArguments($fetchConstructorArguments) {
$this->fetchConstructorArguments = $fetchConstructorArguments;
return $this;
}
public function getFetchCursorOrientation() {
return $this->fetchCursorOrientation;
}
public function setFetchCursorOrientation($fetchCursorOrientation) {
$this->fetchCursorOrientation = $fetchCursorOrientation;
return $this;
}
public function getFetchCursorOffset() {
return $this->fetchCursorOffset;
}
public function setFetchCursorOffset($fetchCursorOffset) {
$this->fetchCursorOffset = $fetchCursorOffset;
return $this;
}
}
关于您的第一个问题:没有关于您应该在哪里存储课程的约定。选择您希望的任何文件系统结构。但请确保:
1)您正在使用PHP MVC: Data Mapper pattern: class design中建议的自动加载器和命名空间。
2)您可以随时唯一标识每个组件类。您可以通过两种方式实现此目的:通过对每个类(UserController
,UserMapper
,UserView
等)应用相应的后缀,或者在{{1中定义相应的类别名语句,如:
use
文件系统结构可能类似于以下内容 - 它是我项目中使用的文件系统结构,如果它乍一看太复杂,那就很抱歉:
在namespace App\Controllers;
use App\Models\DomainObjects\User;
use App\Models\Mappers\User as UserMapper;
use App\Models\Repositories\User as UserRepository;
:
在App/Core
: