Thursday 23 October 2014

Adding a submit callback to an entity form

If you use hook_form_alter(), hook_form_FORM_ID_alter(), or even hook_form_BASE_FORM_ID_alter() to intercept an entity add/edit form and you want to add a #submit callback, then this:

$form['#submit'][] = 'my_module_extra_submit';

won't work. But this will:

$form['actions']['submit']['#submit'][] = 'my_module_extra_submit';

Because the submit callbacks are added to the button and not the form. The same applies to #validate though as usual you're better off adding '#element_validate' to the element you want to check, if possible.

But that probably won't be enough because the default content of the submit callbacks is:

array('::submitForm', '::save')

If you're making a change to the entity you can't add your callback on the end of the array because the entity has already been saved, and your change will make no difference.

Nor can you unshift it on to the start because ::submitForm() overwrites the entity with a new one constructed from the form values. So any change will be wiped out.

You have to insert it just before the ::save(), this code will do the trick:

  // Have to add the submit callback to the button submit on an entity form
  // but it has to go after the '::submitForm()' and before the '::save()'.
  $submits =& $form['actions']['submit']['#submit'];
  $key = array_search('::save', $submits, TRUE);
  array_splice($submits, $key, 0, array('my_module_extra_submit'));

Things to bear in mind here is that the 4th parameter to array_splice() is cast to an array if it isn't one, which could result in strange behaviour if you're using a callback to a method, in which case you need array(array('myClass', 'myMethod')).

BASE_FORM_ID?

The entity form class creates a base_form_id, which is the entity id plus "_form" (e.g. "node_form") which is also called in the form alter sequence. It means you can have a single hook to handle both add and edit forms.

If the base form id would be the same as the form id, it doesn't perform the extra call because that would result in a hook function being called more than once.

Don't forget to sign-up to get information about my Drupal 8 book(s).

Friday 17 October 2014

How to screw up an annotation

When defining plugins in Drupal 8 you use the annotation in the file, like this (example from block_content):


/**
 * Defines the custom block type entity.
 *
 * @ConfigEntityType(
 *   id = "block_content_type",
 *   label = @Translation("Custom block type"),
 *   handlers = {
 *     "form" = {
 *       "default" = "Drupal\block_content\BlockContentTypeForm",
 *       "add" = "Drupal\block_content\BlockContentTypeForm",
 *       "edit" = "Drupal\block_content\BlockContentTypeForm",
 *       "delete" = "Drupal\block_content\Form\BlockContentTypeDeleteForm"
 *     },
 *     "list_builder" = "Drupal\block_content\BlockContentTypeListBuilder"
 *   },
 *   admin_permission = "administer blocks",
 *   config_prefix = "type",
 *   bundle_of = "block_content",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "label"
 *   },
 *   links = {
 *     "delete-form" = "entity.block_content_type.delete_form",
 *     "edit-form" = "entity.block_content_type.edit_form"
 *   }
 * )
 */

Which is all great and we love it.

However I was developing a new ConfigEntityType and the plugin discovery system was just refusing to recognise it. So I was tearing my hair out for a while until I discovered this:

Under absolutely no circumstances place a second docblock after the first one, like this:

/**
 * Defines the custom block type entity.
 *
 * @ConfigEntityType(
 *   id = "block_content_type",
etc...
 */
/**
 * Another docblock...
 */

Because the annotation discovery system won't recognise the first one.

Why would you do such a thing? Well, I did it because I wanted to store some of the annotation fields that I wasn't currently implementing as I developed the plugin. And I used a docblock.

Then wasted several hours.

(Don't forget to sign up for information on the Drupal 8 books I'll be releasing - don't worry you won't be spammed.)