Custom Formatting for Address Fields in Drupal Commerce
Jan 4, 2021 | Last edit: Jan 7, 2021
Drupal offers a lot of flexibility to control how users view content across a web site. These out-of-the-box tools provide a UI to hide or show fields, change how fields are formatted, and create multiple displays for a single content entity (full, teaser, summary, etc.). When content editors have access to tools like these, it's easy to build an entire site with no coding required.
One exception to this is the Address field. And addresses are ubiquitous in an e-commerce platform.
So what gives? Why the exception?
Address Field
An address is a collection of fields, like street address, city, postal code. And depending on the country, the collection of fields for one address may not be the same as another. A U.S. address has a "state" while a Canadian one has "province" for example. At the same time, our users will expect to have a nice user experience, one that performs some error checking and list building so it's easy to define an address that is correct. Once a user selects "United States," the options for "state" are built appropriately.
Fortunately, many Drupal developers have contributed their skills to make this work for us! We simply need to write some code that will format addresses the way we need. The default UI tools we get in Drupal won't help us here, so let's open the code editor.
Views Field Formatter
One of the most common places to encounter issues with the default options for Address field is with Views. Imagine trying to build a CSV data export for completed orders. Almost every data point can be assigned to a column, but if we want to assign just the customer's billing country to a column ... well we can't do that. The Address field is all or nothing in this case, which can be a frustrating experience.
The solution here is to create a custom Views field plugin. The inspiration (or the boilerplate code) for this comes from Drupal core:
Drupal\views\Plugin\views\field\FieldPluginBase
There's a lot going on in this file, core/modules/views/src/Plugin/views/field/FieldPluginBase.php. For our purposes, let's focus on the parts we need to fetch the date we need. A decent starting point is here:
modules/custom/my_module/src/Plugin/views/field/BillingAddressField.php
<?php namespace Drupal\my_module\Plugin\views\field; use Drupal\views\Plugin\views\field\FieldPluginBase; use Drupal\views\ResultRow; /** * @ViewsField("my_module_order_address_line") */ class BillingAddressField extends FieldPluginBase { /** * * @param ResultRow $values * @param string|null $field * @return void */ public function getValue($values, $field = NULL) { /** @var \Drupal\commerce_order\Entity\Order $order */ $order = $this->getEntity($values); if ($order->getEntityTypeId() != 'commerce_order') { return 'Error message'; } if ($billing = $order->getBillingProfile()) { $address = $billing->get('address')->first()->getValue(); return $address['locality']; } return 'Not found'; } public function query() { } }
The getValue() method is where we return the data for each row in the View. In the above sample, we do a basic check for the correct entity type and then fetch the billing profile, which contains the Address field as well as any other fields that may have been added. From this profile, we grab the Address and isolate a single value to return. Finally we override the query method, as we are fetching this data in getValue.
This example is simplified for the sake of clarity. Some improvements could be made for more robust error checking or to better utilize the plugin's design patterns -- we are probably doing too much work in the getValue method. More importantly, we could extend the field's options to allow editors to select the specific Address field they want to return.
Now we have a way to return the data, but we need to tell Drupal about this new Views field. We do that inside a dot-module file.
modules/custom/my_module/my_module.module
/** * Implements hook_views_data_alter(). */ function my_module_views_data_alter(array &$data) { $data['commerce_order']['billing_address_field'] = [ 'field' => [ 'title' => t('Billing profile Address field'), 'help' => t('Select a single line from Address field'), 'id' => 'my_module_order_address_line', ], ]; }
This hook function allows us to add a new entry to the list of Views field that are available for an order entity. Call it 'billing_address_field' or something else, but it must be under the 'commerce_order' ID to be found.
Now we should find a field called "Billing profile Address field" in our View. And the locality field should be returned. (If we build out the plugin's options, we should be able to select the Address line to return.)
Field Formatter
The above example helps with Views, but we need another solution to manage how an Address is displayed elsewhere on the site. For example, we want to modify how the address appears on an order receipt, or the admin view of orders, or in a customer's address book. To that end, we need a custom field formatter that overrides what we find in Core:
Drupal\address\Plugin\Field\FieldFormatter\AddressDefaultFormatter
Again, there's a lot going on in this file! This field formatter pattern is used across Drupal, so it can be helpful to get acquainted with it at some point. Let's focus on viewElements() and viewElement() for our purposes.
When the plugin runs, it calls viewElements(), which in turn calls viewElement(). (Drupal assumes most fields can have multiple values, so this pattern is common.) In both methods, we see some html tags are being applied -- something we would expect to see applied in the template, later in the process.
Looking a little further we see a postRender() method which calls another method, replacePlaceholders(). Without trying to parse all the lines, we can tell from the comments that some extra work is required to avoid empty lines and useless whitespace. Kudos to all those open-source contributors who devised this solution, and left clues for future developers. "Here be dragons!"
Similar to the Views field, we want to extend this plugin and modify only the relevant parts. Start by creating a new file:
modules/custom/my_module/src/Plugin/Field/FieldFormatter/MyAddressFormatter.php
<?php namespace Drupal\my_module\Plugin\Field\FieldFormatter; use Drupal\address\AddressInterface; use Drupal\address\Plugin\Field\FieldFormatter\AddressDefaultFormatter; use Drupal\Core\Field\FormatterBase; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * * @FieldFormatter( * id = "address_single_line", * label = @Translation("Address single line"), * field_types = { * "address" * } * ) */ class MyAddressFormatter extends AddressDefaultFormatter { /** * {@inhertidoc} */ public function settingsSummary() { $summary = []; $summary[] = $this->t('Show address on single line.'); return $summary; } /** * {@inheritdoc} */ public function viewElements(FieldItemListInterface $items, $langcode) { $elements = []; foreach ($items as $delta => $item) { $elements[$delta] = $this->viewElement($item, $langcode); } return $elements; } /** * Builds a renderable array for a single address item. * * @param \Drupal\address\AddressInterface $address * The address. * @param string $langcode * The language that should be used to render the field. * * @return array * A renderable array. */ protected function viewElement(AddressInterface $address, $langcode) { $country_code = $address->getCountryCode(); $address_format = $this->addressFormatRepository->get($country_code); $values = $this->getValues($address, $address_format); // Build string. $markup = []; $markup[] = $values['addressLine1']; if (!empty($values['addressLine2']) && $values['addressLine2'] != '') { $markup[] = $values['addressLine2']; } $markup[] = $values['locality'] . ", " . $values['administrativeArea']; $markup[] = $values['postalCode']; $element = [ '#markup' => implode(", ", $markup), ]; return $element; } }
This sample intends to render the address on a single line, using commas to separate the fields. We override the viewElements() method to eliminate the html structure from the original. And we override viewElement() to format the individual fields into a single string.
Again, this is a simplified example that could be improved for a more robust implementation.
Unlike the Views field, we do not need to add a definition in the dot-module file. The annotation at the top of this class is used to register it with Drupal so it's available when we configure this field. To do that, we can navigate to the "Manage display" tab for a content entity that has an Address field. (The Customer profile is a good example.) Change the format to "Address single line" and Save. Now the Address field will be shown in the format we defined above.