Guide 07 · Developer docs

Members, accounts and gated content

Building a member area on Core dna: who a member is in the data, the sign-in flows and their templates, single sign-on and passkeys, the my-account routing convention, members-only content, sessions and the page cache, and the security checks to run before launch.

L2
Unit 01 · The identity model

Addresses and the address book

A person keeps an address book: any number of addresses, each with an address type and one marked primary. The model, the address types, the screens a member manages it with, rendering it in the member area, and how orders and checkouts use a saved address without linking to it.

Who this is for. The theme developer building the address screens, and the commerce implementer pre-filling checkout from them. Afterwards you know what an address book entry holds, how to render and edit it, and why editing it never changes an old order.

The model

Each entry in a person's address book is a person address with three parts:

PartHoldsIn a template
The addressFirst and last name, company, unit, address lines 1 and 2, suburb, city, postcode, state, country, phone, and any custom fields defined for addressesgetAddress(), then getCity(), getPostcode(), getStateName(), getCountryName() and the rest
The address typeA record with a name, such as "Home" or "Office deliveries", and a kind: billing, shipping or othergetType(), then getName() and getType()
PrimaryWhether this is the person's main addressisPrimary()

A person has at most one primary address across all types: saving a second is refused with "Only one address can be set as primary". Address types are managed under Users › Address Types; a site can have several of each kind. Address types are permission-controlled records, so members need read on them for the type list on the add form to fill.

The address screens

The member area has the screens built in (The built-in member screens):

RequestDoesThen
GET /my-account/addressesLists the address book, paged, as personAddresses
GET /my-account/address, POST /my-account/addressShows the add form with countries and types; saves a new addressRedirects to the list with "Address Added"
GET /my-account/address/{id}Shows one address in an edit form, as personAddress
PUT /my-account/address/{id}Saves changesRedirects to the address with "Address Updated"
DELETE /my-account/address/{id}Deletes itRedirects to the list with "Address Deleted"

The form posts type (the address type's id), primary (1 or 0) and the address as address[firstName], address[addressLine1], address[city], address[postcode], address[country] (a country id from countries), address[stateName] and so on, with the CSRF token; PUT and DELETE travel as a POST with _method. A failed add re-renders the form with HTTP 422 and the field errors; a failed edit or delete redirects with the error as a message, so print the site's messages on these pages. Write your own list template before offering the screen, rather than the framework's (Which screens need a template from you).

Rendering the address book

Template code templates/users/person-addresses/index.html
{extends 'users/frontend/includes/account.html'}
{block 'accountContent'}
  <h1>Your addresses</h1>
  <p><a href="{$_url->getPath('MyAccount', 'addAddress')}">Add an address</a></p>
  {foreach $personAddresses as $entry}
    {$address = $entry->getAddress()}
    <section>
      <h2>{$entry->getType()->getName()|escape}{if $entry->isPrimary()} (main address){/if}</h2>
      <p>{$address->getFirstName()|escape} {$address->getLastName()|escape}<br>
        {$address->getAddressLine1()|escape}<br>
        {$address->getSuburb()|escape} {$address->getStateName()|escape} {$address->getPostcode()|escape}<br>
        {$address->getCountryName()|escape}</p>
      <a href="{$_url->getPath('MyAccount', 'address', ['id' => $entry->getId()])}">Edit</a>
    </section>
  {foreachelse}
    <p>You have no saved addresses.</p>
  {/foreach}
{/block}
{% extends 'users/frontend/includes/account.html' %}
{% block accountContent %}
  <h1>Your addresses</h1>
  <p><a href="{{ _url.getPath('MyAccount', 'addAddress') }}">Add an address</a></p>
  {% for entry in personAddresses %}
    {% set address = entry.getAddress() %}
    <section>
      <h2>{{ entry.getType().getName()|e }}{% if entry.isPrimary() %} (main address){% endif %}</h2>
      <p>{{ address.getFirstName()|e }} {{ address.getLastName()|e }}<br>
        {{ address.getAddressLine1()|e }}<br>
        {{ address.getSuburb()|e }} {{ address.getStateName()|e }} {{ address.getPostcode()|e }}<br>
        {{ address.getCountryName()|e }}</p>
      <a href="{{ _url.getPath('MyAccount', 'address', {'id': entry.getId()}) }}">Edit</a>
    </section>
  {% else %}
    <p>You have no saved addresses.</p>
  {% endfor %}
{% endblock %}

A Twig site extends its own Twig account menu (The account menu). The profile route can change addresses too, since Primary address and Addresses are among its changeable properties by default (The profile route); most sites use the address screens and leave the profile form to names and preferences.

Orders and checkouts

Orders and checkouts do not link to the address book. Each holds its own billing and delivery address, so editing or deleting a saved address never changes an order already placed, and an order's addresses stay as they were at the time.

To save a member typing, pre-fill the checkout's address fields from the address book: read the entry you want in the checkout template and print its values into the fields. The checkout's own validation then applies as usual (Checkout):

Template code In the checkout template
{$saved = null}
{if $_currentUser->isLoggedIn()}
  {foreach $_currentUser->getPerson()->getPersonAddresses() as $entry}
    {if $entry->isPrimary()}{$saved = $entry->getAddress()}{/if}
  {/foreach}
{/if}
<input name="billingAddress[addressLine1]" value="{if $saved}{$saved->getAddressLine1()|escape}{/if}">
{% set saved = null %}
{% if _currentUser.isLoggedIn() %}
  {% for entry in _currentUser.getPerson().getPersonAddresses() %}
    {% if entry.isPrimary() %}{% set saved = entry.getAddress() %}{% endif %}
  {% endfor %}
{% endif %}
<input name="billingAddress[addressLine1]" value="{{ saved ? saved.getAddressLine1()|e : '' }}">

To offer a choice, list the entries of the right kind, billing or shipping, by getType()->getType(), and fill the fields from the one the member picks with a few lines of script. Whatever is pre-filled is printed for one member, so the checkout page must not be cached; checkout routes already prevent caching themselves.

Go deeper

Was this page helpful?

Your answer goes to the docs team.