Skip to main content

Introducing the EntityFormController

Drupal 8 comes with many new concepts. A lot of work has gone into expanding on Entities and forms. One such concept is the EntityFormController. The EntityFormController is a controller class for the management of forms for entities. Say for example your module Foo defines an entity Bar. Say you want to provide a form for this entity so that users can set some properties or field values. In the past you would have created a hook_menu item to a drupal_get_form callback. In Drupal 8 we do it a better way. We define a class which implements the EntityFormController. The base class provides some pre-built entity goodness, and we can extend the class to add whatever we need.

by tim.eisenhuth /

Introduction

Recently I started working on a Drupal 8 issue for converting an existing Form callback function into Drupal 8’s new EntityFormController. As this is a new concept in D8, and many modules are currently using entities with custom form callback’s, I thought I would share my experiences and early knowledge of what the EntityFormController provides, and how it works.

What is it?

The Entity form controller is a controller class that encapsulates all of the functionality one would need for working with forms for their entities. The advantage to providing an interface and class for it is that it provides a consistent and standardised API for form creation, instantiation and management. As well as providing some base methods that would be common to most types of forms. If you think about what HTML forms actually do, it is easy to see that it is simply a means of “posting” data about an object and then doing something with that data. When coding a HTML form manually, we simply create the fields that we need. The advantage of having an API is that Drupal is already aware of the structure of your entity, it know's what field's it has attached, it knows how to store it and handle it. This makes it a lot easier to handle common functionality.

Disclaimer - I am in no way an expert on this topic and it is something I am currently learning myself. I hope to be able to share what I have learnt so far. If you are a module developer and need to convert your D7 modules to D8, this will be helpful to you.

The old way...

Drupal 7 introduced some great enhancements to the Form API, and for the most part it was all good. Forms are simply structured arrays and adding in certain elements could control the output of various parts of your form. The drawback with Forms in Drupal 7 is that you still have to mostly build the form yourself. The concept behind the EntityFormController is that many forms share the same underlying structure. That is, we take some form elements, we validate them, we submit them and we process them. In D7, to do this, we would generally speaking write 3 functions. The Form callback, the validate callback and the submit callback. Also, in Drupal 7, Field API and Storage integration wasn’t quite as mature as it is now.

This works great because in D8, the idea that content and configuration are entities means that all of that built in entity goodness is standardised and easy to access. If your EntityFormController is aware of your entity then you don't even need a submit handler. The EntityFormController interface provides one for you and a default submit button. The beauty is, we know that if a form relates to an entity, submitting that form generally means we are saving/updating that entity. So, the default save() method does simply that. 

Converting old forms to EntityFormController

I dived into the EntityFormControlller by using the core Contact module as an example. The first thing to understand is that the module defines its EntityFormController through an annotation. If you've never worked with annotations in PHP, don't fear, Its pretty simple once you get past the fact that code comment blocks are affecting application functionality. But that’s for another article. Just know that in the following code we define which class to use as the EntityFormController.

The entry point

The contact module provides an EntityFormController for the add/edit category functionality. If you take a look at the module’s code, we start with hook_menu. Similar to Drupal 7, but now using a route:

 $items['admin/structure/contact/manage/%contact_category'] = array(    
'title' => 'Edit contact category',    
'route_name' => 'contact.category_edit', 
);

I won't go into the details of routing in this blog post but if you take a look at the Category entity definition in core/modules/contact/lib/Drupal/contact/Entity.php, you will see an annotation that defines the Category entity and it's associated forms:

 /** * Defines the contact category entity.
 * 
 * @EntityType( 
 *   id = "contact_category", 
 *   label = @Translation("Contact category"), 
 *   module = "contact", 
 *   controllers = { 
 *     "storage" = "Drupal\contact\CategoryStorageController", 
 *     "access" = "Drupal\contact\CategoryAccessController", 
 *     "list" = "Drupal\contact\CategoryListController", 
 *     "form" = { 
 *       "add" = "Drupal\contact\CategoryFormController", 
 *       "edit" = "Drupal\contact\CategoryFormController", 
 *       "delete" = "Drupal\contact\Form\CategoryDeleteForm" 
 *     } 
 *   }, 
 *   config_prefix = "contact.category", 
 *   bundle_of = "contact_message", 
 *   entity_keys = { 
 *     "id" = "id", 
 *     "label" = "label", 
 *     "uuid" = "uuid" 
 *   }, 
 *   links = { 
 *     "edit-form" = "admin/structure/contact/manage/{contact_category}" 
 *   } 
 * ) 
 */

Notice for example the "add" form is the class Drupal\contact\CategoryFormController? This is basically saying, for this entity's "add" form, I want to use this class, which tada extends EntityFormController.

Defining the form controller

So now that we have seen where the CategoryFormController is defined, let’s open it up. As I mentioned above, the first thing you should note is that the CategoryFormController extends EntityFormController:

 class CategoryFormController extends EntityFormController {

The EntityFormController base class, already has quite a lot of built in goodness. It has methods for validate(), submit(), save() - This means that some of the functionality of your form may actually already be built in.

For example, the actions() method provides some default buttons for the form:

/**   
* Returns an array of supported actions for the current entity form.   
*/ 
protected function actions(array $form, array &$form_state) {    
return array(     
'submit' => array(       
'#value' => t('Save'),        
'#validate' => array(         
array($this, 'validate'),       
),       
'#submit' => array(         
array($this, 'submit'),         
array($this, 'save'),       
),     
),     
'delete' => array(       
'#value' => t('Delete'),        // No need to validate the form when deleting the entity.       
'#submit' => array(         
array($this, 'delete'),       
),     
),   
); 
}

 Generally every form will have at least one button, and so providing these defaults saves you from having to code them yourself, and also maintains a consistent structure. However, you can override this method. In my case, I didn't need a delete button, so I simply did this:

 public function actions() {
$actions = parent::actions($form, $form_state);
unset($actions[‘delete’]);
return $actions;
}

If you take a look at the CategoryFormController, you will see that it has overridden some of the methods to implement custom functionality. For instance, see the save() method. It includes some drupal_set_message() notifications specific to the category functionality.

This is an example of one of the many benefits of OOP. Instead of having to code around something I want to change, my implementation of it simply overrides the default and I can make any changes I wish.

Defining the form

 The form() method is the method responsible for rendering the form. This is where you return the form array in the same way that a D7 form callback might work. For example, in my example, we  might have the following code:

publicfunction form($form, &$form_state) {
$form[‘name’] = array(‘#type’ => ‘textfield’,‘#title’ => t(‘Name’),‘#description’ => t(‘Please enter a name’));
return $form;
}

As you can see it feels very familiar. You can do most of what you would do with a D7 form in this method. 

As with D7, you can specify a #validate callback function on a form element, but with EntityFormController you also have a standard validate() method. When you implement these methods yourself don’t forget to include:

parent::validate($form, $form_state);

The EntityFormController provides some standard validation processing. For instance, the validate() method will iterate through each of the entities fields and run the validate() method for those fields. So again you have this validation work already done for you

Note: There is a lot of work still happening around this. The focus on this blog post has been on simple entities that you might use in your own custom modules. There is a lot of work around API’s for Content and Config entities. For instance entities that rely on the new Entity Field API can rely on the Entity Validation API. 

Submitting

As above, the EntityFormController provides a submit() and save() method. The submit() method builds the entity so that it is ready for other submit handlers to be able to work on the validated and constructed entity. From a simple entity perspective you can place your submit handing code in your own classes save() method.

Conclusion

This has been only a quick introduction to the new EntityFormController. There are many more benefits to this approach and much more sophistication to its use, but hopefully this blog post will shed some light on it and make it look a little less scary. The core of the Form API is the currently very similar to D7, but EntityFormController takes that a step further by providing you a lot of default (and usually required) functionality. There might be a little bit of time in creating the directories and classes, but for simple forms you get this time back by not having to code your own submit and validate handlers as well as proper entity loading and handling.

 

Notes

Senior Developer