MVC Form Layer
Photo by amin khorsand on Unsplash.
Forms are a tricky part of any Model-View-Controller (MVC) application. They incorporate validation, presentation, and security logic that spans all tiers of the application. Separating these concerns is difficult but important. I'll walk through how I use Zend_Form
in Zend Framework 1, but the same principles should apply to any MVC application.
A User account form
For this article, imagine a basic form for users to create or update their account. To keep things simple, I've replaced some of the logic with // TODO:
comments. Let's start with the form itself.
class Application_Form_Account
{
const FIELD_USERNAME = 'username';
public function init()
{
$this->setName('account');
$username = new Zend_Form_Element_Text(self::FIELD_USERNAME);
$username->setLabel('Username');
// TODO: Add filters and validators
$this->addElements(array($username));
}
public function populateFromUser(Application_Model_User $user)
{
$this->{self::FIELD_USERNAME}->setValue($user->getUsername());
return $this;
}
}
Because the form field names are referenced outside of this class, I assign them to class constants. The populateFromUser()
function will receive a User object and copy its attributes to the appropriate form fields. Other populateFrom*()
functions could be added to accommodate other objects.
The User model
Next we look at the User model. As a foundation I used the approach suggested by Matthew Weier O'Phinney, Zend Framework Project Lead, in "Using Zend_Form in Your Models". Here's the User model and its base class.
class Application_Model_User extends My_Model_Abstract
{
protected $_formDefault = 'Account';
public function saveFormAccount(Application_Form_Account $form, array $data)
{
if(!$form->isValid($data)) {
return FALSE;
}
$this->setUsername($form->getValue($form::FIELD_USERNAME))
->save(); // Store the object; not discussed in this article
return $this;
}
}
abstract class My_Model_Abstract
{
// The base class name or an array of class names to use when loading forms for this object.
protected $_formBase = 'Application_Form_';
// The name of the default form to load for this object.
protected $_formDefault;
/**
* Retrieve an instance of Zend_Form that can be used to interact with this object.
*
* @param string $type The type of form to retrieve, if many are applicable.
* @param array|Zend_Config $options The options to pass to the form.
* @return Zend_Form
*/
public function getForm($type = NULL, $options = array())
{
// Determine the name of the form to return. This may be passed as a parameter
// or set as a property of the model.
if($type === NULL) {
if(empty($this->_formDefault)) {
throw new LogicException(sprintf('Default form not specified in %s::%s', __CLASS__ . '::' . __FUNCTION__);
} else {
$type = ucfirst($this->_formDefault);
}
} else {
$type = ucfirst($type);
}
// Determine the full name of the form class.
$class = '';
if(empty($this->_formBase)) {
throw new LogicException('Form base path(s) not specified in ' . __CLASS__ . '::' . __FUNCTION__);
} else {
foreach((array)$this->_formBase as $formBase) {
if(class_exists($formBase . $type)) {
$class = $formBase . $type;
break;
}
}
}
if(empty($class)) {
throw new LogicException('Unable to locate form "' . $type . '" in ' . __CLASS__ . '::' . __FUNCTION__);
}
return new $class($options);
}
}
Compared to Matthew's example, I removed the $_forms
property and added support for form options and multiple form bases.
The controller
Finally, we'll tie everything together in the controller.
class Application_IndexController extends Zend_Controller_Action
{
public function indexAction()
{
$userId = $this->_getParam('userId', 0);
$this->view->isNewUser = ($userId == 0);
$userMapper = new Application_Model_Mapper_User(); // The User data mapper; not discussed in this article
if($this->view->isNewUser) {
$this->view->user = $userMapper->create(); // Custom mapper function to return a new Application_Model_User
} else {
$this->view->user = $userMapper->getById($userId);
}
$this->view->form = $this->view->user->getForm('account'); // Parameter is redundant
if($this->_request->isPost()) {
$postData = $this->_request->getPost();
if(!$this->view->user->saveFormAccount($this->view->form, $postData)) {
// Redisplay the form with validation errors.
} else {
// TODO: Display success message, 302-redirect user
}
} else {
if($this->view->isNewUser) {
// Display a blank form.
} else {
// Display the saved form values.
$this->view->form->populateFromUser($this->view->user);
}
}
}
}
The controller retrieves an instance of Application_Model_User
and gets an Application_Form_Account
from it. If loading the form for the first time, it is populated with the user's attributes. If the user submits the form, the form and its contents are passed to the User model to validate and save.
Displaying the form
To cap off this example, let's see the view that renders this action.
$this->form->setMethod('post')
->setAction($this->url());
$submit = new Zend_Form_Element_Submit('submit');
if($this->isNewUser) {
$submit->setLabel('Register');
} else {
$submit->setLabel('Update');
}
$this->form->addElement($submit);
// TODO: Set form and form element decorators
echo $this->form;
Presentation concerns like the submit button and form decorators are handled here because they aren't pertinent to the form-model interaction.
Conclusion
With this approach, all form operations are done in the appropriate tier. The form populates itself from models, the model digests all of the form data, and the view handles all of the rendering. This keeps the form flexible and reusable.