从一般的角度来看,基于 MVC 概念的 Web 应用程序由两层组成:模型层和表示层。它们的实现实现了关注点分离的目标。
model layer由三个子层组成:
表示层包括:
请注意,我没有完成对这一层的描述。我是故意这样做的,因为我认为您最好关注此链接,以获得对该主题的正确看法:
关于您的第二个问题:确实,ORM 自动化了域层和数据库之间的映射。它们很有用,但也有缺点,因为它们迫使您从业务逻辑 PLUS 数据库结构的角度进行思考。 "每个表一个类" 如 (Table Data Gateway),"protected $tableName;" 如 DMM 的父类 Mapper,"class用户扩展 ActiveRecord" 如Active Record 等,是灵活性限制的标志。例如,正如我在 DMM 代码中看到的,它强制您在 Mapper 构造函数中提供 $tableName 和 $identityFields。这是一个很大的限制。
无论如何,如果您想在涉及(复杂)数据库查询的任务中真正灵活,那就保持简单:
稍后您还需要创建存储库和服务。
所以,结束您的第一个问题:有一个很好的解释文章系列,正是关于您感兴趣的内容。阅读它们后,您将不再怀疑所有模型层组件的工作原理一起。注意:您会看到与财产相同的$tableName,但现在您知道从哪个角度考虑它。所以:
这里是映射器的一个版本,灵感来自上述文章。注意没有从父/抽象类继承。要找出原因,请阅读PHP MVC: Data Mapper pattern: class design 的精彩回答。
数据映射类:
<?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) 您正在使用自动加载器和命名空间,正如 PSR-4 Autoloading Standard 中所建议的那样。
2) 您可以随时唯一标识每个组件类。您可以通过两种方式实现此目的:通过为每个类应用相应的后缀(UserController、UserMapper、UserView 等),或者通过在 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:
在App/:
祝你好运!