Overview
PHP 8.1 introduced a new data model, Enumerations. Enumerations allow for the creation of custom data models that can only contain a fixed set of allowed values. These enumerations can be very useful when working with Drupal list fields, which are a field type that allows for selection from a predefined set of values. As both enumerations and list field contain a predefined set of values, using Enumerations to back Drupal list field makes for more stable code that is easier to understand, and less likely to fail.
This tutorial will go over an explanation of what a PHP enum is, how enums can be used to back Drupal list fields, and working with enums in Drupal code. This tutorial requires an understanding of PHP OOP, and basic Drupal development concepts such as what entities and fields are, how they relate to each other, and how to work with them in code.
A Simple Overview of Enumerations
From the PHP manual:
Enumerations, or "Enums", allow a developer to define a custom type that is limited to one of a discrete number of possible values.
To understand how this works, Consider a function that takes a single argument, a fruit name, and returns its vitamin breakdown, with the limitation being that the system only has vitamin information for apples, oranges, and bananas. Any other values passed to this function must not be allowed, so checks are added to ensure that the argument passed to the function contains one of these three values. Exceptions are thrown for any invalid value passed to the function.
Example of traditional method
Here is how this would traditionally be handled, with strings.
function getVitamins(string $fruit) { // Ensure the fruit is in the system fruit types. // Throw an exception, as this function should never be called without a valid fruit. throw new \Exception($fruit . ‘ vitamin information was not found’); } // Return a new Vitamins instance, instantiated with the fruit. return new \Vitamins($fruit); }
This function can then be used as follows:
$fruit = ‘apple’; $vitamins = getVitamins($fruit);
The above function getVitamins()
requires that the value of $fruit
be one of apple
, orange
, or banana
. Any other fruit, or any misspelling of the above values, will throw an exception. This works, but the code can be cleaned up by creating an enum for fruit, instead of using a string.
Same example, using an enum
An enum is a data type that can be assigned to a variable, that limits values to a given value set. The function getVitamins()
can be rewritten to use an enum, but first the enum must be defined:
namespace Food; enum Fruit: string { case APPLE = ‘apple’; case ORANGE = ‘orange’; case BANANA = ‘banana’; }
The enum \Food\Fruit
is defined to only allow for three possible values, apple
, orange
and banana
. This enum can now be used as a type hint to the $fruit argument:
function getVitamins(\Food\Fruit $fruit) { // Return a new Vitamins instance for the given fruit. return new \Vitamins($fruit->value); }
Now this function, instead of accepting any string, only accepts enumerations of type \Food\Fruit
and passing anything other than a \Food\Fruit
instance will throw an exception. This removes tge necessity of checking that $fruit
contains a valid value, due to it only being able to contain allowed values.
Calling getVitamins()
now becomes:
// Create the enum from the string 'apple', which is an allowed value. $apple = \Food\Fruit::from(‘apple’); // Pass the enum as the argument, to retrieve the vitamins. $vitamins = getVitamins($apple);
The usage of enums makes code tighter, as it removes the necessity of checking whether the value of a variable is one a list of allowed values, by creating variables that can only contain allowed values.
Drupal List Fields
Drupal core provides the 'List' type field, which is a field that only allows a limited set of values. Regardless of whether the list field is a string, integer, or float list, all work the same, whereby a limited amount of values are defined for the field, each with a human-friendly label. For example, a List (text) field with label of 'time of day', could have allowed values of morning
, afternoon
, evening
, night
.
Using Enums to Back List Fields
As can be seen, both enums and Drupal list fields are a type of container for a limited set of values. As such enums are a great tool for programmatically working with Drupal list fields.
Example
Architecture
Consider module, example, which creates a custom entity type, Widget, that has the field field_color
. This is a field of type List (Text), with the allowed values for this field having the machine names, red
(label: Red), green
(label: Green), and burgundy
(label Burgundy).
Creating the Enum
Enums are namespaced just like classes, and can have interfaces, just like classes. First, an enum is created for the colors, with a case for each of the machine names that have been set as allowed values on the list type in the Drupal field. The case keys are constants, and written in full caps, while the values are set as the machine names on the Drupal list field. The case definitions are what creates the mapping between the enum and the allowed values on the list field.
namespace Drupal\example\Enum; enum Color: string implements ColorInterface { case RED = ‘red’; case GREEN = ‘green’; case BURGUNDY = ‘burgundy’; }
The interface:
namespace Drupal\example\Enum; interface ColorInterface {}
Using the Enum
A field value in Drupal can now be retrieved as follows:
// Get the machine name of the selected value in the DB: $color_raw = $widget->get(‘field_color’)->value; // Cast the machine name to a Color enum. $color = \Drupal\example\Enum\Color::from($color_raw);
The $color
variable now contains an enum representing the value stored in the database. So how can this be used? The following code block lists getter and setter methods for field_color
on the Widget entity. The traditional getter and setter methods for this field would work with strings, as follows:
/** * Traditional method to get the color. * * @return string|null * The machine name of value in field_color, or NULL if no color has been set. */ public function getColor(): ?string { return $this->get(‘field_color’)->value; } /** * Traditional method to set the color. * * @param string $color * The color to be set on the field. * * @return self * The current Widget. */ public function setColor(string $color): self { $allowed_colors = [ ‘red’, ‘green’, ‘burgundy’, ]; $this->set(‘field_color’, $color); return $this; } // The value passed is not allowed. throw new \Exception($color . ‘ is not a machine name that exists in the database’); }
Now, look at those methods updated to work with enums. Type hinting for both parameters and return values is converted to only allow the Color enum, instead of any string old string.
/** * Enum method to get the color. * * @return \Drupal\example\Enum\ColorInterface|null * An enum for the color, or NULL if no color has been set. */ public function getColor(): \Drupal\example\Enum\ColorInterface { if ($color_raw = $this->get(‘field_color’)->value) { return \Drupal\example\Enum\Color::from($color_raw); } return NULL; } /** * Enum method to set the color. * * @param \Drupal\example\Enum\ColorInterface $color * The color to be set on the field. * * @return self * The current Widget. */ public function setColor(\Drupal\example\Enum\ColorInterface $color): self { $this->set(‘field_color’, $color->value); return $self; }
The code above is tightened up, as developers now know that can the getter method will always be one of a known set of values, and the setter method does not require checks to ensure the passed argument contains an allowed value.
Here is an example of how this code can be used.
// Imagine the value of the color on the widget is 'red'. if ($widget_color = $widget->getColor()) { // The following outputs: The widget is red. \Drupal::messenger()->addStatus(t(‘The widget is @color.’, [‘@color’ => $widget_color->value])); } // Now use the setter. First, create a Color enum for the color green. $green = \Drupal\example\Enum\Color::from(‘green’); // Set the color on the widget. $widget->setColor($green); // For verification, get the color from the widget. $new_widget_color = $widget->getColor(); // The following outputs: The widget is green. \Drupal::messenger()->addStatus(t(‘The widget is @color.’, [ ‘@color’ => $new_widget_color->value, ]));
Enum Methods
Enums can have helper methods, making them even easier to use with. As an example, helper functions can be added to test whether the enum is of a given value. Here is the Color enum from earlier, with some helper methods added:
namespace Drupal\example\Enum; enum Color: string implements ColorInterface { case RED = ‘red’; case GREEN = ‘green’; case BURGUNDY = ‘burgundy’; /** * Determine if the color is red. * * @return bool * A boolean indicating whether the color is red. */ public function isRed(): bool { return $this === self::RED; } /** * Determine if the color is green. * * @return bool * A boolean indicating whether the color is green. */ public function isGreen(): bool { return $this === self::GREEN; } /** * Determine if the color is burgundy. * * @return bool * A boolean indicating whether the color is burgundy. */ public function isBurgundy(): bool { return $this === self::BURGUNDY; } }
These new methods on the enum allow for doing checks on the value retrieved from the field. For example:
$color = $widget->getColor(); if ($color->isRed()) { // Do something for the color red. } elseif ($color->isGreen()) { // Do something for the color green. } elseif ($color->isBurgundy()) { // Do something for the color burgundy. }
Finally, it can be beneficial to create a helper function on the enum mapping the field values to the labels in the database, so that the human friendly labels can be used when necessary:
namespace Drupal\example\Enum; enum Color: string implements ColorInterface { case RED = ‘red’; case GREEN = ‘green’; case BURGUNDY = ‘burgundy’; /** * Get the human-friendly label of the color. * * @return string * The human-friendly label of the color. */ public function label(): string { return match($this) { // The values match the label of the allowed values in field_color in Drupal. self::RED => ‘Red’, self::GREEN => ‘Green’, self::BURGUNDY => ‘Burgundy’, }; } }
Which for example could be used as follows:
$color = $widget->getColor(); \Drupal::messenger()->addStatus(t(‘The color in the database has a machine name of @machine_name and a label of @label', [ ‘@machine_name’ => $color->value, ‘@label’ => $color->label(), ]));
Conclusion
PHP Enumerations are a data type with a limited set of allowed values, and Drupal list fields are fields with a limited set of allowed values. This makes PHP enums a great means of backing Drupal list fields when working with field values in code, making the Drupal code more stable, secure, and less likely to fail. This tutorial has shown how to create an enum that matches a list field in Drupal, and how to work with those enums. Happy Drupaling!
Complete Code
Enum:
namespace Drupal\example\Enum; enum Color: string implements ColorInterface { case RED = ‘red’; case GREEN = ‘green’; case BURGUNDY = ‘burgundy’; /** * {@inheritdoc} */ public function isRed(): bool { return $this === self::RED; } /** * {@inheritdoc} */ public function isGreen(): bool { return $this === self::GREEN; } /** * {@inheritdoc} */ public function isBurgundy(): bool { return $this === self::BURGUNDY; } /** * {@inheritdoc} */ public function label(): string { return match($this) { // The values match the label of the allowed values in field_color in Drupal. self::RED => ‘Red’, self::GREEN => ‘Green’, self::BURGUNDY => ‘Burgundy’, }; } }
Interface:
namespace Drupal\example\Enum; interface ColorInterface { /** * Determine if the color is red. * * @return bool * A boolean indicating whether the color is red. */ public function isRed(): bool; /** * Determine if the color is green. * * @return bool * A boolean indicating whether the color is green. */ public function isGreen(): bool; /** * Determine if the color is burgundy. * * @return bool * A boolean indicating whether the color is burgundy. */ public function isBurgundy(): bool; /** * Get the human-friendly label of the color. * * @return string * The human-friendly label of the color. */ public function label(): string; }