ZendFramework社区最近涌现出许多讨论模型-视图-控制器模式中的模型的博文。ZendFramework从未有过具体的模型类或接口;我们的立场是模型是特定于应用程序的,只有开发人员才能真正知道什么最适合它。
许多其他框架将模型绑定到数据访问——通常通过ActiveRecord模式或表数据网关——这完全忽略了这样一个事实,即这是将模型绑定到它被持久化的方法。如果您开始使用memcached,以后会发生什么?还是迁移到SOA架构?如果从一开始,您的数据就来自Web服务怎么办?如果您确实使用数据库,但您的业务逻辑依赖于表之间的关联怎么办?
虽然上述帖子在讨论各种问题方面做得很好,但它们不一定提供开发人员在创建模型时可以使用的任何具体方法。因此,这将是系列博文中的第一篇,旨在提供一些您在创建模型时可以使用的具体模式和技术。这些示例将主要从ZendFramework组件中提取,但同样适用于各种其他框架。
输入过滤和表单
在大多数情况下,您希望模型执行自己的输入过滤。这是因为输入过滤是域逻辑:它是定义什么输入有效以及如何规范化该输入的一组规则。
但是,这如何适应表单?ZendFramework有一个Zend_Form
组件,它允许您指定验证和过滤器链,以及如何通过装饰器呈现表单的规则。典型的模式是定义一个表单,然后在您的控制器中将输入传递给它;如果它通过验证,则将值传递给模型。
如果您改为将表单附加到模型会怎么样?
有人认为这违反了“关注点分离”的概念,因为它将渲染逻辑混合到模型中。我觉得这是迂腐的论点。当附加到表单时,Zend_Form
可以严格用作输入过滤器;当您希望渲染它时,您可以从模型中拉出表单,并在您的视图中执行任何特定于视图的操作——配置装饰器、设置操作和方法等脚本。此外,各种插件-验证器、过滤器、装饰器-在使用之前不会加载-这意味着当您仅使用Zend_Form
作为输入时,装饰器几乎没有开销过滤器。
基本上,这种方法可以帮助您遵守DRY原则(一个验证/过滤器链),同时帮助您保持业务和视图逻辑的可靠分离。最后,您将获得模型的一种或多种形式表示,这有助于快速开发应用程序,并在模型和视图之间提供可靠的语义联系。
那么,进入技术。
将表单附加到模型
我一直在做的是向我的模型添加一个getForm()
访问器,它带有一个可选参数,即要检索的表单类型。然后在任何需要验证的时候在模型中使用它。(有些模型需要多种形式,所以最好尽早计划。一个很好的例子是代表一个用户的模型——你将需要一个登录和注册表。)让我们看看它的无为:
class Spindle_Model_Bug { protected $_forms = array(); public function getForm($type = 'bug') { $type = ucfirst($type); if (!isset($this->_forms[$type])) { $class = 'Spindle_Model_Form_' . $type; $this->_forms[$type] = new $class; } return $this->_forms[$type]; } public function save(array $data) { $form = $this->getForm(); if (!$form->isValid($data)) { return false; } $storage = $this->getStorage(); if ($form->getValue('id')) { $id = $form->getValue('id'); $storage->update($form->getValues(), $id)); } else { $id = $storage->insert($form->getValues()); } return $id; } }
如上面的代码片段所示,表单充当输入过滤器:您首先使用它来确保提供的数据有效,然后确保传递给持久层的数据根据您的规则进行规范化。您还可以使用它来验证某些可选值是否存在,如此处所示,以确定持久保存数据所需的实际操作。
控制器和视图中发生了什么?
然后,在您的控制器操作中,您会发生轻微的范式转变。不是验证表单然后将过滤后的数据传递给模型,而是简单地尝试将数据保存到模型:
class BugController { public function processAction() { $request = $this->getRequest(); if (!$request->isPost()) { return $this->_helper->redirector('new'); } if (!$id = $this->model->save($request->getPost())) { // Failed validation; re-render form page $this->view->model = $model; return $this->render('new'); } // redirect to view newly saved bug $this->_helper->redirector('view', null, null, array('id' => $id)); } }
那里几乎没有逻辑,也没有提到任何形式。那么,我们实际上是如何呈现表单的呢?请注意,模型被传递给视图——这最终使我们能够访问表单。
$form = $this->model->getForm(); $form->setMethod('post') ->setAction($this->url(array('action' => 'process'))); echo $form;
这在语义上是有意义的;您正在呈现一个表单,该表单将用于过滤给定模型的数据。请注意,已提供一些视图逻辑——表单方法和操作在视图层中设置。这是合适的,因为我们现在正在执行与显示相关的逻辑。
总结
当然还有其他方法可以解决这个问题,但这是一种方便快捷的解决方案,可以最大限度地利用各种现有组件。将表单附加到您的模型可以将所有与输入验证相关的逻辑——包括错误报告——保存在一个地方,并确保您的表单在您更改模型时不会过时——因为您将更新表单本身的验证规则和允许输入列表。
在下一篇文章中,我们将探讨在您的模型中使用和应用访问控制列表(ACL)。