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.

Drew

Drew

Hi! I'm Drew, the Wimpy Programmer. I'm a software developer and formerly a Windows server administrator. I use this blog to share my mistakes and ideas.