昨天我与AndriesSeutens和NickBelhomme就在其布局中包含小部件的应用程序进行了Twitter/IRC交流。在交流过程中,我告诉Andriess不要使用action()
视图助手,然后Andriess和Nick都询问如果他们不应该使用该助手,如何实现小部件。虽然我最终与Nick进行了一次IRC交流,让他了解如何完成任务,但我决定写一篇更长的文章。
背景
当Andries在推特上询问他认为action()
视图助手的一些不当行为时,情况就开始了——这种情况最终证明不是问题,perse,但更多的是ZendFramework中糟糕架构的案例。他的假设是调用action()
会触发前端控制器调度循环的另一个电路——这意味着他可以依赖他建立的插件来触发。然而,action()
没有做任何事情。相反,它从前端控制器中提取调度程序,并在新操作上手动调用dispatch()
。因此,将触发动作助手,但不会触发前端控制器插件。此外,如果检测到重定向或“转发”条件,它只会返回一个空字符串。
helper是这样完成的,因为ZendFramework不会一次性渲染视图——而是在每个动作之后渲染,并在布局中累积视图以进行渲染。如果我们累积视图变量并渲染一次,并且如果我们使用某种有限状态机,我们可能会按照人们期望的方式进行操作——在调度循环中。因为我们不这样做,所以围绕动作循环(例如ActionStack
动作助手/前端控制器插件)或呈现执行动作的内容的任何解决方案都将是一种hack。注意:ZF2的MVC层可能使这成为可能……尽管仍然没有必要推荐。
不过,还有其他原因可以避免使用这些解决方案。如果您正在调用额外的控制器操作以帮助填充您的视图,您可能会将域逻辑放入您的控制器中。想想看。控制器应该只负责获取输入,将其传送到正确的一个或多个模型,然后将信息传递给视图
考虑到这一点,这是我向Nick和Andries推荐的方法。
秘密武器:行动助手
我以前写过关于actionhelpers的博客。它们是ZendFramework中的内置机制,允许您以使用组合而非继承的方式扩展您的动作控制器。
ZendFramework的一种小部件方法就是利用这些。考虑以下“用户”模块:
user |-- Bootstrap.php |-- configs | `-- user.ini |-- controllers |-- forms | `-- Login.php |-- helpers | `-- HandleLogin.php `-- views `-- scripts |-- login.phtml `-- profile.phtml
现在,注意一些关于它的事情。首先,它有视图、助手和表单——但没有控制器。因此,没有可以调用的控制器或操作;你当然可以定义一些,但你不需要;您的小部件可以使用或不使用它们。其次,注意views/scripts/
子目录没有进一步细分;它的视图脚本不是任何操作的一部分,因此它们可以位于该模块的顶层。最后,请注意它同时具有引导程序和配置。
让我们看一下引导程序文件。由于这是在一个模块中,它有一个以模块命名的前缀,因此是User_Bootstrap
。
class User_Bootstrap extends Zend_Application_Module_Bootstrap { public function initResourceLoader() { $loader = $this->getResourceLoader(); $loader->addResourceType('helper', 'helpers', 'Helper'); } protected function _initConfig() { $env = $this->getEnvironment(); $config = new Zend_Config_Ini( dirname(__FILE__) . '/configs/user.ini', $this->getEnvironment() ); return $config; } protected function _initHelpers() { $this->bootstrap('config'); $config = $this->getResource('config'); Zend_Controller_Action_HelperBroker::addHelper( new User_Helper_HandleLogin($config) ); } }
我已经覆盖了initResourceLoader()
方法,这样我就可以添加一个与我的helpers/
子目录相对应的“helper”资源;这将用于允许自动加载操作助手。
_initConfig()
方法初始化引导配置。我拉入与Bootstrap类文件相关的配置文件,并使用已注册的环境来确定要使用该配置的哪个部分。
最后,我有一个初始化方法来加载我的动作助手。此方法依赖于_initConfig()
方法,因为我想将我的配置传递给助手。在这里,我实例化了一个动作助手User_Helper_HandleLogin
。
接下来,让我们看一下配置:
[production] salt = "1471998176" adapter.table = "users" adapter.identity_column = "username" adapter.password_column = "password" [testing : production] [development : production]
这些是我将在我的动作助手中使用的值。我们稍后会重新访问它们;我在这里得到的一般要点是这只是一个普通的配置文件。
现在,让我们看看动作助手本身。提醒一下,动作助手可以为init()
(每次传递给新控制器时由助手代理调用)、preDispatch()
(在执行之前调用)定义钩子控制器的preDispatch()
挂钩并执行操作本身)和postDispatch()
(在操作和控制器的postDispatch()
例程之后执行).在这个特定的帮助程序中,我将定义一个preDispatch()
挂钩来执行以下操作:
- 如果我们没有确定的身份验证身份,则呈现一个登录小部件。
- 如果我们确实有一个确定的身份验证身份,则呈现一个用户配置文件小部件。
- 如果我们没有已建立的身份验证身份,但发生了
POST
,尝试登录用户;如果成功,则显示用户配置文件小部件,但如果不成功,则重新显示登录小部件并显示一条错误消息。
初始定义如下所示:
class User_Helper_HandleLogin extends Zend_Controller_Action_Helper_Abstract { protected $config; public function __construct(Zend_Config $config) { $this->config = $config; } public function preDispatch() { if (null === ($controller = $this->getActionController())) { return; } $auth = Zend_Auth::getInstance(); if (!$auth->hasIdentity()) { $this->handleLogin(); return; } $this->createProfileWidget(); } /* ... */ }
如前所述,我们期望构造函数有一个配置对象。稍后我们将使用它来获取一些用于身份验证的值。当我们启动preDispatch()
例程时,我们首先检查是否有可用的动作控制器;如果没有,我们将继续前进,因为我们没有要采取行动的观点。
接下来,我们检查身份。如果我们没有,我们委托给一个handleLogin()
方法;否则,一个createWidgetProfile()
方法。我们将首先查看后者,因为它更简单—但首先,我们将稍微题外话,看看我们如何获取视图对象。
class User_Helper_HandleLogin extends Zend_Controller_Action_Helper_Abstract { protected $view; /* ... */ public function getView() { if (null !== $this->view) { return $this->view } $controller = $this->getActionController(); $view = $controller->view; if (!$view instanceof Zend_View_Abstract) { return; } $view->addScriptPath(dirname(__FILE__) . '/../views/scripts'); $this->view = $view; return $view; } }
在这里,我们从控制器中获取视图。如果我们没有可用的,我们只需返回一个空值。但是,如果我们这样做,我们将添加一个指向模块脚本路径的脚本路径,并返回视图。
现在,createWidgetProfile()
方法:
class User_Helper_HandleLogin extends Zend_Controller_Action_Helper_Abstract { /* ... */ public function createProfileWidget() { if (!$view = $this->getView()) { return; } $view->user = $view->partial('profile.phtml', array( 'identity' => Zend_Auth::getInstance()->getIdentity(), )); } /* ... */ }
同样,一个简单的逻辑。我们尝试获取一个视图,如果没有获取到就提前退出。接下来,我们使用身份验证存储中的身份呈现部分视图,并将其分配给视图成员“用户”。视图脚本如下所示:
<?php $identity = (array) $this->identity; ?> <div id="user-profile"> <h4><?php echo $this->escape($identity['username']) ?></h4> <dl> <?php foreach ($identity as $field => $value): ?> <?php if ($field == 'username'): continue; endif ?> <dt><?php echo ucfirst($field) ?>:</dt> <dd><?php echo $this->escape($value) ?>:</dd> <?php endforeach ?> </dl> </div>
没什么特别的——只是一个带有标题和定义列表的div
。
接下来,handleLogin()
方法。在这个方法中我们需要做几件事:
- 检查我们是否有一个
POST
请求;如果没有,只需呈现表单。 - 验证表单;如果我们有错误,请重新呈现它。
- 尝试根据表单值进行身份验证;如果我们失败,则重新呈现表单,并出现错误情况。
- 最后,在身份验证成功后,存储身份,然后呈现配置文件。
逻辑是这样的:
class User_Helper_HandleLogin extends Zend_Controller_Action_Helper_Abstract { /* ... */ public function renderLoginForm(Zend_Form $form, $error = null) { if (!$view = $this->getView()) { return; } $view->user = $view->partial('login.phtml', array( 'form' => $form, 'error' => $error, )); } public function handleLogin() { $request = $this->getRequest(); $form = new User_Form_Login(); if (!$request->isPost()) { $this->renderLoginForm($form); } if (!$form->isValid($request->getPost())) { $this->renderLoginForm($form); return; } // Does the POST contain the form namespace? If not, just render the form $namespace = $form->getElementsBelongTo(); if (!empty($namespace) && !is_array($request->getPost($namespace))) { $this->renderLoginForm($form); return; } $username = $form->username->getValue(); $password = $form->password->getValue(); $password = substr($username, 0, 3) . $password . $this->config->salt; $password = hash('sha256', $password); $adapter = new Zend_Auth_Adapter_DbTable( Zend_Db_Table_Abstract::getDefaultAdapter(), $this->config->adapter->table, $this->config->adapter->identity_column, $this->config->adapter->password_column ); $adapter->setIdentity($username) ->setCredential($password); $auth = Zend_Auth::getInstance(); $result = $auth->authenticate($adapter); if (!$result->isValid()) { $this->renderLoginForm($form, 'Invalid Credentials'); return; } $auth->getStorage()->write( $adapter->getResultRowObject(null, 'password') ); $this->createProfileWidget(); } /* ... */ }
如果仔细观察,您会发现传入的配置用于配置身份验证适配器,并在散列之前对密码加盐。我们重新使用createProfileWidget()
方法以便在完成后呈现配置文件,新的renderLoginForm()
方法将为我们呈现我们的登录表单。
部分表单如下所示:
<div id="login-widget"> <?php if ($this->error): ?> <p class="error"><?php echo $this->escape($this->error) ?></p> <?php endif ?> <?php $this->form->setAction('#') ->setMethod('post'); echo $this->form; ?> </div>
我们可以更花哨,设置装饰器等等,但在示例范围内没有必要。我没有在这里展示表单定义,因为它有点超出了本文的范围;然而,任何旧形式都应该这样做。
如果您一直注意,您会注意到在这两种情况下——显示登录表单或显示用户配置文件——我将呈现的视图捕获到相同的视图变量“用户”。此时,您可以在操作的视图脚本中执行以下操作,以便在您生成的页面中显示小部件:
<?php echo $this->user ?>
总结
这个例子在提供的功能和结构方面是相当基础的。您可以通过多种方式扩展它:
不要使用preDispatch()
,让您的控制器显式调用它们需要使用的widget操作助手。
- 可能,您的控制器可以定义他们需要的“小部件”列表,并且每个小部件可以在
preDispatch()
上检查它以确定他们是否需要做任何工作。 - 或者,小部件可以维护它们应该在其中呈现(或不呈现)的操作、控制器或模块的列表。
让您的操作助手使用他们自己的模块中的模型和服务类来执行他们的工作。例如,我可以编写一个身份验证模型并简单地从操作助手中使用它,以提供更好的关注点分离。
您还可以编写特定于您在模块中编写的某些模型的视图助手。然后,您不仅需要向视图添加脚本路径,还需要添加辅助路径。
您应该设置一些清晰、干净的规则,以确保在您的视图中如何命名小部件以及如何命名助手。
这种技术相当灵活,可以让您的代码干净利落地分开,并保护您免受ActionStack
和action()
视图助手中固有的不一致和问题的影响。通过一定的纪律和创造性思维,您应该能够实现各种效果,以及制作可重复使用的小部件。
注意事项
我已将这篇文章中的完整代码示例放在GitHub上我的zf-examples存储库中。
说明
许多人在评论中表示他们一直在使用viewhelpers来影响他们网站上的小部件内容,并询问这是否是一种合适的方法(或争论它是)。
使用视图助手很有意义,但是如果且仅当满足以下条件:
- 助手不需要请求数据来完成它的工作,或者可以只依赖注入到视图中的数据(不要欺骗和注入请求对象!)。
- 助手不会更新模型。
如果您正在执行以上两项中的任何一项,您应该考虑使用一个动作助手。视图应该只负责显示逻辑,可能包括从模型中检索数据。控制器负责检查请求并确定要编组的模型和视图,以及更新模型。
如果您的小部件只是简单地从模型中提取数据,或显示一些标记,请务必使用视图助手。如果它做的不止于此,请不要。
这也涉及评论中提出的一个相关主题:如果您要保留替代内容类型(例如,application/json
)怎么办?同样,这是我觉得使用动作助手对您有利的地方。为您的操作助手定义一个接口将非常容易,该接口允许它们指示允许它们操作的内容类型。然后,在动作助手逻辑或编组动作助手的插件中,如果它们不能提供该内容类型,您可以轻松地禁止它们执行。在视图中,您根本不会引用它们捕获的输出—即使您这样做,如果它们被禁用,该值也将简单地返回为null
。