Back in August 2013, I wrote the original version of this article on my Drupal Gardens blog. Drupal 8 has continued to be refined since then, so I updated the code to work against the current state of Drupal 8, and am cross posting it here.
Thank you to everyone who provided feedback and encouragement on my first blog post about Object-oriented programming (OOP) in Drupal 8. This is the next installment. In the first post, we examined a very simple controller: one that just outputs "Hello.". In the real world, of course, Drupal modules need to do more than that. Often, they provide forms, store the information that's submitted via those forms, and provide pages that display that stored information. So in this post, let's look at how to do that in Drupal 8. In the process, we'll take a brief look at some key OOP concepts: interfaces and inheritance. The example I'll use for this is a module named firewall. Its job is to restrict access to the website to only visitors coming from an allowed IP address. So it needs to:
A real module like this would also need to implement the logic that denies access to the website for every visitor not coming from an allowed IP address, but I'm leaving that part out of this example, and will cover it in a future blog post about event subscribers. This example is based on functionality that's in Drupal core that lets you maintain a list of IP addresses to ban. In Drupal 7, you can find the code for that functionality within all the functions named system_ip_blocking*(). In Drupal 8, that functionality has been moved to the ban module (within the core/modules directory along with all the other core modules), so you can look through that to see a more "real" example. While I based this post's firewall module on that, I stripped it down to focus on the essential material relevant to this post. Here's the Drupal 7 version of the firewall module:
name = Firewallcore = 7.xfiles[] = lib/FirewallStorage.php
array( 'ip' => array( 'type' => 'varchar', 'length' => 40, 'not null' => TRUE, ), ), 'primary key' => array('ip'), ); return $schema;}
array( 'title' => 'Allowed IP addresses', 'page callback' => 'firewall_list', 'access callback' => 'user_access', 'access arguments' =>>array('administer site configuration'), 'file' => 'includes/firewall.list_page.inc', ), 'admin/config/people/firewall/add' => array( 'title' => 'Add allowed IP address', 'page callback' => 'drupal_get_form', 'page arguments' => array('firewall_add'), 'access callback' => 'user_access', 'access arguments' => array('administer site configuration'), 'file' => 'includes/firewall.add_form.inc', ), 'admin/config/people/firewall/delete/%' => array( 'title' => 'Delete allowed IP address', 'page callback' => 'drupal_get_form', 'page arguments' => array('firewall_delete', 5), 'access callback' => 'user_access', 'access arguments' => array('administer site configuration'), 'file' => 'includes/firewall.delete_form.inc', ), );}
fetchCol(); } static function exists($ip) { $result = db_query('SELECT 1 FROM {firewall} WHERE ip = :ip', array(':ip' => $ip))->fetchField(); return (bool) $result; } static function add($ip) { db_insert('firewall')->fields(array('ip' => $ip))->execute(); } static function delete($ip) { db_delete('firewall')->condition('ip', $ip)->execute(); }}
'item_list', '#items' => $items, );}
'textfield', '#title' => t('IP address'), ); $form['actions'] = array('#type' => 'actions'); $form['actions']['submit'] = array( '#type' => 'submit', '#value' => t('Add'), ); return $form;}function firewall_add_validate($form, &$form_state) { $ip = $form_state['values']['ip']; if (FirewallStorage::exists($ip)) { form_set_error('ip', t('This IP address is already listed.')); } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) == FALSE) { form_set_error('ip', t('Enter a valid IP address.')); }}function firewall_add_submit($form, &$form_state) { $ip = $form_state['values']['ip']; FirewallStorage::add($ip); watchdog('firewall', 'Added IP address %ip.', array('%ip' => $ip)); drupal_set_message(t('Added IP address %ip.', array('%ip' => $ip))); $form_state['redirect'] = 'admin/config/people/firewall';}
'value', '#value' => $ip, ); $question = t('Are you sure you want to delete %ip?', array('%ip' => $ip)); $description = t('This action cannot be undone.'); $confirm_text = t('Delete'); $cancel_text = t('Cancel'); $cancel_path = 'admin/config/people/firewall'; return confirm_form($form, $question, $cancel_path, $description, $confirm_text, $cancel_text);}function firewall_delete_submit($form, &$form_state) { $ip = $form_state['values']['ip']; FirewallStorage::delete($ip); watchdog('firewall', 'Deleted IP address %ip.', array('%ip' => $ip)); drupal_set_message(t('Deleted IP address %ip.', array('%ip' => $ip))); $form_state['redirect'] = 'admin/config/people/firewall';}
There are a couple things that are different in the above than how many Drupal 7 modules are written. The first is that the list page and the two forms are each separated into their own file. Typically, all 3 would be put into a file like firewall.admin.inc. However, I think the more fine grained separation makes it easier to read in the context of this article. It also matches the granularity at which files are separated in Drupal 8, so makes it easier to compare the before and after.
The more important difference is that there's a FirewallStorage class that's an intermediary between the pages/forms and the database queries. In MVC terminology, this is called the model. What I've noticed to be more common in Drupal 7 modules is one of:
Now, here's the Drupal 8 code for the firewall module:
name: Firewalltype: modulecore: 8.x
array( 'ip' => array( 'type' => 'varchar', 'length' => 40, 'not null' => TRUE, ), ), 'primary key' => array('ip'), ); return $schema;}
firewall.list: path: 'admin/config/people/firewall' defaults: _content: '\Drupal\firewall\ListController::content' _title: 'Allowed IP addresses' requirements: _permission: 'administer site configuration'firewall.add: path: 'admin/config/people/firewall/add' defaults: _form: '\Drupal\firewall\AddForm' _title: 'Add allowed IP address' requirements: _permission: 'administer site configuration'firewall.delete: path: 'admin/config/people/firewall/delete/{ip}' defaults: _form: '\Drupal\firewall\DeleteForm' requirements: _permission: 'administer site configuration'
array( 'link_title' => 'Allowed IP addresses', 'route_name' => 'firewall.list', 'parent' => 'user.admin_index', ), );}
fetchCol(); } static function exists($ip) { $result = db_query('SELECT 1 FROM {firewall} WHERE ip = :ip', array(':ip' => $ip))->fetchField(); return (bool) $result; } static function add($ip) { db_insert('firewall')->fields(array('ip' => $ip))->execute(); } static function delete($ip) { db_delete('firewall')->condition('ip', $ip)->execute(); }}
'link', '#title' => $ip, '#route_name' => 'firewall.delete', '#route_parameters' => array('ip' => $ip), ); } $items[] = array( '#type' => 'link', '#title' => t('Add'), '#route_name' => 'firewall.add', ); return array( '#theme' => 'item_list', '#items' => $items, ); }}
'textfield', '#title' => t('IP address'), ); $form['actions'] = array('#type' => 'actions'); $form['actions']['submit'] = array( '#type' => 'submit', '#value' => t('Add'), ); return $form; } function validateForm(array &$form, array &$form_state) { $ip = $form_state['values']['ip']; if (FirewallStorage::exists($ip)) { form_set_error('ip', $form_state, t('This IP address is already listed.')); } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) == FALSE) { form_set_error('ip', $form_state, t('Enter a valid IP address.')); } } function submitForm(array &$form, array &$form_state) { $ip = $form_state['values']['ip']; FirewallStorage::add($ip); watchdog('firewall', 'Added IP address %ip.', array('%ip' => $ip)); drupal_set_message(t('Added IP address %ip.', array('%ip' => $ip))); $form_state['redirect_route']['route_name'] = 'firewall.list'; }}
$this->ip)); } function getConfirmText() { return t('Delete'); } function getCancelRoute() { return array('route_name' => 'firewall.list'); } function buildForm(array $form, array &$form_state, $ip = '') { $this->ip = $ip; return parent::buildForm($form, $form_state); } function submitForm(array &$form, array &$form_state) { FirewallStorage::delete($this->ip); watchdog('firewall', 'Deleted IP address %ip.', array('%ip' => $this->ip)); drupal_set_message(t('Deleted IP address %ip.', array('%ip' => $this->ip))); $form_state['redirect_route']['route_name'] = 'firewall.list'; }}
A bunch of things here should look familiar from the previous article:
What's new in this example are the two form classes. Let's look at AddForm first. In the Drupal 7 version of this module, within firewall.add_form.inc, we had the primary form function: firewall_add(), and then two other functions: firewall_add_validate() and firewall_add_submit(), for the form to actually be useful. In OOP terminology, when you have a set of functions that need to exist together in order to accomplish something, that's an interface. In Drupal 7, documentation of the Form API is how we communicate to developers that whenever you define a form generation function, then there are also two additional specially named functions for your validation and submission logic. With OOP, we can use PHP's language feature of defining interfaces explicitly, and so Drupal 8 core provides a Drupal\Core\Form\FormInterface interface to do that. The primary form generation function is buildForm(), and the validation and submission functions are validateForm() and submitForm(). There's also a new function, getFormID(), which needs to return the form id, which is used in various places, like resolving which hook_form_FORM_ID_alter() functions to call. In Drupal 7, the form id by default is the same as the name of the form generation function, but with the move to that being a buildForm() function in a namespaced class, we needed a separate function for getting a short id. With the interface defined, a particular form just needs to implement the interface, and you can see in the AddForm code above how that's done. You'll notice that the contents of the build, validate, and submit functions are the same as in the Drupal 7 version of the form other than passing $form_state to form_set_error(), and upon completion, redirecting to a route name rather than a path.
Now let's look at DeleteForm. This isn't just a regular form, it's a confirmation form. There are certain things that are common to all confirmation forms. They typically don't need validation (they're usually just a submit button and a cancel link), and they share a common pattern of a question, a description, a label for the confirmation button, a path to go to for the cancel link, etc. With OOP, all of these commonalities can be built into a base class, Drupal\Core\Form\ConfirmFormBase, and via inheritance, each specific confirmation form can just override the parts that it needs to. With that in mind, I hope that the code above for DeleteForm makes sense. To sum up:
If this content did not answer your questions, try searching or contacting our support team for further assistance.
Fri Sep 12 2025 08:32:32 GMT+0000 (Coordinated Universal Time)