acquia_commercemanager-8.x-1.122/modules/acm/src/Element/AcmAddress.php
modules/acm/src/Element/AcmAddress.php
<?php
namespace Drupal\acm\Element;
use Drupal\acm\Connector\RouteException;
use Drupal\address\LabelHelper;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\FormElement;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides an ACM Address form element.
*
* ACM Address form elements contain a group of sub-elements for each address
* components.
*
* @FormElement("acm_address")
*/
class AcmAddress extends FormElement {
/**
* The default address values.
*
* @var array
*/
private static $addressDefaults = [
'telephone' => '',
'address_id' => 0,
'title' => '',
'firstname' => '',
'lastname' => '',
'street' => '',
'street2' => '',
'city' => '',
'region' => '',
'postcode' => '',
'country_id' => 'US',
'default_billing' => 0,
'default_shipping' => 0,
];
/**
* Calculate dynamic address parts per country.
*
* @param string $country
* Country code.
*
* @return array
* Calculated dynamic form parts.
*/
public static function calculateDynamicParts($country) {
$dynamic_parts = [];
$addressFormatRepository = \Drupal::service('address.address_format_repository');
$address_format = $addressFormatRepository->get($country);
$subdivisionRepository = \Drupal::service('address.subdivision_repository');
$options = $subdivisionRepository->getList([$country]);
$labels = LabelHelper::getFieldLabels($address_format);
// Update region options based on country.
$dynamic_parts['region']['#options'] = $options;
$dynamic_parts['region']['#required'] = TRUE;
$dynamic_parts['region']['#access'] = TRUE;
// Update labels.
$cityLabel = $labels['locality'];
$postcodeLabel = $labels['postalCode'];
$regionLabel = $labels['administrativeArea'];
$dynamic_parts['region']['#title'] = $regionLabel;
$dynamic_parts['postcode']['#title'] = $postcodeLabel;
$dynamic_parts['city']['#title'] = $cityLabel;
if (empty($cityLabel)) {
$dynamic_parts['city']['#required'] = FALSE;
$dynamic_parts['city']['#access'] = FALSE;
}
if (empty($postcodeLabel)) {
$dynamic_parts['postcode']['#required'] = FALSE;
$dynamic_parts['postcode']['#access'] = FALSE;
}
if (empty($regionLabel) || empty($options)) {
$dynamic_parts['region']['#required'] = FALSE;
$dynamic_parts['region']['#access'] = FALSE;
}
return $dynamic_parts;
}
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#markup' => '',
'#process' => [
[$class, 'processAcmAddress'],
],
'#theme_wrappers' => ['form_element'],
'#display_telephone' => FALSE,
'#display_billing' => FALSE,
'#display_shipping' => FALSE,
'#display_title' => FALSE,
'#display_firstname' => FALSE,
'#display_lastname' => FALSE,
'#include_address_id' => FALSE,
'#validate_address' => FALSE,
'#address_review_text' => '',
'#address_failed_text' => '',
];
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input === FALSE || empty($input)) {
$element += ['#default_value' => []];
return $element['#default_value'];
}
$address_input = [];
// Flatted the input.
array_walk_recursive($input, function ($value, $key) use (&$address_input) {
$address_input[$key] = $value;
});
// Throw out all invalid array keys.
$value = [];
foreach (static::$addressDefaults as $allowed_key => $default) {
// These should be strings, but allow other scalars since they might be
// valid input in programmatic form submissions. Any nested array values
// are ignored.
if (isset($address_input[$allowed_key]) && is_scalar($address_input[$allowed_key])) {
$value[$allowed_key] = (string) $address_input[$allowed_key];
}
}
return $value;
}
/**
* Expand a acm_address field into multiple inputs.
*/
public static function processAcmAddress(&$element, FormStateInterface $form_state, &$complete_form) {
$default_address = (array) $element['#default_value'];
$address = $default_address + static::$addressDefaults;
$country = !empty($address['country_id']) ? $address['country_id'] : 'US';
$countryRepository = \Drupal::service('address.country_repository');
$subdivisionRepository = \Drupal::service('address.subdivision_repository');
$addressFormatRepository = \Drupal::service('address.address_format_repository');
$address_format = $addressFormatRepository->get($country);
$labels = LabelHelper::getFieldLabels($address_format);
if (!empty($element['#include_address_id'])) {
$element['address_id'] = [
'#type' => 'hidden',
'#default_value' => $address['address_id'],
];
}
if (!empty($element['#display_title'])) {
$element['title'] = [
'#type' => 'acm_title_select',
'#title' => t('Title'),
'#default_value' => empty($address['title']) ? NULL : $address['title'],
'#required' => TRUE,
'#placeholder' => t('Title*'),
];
}
if (!empty($element['#display_firstname'])) {
$element['firstname'] = [
'#type' => 'textfield',
'#title' => t('First Name'),
'#default_value' => $address['firstname'],
'#required' => TRUE,
'#placeholder' => t('First Name*'),
];
}
if (!empty($element['#display_lastname'])) {
$element['lastname'] = [
'#type' => 'textfield',
'#title' => t('Last Name'),
'#default_value' => $address['lastname'],
'#required' => TRUE,
'#placeholder' => t('Last Name*'),
];
}
if (!empty($element['#display_telephone'])) {
$element['telephone'] = [
'#type' => 'textfield',
'#title' => t('Telephone'),
'#default_value' => $address['telephone'],
'#required' => TRUE,
'#placeholder' => t('Telephone*'),
];
}
$element['street'] = [
'#type' => 'textfield',
'#title' => t('Address Line 1'),
'#default_value' => $address['street'],
'#required' => TRUE,
'#placeholder' => t('Address Line 1*'),
];
$element['street2'] = [
'#type' => 'textfield',
'#title' => t('Address Line 2'),
'#default_value' => $address['street2'],
'#required' => FALSE,
'#placeholder' => t('Address Line 2'),
];
$element['dynamic_parts'] = [
'#type' => 'container',
'#attributes' => [
'id' => ['dynamic_parts'],
],
'#parents' => $element['#parents'],
];
$element['dynamic_parts']['city'] = [
'#type' => 'textfield',
'#title' => $labels['locality'],
'#default_value' => $address['city'],
'#required' => TRUE,
'#placeholder' => $labels['locality'],
];
$dynamic_parts = self::calculateDynamicParts($country);
$regions = $subdivisionRepository->getList([$country]);
$possiblyMatchingRegion = "";
// If address has region and this country has regions to choose from
// then try to fix the region mess, otherwise set region to "".
if ($address['region'] && $dynamic_parts['region']['#access']) {
// Some e-commerce back-ends send back region as an abbreviation and some
// don't. If we don't have an abbreviation we'll need to flip the regions
// array in order to find the default value to use.
$possiblyMatchingRegion = AcmAddress::fixRegionMess($address['region'], $regions);
}
$default_region = $possiblyMatchingRegion;
$element['dynamic_parts']['region'] = [
'#type' => 'select',
'#title' => $labels['administrativeArea'],
'#options' => $regions,
'#default_value' => $default_region,
'#empty_option' => '- ' . $labels['administrativeArea'] . ' -',
'#required' => TRUE,
'#validated' => TRUE,
];
$element['dynamic_parts']['postcode'] = [
'#type' => 'textfield',
'#title' => $labels['postalCode'],
'#default_value' => $address['postcode'],
'#required' => TRUE,
'#placeholder' => $labels['postalCode'],
];
$element['dynamic_parts']['country_id'] = [
'#type' => 'select',
'#title' => t('Country'),
'#options' => $countryRepository->getList(),
'#default_value' => $country,
'#required' => TRUE,
'#ajax' => [
'callback' => [get_called_class(), 'addressAjaxCallback'],
'wrapper' => 'dynamic_parts',
'options' => [
'query' => [
'element_parents' => implode('/', $element['#array_parents']),
],
],
],
];
// We created the dynamic parts as we may want them,
// But now we calculate what is hidden or showing or required
// and what labels are translated based on the country (not the locale...)
$element['dynamic_parts'] = array_replace_recursive($element['dynamic_parts'], $dynamic_parts);
if (!empty($element['#display_billing'])) {
$element['default_billing'] = [
'#type' => 'checkbox',
'#title' => t('Default billing address'),
'#default_value' => $address['default_billing'],
];
}
if (!empty($element['#display_shipping'])) {
$element['default_shipping'] = [
'#type' => 'checkbox',
'#title' => t('Default shipping address'),
'#default_value' => $address['default_shipping'],
];
}
// TODO (Malachy): Consider adding 'save address to address book' here.
if (!empty($element['#validate_address'])) {
$element['#element_validate'] = [[get_called_class(), 'validateAddress']];
if (!isset($element['#address_review_text'])) {
$element['#address_review_text'] = t('Address validation suggested a different address.');
}
if (!isset($element['#address_failed_text'])) {
$element['#address_failed_text'] = t('Address validation failed.');
}
}
$element['#tree'] = TRUE;
return $element;
}
/**
* Validates an acm_address element.
*/
public static function validateAddress(&$element, FormStateInterface $form_state, &$complete_form) {
$address = $element['#value'];
$address_review_text = $element['#address_review_text'];
$address_failed_text = $element['#address_failed_text'];
// Make sure these fields have values before trying to validate the address.
$required_fields = [
'street',
'city',
'region',
'postcode',
];
$skip = FALSE;
foreach ($required_fields as $required_field) {
if (empty($address[$required_field])) {
$skip = TRUE;
break;
}
}
// Skip validation if not all fields are filled out yet.
if ($skip) {
return $element;
}
try {
$response = \Drupal::service('acm.api')
->validateCustomerAddress($address);
// Address is valid and no suggestion came back.
if (isset($response['result']['valid']) && empty($response['result']['suggested'])) {
drupal_set_message($address_review_text, 'status');
}
// Address is in review and there's a suggestion that we use to pre-fill
// the address fields.
elseif (isset($response['result']['suggested']) && !empty($response['result']['suggested'])) {
$suggested_address = reset($response['result']['suggested']);
foreach ($suggested_address as $field => $value) {
if (!empty($value)) {
if (isset($element[$field])) {
$form_state->setValueForElement($element[$field], $value);
}
elseif (isset($element['dynamic_parts'][$field])) {
$form_state->setValueForElement($element['dynamic_parts'][$field], $value);
}
}
}
drupal_set_message($address_review_text, 'status');
}
// Address failed validation.
else {
$form_state->setError($element, $address_failed_text);
}
}
catch (RouteException $e) {
$form_state->setError($element, $address_failed_text);
}
return $element;
}
/**
* Ajax handler for country selector.
*
* @param array $form
* The build form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The ajax response of the ajax upload.
*/
public static function addressAjaxCallback(array $form, FormStateInterface $form_state, Request $request) {
$form_parents = explode('/', $request->query->get('element_parents'));
// Retrieve the element to be rendered.
$form = NestedArray::getValue($form, $form_parents);
$values = $form_state->getValue($form['#parents']);
$country = $values['country_id'];
$dynamic_parts = self::calculateDynamicParts($country);
return array_replace_recursive($form['dynamic_parts'], $dynamic_parts);
}
/**
* Function fixRegionMess().
*
* Some ecommerce back-ends send back region as an abbreviation and some
* don't. If we don't have an abbreviation we'll need to flip the regions
* array in order to find the default value to use.
*
* @param string $region
* The region to fix.
* @param array $regions
* The regions to fix against (array of strings expected).
*
* @return null|string
* The fixed region string.
*/
public static function fixRegionMess(string $region, array $regions) {
if ($region) {
if (!preg_match('/\b([A-Z]{2})\b/', $region)) {
$flipped_regions = array_flip($regions);
$region = isset($flipped_regions[$region]) ? $flipped_regions[$region] : "";
}
}
return $region;
}
}
