Saturday, November 24, 2012 - 05:12

Reordering and extending forms in Drupal 7.

This is going to be a multi part article covering some aspects of the forms api in drupal 7 explaining some common tasks and their pitfalls when working with forms. Usually there is more than one way to get where you want to be.

Most of them however are not the right way and may or may not lead you into a trap sometime down the road. In many places you can do a lot of unconventional and dirty coding. It's not good practice but well. What the heck. Forms however are less forgiving since they are likely targeted by other modules. If you mess up a form your could run into trouble with some modules messing with that form as well.

The example form we are going to work with in this part is the login block. We are going to extend and reorder it. A full module should also mangle the user login page. We will omit that for the sake of the example.

The standard Drupal 7 login block has username and password fields. Some auxiliary links and the submit button. It's a fairly simple form and a good example.

When developing you should always have the developer module ready. Specifically when working with forms. dpm() significantly speeds up the understanding of variables. Specifically that of complex arrays.

Since we are working with the login form you may want to adjust the developer module's permissions to include anonymous. Otherwise you won't get any output from it when you are not logged in. And you want that.

When messing with forms you have two choices. You can go the module way or the template.php way. Both pretty much leads to the same result. You'll work with hook_form_alter. Since we are not just doing cosmetic changes we are going the module way.

Our module will add a code field to the login form. If you are missing something have a look at the attachment. It is the module as a working example including the parts not explained here. The module's .info file for example.

To capture hook_form_alter we need to namespace it to our module as with any hook. Our module is called cauth. So this is our function in the making...

function cauth_form_alter( &$form, &$form_state, $form_id ){}

The function has 3 parameters.

  • $form is an array of elements that make up the form. I.e. the two input fields and the submit button.
  • $form_state is the current state of the form. For example the value of the username field after the form was submitted.
  • $form_id is basically its name.

The first problem is to figure out the form_id of the form we want to work with because we only want to work on the login form. If you have the developer module up and running this is a piece of cake. Simply dpm the form_id.

function cauth_form_alter( &$form, &$form_state, $form_id )
{
    dpm( $form_id );
}

As a side note. When you extend your module with a hook you need to clean the cache or drupal most certainly won't pick it up.

Visiting the login block dpm tells us that the form_id in question surprisingly is user_login_block. That's what we want to work with. So let's have a look at the form itself.

function cauth_form_alter( &$form, &$form_state, $form_id )
{
	switch( $form_id )
	{
		case 'user_login_block' :
			dpm( $form );
		break;
	}
}

Visiting the login block again we get a slightly more complex output from dpm that may require some explanation.

The visible elements of that form are name,pass,actions and links. Name and pass are obviously the two input fields. Actions contains the login button and links is simply a markup.

Now how do we add a field to that mess?

We simply append the correct field array to the form. Our code field is a simple textfield.

function cauth_form_alter( &$form, &$form_state, $form_id )
{
	switch( $form_id )
	{
		case 'user_login_block' :
			$form['cauth'] = array
			(
				'#type' => 'textfield',
				'#title' => t( 'PIN Code' ),
				'#maxlength' => 4,
				'#size' => 4,
				'#required' => TRUE,
			);
		break;
	}
}

We have a field of type textfield. We are going to call it PIN CODE and it's going to be a 4 digit pin code. And since it's a login field we might want to make it a requirement.

Testing this form you'll see our new pin code field at the bottom of the form. You cannot login without input but of course right now any input would suffice. We have two more things to do

  • The field looks rather shitty at the bottom of the form. It should be placed after the password field.
  • It has no function...yet.

Let's start with the easier part first. Moving the pin code below the password field. In this case we actually could easily achieve it by taking the markup code from the links out of the form and put it in again at the bottom. This however is not that easy with more complex forms. The forms API knows another field element. weight. It works as expected. Lower numbers place an element towards the top. Higher numbers towards the bottom and more complex forms - or those already tampered with - may already have weight elements. Right now we have 4 visible elements. So let's give them some weight.

function cauth_form_alter( &$form, &$form_state, $form_id )
{
	switch( $form_id )
	{
		case 'user_login_block' :
			$form['cauth'] = array
			(
				'#type' => 'textfield',
				'#title' => t( 'PIN Code' ),
				'#maxlength' => 4,
				'#size' => 4,
				'#required' => TRUE,
				// our own weight goes here
				'#weight' => 2,
			);
 
			// reorder the messy rest
			$form['name']['#weight'] = 0;
			$form['pass']['#weight'] = 1;
			$form['actions']['#weight'] = 3;
 
			// we do not necessarily have this one
			if( isset( $form['links'] ) )
				$form['links']['#weight'] = 4;
 
		break;
	}
}

We now have the PIN code below the password field and also moved the links below the submit button. Reordering forms should, if possible, be achieved with weighting them. Not by mangling with the array directly.

One thing on reordering that is not important in this case but generally does apply. Reordering is a cosmetic issue and really should be done in the template.php if you program a module for the public. Why? You cannot know what other items might be injected into the form. So forcing an order might cause unpleasant results. Just because it looks right for you it doesn't have to for the rest if they have other modules plugged in as well. Design should be left to the design guys. Or the coder supporting them. It's not that much of a big deal since you could reorder it anyway. Still. Work that is really not necessary.

The PIN code still has no function. We are going to implement that now. Looking at our dpm'ed $form array again you'll notice an element #validate. Depending on your setup this should contain a couple of entries. These are basically functions being called to determine the validity of our input. In this case our login attempt. They are drupal functions and extending this functionality is actually as simple as it looks. We just have to append a function.

Do not replace and rewrite the entire array. I've seen this even in more prominent modules. It is completely 100% certified fucked up illegal crap. Why? For one it's more work. More importantly it fucks up with the very system you are using right now. The option to append functions to the validation process. Imagine your module comes after another module that also injects a function. If you assume anything you just removed the functions you didn't assume.

Once you did that people will start to complain with the other guy why his stupid bullshit module isn't working. If you pull that stunt with me I'll be very unhappy. I might even cry.

More important for you: I'll break your illegal coding fingers. So don't do it. Our final hook looks like this:

function cauth_form_alter( &$form, &$form_state, $form_id )
{
	switch( $form_id )
	{
		case 'user_login_block' :
			$form['cauth'] = array
			(
				'#type' => 'textfield',
				'#title' => t( 'PIN Code' ),
				'#maxlength' => 4,
				'#size' => 4,
				'#required' => TRUE,
				'#weight' => 2,
			);
 
			$form['name']['#weight'] = 0;
			$form['pass']['#weight'] = 1;
			$form['actions']['#weight'] = 3;
 
			// we do not necessarily have this one
			if( isset( $form['links'] ) )
				$form['links']['#weight'] = 4;
 
			// this is our new validator added to the chain.
			$form['#validate'][] = 'cauth_login_validate';
 
		break;
	}
}

I don't assume anyone reading this will notice that the validator is appended to the chain and this - according to the user module - should not be done as user_login_final_validate() should be the last function called. The solution to fix it would be to prepend it with unshifting the array. I however fail to see why this 'should be' the case. It is true - an required - in the context of the login module. I however fail to see what's special about the final_login validator that requires it to sit in the back of the train. The only reason for that statement - if meant globally - I can imagine is the watchdog entry being set there that indicates a failed login attempt. This however would be an awkward statement since the validation failure caused by our module would not trigger the watchdog entry.

Now we need to implement our validator. Apparently the function is going to be called cauth_login_validate. They have 2 parameters. $form and - this time important - $form_state.

Validators work in a way that could be described as NO! Otherwise I don't give a shit. So we basically just invalidate the form in case we don't like the input. Here's our function. We inspect the $form_state this time because that's where submitted values are hiding.

function cauth_login_validate( $form, $form_state )
{
	// not overly sophisticated. May need some work ;-)
	$valid_code = "1234";
 
	// this is where our code is hiding
	$code = $form_state['values']['cauth'];
 
	// if you're not one of us get the fuck out.
	if( $valid_code !== $code )
		form_set_error( 'cauth', t( '@code is not what I wanted to hear.', array( '@code' => $code ) ) );
}

That's it. Done for the day. The module reorders the login block and adds a code field that is also validated. The same formula can be applied to virtually any form in Drupal. Happy form fixing. The entire module is in the archive provided at the bottom of the page. If you managed to lock you out installing it...it's 1234.

Add new comment

This form is protected by Google Recaptcha. By clicking here you agree to include Google Recaptcha for this session. The page will reload and the form will become avaiable.