SU-SWS/stanford_profile_helper

View on GitHub
modules/stanford_policy/src/EventSubscriber/StanfordPolicySubscriber.php

Summary

Maintainability
A
1 hr
Test Coverage
A
93%
<?php

namespace Drupal\stanford_policy\EventSubscriber;

use Drupal\book\BookManagerInterface;
use Drupal\config_pages\ConfigPagesLoaderServiceInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\core_event_dispatcher\EntityHookEvents;
use Drupal\core_event_dispatcher\Event\Entity\AbstractEntityEvent;
use Drupal\core_event_dispatcher\Event\Entity\EntityPresaveEvent;
use Drupal\core_event_dispatcher\Event\Form\FormAlterEvent;
use Drupal\core_event_dispatcher\FormHookEvents;
use Drupal\field_event_dispatcher\Event\Field\WidgetSingleElementFormAlterEvent;
use Drupal\field_event_dispatcher\FieldHookEvents;
use Drupal\node\NodeInterface;
use Drupal\stanford_fields\Event\BookOutlineUpdatedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Stanford Policy event subscriber.
 */
class StanfordPolicySubscriber implements EventSubscriberInterface {

  /**
   * Flag to prevent recursion.
   *
   * @var bool
   */
  protected $alreadyHere = FALSE;

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      BookOutlineUpdatedEvent::OUTLINE_UPDATED => 'onBookOutlineUpdate',
      FormHookEvents::FORM_ALTER => 'onFormAlter',
      EntityHookEvents::ENTITY_PRE_SAVE => 'onEntityPreSave',
      EntityHookEvents::ENTITY_UPDATE => 'onEntityCrud',
      EntityHookEvents::ENTITY_INSERT => 'onEntityCrud',
      EntityHookEvents::ENTITY_DELETE => 'onEntityCrud',
      FieldHookEvents::WIDGET_SINGLE_ELEMENT_FORM_ALTER => 'onWidgetFormAlter',
    ];
  }

  /**
   * Event subscriber constructor.
   *
   * @param \Drupal\book\BookManagerInterface $bookManager
   *   Book manager service.
   * @param \Drupal\config_pages\ConfigPagesLoaderServiceInterface $configPagesLoader
   *   Config page loader service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager service.
   */
  public function __construct(protected BookManagerInterface $bookManager, protected ConfigPagesLoaderServiceInterface $configPagesLoader, protected EntityTypeManagerInterface $entityTypeManager) {
  }

  /**
   * Alter the policy form widgets.
   *
   * @param \Drupal\field_event_dispatcher\Event\Field\WidgetSingleElementFormAlterEvent $event
   *   Widget form alter event.
   */
  public function onWidgetFormAlter(WidgetSingleElementFormAlterEvent $event) {
    /** @var \Drupal\Core\Field\FieldItemListInterface $field_items */
    $field_items = $event->getContext()['items'];
    $element = &$event->getElement();
    if ($field_items->getName() == 'su_policy_related') {
      $element['#chosen'] = TRUE;
    }
  }

  /**
   * Resave all books if the policy config page was saved/deleted.
   *
   * @param \Drupal\core_event_dispatcher\Event\Entity\AbstractEntityEvent $event
   *   Triggered event.
   */
  public function onEntityCrud(AbstractEntityEvent $event) {
    $entity = $event->getEntity();
    if ($entity->getEntityTypeId() == 'config_pages' && $entity->bundle() == 'policy_settings') {
      $book_node_ids = array_keys($this->bookManager->getAllBooks());
      foreach ($book_node_ids as $node_id) {
        $this->resaveBookNodes($node_id);
      }
    }
  }

  /**
   * Reset the policy node label from the other field.
   *
   * @param \Drupal\core_event_dispatcher\Event\Entity\EntityPresaveEvent $event
   *   Triggered event.
   */
  public function onEntityPreSave(EntityPresaveEvent $event): void {
    $entity = $event->getEntity();
    // Since the settings for the auto entity label have to be "Preserve
    // Existing" so that we don't get errors, we still need to update the node
    // label if the field changed. Use the "Changed" field to determine if this
    // has already been done because the node will be re-saved with the book
    // outline update.
    if (
      $entity->getEntityTypeId() == 'node' &&
      $entity->bundle() == 'stanford_policy' &&
      (empty($entity->book['pid']) || $entity->book['pid'] == -1)
    ) {
      $entity->set('title', trim($entity->get('su_policy_title')->getString()));
      $entity->setChangedTime(time());
    }
  }

  /**
   * Alter the book admin form to add submit handler.
   *
   * @param \Drupal\core_event_dispatcher\Event\Form\FormAlterEvent $event
   *   Triggered Event.
   */
  public function onFormAlter(FormAlterEvent $event): void {
    $form = &$event->getForm();

    if ($event->getFormId() == 'book_admin_edit') {
      $build_args = $event->getFormState()->getBuildInfo()['args'];
      $book_node = $build_args[0];

      if ($book_node->bundle() == 'stanford_policy') {
        $form['#submit'][] = [self::class, 'onBookAdminEditSubmit'];
      }
    }
    if (in_array($event->getFormId(), [
      'node_stanford_policy_form',
      'node_stanford_policy_edit_form',
    ])) {
      $form['su_policy_title']['#attributes']['class'][] = 'js-form-item-title-0-value';
    }
  }

  /**
   * Dispatch the event to update the book outline.
   *
   * @param array $form
   *   Complete form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Submitted form state.
   */
  public static function onBookAdminEditSubmit(array &$form, FormStateInterface $form_state): void {
    $build_args = $form_state->getBuildInfo()['args'];
    $book_node = $build_args[0];
    \Drupal::service('event_dispatcher')
      ->dispatch(new BookOutlineUpdatedEvent($book_node), BookOutlineUpdatedEvent::OUTLINE_UPDATED);
  }

  /**
   * After the book outline is updated, re-save node titles to match.
   *
   * @param \Drupal\stanford_fields\Event\BookOutlineUpdatedEvent $event
   *   Triggered event.
   */
  public function onBookOutlineUpdate(BookOutlineUpdatedEvent $event) {
    if ($this->alreadyHere) {
      return;
    }

    $this->alreadyHere = TRUE;
    if ($book_id = $event->getUpdatedBookId()) {
      $this->resaveBookNodes($book_id);
    }
  }

  /**
   * Traverse the book and modify and resave all nodes.
   *
   * @param int $book_id
   *   Book node id.
   */
  protected function resaveBookNodes(int $book_id): void {
    $book_contents = $this->bookManager->getTableOfContents($book_id, 9);
    foreach (array_keys($book_contents) as $nid) {
      $node = $this->entityTypeManager->getStorage('node')->load($nid);
      $previous_title = $node->label();
      $this->modifyPolicyEntity($node);
      if ($node->label() != $previous_title) {
        $node->save();
      }
    }
  }

  /**
   * Modify the fields and the label on policy nodes.
   *
   * @param \Drupal\node\NodeInterface $node
   *   Node entity.
   */
  public function modifyPolicyEntity(NodeInterface $node): void {
    // Book settings not set.
    if ($node->bundle() != 'stanford_policy' || empty($node->book['pid'])) {
      return;
    }

    if ($node->get('su_policy_auto_prefix')->getString()) {
      $node->set('su_policy_chapter', NULL);
      $node->set('su_policy_subchapter', NULL);
      $node->set('su_policy_policy_num', NULL);

      foreach ($this->getAutomaticPrefix($node) as $field => $value) {
        $node->set($field, $value);
      }
    }

    /** @var \Drupal\node\NodeInterface $entity */
    $prefix = [
      $node->get('su_policy_chapter')->getString(),
      $node->get('su_policy_subchapter')->getString(),
      $node->get('su_policy_policy_num')->getString(),
    ];
    $prefix = array_filter($prefix);
    if (count($prefix) == 1) {
      $prefix[] = '';
    }

    $title = implode('.', $prefix);
    $title .= ' ' . $node->get('su_policy_title')->getString();
    $node->set('title', trim($title));
  }

  /**
   * Get the prefix field values that will be used for the title.
   *
   * @param \Drupal\node\NodeInterface $node
   *   Node entity.
   *
   * @return array
   *   Keyed array of field names => values.
   */
  protected function getAutomaticPrefix(NodeInterface $node): array {
    $book_link = $this->bookManager->loadBookLink($node->id());
    if (!$book_link) {
      return [];
    }
    $prefix_strings = $this->getLinkPrefix($book_link, $node->id());

    $field_names = [
      'su_policy_chapter',
      'su_policy_subchapter',
      'su_policy_policy_num',
    ];

    $field_names = array_slice($field_names, 0, count($prefix_strings));
    return array_combine($field_names, array_slice($prefix_strings, 0, 3));
  }

  /**
   * Get the prefix array for the given book link.
   *
   * @param array $book_link
   *   Book link keyed array.
   * @param int $node_id
   *   Node entity id.
   *
   * @return array
   *   Associative array of prefix strings.
   */
  protected function getLinkPrefix(array $book_link, int $node_id): array {
    if (!$book_link['pid']) {
      return [];
    }
    $parent_book_link = $this->bookManager->loadBookLink($book_link['pid']);
    $parent_tree = $this->bookManager->bookSubtreeData($parent_book_link);

    $position = 1;
    foreach (reset($parent_tree)['below'] as $sibling) {
      if ($sibling['link']['nid'] == $node_id) {
        break;
      }
      $position++;
    }

    if ($book_link['pid'] != $book_link['bid']) {
      $parent_node = $this->entityTypeManager->getStorage('node')
        ->load($book_link['pid']);

      preg_match('/^.*? /', $parent_node->label(), $parent_prefix);
      $prefix = [trim(reset($parent_prefix), ' .')];
    }

    $prefix[] = $this->getPrefix($book_link['depth'], $position);
    return $prefix;
  }

  /**
   * Use state to allow customizing which characters are used for the prefix.
   *
   * @param int $depth
   *   Depth level 1-9.
   * @param int $position
   *   Position in the given depth level.
   *
   * @return string
   *   Character(s) prefix to use.
   */
  protected function getPrefix(int $depth, int $position): string {
    $field_name = $depth == 2 ? 'su_policy_prefix_first' : ($depth == 3 ? 'su_policy_prefix_sec' : 'su_policy_prefix_third');
    $prefix_set = $this->configPagesLoader->getValue('policy_settings', $field_name, 0, 'value');

    $letters = range('A', 'Z');

    return match($prefix_set) {
      'alpha_uppercase' => $letters[$position - 1],
      'alpha_lowercase' => strtolower($letters[$position - 1]),
      'roman_numeral_uppercase' => $this->getRomanNumeral($position),
      'roman_numeral_lowercase' => strtolower($this->getRomanNumeral($position)),
      default => $position,
    };
  }

  /**
   * Get the roman numeral representation of a number.
   *
   * @param int $num
   *   Number to convert to roman numeral.
   *
   * @return string
   *   Roman numeral.
   *
   * @link https://stackoverflow.com/questions/14994941/numbers-to-roman-numbers-with-php
   */
  protected function getRomanNumeral(int $num): string {
    $map = [
      'M' => 1000,
      'CM' => 900,
      'D' => 500,
      'CD' => 400,
      'C' => 100,
      'XC' => 90,
      'L' => 50,
      'XL' => 40,
      'X' => 10,
      'IX' => 9,
      'V' => 5,
      'IV' => 4,
      'I' => 1,
    ];
    $returnValue = '';
    while ($num > 0) {
      foreach ($map as $roman => $int) {
        if ($num >= $int) {
          $num -= $int;
          $returnValue .= $roman;
          break;
        }
      }
    }
    return $returnValue;
  }

}