Drupal 8 OOP Part 3: Custom Field Formatter

Posted By Hillary Lewandowski on Monday, June 27, 2016 - 13:49

Moving right along demonstrating the new Drupal 8 Object Oriented Programming structure, this next part of my blog series is going to focus on creating a custom field formatter. In part 1 of this series I demonstrated how to create a custom block in code, and in part 2 I showed how to create a custom admin form.

So far, we’ve gone over the basics of inheritance, examples of interfaces, touched on design patterns, and introduced some of the contributions from Symfony. Through custom field formatters, we will see another functional example of annotations, and a functional example of encapsulation.

The code for a field formatter is very similar between Drupal 7 and 8, so I will just be showing the Drupal 8 code. All of the snippets of code that follow are a part of the ComproCustomFileLink class. The methods have an {@inheritdoc} by default, but I have included the PHPDoc comment for clarity. This specific field formatter creates a title for a file field link.

Field Formatter Info

This is where we tell the plugin system that we are registering a Field Formatter. The Drupal 8 implementation of hook_field_formatter_info() is all done in annotations. You can set the default values in the annotations, but if you have a problem saving the values, pulling the default settings out into its own method solves that issue (at least it did for me).

  1. namespace Drupal\compro_custom\Plugin\Field\FieldFormatter;
  2.  
  3. use Drupal\Core\Field\FieldItemListInterface;
  4. use Drupal\Core\Field\FormatterBase;
  5. use Drupal\Core\Form\FormStateInterface;
  6. use Drupal\Core\Url;
  7.  
  8. /**
  9.  * Plugin implementation of the 'compro_custom_file_link' formatter
  10.  *
  11.  * @FieldFormatter(
  12.  *   id = "compro_custom_file_link",
  13.  *   label = @Translation("Link"),
  14.  *   field_types = {
  15.  *     "file"
  16.  *   }
  17.  * )
  18.  */
  19. class ComproCustomFileLink extends FormatterBase {
  20.  
  21. /**
  22.  * Defines the default settings for this plugin.
  23.  *
  24.  * @return array
  25.  *   A list of default settings, keyed by the setting name.
  26.  */
  27. public static function defaultSettings() {
  28.   return array(
  29.     'compro_custom_link_title' => '',
  30.       ) + parent::defaultSettings();
  31.   }
  32. }

Annotations

We need to exercise caution when writing annotations. For example, leaving a trailing comma at the end of an array is common practice in generic PHP code (and is even part of the Drupal coding standard), but it will break the annotations with no easy way to debug them. Writing plugin annotations will be cumbersome at first, but once you have an arsenal of plugins, it will be easy to copy annotations from an existing plugin. Consequently, problems from a trailing comma or other syntax errors will be few and far between. There are also new tools, like the Drupal 8 Console, to avoid the headache of building components like this field formatter from scratch.

Annotations are a useful OOP strategy because they contain metadata about the class without putting it in another file; it's all part of the same code. There are already a massive amount of YAML files in Drupal 8, but we can mitigate adding more by using annotations. It’s also easier to parse the annotations for plugin discovery without having to load each class separately for a specific property method (similar to how hook_info() works in D7). This is part of what makes a large Drupal 8 site more performant than a comparable Drupal 7 site.

Field Formatter Settings Form

This is where we define what settings appear when configuring the field formatter. The Drupal 8 version of hook_field_formatter_settings_form() looks very similar to the D7 version, but is only going to be called for this specific field formatter. Because of this, we don’t have to check for our specific display type before adding our form like we did in Drupal 7. We do, however, append our form onto the parent form so we don’t override any part of the settings form. This is also where we map our settings object to the correct form item for proper saving, which is handled by the “grandparent” class abstract class PluginSettingsBase.

  1. /**
  2.  * Returns a form to configure settings for the formatter.
  3.  *
  4.  * Invoked from \Drupal\field_ui\Form\EntityDisplayFormBase to allow
  5.  * administrators to configure the formatter. The field_ui module takes care
  6.  * of handling submitted form values.
  7.  *
  8.  * @param array $form
  9.  *   The form where the settings form is being included in.
  10.  * @param \Drupal\Core\Form\FormStateInterface $form_state
  11.  *   The current state of the form.
  12.  *
  13.  * @return array
  14.  *   The form elements for the formatter settings.
  15.  */
  16. public function settingsForm(array $form, FormStateInterface $form_state) {
  17.   $form = parent::settingsForm($form, $form_state);
  18.  
  19.   $form['compro_custom_link_title'] = array(
  20.     '#type' => 'textfield',
  21.     '#title' => t('Link title'),
  22.     '#description' => t('Enter an optional link title to be shown instead of file name'),
  23.     '#default_value' => $this->getSetting('compro_custom_link_title'),
  24.   );
  25.  
  26.   return $form;
  27. }

Field Formatter Settings Summary

This is where we put a small description of the settings before showing the settings form with AJAX. Again, for Drupal 7 we had to insert a check for the targeted display, but this is not necessary for Drupal 8.

  1. /**
  2.  * Returns a short summary for the current formatter settings.
  3.  *
  4.  * If an empty result is returned, a UI can still be provided to display
  5.  * a settings form in case the formatter has configurable settings.
  6.  *
  7.  * @return string[]
  8.  *   A short summary of the formatter settings.
  9.  */
  10. public function settingsSummary() {
  11.   $summary = array();
  12.   $summary[] = t('Displays a title link');
  13.   return $summary;
  14. }

Field Formatter View

Here we assemble the URL from the file’s link using the saved link title from the formatter, and pull in the file’s URI, using the Drupal 8 Url class.

  1. /**
  2.  * Builds a renderable array for a field value.
  3.  *
  4.  * @param \Drupal\Core\Field\FieldItemListInterface $items
  5.  *   The field values to be rendered.
  6.  * @param string $langcode
  7.  *   The language that should be used to render the field.
  8.  *
  9.  * @return array
  10.  *   A renderable array for $items, as an array of child elements keyed by
  11.  *   consecutive numeric indexes starting from 0.
  12.  */
  13. public function viewElements(FieldItemListInterface $items, $langcode) {
  14.   $elements = array();
  15.  
  16.   // Loop through items
  17.   foreach ($items as $delta => $file) {
  18.     $file = $file->entity;
  19.     $url = Url::fromUri($file->url() );
  20.     $file_link = \Drupal::l($this->getSetting('compro_custom_link_title') !== NULL ?
  21.       $this->getSetting('compro_custom_link_title') : $file->getFileName(), $url);
  22.     $elements[$delta] = array(
  23.       '#markup' => $file_link,
  24.     );
  25.   }
  26.  
  27.   return $elements;
  28. }

Object Properties

In a lot of Drupal 7 code, object properties were accessed directly, for example $node->title, but now we’re seeing Drupal 8 move toward calling object properties through class methods, such as $file->getFileName(). This seems to be overkill for easy pieces of data, like a node title, but when you access these from a class method, the data can be manipulated for you. For instance, the node title can be translated during a $node->getTitle() call. The data manipulation (or implementation) details become unimportant, as long as you get your translated title.

In our example, our grandparent class PluginSettingsBase is the class who is storing the information for our plugin’s $settings. The only way we access these settings is with $this->getSetting('compro_custom_link_title'). We don’t have to worry about where the data is stored, how it’s retrieved, or if it has to be unserialized; our getSetting() method takes care of all that.

Encapsulation

The practice of data hiding is incorporated in good encapsulation, but many like to define the distinction between the two. When we look at our parent class, abstract class FormatterBase, it contains 4 protected variables that can be accessed with getters. A getter can either return the plain value of the variable, or some more useful value. Along with putting them on variables, accessors (i.e. public, protected, private) on methods can define whether they’re publicly available, only relatives can view them, or if no one can use these methods, respectively.

Putting accessors on variables and methods within a class, as well as wrapping data and implementation together in a class, is a form of encapsulation. Encapsulation is a way of separating concerns; the less an outside class knows about and interacts with the implementation details, the more robust the implementation.

One great reason to use encapsulation, especially in PHP, is that it avoids bugs that accidentally overwrite a class property and cause data corruption. For example, $node->title = 'Test' would assign the string 'Test' to $node->title when I really wanted to check for equality. Using encapsulation, you would have to call a method such as $node->setTitle('Test'). With data hiding, the use of encapsulation keeps others from using your code incorrectly, but you’re also implementing additional measures to keep data secure.

Implementation Hiding

Good encapsulation leads to good implementation hiding. Drupal developers are no stranger to implementation hiding: during the site building phase of starting a Drupal website, the site builder is adding fields to content types through an interface. When a new module adds a different kind of field, you still use the same UI to add that new field to your content types. The same interface is used, regardless of the actual implementation that makes the field work - that’s the high level definition of implementation hiding.

This principle applies to any plugin you create; you’re writing your implementation details in these new classes, and their methods are called through an interface. When methods are called through an interface, the implementation details are hidden. Implementation hiding is less about code security, and more about code resiliency. By hiding the implementation details, the way data is stored and used inside the class can change, as long as results are returned in the same form ($file->getFileName() always returns a string).

Conclusion

Drupal 8 is changing a lot of syntax, but it's becoming faster, more robust, and secure. Following the new OOP practices during Drupal development will provide us with a much more useful framework in the long run.

Drupal 8 website design and development by Commercial Progression