Die*_*xel 541 php architecture oop model-view-controller model
我只是掌握了MVC框架,我常常想知道模型中应该有多少代码.我倾向于有一个数据访问类,其方法如下:
public function CheckUsername($connection, $username)
{
try
{
$data = array();
$data['Username'] = $username;
//// SQL
$sql = "SELECT Username FROM" . $this->usersTableName . " WHERE Username = :Username";
//// Execute statement
return $this->ExecuteObject($connection, $sql, $data);
}
catch(Exception $e)
{
throw $e;
}
}
Run Code Online (Sandbox Code Playgroud)
我的模型往往是映射到数据库表的实体类.
模型对象是否应具有所有数据库映射属性以及上面的代码,或者可以将实际上数据库工作的代码分开吗?
我最终会有四层吗?
ter*_*ško 893
免责声明:以下是我如何在基于PHP的Web应用程序的上下文中理解类似MVC的模式的描述.内容中使用的所有外部链接都用于解释术语和概念,而不是暗示我自己在该主题上的可信度.
我必须清楚的第一件事是:模型是一个层.
第二:经典MVC与我们在Web开发中使用的内容之间存在差异.这是我写的一个较旧的答案,简要描述了它们的不同之处.
该模型不是类或任何单个对象.这是一个非常常见的错误(我也做了,虽然最初的答案是在我开始学习时写的),因为大多数框架都会延续这种误解.
它既不是对象关系映射技术(ORM)也不是数据库表的抽象.任何告诉你的人最有可能试图"出售"另一个全新的ORM或整个框架.
在适当的MVC改编,M含有所有域业务逻辑和模型层是主要由三种类型的结构制成:
域对象是纯域信息的逻辑容器; 它通常代表问题域空间中的逻辑实体.通常称为业务逻辑.
您可以在此处定义如何在发送发票之前验证数据,或计算订单的总成本.与此同时,域对象完全不知道存储-无论从哪里(SQL数据库,REST API,文本文件等),甚至也不是,如果他们得到保存或检索.
这些对象仅负责存储.如果将信息存储在数据库中,这将是SQL所在的位置.或者您可能使用XML文件来存储数据,而您的数据映射器正在解析XML文件.
您可以将它们视为"更高级别的域对象",而不是业务逻辑,服务负责域对象和映射器之间的交互.这些结构最终创建了一个"公共"接口,用于与域业务逻辑交互.您可以避免它们,但是会将某些域逻辑泄漏到控制器中.
在ACL实现问题中,这个主题有一个相关的答案- 它可能很有用.
模型层与MVC三元组其他部分之间的通信应仅通过服务进行.明确的分离有一些额外的好处:
先决条件:观看讲座"全球状态和单身人士"和"不要寻找事物!" 来自清洁代码会谈.
对于要访问这些服务的View和Controller实例(您可以调用的内容:"UI层"),有两种常规方法:
正如您可能怀疑的那样,DI容器是一个更优雅的解决方案(虽然对初学者来说不是最简单的).我建议考虑使用这个功能的两个库是Syfmony的独立DependencyInjection组件或Auryn.
使用工厂和DI容器的解决方案都允许您共享各个服务器的实例,以便在给定的请求 - 响应周期内在所选控制器和视图之间共享.
现在您可以访问控制器中的模型层,您需要开始实际使用它们:
public function postLogin(Request $request)
{
$email = $request->get('email');
$identity = $this->identification->findIdentityByEmailAddress($email);
$this->identification->loginWithPassword(
$identity,
$request->get('password')
);
}
Run Code Online (Sandbox Code Playgroud)
您的控制器有一个非常明确的任务:获取用户输入,并根据此输入更改业务逻辑的当前状态.在此示例中,更改的状态是"匿名用户"和"登录用户".
控制器不负责验证用户的输入,因为这是业务规则的一部分,控制器绝对不会调用SQL查询,就像你在这里或这里看到的那样(请不要讨厌它们,它们是误导的,不是邪恶的).
好的,用户已登录(或失败).怎么办?所述用户仍然没有意识到它.因此,您需要实际生成响应,这是视图的责任.
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,视图基于模型层的当前状态产生两种可能响应之一.对于不同的用例,您可以根据"当前选定的文章"等内容选择要渲染的不同模板.
表示层实际上可以非常精细,如下所述:了解PHP中的MVC视图.
当然,有些情况下,这是一种矫枉过正.
MVC只是分离关注原则的具体解决方案.MVC将用户界面与业务逻辑分开,在UI中它将用户输入和表示的处理分开.这很关键.虽然人们经常将其形容为"三元组",但它实际上并不是由三个独立的部分组成.结构更像是这样的:
这意味着,当您的表示层的逻辑几乎不存在时,实用的方法是将它们保持为单层.它还可以大大简化模型层的某些方面.
使用此方法,登录示例(对于API)可以写为:
public function postLogin(Request $request)
{
$email = $request->get('email');
$data = [
'status' => 'ok',
];
try {
$identity = $this->identification->findIdentityByEmailAddress($email);
$token = $this->identification->loginWithPassword(
$identity,
$request->get('password')
);
} catch (FailedIdentification $exception) {
$data = [
'status' => 'error',
'message' => 'Login failed!',
]
}
return new JsonResponse($data);
}
Run Code Online (Sandbox Code Playgroud)
虽然这是不可持续的,但当您为渲染响应体提供复杂的逻辑时,这种简化对于更简单的场景非常有用.但要注意,当尝试在具有复杂表示逻辑的大型代码库中使用时,这种方法将成为一场噩梦.
由于没有单一的"模型"类(如上所述),因此您实际上并不"构建模型".相反,您可以从制作能够执行某些方法的服务开始.然后实现Domain Objects和Mappers.
在上述两种方法中,都有这种识别服务的登录方法.实际上会是什么样子.我正在使用库中相同功能的略微修改版本,我写的...因为我很懒:
public function loginWithPassword(Identity $identity, string $password): string
{
if ($identity->matchPassword($password) === false) {
$this->logWrongPasswordNotice($identity, [
'email' => $identity->getEmailAddress(),
'key' => $password, // this is the wrong password
]);
throw new PasswordMismatch;
}
$identity->setPassword($password);
$this->updateIdentityOnUse($identity);
$cookie = $this->createCookieIdentity($identity);
$this->logger->info('login successful', [
'input' => [
'email' => $identity->getEmailAddress(),
],
'user' => [
'account' => $identity->getAccountId(),
'identity' => $identity->getId(),
],
]);
return $cookie->getToken();
}
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,在此抽象级别,没有指示数据从何处获取.它可能是一个数据库,但它也可能只是一个用于测试目的的模拟对象.即使是实际用于它的数据映射器也隐藏在private
此服务的方法中.
private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
$identity->setStatus($status);
$identity->setLastUsed(time());
$mapper = $this->mapperFactory->create(Mapper\Identity::class);
$mapper->store($identity);
}
Run Code Online (Sandbox Code Playgroud)
要实现持久性的抽象,最灵活的方法是创建自定义数据映射器.
来自:PoEAA书
实际上,它们是为与特定类或超类的交互而实现的.让我们说你有Customer
和Admin
你的代码(都继承自User
超类).两者可能最终都有一个单独的匹配映射器,因为它们包含不同的字段.但是,您最终还将获得共享和常用操作.例如:更新"上次在线时间".而不是让现有的映射器更复杂,更实用的方法是拥有一个通用的"用户映射器",它只更新该时间戳.
数据库表和模型
虽然有时在数据库表,域对象和Mapper之间存在直接的1:1:1关系,但在较大的项目中,它可能不像您期望的那样常见:
单个域对象使用的信息可能从不同的表映射,而对象本身在数据库中没有持久性.
示例:如果您要生成月度报告.这将从不同的表中收集信息,但MonthlyReport
数据库中没有神奇的表.
单个Mapper可以影响多个表.
示例:当您从User
对象存储数据时,此域对象可以包含其他域对象的集合 - Group
实例.如果您更改它们并存储它User
,Data Mapper将必须更新和/或插入多个表中的条目.
来自单个域对象的数据存储在多个表中.
示例:在大型系统中(思考:中型社交网络),将用户身份验证数据和经常访问的数据与较大的内容块分开存储可能是务实的,这是很少需要的.在这种情况下,您可能仍然只有一个User
类,但它包含的信息将取决于是否提取了完整的详细信息.
对于每个域对象,可以有多个映射器
示例:您有一个新闻站点,其中包含面向公共和管理软件的共享代码.但是,虽然两个接口都使用相同的Article
类,但管理层需要填充更多信息.在这种情况下,您将有两个单独的映射器:"内部"和"外部".每个执行不同的查询,甚至使用不同的数据库(如在主服务器或从服务器中).
视图不是模板
在MVC中查看实例(如果您没有使用模式的MVP变体)负责表示逻辑.这意味着每个View通常都会处理至少一些模板.它从模型层获取数据,然后根据接收的信息选择模板并设置值.
您从中获得的好处之一是可重用性.如果您创建一个ListView
类,那么,使用编写良好的代码,您可以将同一个类交给文章下面的用户列表和注释的表示.因为它们都具有相同的表示逻辑.你只需切换模板.
您可以使用本机PHP模板或使用某些第三方模板引擎.也可能有一些第三方库,它们能够完全取代View实例.
旧版本的答案怎么样?
唯一的主要变化是,在旧版本中称为Model,实际上是一个服务."库类比"的其余部分保持良好状态.
我看到的唯一缺陷是,这将是一个非常奇怪的库,因为它会从书中返回你的信息,但不会让你触摸书本身,因为否则抽象会开始"泄漏".我可能不得不考虑一个更合适的类比.
View和Controller实例之间有什么关系?
MVC结构由两层组成:ui和model.UI层中的主要结构是视图和控制器.
当您处理使用MVC设计模式的网站时,最好的方法是在视图和控制器之间建立1:1的关系.每个视图代表您网站中的整个页面,它有一个专用控制器来处理该特定视图的所有传入请求.
例如,要表示已打开的文章,您将拥有\Application\Controller\Document
和\Application\View\Document
.这将包含UI层的所有主要功能,当涉及到处理文章时(当然,您可能有一些与文章没有直接关系的XHR组件).
net*_*der 36
作为业务逻辑的一切都属于模型,无论是数据库查询,计算,REST调用等.
您可以在模型本身中访问数据,MVC模式不会限制您这样做.您可以使用服务,映射器和其他方法对其进行糖涂层,但模型的实际定义是处理业务逻辑的层,仅此而已.它可以是一个类,一个函数,或一个包含大量对象的完整模块,如果这是你想要的.
拥有一个实际执行数据库查询的单独对象总是更容易,而不是让它们直接在模型中执行:这在单元测试时会特别有用(因为在模型中注入模拟数据库依赖项很容易):
class Database {
protected $_conn;
public function __construct($connection) {
$this->_conn = $connection;
}
public function ExecuteObject($sql, $data) {
// stuff
}
}
abstract class Model {
protected $_db;
public function __construct(Database $db) {
$this->_db = $db;
}
}
class User extends Model {
public function CheckUsername($username) {
// ...
$sql = "SELECT Username FROM" . $this->usersTableName . " WHERE ...";
return $this->_db->ExecuteObject($sql, $data);
}
}
$db = new Database($conn);
$model = new User($db);
$model->CheckUsername('foo');
Run Code Online (Sandbox Code Playgroud)
此外,在PHP中,您很少需要捕获/重新抛出异常,因为保留了回溯,特别是在您的示例中.只是抛出异常并在控制器中捕获它.
更常见的是,大多数应用程序都有数据,显示和处理部分,我们只是将所有这些应用程序放在字母中M
,V
并且C
.
Model(M
) - >具有保存应用程序状态的属性,它不知道任何关于V
和的事情C
.
View(V
) - >显示应用程序的格式,并且只知道如何在其上消化模型并且不打扰C
.
Controller(C
) ---->具有应用程序的处理部分,作为M和V之间的接线,并且它依赖于两者M
,V
不像M
和V
.
总的来说,每个人之间都存在分离关系.将来可以非常轻松地添加任何更改或增强功能.
归档时间: |
|
查看次数: |
172713 次 |
最近记录: |