Commit db1e20f1 authored by James Candan's avatar James Candan
Browse files

feat: #3294769 Add support for Address field

parent 9ab91586
Loading
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@
  "require-dev": {
    "drupal/name": "^1.0",
    "drupal/paragraphs": ">=1.15",
    "drupal/address": "^2.0",
    "drush/drush": "*"
  }
}
+84 −7
Original line number Diff line number Diff line
@@ -5,13 +5,14 @@
 * Module file for the Require on Publish module.
 */

use Drupal\Core\Template\Attribute;
use Drupal\field\FieldConfigInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityFormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Template\Attribute;
use Drupal\field\FieldConfigInterface;
use Drupal\require_on_publish\Plugin\Validation\Constraint\RequireOnPublishAddressFormat;

/**
 * Implements hook_help().
@@ -27,6 +28,15 @@ function require_on_publish_help($route_name, RouteMatchInterface $route_match)
  }
}

/**
 * Implements hook_validation_constraint_alter().
 */
function require_on_publish_validation_constraint_alter(array &$definitions): void {
  if (isset($definitions['AddressFormat']) && \Drupal::moduleHandler()->moduleExists('address')) {
    $definitions['AddressFormat']['class'] = RequireOnPublishAddressFormat::class;
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
@@ -158,11 +168,18 @@ function require_on_publish_preprocess_field_multiple_value_form(&$variables): v
/**
 * Implements hook_preprocess_HOOK().
 */
function require_on_publish_preprocess_fieldset(&$variables): void {
  $element = $variables['element'];
  if (!empty($element['#required_on_publish'])) {
    $variables['legend_span']['attributes'] = new Attribute(['class' => ['form-required-on-publish']]);
function require_on_publish_preprocess_fieldset(array &$variables): void {
  if (empty($variables['element']['#required_on_publish'])) {
    return;
  }

  $attributes = $variables['legend_span']['attributes'] ?? [];
  if (!$attributes instanceof Attribute) {
    $attributes = new Attribute($attributes);
  }

  $attributes->addClass('form-required-on-publish');
  $variables['legend_span']['attributes'] = $attributes;
}

/**
@@ -543,6 +560,29 @@ function _require_on_publish_handle_field_indicator(array &$form, string $field_
    return;
  }

  // Address: mark the wrapper and convert Address module's
  // country-specific required subfields into required-on-publish subfields.
  if ($field_config->getType() === 'address') {
    if (empty($field_element['widget']) || !is_array($field_element['widget'])) {
      return;
    }

    $field_element['widget']['#required_on_publish'] = TRUE;

    // Address renders each delta as the fieldset that owns the visible
    // legend. Mark it immediately so fieldset preprocess can add the ROP
    // marker during the initial render, before any validation rebuild.
    foreach (Element::children($field_element['widget']) as $delta) {
      if (isset($field_element['widget'][$delta])
        && is_array($field_element['widget'][$delta])) {
        $field_element['widget'][$delta]['#required_on_publish'] = TRUE;
      }
    }

    $field_element['widget']['#after_build'][] = '_require_on_publish_address_after_build';
    return;
  }

  // Non-Name: mark via legacy helper (preprocess hooks paint the label).
  if (empty($field_element['widget']) || !is_array($field_element['widget'])) {
    return;
@@ -550,6 +590,43 @@ function _require_on_publish_handle_field_indicator(array &$form, string $field_
  _require_on_publish_add_indicator($field_element['widget']);
}

/**
 * After_build: convert Address required subfields to required-on-publish.
 *
 * Address calculates required subfields from the selected country's address
 * format. ROP preserves those semantics for publishing while removing the
 * Form API/HTML5 required behavior that would otherwise block submission.
 */
function _require_on_publish_address_after_build(array $widget, FormStateInterface $form_state): array {
  _require_on_publish_address_convert_required_children($widget);
  return $widget;
}

/**
 * Recursively marks required Address children as required-on-publish.
 */
function _require_on_publish_address_convert_required_children(array &$element): void {
  foreach (Element::children($element) as $key) {
    if (!isset($element[$key]) || !is_array($element[$key])) {
      continue;
    }

    if (!empty($element[$key]['#required'])) {
      $element[$key]['#required'] = FALSE;
      $element[$key]['#required_on_publish'] = TRUE;
      if (isset($element[$key]['#attributes']['class']) && is_array($element[$key]['#attributes']['class'])) {
        $element[$key]['#attributes']['class'] = array_values(array_diff(
          $element[$key]['#attributes']['class'],
          ['required']
        ));
      }
      unset($element[$key]['#attributes']['required']);
    }

    _require_on_publish_address_convert_required_children($element[$key]);
  }
}

/**
 * After_build: ensure on-load marker class exists for non-standard label flows.
 *
+19 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\require_on_publish\Plugin\Validation\Constraint;

use Drupal\address\Plugin\Validation\Constraint\AddressFormatConstraint;

/**
 * Address format constraint replacement for Require on Publish address fields.
 */
class RequireOnPublishAddressFormat extends AddressFormatConstraint {

  /**
   * {@inheritdoc}
   */
  public function validatedBy(): string {
    return RequireOnPublishAddressFormatValidator::class;
  }

}
+111 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\require_on_publish\Plugin\Validation\Constraint;

use CommerceGuys\Addressing\AddressFormat\AddressFormat;
use CommerceGuys\Addressing\Validator\Constraints\AddressFormatConstraint;
use Drupal\address\Plugin\Validation\Constraint\AddressFormatConstraintValidator;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Field\FieldConfigInterface;
use Drupal\Core\Field\FieldItemInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;

/**
 * Allows ROP to own Address required-parts validation for ROP fields.
 */
class RequireOnPublishAddressFormatValidator extends AddressFormatConstraintValidator implements ContainerInjectionInterface {

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('address.address_format_repository'),
      $container->get('address.subdivision_repository')
    );
  }

  /**
   * The current AddressFormat not-blank message.
   *
   * @var string|null
   */
  protected ?string $notBlankMessage = NULL;

  /**
   * {@inheritdoc}
   */
  public function validate($value, Constraint $constraint): void {
    $previous_not_blank_message = $this->notBlankMessage;
    $this->notBlankMessage = $constraint instanceof AddressFormatConstraint
      ? $constraint->notBlankMessage
      : NULL;

    try {
      parent::validate($value, $constraint);
    }
    finally {
      $this->notBlankMessage = $previous_not_blank_message;
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function addViolation(string $field, string $message, mixed $invalid_value, AddressFormat $address_format): void {
    if ($this->isRequireOnPublishAddressValue(NULL)
      && $this->notBlankMessage !== NULL
      && $message === $this->notBlankMessage) {
      return;
    }

    parent::addViolation($field, $message, $invalid_value, $address_format);
  }

  /**
   * Whether ROP should replace AddressFormat validation for this field item.
   */
  protected function isRequireOnPublishAddressValue(mixed $value): bool {
    if ($this->isRequireOnPublishAddressItem($value)) {
      return TRUE;
    }

    if (!isset($this->context)) {
      return FALSE;
    }

    $object = $this->context->getObject();
    if ($this->isRequireOnPublishAddressItem($object)) {
      return TRUE;
    }

    $parent = $object;
    while (is_object($parent) && method_exists($parent, 'getParent')) {
      $parent = $parent->getParent();
      if ($this->isRequireOnPublishAddressItem($parent)) {
        return TRUE;
      }
    }

    return FALSE;
  }

  /**
   * Whether a value is an ROP-enabled Address field item.
   */
  protected function isRequireOnPublishAddressItem(mixed $value): bool {
    return $value instanceof FieldItemInterface
      && $this->isRequireOnPublishAddressDefinition($value->getFieldDefinition());
  }

  /**
   * Whether a field definition is an ROP-enabled Address field.
   */
  protected function isRequireOnPublishAddressDefinition(mixed $definition): bool {
    return $definition instanceof FieldConfigInterface
      && $definition->getType() === 'address'
      && (bool) $definition->getThirdPartySetting('require_on_publish', 'require_on_publish', FALSE);
  }

}
+121 −0
Original line number Diff line number Diff line
@@ -3,6 +3,11 @@
namespace Drupal\require_on_publish\Plugin\Validation\Constraint;

use Drupal\node\NodeInterface;
use Drupal\address\FieldHelper;
use Drupal\address\LabelHelper;
use CommerceGuys\Addressing\AddressFormat\AddressFormatHelper;
use CommerceGuys\Addressing\AddressFormat\FieldOverrides;
use Drupal\address\Repository\AddressFormatRepository;
use Drupal\paragraphs\Entity\Paragraph;
use Drupal\Core\Field\FieldConfigInterface;
use Symfony\Component\Validator\Constraint;
@@ -41,6 +46,7 @@ class RequireOnPublishValidator extends ConstraintValidator implements Container
    protected RequestStack $requestStack,
    protected MessengerInterface $messenger,
    protected ?ModerationInformationInterface $moderationInfo = NULL,
    protected ?AddressFormatRepository $addressFormatRepository = NULL,
  ) {}

  /**
@@ -53,6 +59,9 @@ class RequireOnPublishValidator extends ConstraintValidator implements Container
      $container->get('messenger'),
      $container->has('content_moderation.moderation_information')
        ? $container->get('content_moderation.moderation_information')
        : NULL,
      $container->has('address.address_format_repository')
        ? $container->get('address.address_format_repository')
        : NULL
    );
  }
@@ -88,6 +97,9 @@ class RequireOnPublishValidator extends ConstraintValidator implements Container
      elseif ($this->isNameField($field_definition)) {
        $this->validateNameField($items, $constraint);
      }
      elseif ($this->isAddressField($field_definition)) {
        $this->validateAddressField($items, $constraint);
      }
      else {
        $this->validateField($items, $constraint);
      }
@@ -164,6 +176,13 @@ class RequireOnPublishValidator extends ConstraintValidator implements Container
          continue;
        }

        // Address fields: use Address-aware validation and pass down the
        // parent ROP state.
        if ($this->isAddressField($subfield_definition)) {
          $this->validateAddressField($subfield, $constraint, $parent_field_name, $delta, $parent_rop_active);
          continue;
        }

        // Non-Name subfields: existing ROP logic, but OR with parent ROP state.
        $label = $subfield_definition->getLabel();
        $sub_name = $subfield_definition->getName();
@@ -204,6 +223,108 @@ class RequireOnPublishValidator extends ConstraintValidator implements Container
      && $definition->getType() === 'name';
  }

  /**
   * Whether the field is an Address field.
   */
  protected function isAddressField(FieldDefinitionInterface $definition): bool {
    return $this->moduleHandler->moduleExists('address')
      && $definition->getType() === 'address';
  }

  /**
   * Validate an Address field honoring country-specific required parts.
   */
  protected function validateAddressField(FieldItemListInterface $items, Constraint $constraint, ?string $anchorField = NULL, ?int $anchorDelta = NULL, ?bool $parentRopActive = NULL): void {
    $field_definition = $items->getFieldDefinition();
    if (!($field_definition instanceof FieldConfigInterface)) {
      return;
    }

    $require_on_publish = (bool) $field_definition->getThirdPartySetting('require_on_publish', 'require_on_publish', FALSE);
    $warn_on_empty = (bool) $field_definition->getThirdPartySetting('require_on_publish', 'warn_on_empty', FALSE);

    $states_condition = $items->getEntity()->_requireOnPublish[$items->getName()]['condition'] ?? NULL;
    if (!empty($states_condition)) {
      $require_on_publish = _require_on_publish_evaluate_state_condition(
        $states_condition,
        $this->requestStack->getCurrentRequest()->request->all()
      );
    }

    if ($parentRopActive === TRUE) {
      $require_on_publish = TRUE;
    }

    if (!$require_on_publish && !$warn_on_empty) {
      return;
    }

    $label = $field_definition->getLabel();
    $is_empty = $items->isEmpty();
    $missing_parts = [];

    if (!$is_empty) {
      $item = $items->first();
      $country_code = (string) ($item->country_code ?? '');
      if ($country_code !== '') {
        $address_format = $this->addressFormatRepository->get($country_code);
        $field_overrides = new FieldOverrides(method_exists($item, 'getFieldOverrides') ? $item->getFieldOverrides() : []);
        $required_fields = AddressFormatHelper::getRequiredFields($address_format, $field_overrides);
        $labels = LabelHelper::getFieldLabels($address_format);

        foreach ($required_fields as $required_field) {
          $property = FieldHelper::getPropertyName($required_field);
          $value = $item->get($property)->getValue();
          if ($value === NULL || $value === '') {
            $missing_parts[] = [
              'property' => $property,
              'label' => (string) ($labels[$required_field] ?? $property),
            ];
          }
        }
      }
    }

    if (!$this->enforceRequireOnPublish) {
      if ($warn_on_empty) {
        if ($is_empty) {
          $this->messenger->addWarning($this->t('@label may be empty until publishing.', ['@label' => $label]));
        }
        elseif ($missing_parts) {
          $this->messenger->addWarning($this->t('The following parts of @label may be empty until publishing: <em>@components</em>.', [
            '@label' => $label,
            '@components' => implode(', ', array_column($missing_parts, 'label')),
          ]));
        }
      }
      return;
    }

    if ($require_on_publish) {
      $path = ($anchorField !== NULL && $anchorDelta !== NULL)
        ? "{$anchorField}.{$anchorDelta}"
        : $field_definition->getName();

      if ($is_empty) {
        $this->context
          ->buildViolation($constraint->message, ['@field_label' => $label])
          ->atPath($path)
          ->addViolation();
        return;
      }

      foreach ($missing_parts as $missing_part) {
        $missing_path = $path . '.0.' . $missing_part['property'];
        $this->context
          ->buildViolation($this->t('@label field is required when publishing.', [
            '@label' => $missing_part['label'],
          ]))
          ->atPath($missing_path)
          ->addViolation();
      }
    }
  }

  /**
   * Validate a Name field honoring "require on publish" and "warn on empty".
   *
Loading