Adding custom fields to ZfcUser register form

The ZfcUser module

ZfcUser is a Zend Framework commons module that handles basic use cases related to users. It takes care of handling the registration, logging in, logging out, authenticating and user profile management. It is a very well written module, allowing the user of the module to change the behavior without having to hack in the module’s codebase. Tip: never change the code of a vendor module.

The problem

A common use-case for the ZfcUser module is to add more fields to the registration form (i.e. a firstname and lastname or date of birth). Of course when I needed this functionality myself, I did a quick search and figured I wasn’t the only one needing this. However there were no real conclusive answers of how to do this. I did find one article[1] that helped me to get started.

What I needed

What I needed was the following:

  1. Add the extra fields to the registration form;
  2. Make sure the fielddata gets saved to the database when the user registers;
  3. Adding validators and filters to the custom fields;
  4. Changing the validator messages of the standard ZfcUser userfields;
  5. Changing the order of the fields in my registration form.

How I accomplished it

I am not saying this is necessarily the best way to do it, but it does what I need it to do. If you have any suggestions to make it better/cleaner please let me know.

First of all you need to install the ZfcUser module and activate it, but I am assuming here that you have already done so. In case you need help with that check the ZfcUser readme.

After installing and activating the module the first choice to make is if you are going to create a separate module for your ZfcUser extensions or if you are going to add it to an existing module, such as the Application module. In this example I chose for the latter.

User Entity

The first thing to do is to let ZfcUser know what entity class to use for the User entity. Luckily the ZfcUser module allows you to specify this using the configuration. If you haven’t already done so, first copy zfcuser.global.php.dist to your project’s config/autoload directory and remove the .dist extension.
Then open it and change the ‘user_entity_class’ setting. In my case it looks like this:

zfcuser.global.php:

'user_entity_class' => 'Application\Entity\User',

Your custom User entity class has to implement the \ZfcUser\Entity\UserInterface interface:

Application/Entity/User.php:

<?php
namespace Application\Entity;

use ZfcUser\Entity\UserInterface

class User implements UserInterface
{
...
}

User Mapper

In my case I also needed to use my own User mapper class. If you also happen to need this you have to make sure to change the factory closure for zfcuser_user_mapper in module.config. For example this is what mine looks like in my module.config.php of my Application module:

    'service_manager' => array(
        'factories' => array(
            // Error logging service
            'Application\Service\ErrorHandling' =>  function($sm) {
                $logger = $sm->get('\Zend\Log');
                $service = new Application\Service\ErrorHandling($logger);
                return $service;
            },

            // Override ZfcUser User Mapper factory
            'zfcuser_user_mapper' => function ($sm) {
                $options = $sm->get('zfcuser_module_options');
                $mapper = new Application\Mapper\User($sm->get('Zend\Db\Adapter\Adapter'));
                $entityClass = $options->getUserEntityClass();
                $mapper->setHydrator(new ZfcUser\Mapper\UserHydrator());
                $mapper->setTableName($options->getTableName());
                return $mapper;
            },
        )
    ),

Edit (2013/08/15):  Mike pointed out that my User mapper factory code didn’t work for him. This is because of the fact that my User mapper is not a subclass of AbstractDbMapper (zf-commons), but of my own abstract mapper class instead. In the constructor of my mapper the db adapter is being set. If your own User Mapper is a subclass of zf-commons’ AbstractDbMapper you have to explicitly call setDbAdapter:

'zfcuser_user_mapper' => function ($sm) {
                $options = $sm->get('zfcuser_module_options');
                $mapper = new Application\Mapper\User();
                $mapper->setDbAdapter($sm->get('Zend\Db\Adapter\Adapter'));
                $entityClass = $options->getUserEntityClass();
                $mapper->setHydrator(new ZfcUser\Mapper\UserHydrator());
                $mapper->setTableName($options->getTableName());
                return $mapper;
            },

If you use this approach then make sure that your Application module is loaded after the ZfcUser module.

Make sure that your User mapper implements the \ZfcUser\Mapper\UserInterface. Of special importance are the two methods findByEmail and findByUsername, which you have to implement for ZfcUser to work properly.

Adding the fields and validators to the registration form

To add the actual fields to the register form I use the init event of the ZfcUser\Form\Register class. To add filters and validators to the fields I use the init event of the ZfcUser\Form\RegisterFilter class. In my Application module’s Module.php I have a private helper method to perform both of these tasks. This method is called in onBootstrap in the same Module class:

class Module
{
	/**
	 * Extends the ZfcUser registration form with custom fields
	 * 
	 * @param EventManager $eventManager
	 */
	protected function extendUserRegistrationForm(EventManager $eventManager)
	{
		// custom fields of registration form (ZfcUser)
		$sharedEvents = $eventManager->getSharedManager();
		$sharedEvents->attach('ZfcUser\Form\Register',
				'init',
				function($e)
				{
					/* @var $form \ZfcUser\Form\Register */
					$form = $e->getTarget();

					$form->add(
							array(
									'name' => 'firstname',
									'type' => 'text',
									'options' => array(
											'label' => 'First name',
									),
							)
					);

					$form->add(
							array(
									'name' => 'lastname',
									'type' => 'text',
									'options' => array(
											'label' => 'Last name',
									),
							)
					);

					$form->add(
							array(
									'name' => 'newsletter',
									'type' => 'Zend\Form\Element\Checkbox',
									'options' => array(
											'label' => 'Sign up for our newsletter',
											'checked_value' => 1,
											'unchecked_value' => 0,
									),
							)
					);
				}
		);

		// Validators for custom fields
		$sharedEvents->attach('ZfcUser\Form\RegisterFilter',
				'init',
				function($e)
				{
					/* @var $form \ZfcUser\Form\RegisterFilter */
					$filter = $e->getTarget();

					// Custom field firstname
					$filter->add(array(
										'name'       => 'firstname',
										'required'   => true,
										'filters'  => array(
												array('name' => 'StripTags'),
												array('name' => 'StringTrim'),
										),
										'validators' => array(
												array(
														'name'    => 'StringLength',
														'options' => array(
																'min' => 3,
																'max' => 255,
														),
												),
										),
									)
					);

					// Custom field lastname					
					$filter->add(array(
										'name'		=> 'lastname',
										'required'	=> true,
										'filters'  => array(
												array('name' => 'StripTags'),
												array('name' => 'StringTrim'),
										),
										'validators' => array(
												array(
														'name'    => 'StringLength',
														'options' => array(
																'min' => 3,
																'max' => 255,
														),
												),
										),
							)
					);
				}
		);
	}

Custom views

If you implemented the above steps you’re good to go, but you may want to customize your views too. It’s pretty simple to do that: just create a new directory in your own module’s view directory named zfc-user and another directory inside of that directory named user. In my example case this would be Application/view/zfc-user/user. Whatever view template you want to override just implement it in there. To override the register view, just create your own register.phtml in that directory. Again, as long as your own module is being loaded after the ZfcUser module (check in your application.config.php) this will override the default view.

All in all pretty simple, but hopefully it helps someone else. Feedback is welcome in the comments.

References

[1] Circlical – ZF2, Doctrine2, ZfcUser – customizing the registration form and storing the custom user entity