In part 2 of my Object Oriented Programming (OOP) for Drupal 8 series, we are going to create an administration form. If you missed part 1, I talked about how to make a simple custom block in D8, which is something we do here at Commercial Progression to brand our sites. I chose a custom block to show that the new changes in D8 aren’t so scary, and to introduce object oriented concepts and definitions with a real-world example.
Another common example of custom Drupal code is to create a short administration form so that configurations can be easily modified in the UI. As well as incorporating an OOP lesson, this example will also illustrate how Symfony routing works (the hook_menu()
replacement), and how the variables system will be replaced in Drupal 8.
Compro Custom: D7 Style
The following code is part of our Compro Custom module that saves a title for the site logo.
Before diving right into the same admin form code for D8, I’ll describe the changes to the Menu API.
Routing
Here’s where we get a glimpse into the Symfony world. A common feature in a lot of OO web frameworks is the implementation of a routing system. This is a separate file that links a path to its Controller (as part of the Model-View-Controller pattern). Instead of using hook_menu()
to set up a URL path, we put it in our compro_custom.routing.yml file. It uses a lot of the same settings that we are used to: a path, title, permission, and the form we use.
Menu API
In order to see a link to our form under Configuration in the admin toolbar, we need to add another file called compro_custom.links.menu.yml to define the menu link.
We are referencing back to the first line of our routing file with route_name
to give our link a direction. Using the parent
attribute, we can specify that we want to nest it under the system.admin_config_development
menu item. If we wanted to nest another link under this menu item, the parent attribute would be the first line of this file: compro_custom.compro_custom_form
.
The D8 Form API
Here are the contents of ComproCustomForm.php. A lot of these methods use {@inheritdoc}
in the PHPDoc comment, but I’ve supplied the actual comment they’re inheriting for clarity.
We can see that the hooks are gone, and instead we are implementing an entirely new class for a system admin form. We still build the form in the same array structure we are used to, but now we are implementing the method buildForm()
. In Drupal 8 we are also able to set our own form ID as part of building a system form.
Another big change to admin forms in particular is that we can no longer rely on a built-in form submission with system_settings_form($form)
; we have to implement our own submitForm()
method. In this method, we map the values from $form_state
into a configuration object. Our $form_state
object allows us to call it’s getValue()
method, and pass it an array indicating the form value we want. Because we nested our logo title in $form['compro_custom']['logo']['title']
, we can retrieve that value by passing in the multi-element array('compro_custom', 'logo', 'title')
.
Configuration System
The configuration system (as well as the state system) has replaced the variable system in D8. This means there is no more variable table, and no more variable_get()
or variable_set()
calls. In our form class, we extended the ConfigFormBase class so that we can save our data in the config table. When we __construct()
our form we accept a ConfigFactoryInterface
, and we also implement a getEditableConfigNames()
method to set up our configuration object before saving these values during submit.
One more step we must take to ensure that our configurations are stored correctly is to provide a mapping schema for the configuration system. The schema describes the overall structure of the configuration object, and its data types. It is also useful for describing which of its values is translatable, but that is not shown in this example. We will put this in the compro_custom.schema.yml file.
Interfaces
You might have noticed that our __construct(ConfigFactoryInterface $config_factory)
method looks really strange - why is our parameter an interface? In part 1, I explained very briefly that an interface was a list of methods its classes were required to implement. An interface doesn’t contain any implementation, just method signatures. When we include an interface as a parameter, it doesn’t really mean it needs an interface, but instead it uses a concrete class that implements ConfigFactoryInterface. At the time of this writing, the only class that implements ConfigFactoryInterface
is ConfigFactory
. Passing in an interface instead of the concrete class is useful if you wanted to implement your own config factory class without rewriting every class that will be passing it as a parameter.
Factory Classes
The purpose of a factory class is to create an object. Factory classes are a design pattern that allow a client to create objects without knowledge of how they are made - the client just accesses the methods defined in the factory interface to produce the desired object. In our example, the job of ConfigFactory
is to initialize a configuration object from the schema we defined in our compro_custom.schema.yml file.
Polymorphism
Polymorphism is an advanced OOP practice, but this is a great example of how it's done. Polymorphism is a very broad term that refers to interacting with different classes in the same way. Using an interface is one of the most common methods of achieving polymorphism. One of the big advantages of object oriented programming is the multitude of ways in which code can be reused.
Longer, But Better
Unfortunately in this example, the object oriented Drupal 8 implementation produced quite a few more lines of code than the procedural Drupal 7 version. But we also saw that in Drupal 8 we are seeing more separation of concerns between the menu structure, system configurations, et al. Drupal 8’s OOP practices will also help future development go smoothly with more abstract code that is harder to break (if used correctly).