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 = Firewall
core = 7.x
files[] = lib/FirewallStorage.php
<?php
function firewall_schema() {
$schema['firewall'] = array(
'fields' => array(
'ip' => array(
'type' => 'varchar',
'length' => 40,
'not null' => TRUE,
),
),
'primary key' => array('ip'),
);
return $schema;
}
<?php
function firewall_menu() {
return array(
'admin/config/people/firewall' => 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',
),
);
}
<?php
class FirewallStorage {
static function getAll() {
return db_query('SELECT ip FROM {firewall}')->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();
}
}
<?php
function firewall_list() {
$items = array();
foreach (FirewallStorage::getAll() as $ip) {
$items[] = l($ip, "admin/config/people/firewall/delete/$ip");
}
$items[] = l(t('Add'), 'admin/config/people/firewall/add');
return array(
'#theme' => 'item_list',
'#items' => $items,
);
}
<?php
function firewall_add($form, $form_state) {
$form['ip'] = array(
'#type' => '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';
}
<?php
function firewall_delete($form, &$form_state, $ip) {
$form['ip'] = array(
'#type' => '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: Firewall
type: module
core: 8.x
<?php
function firewall_schema() {
$schema['firewall'] = array(
'fields' => 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'
<?php
function firewall_menu_link_defaults() {
return array(
'firewall.list' => array(
'link_title' => 'Allowed IP addresses',
'route_name' => 'firewall.list',
'parent' => 'user.admin_index',
),
);
}
<?php
namespace Drupal\firewall;
class FirewallStorage {
static function getAll() {
return db_query('SELECT ip FROM {firewall}')->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();
}
}
<?php
namespace Drupal\firewall;
class ListController {
function content() {
$items = array();
foreach (FirewallStorage::getAll() as $ip) {
$items[] = array(
'#type' => '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,
);
}
}
<?php
namespace Drupal\firewall;
use Drupal\Core\Form\FormInterface;
class AddForm implements FormInterface {
function getFormID() {
return 'firewall_add';
}
function buildForm(array $form, array &$form_state) {
$form['ip'] = array(
'#type' => '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';
}
}
<?php
namespace Drupal\firewall;
use Drupal\Core\Form\ConfirmFormBase;
class DeleteForm extends ConfirmFormBase {
protected $ip;
function getFormID() {
return 'firewall_delete';
}
function getQuestion() {
return t('Are you sure you want to delete %ip?', array('%ip' => $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.
Sun Mar 31 2019 01:07:39 GMT+0000 (Coordinated Universal Time)