Addresses

Addresses are a type of element you’ll most commonly encounter in conjunction with Users. Querying addresses and working with their field data is nearly identical to the experience working with any other element type.

For sites supporting public registration (like a storefront built on Craft Commerce) users can manage their own address book.

Plugins are also able to use addresses to store their own location data.

# Setup Pro

The Address management interface can be added to the User field layout by navigating to SettingsUsers -> User Fields.

Screenshot of User Fields’ Field Layout editor, with an empty layout and an available Addresses field under Native Fields in the sidebar

Create a “Contact Information” tab and drag the Addresses field layout element into it to make the interface available on every user detail page.

Clicking the

Gear icon
settings icon on the address field layout element opens additional settings for the address management UI, including tools for displaying it conditionally.

Take a look at any User’s edit screen to get familiar with the interface:

Screenshot of My Account page with a “Contact Information” tab selected and the “Addresses” field heading with “+ Add an address” just underneath it

Back in User Settings, the Address Fields editor lets you manage the fields that are part of each address. Label, Country, and Address are included by default, with several other native fields available:

Screenshot of Address Fields’ Field Layout editor, with an existing Content tab containing Label, Country, and Address fields

# Native and Custom Fields

The address field layout has additional native (but optional) fields for a handful of useful attributes. Addresses—just like other element types—support custom fields for anything else you might need to store.

For compatibility and localization, core address components (aside from the Country Code) can’t be separated from one another in the field layout.

# Querying Addresses

You can fetch addresses in your templates or PHP code using an AddressQuery (opens new window).

{# Create a new address query #}
{% set myAddressQuery = craft.addresses() %}

Addresses are just elements, so everything you know about Element Queries applies here!

# Example

Let’s output a list of the logged-in user’s addresses:

  1. Create an address query with craft.addresses().
  2. Restrict the query to addresses owned by the current User, with the owner parameter.
  3. Fetch the addresses with .all().
  4. Loop through the addresses using a {% for %} tag (opens new window).
  5. Output preformatted address details with the |address filter.
{% requireLogin #}

{% set addresses = craft.addresses()
  .owner(currentUser)
  .all() %}

{% for addr in addresses %}
  <address>{{ addr|address }}</address>
{% endfor %}

We’ll expand on this example in the Managing Addresses section.

Protect your users’ personal information by carefully auditing queries and displaying addresses only on pages that require login.

# Parameters

Address queries support the following parameters:

Param Description
administrativeArea Narrows the query results based on the administrative area the assets belong to.
afterPopulate Performs any post-population processing on elements.
andRelatedTo Narrows the query results to only addresses that are related to certain other elements.
asArray Causes the query to return matching addresses as arrays of data, rather than Address (opens new window) objects.
cache Enables query cache for this Query.
clearCachedResult Clears the cached result (opens new window).
countryCode Narrows the query results based on the country the assets belong to.
dateCreated Narrows the query results based on the addresses’ creation dates.
dateUpdated Narrows the query results based on the addresses’ last-updated dates.
fixedOrder Causes the query results to be returned in the order specified by id.
id Narrows the query results based on the addresses’ IDs.
ignorePlaceholders Causes the query to return matching addresses as they are stored in the database, ignoring matching placeholder elements that were set by craft\services\Elements::setPlaceholderElement() (opens new window).
inReverse Causes the query results to be returned in reverse order.
limit Determines the number of addresses that should be returned.
offset Determines how many addresses should be skipped in the results.
orderBy Determines the order that the addresses should be returned in. (If empty, defaults to dateCreated DESC.)
owner Sets the ownerId parameter based on a given owner element.
ownerId Narrows the query results based on the addresses’ owner elements, per their IDs.
preferSites If unique() (opens new window) is set, this determines which site should be selected when querying multi-site elements.
prepareSubquery Prepares the element query and returns its subquery (which determines what elements will be returned).
relatedTo Narrows the query results to only addresses that are related to certain other elements.
search Narrows the query results to only addresses that match a search query.
siteSettingsId Narrows the query results based on the addresses’ IDs in the elements_sites table.
trashed Narrows the query results to only addresses that have been soft-deleted.
uid Narrows the query results based on the addresses’ UIDs.
with Causes the query to return matching addresses eager-loaded with related elements.

# administrativeArea

Narrows the query results based on the administrative area the assets belong to.

Possible values include:

Value Fetches addresses…
'AU' with a administrativeArea of AU.
'not US' not in a administrativeArea of US.
['AU', 'US'] in a administrativeArea of AU or US.
['not', 'AU', 'US'] not in a administrativeArea of AU or US.
{# Fetch addresses in the AU #}
{% set addresses = craft.addresses()
  .administrativeArea('AU')
  .all() %}

# afterPopulate

Performs any post-population processing on elements.

# andRelatedTo

Narrows the query results to only addresses that are related to certain other elements.

See Relations (opens new window) for a full explanation of how to work with this parameter.

{# Fetch all addresses that are related to myCategoryA and myCategoryB #}
{% set addresses = craft.addresses()
  .relatedTo(myCategoryA)
  .andRelatedTo(myCategoryB)
  .all() %}

# asArray

Causes the query to return matching addresses as arrays of data, rather than Address (opens new window) objects.

{# Fetch addresses as arrays #}
{% set addresses = craft.addresses()
  .asArray()
  .all() %}

# cache

Enables query cache for this Query.

# clearCachedResult

Clears the cached result (opens new window).

# countryCode

Narrows the query results based on the country the assets belong to.

Possible values include:

Value Fetches addresses…
'AU' with a countryCode of AU.
'not US' not in a countryCode of US.
['AU', 'US'] in a countryCode of AU or US.
['not', 'AU', 'US'] not in a countryCode of AU or US.
{# Fetch addresses in the AU #}
{% set addresses = craft.addresses()
  .countryCode('AU')
  .all() %}

# dateCreated

Narrows the query results based on the addresses’ creation dates.

Possible values include:

Value Fetches addresses…
'>= 2018-04-01' that were created on or after 2018-04-01.
'< 2018-05-01' that were created before 2018-05-01.
['and', '>= 2018-04-04', '< 2018-05-01'] that were created between 2018-04-01 and 2018-05-01.
now/today/tomorrow/yesterday that were created at midnight of the specified relative date.
{# Fetch addresses created last month #}
{% set start = date('first day of last month')|atom %}
{% set end = date('first day of this month')|atom %}

{% set addresses = craft.addresses()
  .dateCreated(['and', ">= #{start}", "< #{end}"])
  .all() %}

# dateUpdated

Narrows the query results based on the addresses’ last-updated dates.

Possible values include:

Value Fetches addresses…
'>= 2018-04-01' that were updated on or after 2018-04-01.
'< 2018-05-01' that were updated before 2018-05-01.
['and', '>= 2018-04-04', '< 2018-05-01'] that were updated between 2018-04-01 and 2018-05-01.
now/today/tomorrow/yesterday that were updated at midnight of the specified relative date.
{# Fetch addresses updated in the last week #}
{% set lastWeek = date('1 week ago')|atom %}

{% set addresses = craft.addresses()
  .dateUpdated(">= #{lastWeek}")
  .all() %}

# fixedOrder

Causes the query results to be returned in the order specified by id.

If no IDs were passed to id, setting this to true will result in an empty result set.

{# Fetch addresses in a specific order #}
{% set addresses = craft.addresses()
  .id([1, 2, 3, 4, 5])
  .fixedOrder()
  .all() %}

# id

Narrows the query results based on the addresses’ IDs.

Possible values include:

Value Fetches addresses…
1 with an ID of 1.
'not 1' not with an ID of 1.
[1, 2] with an ID of 1 or 2.
['not', 1, 2] not with an ID of 1 or 2.
{# Fetch the address by its ID #}
{% set address = craft.addresses()
  .id(1)
  .one() %}

This can be combined with fixedOrder if you want the results to be returned in a specific order.

# ignorePlaceholders

Causes the query to return matching addresses as they are stored in the database, ignoring matching placeholder elements that were set by craft\services\Elements::setPlaceholderElement() (opens new window).

# inReverse

Causes the query results to be returned in reverse order.

{# Fetch addresses in reverse #}
{% set addresses = craft.addresses()
  .inReverse()
  .all() %}

# limit

Determines the number of addresses that should be returned.

{# Fetch up to 10 addresses  #}
{% set addresses = craft.addresses()
  .limit(10)
  .all() %}

# offset

Determines how many addresses should be skipped in the results.

{# Fetch all addresses except for the first 3 #}
{% set addresses = craft.addresses()
  .offset(3)
  .all() %}

# orderBy

Determines the order that the addresses should be returned in. (If empty, defaults to dateCreated DESC.)

{# Fetch all addresses in order of date created #}
{% set addresses = craft.addresses()
  .orderBy('dateCreated ASC')
  .all() %}

# owner

Sets the ownerId parameter based on a given owner element.

{# Fetch addresses for the current user #}
{% set addresses = craft.addresses()
  .owner(currentUser)
  .all() %}

# ownerId

Narrows the query results based on the addresses’ owner elements, per their IDs.

Possible values include:

Value Fetches addresses…
1 created for an element with an ID of 1.
[1, 2] created for an element with an ID of 1 or 2.
{# Fetch addresses created for an element with an ID of 1 #}
{% set addresses = craft.addresses()
  .ownerId(1)
  .all() %}

# preferSites

If unique() (opens new window) is set, this determines which site should be selected when querying multi-site elements.

For example, if element “Foo” exists in Site A and Site B, and element “Bar” exists in Site B and Site C, and this is set to ['c', 'b', 'a'], then Foo will be returned for Site B, and Bar will be returned for Site C.

If this isn’t set, then preference goes to the current site.

{# Fetch unique addresses from Site A, or Site B if they don’t exist in Site A #}
{% set addresses = craft.addresses()
  .site('*')
  .unique()
  .preferSites(['a', 'b'])
  .all() %}

# prepareSubquery

Prepares the element query and returns its subquery (which determines what elements will be returned).

# relatedTo

Narrows the query results to only addresses that are related to certain other elements.

See Relations (opens new window) for a full explanation of how to work with this parameter.

{# Fetch all addresses that are related to myCategory #}
{% set addresses = craft.addresses()
  .relatedTo(myCategory)
  .all() %}

Narrows the query results to only addresses that match a search query.

See Searching (opens new window) for a full explanation of how to work with this parameter.

{# Get the search query from the 'q' query string param #}
{% set searchQuery = craft.app.request.getQueryParam('q') %}

{# Fetch all addresses that match the search query #}
{% set addresses = craft.addresses()
  .search(searchQuery)
  .all() %}

# siteSettingsId

Narrows the query results based on the addresses’ IDs in the elements_sites table.

Possible values include:

Value Fetches addresses…
1 with an elements_sites ID of 1.
'not 1' not with an elements_sites ID of 1.
[1, 2] with an elements_sites ID of 1 or 2.
['not', 1, 2] not with an elements_sites ID of 1 or 2.
{# Fetch the address by its ID in the elements_sites table #}
{% set address = craft.addresses()
  .siteSettingsId(1)
  .one() %}

# trashed

Narrows the query results to only addresses that have been soft-deleted.

{# Fetch trashed addresses #}
{% set addresses = craft.addresses()
  .trashed()
  .all() %}

# uid

Narrows the query results based on the addresses’ UIDs.

{# Fetch the address by its UID #}
{% set address = craft.addresses()
  .uid('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx')
  .one() %}

# with

Causes the query to return matching addresses eager-loaded with related elements.

See Eager-Loading Elements (opens new window) for a full explanation of how to work with this parameter.

{# Fetch addresses eager-loaded with the "Related" field’s relations #}
{% set addresses = craft.addresses()
  .with(['related'])
  .all() %}

# Address Repository

The commerceguys/addressing (opens new window) library powers planet-friendly address handling and formatting, and its exhaustive repository of global address information is available to all Craft projects. If you need a list of countries, states, or provinces, for example, you can fetch them via Craft’s Addresses (opens new window) service, from Twig templates or PHP:

{% set countries = craft.app.getAddresses().getCountryRepository().getAll() %}

This returns an array of Country (opens new window) objects, indexed by their two-letter code. You might use this to populate a drop-down menu:

<select name="myCountry">
  {% for code, country in countries %}
    <option value="{{ code }}">{{ country.name }}</option>
  {% endfor %}
</select>

{# Output:
<select name="myCountry">
  <option value="AF">Afghanistan</option>
  <option value="AX">Åland Islands</option>
  ...
</select>
#}

Similarly, a repository of subdivisions (opens new window) are available, hierarchically—with up to three levels, depending on how a given country is organized: Administrative AreaLocalityDependent Locality.

Expanding upon our previous example, we could output a nicely organized list of “administrative areas,” like this:

{% set subdivisionRepo = craft.app.getAddresses().getSubdivisionRepository() %}
{% set countriesWithSubdivisions = countries | filter(c => subdivisionRepo.getAll([c.countryCode]) | length) %}

<select name="administrativeArea">
  {% for country in countriesWithSubdivisions %}
    {% set administrativeAreas = subdivisionRepo.getAll([country.countryCode]) %}

    <optgroup label="{{ country.name }}">
      {% for a in administrativeAreas %}
        <option value="{{ a.code }}">{{ a.name }}</option>
      {% endfor %}
    </optgroup>
  {% endfor %}
</select>

Either repository’s getList() method is a shortcut that returns only key-value pairs, suitable for our examples—it also accepts.

Check out the addressing docs (opens new window) for more details and examples of what’s possible—including translation of place names, postal codes, timezones, and formatting!

# Fields and Formatting

# Field Handles

Individual fields—native and custom—are accessed via their handles, like any other element:

<ul>
  <li>Name: {{ myAddress.title }}</li>
  <li>Postal Code: {{ myAddress.postalCode }}</li>
  <li>Custom Label Color: {{ myAddress.myCustomColorFieldHandle }}</li>
</ul>

# Attribute Labels

The addressing library’s abstracted Administrative AreaLocalityDependent Locality terminology probably isn’t what you are accustomed to calling those address components in your part of the world—and it’s even less likely you’d want to show those terms to site visitors.

You can use any address element’s attributeLabel() method to get human-friendly labels for a given locale. Assuming we’re working with a U.S. address…

{{ myAddress.attributeLabel('administrativeArea') }} {# State #}
{{ myAddress.attributeLabel('locality') }} {# City #}
{{ myAddress.attributeLabel('dependentLocality') }} {# Suburb #}
{{ myAddress.attributeLabel('postalCode') }} {# Zip Code #}

Labels use the address’s current countryCode value for localization.

# |address Formatter

You can use the |address filter to output a formatted address with basic HTML:

{{ myAddress|address }}
{# Output:
  <p translate="no">
    <span class="address-line1">1234 Balboa Towers Circle</span><br>
    <span class="locality">Los Angeles</span>, <span class="administrative-area">CA</span> <span class="postal-code">92662</span><br>
    <span class="country">United States</span>
  </p>
#}

The default formatter includes the following options:

  • locale – defaults to 'en'
  • html – defaults to true; disable with false to maintain line breaks but omit HTML tags
  • html_tag – defaults to p
  • html_attributes – is an array that defaults to ['translate' => 'no']
{# Swap enclosing paragraph tag for a `div`: #}
{{ myAddress|address({ html_tag: 'div' }) }}
{# Output:
  <div translate="no">
    <span class="address-line1">1234 Balboa Towers Circle</span><br>
    <span class="locality">Los Angeles</span>, <span class="administrative-area">CA</span> <span class="postal-code">92662</span><br>
    <span class="country">United States</span>
  </div>
#}

{# Turn the entire address into a Google Maps link: #}
{{ myAddress|address({
  html_tag: 'a',
  html_attributes: {
    href: url('https://maps.google.com/maps/search/', {
      query_place_id: address.myCustomPlaceIdField,
    }),
  },
}) }}
{# Output:
  <a href="https://maps.google.com/maps/search/?query_place_id=...">
    <span class="address-line1">1234 Balboa Towers Circle</span><br>
    <span class="locality">Los Angeles</span>, <span class="administrative-area">CA</span> <span class="postal-code">92662</span><br>
    <span class="country">United States</span>
  </a>
#}

{# Omit all HTML tags: #}
{{ myAddress|address({ html: false }) }}
{# Output:
  1234 Balboa Towers Circle
  Los Angeles, CA 92662
  United States
#}

{# Force output in the Ukrainian (`uk`) locale: #}
{{ myAddress|address({ html: false, locale: 'uk' }) }}
{# Output:
  1234 Balboa Towers Circle
  Los Angeles, CA 92662
  Сполучені Штати
#}

# Customizing the Formatter

You can also pass your own formatter to the |address filter. The addressing library includes PostalLabelFormatter (opens new window) to make it easier to print shipping labels. Here, we can specify that formatter and set its additional origin_country option:

{# Use the postal label formatter #}
{% set addressService = craft.app.getAddresses() %}
{% set labelFormatter = create(
  'CommerceGuys\\Addressing\\Formatter\\PostalLabelFormatter',
  [
    addressService.getAddressFormatRepository(),
    addressService.getCountryRepository(),
    addressService.getSubdivisionRepository(),
  ]) %}
{{ addr|address({ origin_country: 'GB' }, labelFormatter) }}
{# Output:
  1234 Balboa Towers Circle
  LOS ANGELES, CA 92662
  UNITED STATES
#}

You can also write a custom formatter that implements FormatterInterface (opens new window). We might extend the default formatter, for example, to add a hide_countries option that avoids printing the names of specified countries:

<?php

namespace mynamespace;

use CommerceGuys\Addressing\AddressInterface;
use CommerceGuys\Addressing\Formatter\DefaultFormatter;
use CommerceGuys\Addressing\Locale;
use craft\helpers\Html;

class OptionalCountryFormatter extends DefaultFormatter
{
  /**
   * @inheritdoc
   */
  protected $defaultOptions = [
    'locale' => 'en',
    'html' => true,
    'html_tag' => 'p',
    'html_attributes' => ['translate' => 'no'],
    'hide_countries' => [],
  ];

  /**
   * @inheritdoc
   */
  public function format(AddressInterface $address, array $options = []): string
  {
    $this->validateOptions($options);
    $options = array_replace($this->defaultOptions, $options);
    $countryCode = $address->getCountryCode();
    $addressFormat = $this->addressFormatRepository->get($countryCode);

    if (!in_array($countryCode, $options['hide_countries'])) {
      if (Locale::matchCandidates($addressFormat->getLocale(), $address->getLocale())) {
        $formatString = '%country' . "\n" . $addressFormat->getLocalFormat();
      } else {
        $formatString = $addressFormat->getFormat() . "\n" . '%country';
      }
    } else {
      // If this is in our `hide_countries` list, omit the country
      $formatString = $addressFormat->getFormat();
    }

    $view = $this->buildView($address, $addressFormat, $options);
    $view = $this->renderView($view);
    $replacements = [];
    foreach ($view as $key => $element) {
      $replacements['%' . $key] = $element;
    }
    $output = strtr($formatString, $replacements);
    $output = $this->cleanupOutput($output);

    if (!empty($options['html'])) {
      $output = nl2br($output, false);

      // Add the HTML wrapper element with Craft’s HTML helper:
      $output = Html::tag($options['html_tag'], $output, $options['html_attributes']);
    }

    return $output;
  }
}

We can instantiate and use that just like the postal label formatter:

{# Use our custom formatter #}
{% set addressService = craft.app.getAddresses() %}
{% set customFormatter = create(
  'mynamespace\\OptionalCountryFormatter',
  [
    addressService.getAddressFormatRepository(),
    addressService.getCountryRepository(),
    addressService.getSubdivisionRepository(),
  ]
) %}
{{ addr|address({ hide_countries: ['US'] }, customFormatter) }}
{# Output:
  1234 Balboa Towers Circle
  Los Angeles, CA 92662
#}

# Managing Addresses

Users can add, edit, and delete their own addresses from the front-end via the users/save-address and users/delete-address controller actions.

Craft doesn’t automatically give Addresses their own URLs, though—so it’s up to you to define a routing scheme for them via routes.php. We’ll cover each of these three routes in the following sections:

<?php
return [
  // Listing Addresses
  'account' => ['template' => '_account/dashboard'],

  // New Addresses
  'account/addresses/new' => ['template' => '_account/edit-address'],

  // Existing Addresses
  'account/addresses/<addressUid:{uid}>' => ['template' => '_account/edit-address'],
];

The next few snippets may be a bit dense—numbered comments are peppered throughout, corresponding to items in the Guide section just below it.

# Scaffolding

The following templates assume you have a layout functionally equivalent to the models and validation example.

# Listing Addresses

Let’s display the current user’s address book on their account “dashboard.”

{% extends '_layouts/default' %}

{% requireLogin %}

{# 1. Load Addresses: #}
{% set addresses = currentUser.getAddresses() %}

{% block content %}
  <h1>Hello, {{ currentUser.fullName }}!</h1>

  {% if addresses | length %}
    <ul>
      {% for address in addresses %}
        <li>
          {{ address.title }}<br>
          {{ address|address }}<br>

          {# 2. Build an edit URL: #}
          <a href="{{ url("account/addresses/#{address.uid}") }}">Edit</a>

          {# 3. Use a form to delete Addresses: #}
          <form method="post">
            {{ csrfInput() }}
            {{ actionInput('users/delete-address') }}
            {{ hiddenInput('addressId', address.id) }}

            <button>Delete</button>
          </form>
        </li>
      {% endfor $}
    </ul>
  {% else %}
    <p>You haven’t added any addresses, yet!</p>
  {% endif %}

  {# 4. Link to "new" route: #}
  <p><a href="{{ url('account/addresses/new') }}">New Address</a></p>
{% endblock %}

# Guide

  1. We’re using a craft\elements\User (opens new window) convenience method to load the current user’s saved addresses, but this is equivalent to our earlier query example.
  2. These URLs will need to match the pattern defined in routes.php. In our case, that means we need to interpolate the Address’s UID into the path.
  3. Deleting an Address requires a POST request, which—for the sake of simplicity—we’re handling with a regular HTML form.
  4. The New Address route is static—there’s nothing to interpolate or parameterize.

# New Addresses

The code for new addresses will end up being reused for existing addresses.

{% extends '_layouts/default' %}

{% requireLogin %}

{% block content %}
  <h1>New Address</h1>

  {# 1. Render the form #}
  {{ include('_account/address-form', {
    address: address ?? create('craft\\elements\\Address'),
  }) }}
{% endblock %}

# Guide

  1. We pass an craft\elements\Address (opens new window) to the form partial—either from an address variable that is available to the template after an attempted submission (say, due to validation errors), or a new one instantiated with the create() function.
  2. Whether we’re creating a new Address or editing an existing one (this partial handles both), the request should be sent to the users/save-address action.
  3. Addresses that have been previously saved will have an id, so we need to send that back to apply updates to the correct Address.
  4. The redirectInput() function accepts an “object template,” which can include properties of the thing we’re working with. The template won’t be evaluated when it appears in the form—instead, Craft will render it using the Address object after it’s been successfully saved. In this case, we’ll be taken to the edit screen for the newly-saved address.
  5. Which fields are output is up to you. If you want to capture input for custom fields, it should be nested under the fields key: <input type="text" name="fields[myCustomFieldHandle]" value="...">

See the complete list of parameters that can be sent

# Existing Addresses

To edit an existing address, we’ll use the addressUid parameter from our route.

{% extends '_layouts/default' %}

{% requireLogin %}

{# 1. Resolve the address: #}
{% set address = address ?? craft.addresses
  .owner(currentUser)
  .uid(addressUid)
  .one() %}

{# 2. Make sure we got something: #}
{% if not address %}
  {% exit 404 %}
{% endif %}

{% block content %}
  <h1>Edit Address: {{ address.title }}</h1>

  {# 3. Render form: #}
  {{ include('_account/address-form', {
    address: address,
  }) }}
{% endblock %}

# Guide

  1. In one statement, we’re checking for the presence of an address variable sent back to the template by a prior submission, and falling back to a lookup against the user’s Addresses. By calling .owner(currentUser), we can be certain we’re only ever showing a user an Address they own.
  2. If an Address wasn’t passed back to the template, and the UID from our route didn’t match one of the current user’s addresses, we bail.
  3. The Address is passed to the form partial for rendering—see the new address example for the form’s markup.

# Validating Addresses

Addresses are validated like any other type of element, but some of the rules are dependent upon its localized format.

You can set requirements for custom fields in the Address Fields field layout, but additional validation of any address properties requires a custom plugin or module.

Take a look at the Using Events in a Custom Module (opens new window) article for a dedicated primer on module setup and events.

Validation rules (opens new window) are added via the Model::EVENT_DEFINE_RULES event:

use yii\base\Event;
use craft\base\Model;
use craft\elements\Address;
use craft\events\DefineRulesEvent;

Event::on(
  Address::class,
  Model::EVENT_DEFINE_RULES,
  function(DefineRulesEvent $event) {
    $event->rules[] = [
      ['fullName'],
      'match',
      'pattern' => '/droid|bot/i',
      'message' => Craft::t('site', 'Robots are not allowed.'),
    ];
  }
);

Errors are available through the same address.getErrors() method used in other action and model validation examples, regardless of whether they were produced by built-in rules or ones you added.