Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
novicell/custom_forms / src / CustomFormItemFactory.php
Size: Mime:
<?php

namespace Drupal\custom_forms;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\custom_forms\Entity\CustomFormType;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Factory services for handling various CRUD operations for CustomFormItem.
 *
 * @package Drupal\custom_forms
 */
class CustomFormItemFactory implements ContainerInjectionInterface {

  use StringTranslationTrait;

  /**
   * @var \Drupal\Core\Database\Connection
   *   The database connection.
   */
  private $connection;

  /**
   * @var \Drupal\Core\Language\LanguageManagerInterface
   *   The language manager.
   */
  private $languageManager;

  /**
   * @var \Drupal\Core\Messenger\MessengerInterface
   *   The messenger service.
   */
  private $messenger;

  /**
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   *   The logging channel.
   */
  private $logger;

  /**
   * @var \Drupal\custom_forms\CustomFormsFieldTypeManager
   *   The plugin manager for fields.
   */
  private $fieldPluginManager;

  /**
   * @var \Drupal\custom_forms\CustomFormsGroupTypeManager
   *   The plugin manager for groups.
   */
  private $groupPluginManager;

  /**
   * @var \Drupal\custom_forms\CustomFormsFieldMappingManager
   *   The plugin manager for field mappings.
   */
  private $mappingPluginManager;

  /**
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected $datetime;

  /**
   * CustomFormItemFactory constructor.
   *
   * @param \Drupal\Core\Database\Connection $connection
   *   The database connection.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The service to use Drupal's messenger API.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_channel_factory
   *   The logging channel.
   * @param \Drupal\custom_forms\CustomFormsFieldTypeManager $field_type_manager
   *   The plugin manager for fields.
   * @param \Drupal\custom_forms\CustomFormsGroupTypeManager $group_type_manager
   *   The plugin manager for groups.
   * @param \Drupal\custom_forms\CustomFormsFieldMappingManager $field_mapping_manager
   *   The plugin manager for field mappings.
   * @param \Drupal\Component\Datetime\TimeInterface $datetime
   *   The date time service.
   */
  public function __construct(
    Connection $connection,
    LanguageManagerInterface $language_manager,
    MessengerInterface $messenger,
    LoggerChannelFactoryInterface $logger_channel_factory,
    CustomFormsFieldTypeManager $field_type_manager,
    CustomFormsGroupTypeManager $group_type_manager,
    CustomFormsFieldMappingManager $field_mapping_manager,
    TimeInterface $datetime
  ) {
    $this->connection             = $connection;
    $this->languageManager        = $language_manager;
    $this->messenger              = $messenger;
    $this->logger                 = $logger_channel_factory->get('Custom Forms');
    $this->fieldPluginManager     = $field_type_manager;
    $this->groupPluginManager     = $group_type_manager;
    $this->mappingPluginManager   = $field_mapping_manager;
    $this->datetime               = $datetime;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): CustomFormItemFactory {
    return new static(
      $container->get('database'),
      $container->get('language_manager'),
      $container->get('messenger'),
      $container->get('logger.factory'),
      $container->get('plugin.manager.custom_forms_field_types'),
      $container->get('plugin.manager.custom_forms_group_types'),
      $container->get('plugin.manager.custom_forms_field_mappings'),
      $container->get('datetime.time')
    );
  }

  /**
   * Gets the language manager service.
   *
   * @return \Drupal\Core\Language\LanguageManagerInterface
   */
  public function getLanguageManager(): LanguageManagerInterface {
    return $this->languageManager;
  }

  /**
   * Gets the definition of the plugin used by the item.
   *
   * @param string $type
   *   The type of the custom form item, can either be "field", "group"
   *   or "mapping".
   * @param string $plugin_id
   *   The plugin id used by the item.
   *
   * @return array
   *   Returns an array containing the plugin definition
   */
  public function getItemPluginDefinition($type, $plugin_id): ?array {
    // We need to load the proper plugin definition based on the specified item type.
    switch ($type) {
      case 'group':
        return $this->groupPluginManager->getDefinition($plugin_id);
        break;
      case 'mapping':
        return $this->mappingPluginManager->getDefinition($plugin_id);
        break;
      case 'field':
      default:
        return $this->fieldPluginManager->getDefinition($plugin_id);
        break;
    }
  }

  /**
   * Gets the plugin manager used by the item.
   *
   * @param string $type
   *   The type of the custom form item, can either be "field", "group"
   *   or "mapping".
   *
   * @return \Drupal\Component\Plugin\PluginManagerInterface
   */
  public function getItemPluginManager($type): PluginManagerInterface {
    switch ($type) {
      case 'group':
        return $this->groupPluginManager;
        break;
      case 'mapping':
        return $this->mappingPluginManager;
        break;
      case 'field':
      default:
        return $this->fieldPluginManager;
        break;
    }
  }

  /**
   * Gets the plugin associated with the item.
   *
   * @param string $type
   *   The type of the custom form item, can either be "field", "group"
   *   or "mapping".
   * @param string $plugin_id
   *   The plugin id used by the item.
   * @param array $settings
   *   (Optional) The array of settings to pass to the plugin.
   *
   * @return \Drupal\custom_forms\Plugin\CustomForms\FieldType\CustomFormsFieldTypeInterface|\Drupal\custom_forms\Plugin\CustomForms\GroupType\CustomFormsGroupTypeInterface|\Drupal\custom_forms\Plugin\CustomForms\FieldMapping\CustomFormsFieldMappingInterface
   *   Returns either a field, group or mapping plugin.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginException
   *   If the instance cannot be created, such as if the ID is invalid.
   */
  public function getItemPlugin($type, $plugin_id, array $settings = []) {
    switch ($type) {
      case 'group':
        return $this->groupPluginManager->createInstance($plugin_id, $settings);
        break;
      case 'mapping':
        return $this->mappingPluginManager->createInstance($plugin_id, $settings);
        break;
      case 'field':
      default:
        return $this->fieldPluginManager->createInstance($plugin_id, $settings);
        break;
    }
  }

  /**
   * Saves the custom form item to the database.
   *
   * @param \Drupal\custom_forms\CustomFormItem $item
   *   The custom form item to be saved.
   * @param bool $create_new_revision
   *   Whether a new revision should be created of the field when saved.
   *
   * @return \Drupal\custom_forms\CustomFormItem
   *   Returns the saved custom form item.
   *
   * @throws \Exception
   *   If the insert failed, an exception is thrown.
   */
  public function saveItem(CustomFormItem $item, $create_new_revision = TRUE): CustomFormItem {
    if ($create_new_revision) {
      $query = $this->connection->insert('custom_forms__items');
      // If ID is null, generate one as it means this is a new field.
      if ($item->id() === NULL) {
        // We need to get the highest id currently in use and add one, so that
        // our new field gets it's own fid.
        $id_query = $this->connection->query('SELECT MAX(id) AS `id` FROM custom_forms__items');
        $id_result = $id_query->fetchAssoc();
        // Cast as int so that NULL automatically gets converted to 0.
        $id = (int) $id_result['id'];
        $item->setId($id + 1);
        $item->setRevision(1);
      }
      else {
        // We need to get the highest revision currently in use and add one, so that
        // our new field gets it's own revision.
        $revision_query = $this->connection->query('SELECT MAX(revision) AS `revision` FROM custom_forms__items WHERE id = :id AND form = :form', [':id' => $item->id(), ':form' => $item->getFormId()]);
        $revision_result = $revision_query->fetchAssoc();
        // Cast as int so that NULL automatically gets converted to 0.
        $revision = (int) $revision_result['revision'];
        $item->setRevision($revision + 1);
      }
    }
    else {
      $query = $this->connection->update('custom_forms__items');
      $query->condition('id', $item->id());
      $query->condition('revision', $item->getRevision());
    }

    // Add values to the query.
    $query->fields([
      'id' => $item->id(),
      'revision' => $item->getRevision(),
      'form' => $item->getFormId(),
      'form_revision' => $item->getFormRevisionId(),
      'created' => $item->getCreated()->getTimestamp(),
      'changed' => $item->getChanged()->getTimestamp(),
      'langcode' => $item->getLangcode(),
      'type' => $item->getType(),
      'plugin' => $item->getPluginId(),
      'mapping' => $item->getMappingId(),
      'machine_name' => $item->getMachineName(),
      'settings' => serialize($item->getSettings()),
      'states' => serialize($item->getStates()),
      'weight' => $item->getWeight(),
      'pid' => $item->getParentId(),
    ]);
    $query->execute();

    return $item;
  }

  /**
   * Load a custom form item based on it's machine name.
   *
   * Use this when you do not know it's specific id, but do know it's
   * machine name.
   *
   * @param string $machine_name
   *   The machine name of the custom form item.
   * @param int $form_id
   *   The form id that the custom form item belongs to.
   * @param int $form_revision
   *   The revision id to load the custom form item from.
   * @param string|null $langcode
   *   (Optional) The language code to load the custom form item as.
   *
   * @return bool|\Drupal\custom_forms\CustomFormItem
   *   Returns the matching custom form item on success, FALSE on failure.
   */
  public function loadItemByMachineName($machine_name, $form_id, $form_revision, $langcode = NULL) {
    $query = $this->connection->select('custom_forms__items', 'item');
    $query->fields('item', ['id', 'revision', 'form_revision']);
    $query->orderBy('item.revision', 'DESC');
    $query->condition('item.form', $form_id);
    $query->condition('item.form_revision', $form_revision);
    $query->condition('item.machine_name', $machine_name);
    if ($langcode !== NULL) {
      $query->condition('item.langcode', $langcode);
    }
    $executed = $query->execute();
    // Execute returns null if query is invalid, this is just extra safety check.
    if ($executed instanceof StatementInterface) {
      // We only fetch the latest revision.
      $result = $executed->fetch();
    }

    if (!empty($result)) {
      return new CustomFormItem(
        $result->form,
        $result->form_revision,
        $result->created,
        $result->changed,
        $result->langcode,
        $result->type,
        $result->plugin,
        $result->machine_name,
        $result->settings,
        $result->states,
        $result->mapping,
        $result->weight,
        $result->pid,
        $result->revision,
        $result->id
      );
    }

    return FALSE;
  }

  /**
   * Checks if a custom form item exists.
   *
   * @param integer $id
   *   The id of the item.
   * @param integer $revision
   *   The field id of the item.
   * @param string $langcode
   *   The language code for the translation language.
   *
   * @return bool
   *   Returns TRUE if the item exists, otherwise FALSE.
   */
  public function itemExists($id, $revision, $langcode): bool {
    $query = $this->connection->select('custom_forms__items', 'item');
    $query->fields('item');
    $query->condition('item.id', $id);
    $query->condition('item.revision', $revision);
    $query->condition('item.langcode', $langcode);

    $executed = $query->execute();
    // Execute returns null if query is invalid, this is just extra safety check.
    if ($executed instanceof StatementInterface) {
      $result = $executed->fetch();
    }

    // If the result is empty, there is no item with the specified machine name.
    if (empty($result)) {
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Check if a translation exists for a custom form item.
   *
   * @param int $id
   *   The id of the custom form item to check for the translation.
   * @param int $form
   *   The id of the form that the custom form item belongs to.
   * @param int $form_revision
   *   The revision id of the form to check for translations under.
   * @param string $langcode
   *   The language code for the translation language.
   *
   * @return bool
   *   Returns TRUE if a translation exists, otherwise FALSE.
   */
  public function itemTranslationExists($id, $form, $form_revision, $langcode): bool {
    $query = $this->connection->select('custom_forms__items', 'item');
    $query->fields('item', ['id']);
    $query->condition('item.id', $id);
    $query->condition('item.form', $form);
    $query->condition('item.form_revision', $form_revision);
    $query->condition('item.langcode', $langcode);

    $executed = $query->execute();
    // Execute returns null if query is invalid, this is just extra safety check.
    if ($executed instanceof StatementInterface) {
      $result = $executed->fetch();
    }

    // If the result is empty, there are no translations for said language.
    if (empty($result)) {
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Checks if a custom form item with the specified machine name exists.
   *
   * @param string $machine_name
   *   The machine readable name of the item.
   * @param array $element
   *   The machine_name element
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return bool
   *   Returns TRUE if the item exists, otherwise FALSE.
   */
  public static function itemMachineNameExists($machine_name, array $element, FormStateInterface $form_state): bool {
    // Return false if we do not have a form, as this means we are getting a
    // malformed request.
    if (empty($form_state->getValue('form'))) {
      return FALSE;
    }

    /** @var \Drupal\custom_forms\CustomFormInterface $form */
    $form = $form_state->getValue('form');

    $connection = \Drupal::database();
    $query = $connection->select('custom_forms__items', 'item');
    $query->fields('item');
    $query->condition('item.machine_name', $machine_name);
    $query->condition('item.form', $form->id());
    $query->condition('item.form_revision', $form->getLoadedRevisionId());
    $query->condition('item.langcode', $form->language()->getId());

    $executed = $query->execute();
    // Execute returns null if query is invalid, this is just extra safety check.
    if ($executed instanceof StatementInterface) {
      $result = $executed->fetch();
    }

    // If the result is empty, there is no item with the specified machine name.
    if (empty($result)) {
      return FALSE;
    }
    return TRUE;
  }

  /**
   * Clone a specific item.
   *
   * @param \Drupal\custom_forms\CustomFormItem $original_item
   *   The original item to be cloned.
   * @param \Drupal\custom_forms\CustomFormInterface $cloned_form
   *   The form that the item should be cloned to.
   * @param int $parent_id
   *   The parent id to set for the cloned item.
   */
  public function cloneItem(CustomFormItem $original_item, CustomFormInterface $cloned_form, $parent_id = 0) : void {
    $current_time = $this->datetime->getCurrentTime();

    try {
      $new_item = new CustomFormItem(
        $cloned_form->id(),
        $cloned_form->getLoadedRevisionId(),
        $current_time,
        $current_time,
        $original_item->getLangcode(),
        $original_item->getType(),
        $original_item->getPluginId(),
        $original_item->getMachineName(),
        serialize($original_item->getSettings()),
        serialize($original_item->getStates()),
        $original_item->getMappingId(),
        $original_item->getWeight(),
        $parent_id
      );

      $new_item->save(TRUE);

      $original_children = $original_item->getChildren();

      if (!empty($original_children)) {
        foreach ($original_children as $child_item) {
          $this->cloneItem($child_item, $cloned_form, $new_item->id());
        }
      }
    } catch (\Exception $e) {
      $error_message = t('Failed during cloning of field %field-name. Message = %message', [
        '%field-name' => $original_item->getSetting('label'),
        '%message' => $e->getMessage(),
      ]);
      $this->logger->error($error_message);
      \Drupal::messenger()->addError(t('Failed during cloning of field %field-name.',
        [
          '%field-name' => $original_item->getSetting('label'),
        ]
      ));
    }
  }

  /**
   * Clone all children belonging to a custom form item.
   *
   * @param \Drupal\custom_forms\CustomFormItem $original
   *   The original (old) custom form item to clone children from.
   * @param \Drupal\custom_forms\CustomFormItem $clone
   *   The new custom form item to clone children to.
   */
  public function cloneItemChildren(CustomFormItem $original, CustomFormItem $clone): void {
    /** @var \Drupal\custom_forms\CustomFormItem $childItem */
    foreach ($original->getChildren() as $childItem) {
      $newChildItem = clone $childItem;
      $newChildItem->setFormRevisionId($clone->getFormRevisionId());
      $newChildItem->setParentId($clone->id());

      try {
        $newChildItem->save();

        // Remember to clone children, used for nested groups with fields, etc.
        if (count($newChildItem->getChildren()) > 0) {
          $this->cloneItemChildren($childItem, $newChildItem);
        }
      } catch (\Exception $e) {
        $error_message = $this->t('Failed during cloning of child items. Message = %message',
          [
            '%message' => $e->getMessage(),
          ]
        );
        $this->logger->error($error_message);
        $this->messenger->addError(
          $this->t('Failed during cloning of child items. Please contact an administrator.')
        );
      }
    }
  }

  /**
   * Deletes all custom form items from the specified form.
   *
   * @param \Drupal\custom_forms\CustomFormInterface $form
   *   The form to delete the associated custom form items from.
   * @param string $langcode
   *   The langcode to limit deletion to.
   */
  public function deleteFormItems(CustomFormInterface $form, $langcode = NULL): void {
    $query = $this->connection->select('custom_forms__items', 'item');
    $query->condition('item.form', $form->id());
    if (!empty($langcode)) {
      $query->condition('item.langcode', $langcode);
    }
    $query->fields('item');

    $executed = $query->execute();
    // Execute returns null if query is invalid, this is just extra safety check.
    if ($executed instanceof StatementInterface) {
      // We only want the first result.
      $results = $executed->fetchAll();
    }

    if (!empty($results)) {
      foreach ($results as $result) {
        $item = new CustomFormItem(
          $result->form,
          $result->form_revision,
          $result->created,
          $result->changed,
          $result->langcode,
          $result->type,
          $result->plugin,
          $result->machine_name,
          $result->settings,
          $result->states,
          $result->mapping,
          $result->weight,
          $result->pid,
          $result->revision,
          $result->id
        );

        $this->deleteItem($item);
      }
    }
  }

  /**
   * Deletes the custom form item from the database and updates any child items
   * to move out nesting level out.
   *
   * @param \Drupal\custom_forms\CustomFormItem $item
   *   The custom form item to be deleted.
   */
  public function deleteItem(CustomFormItem $item): void {
    // Delete the item
    $query = $this->connection->delete('custom_forms__items');
    $query->condition('id', $item->id());
    $query->condition('revision', $item->getRevision());
    $query->condition('langcode', $item->getLangcode());
    $query->execute();

    // At this point new revisions have already been made so we just
    // need to update their parent id.
    $child_items = $this->loadMultipleItems([
      'form' => $item->getFormId(),
      'form_revision' => $item->getFormRevisionId(),
      'pid' => $item->id(),
      'langcode' => $item->getLangcode(),
    ]);

    // Return if there are no child items.
    if (!$child_items) {
      return;
    }

    foreach ($child_items as $child_item) {
      $child_item->setParentId($item->getParentId());
      try {
        // We save it without creating a new revision, as that has already been done at this point.
        $child_item->save(FALSE);
      } catch (\Exception $e) {
        $error_message = $this->t('Failed during updating field parent. Message = %message', [
          '%message' => $e->getMessage(),
        ]);
        $this->logger->error($error_message);
        $this->messenger->addError(
          $this->t('Failed during updating field parent. If it continues please contact an administrator.')
        );
      }
    }
  }

  /**
   * Loads multiple existing custom form item.
   *
   * @param array $conditions
   *   An array of conditions to load by.
   * @param string $sort_by
   *   The field name to sort by.
   * @param string $sort_direction
   *   The direction to sort by.
   *
   * @return \Drupal\custom_forms\CustomFormItem[]
   *   Returns an array of CustomFormItem keyed by item ID if an item was
   *   found, otherwise an empty array.
   */
  public function loadMultipleItems(array $conditions, $sort_by = 'id', $sort_direction = 'ASC'): array {
    $query = $this->connection->select('custom_forms__items', 'item');
    $query->fields('item', ['revision', 'id', 'form_revision', 'langcode']);
    $query->groupBy('item.id, item.revision, item.form_revision, langcode');

    foreach ($conditions as $field => $value) {
      $query->condition('item.' . $field, $value);
    }

    if (!empty($sort_by)) {
      $query->orderBy($sort_by, $sort_direction);
    }

    $executed = $query->execute();
    // Execute returns null if query is invalid, this is just extra safety check.
    if ($executed instanceof StatementInterface) {
      // We fetch by the id so we only get the latest revision of the field.
      $result = $executed->fetchAllAssoc('id');
    }

    if (!empty($result)) {
      $items = [];
      foreach ($result as $item) {
        $items[$item->id] = $this->loadItem($item->id, $item->revision, $item->form_revision, $item->langcode);
      }
      return $items;
    }

    return [];
  }

  /**
   * Loads an existing custom form item.
   *
   * @param integer $id
   *   The ID of the item to load.
   * @param integer $revision
   *   (Optional) The ID of the item revision to load.
   * @param integer $form_revision
   *   (Optional) The ID of the associated form revision.
   * @param string $langcode
   *   (Optional) The language code of the item to load.
   *
   * @return bool|\Drupal\custom_forms\CustomFormItem
   *   Returns a CustomFormItem if an item was found, otherwise FALSE.
   */
  public function loadItem($id, $revision = NULL, $form_revision = NULL, $langcode = NULL) {
    $query = $this->connection->select('custom_forms__items', 'item');
    $query->fields('item');
    $query->orderBy('item.revision', 'DESC');
    $query->condition('item.id', $id);
    if ($revision !== NULL && is_int($revision)) {
      $query->condition('item.revision', $revision);
    }
    if ($form_revision !== NULL) {
      $query->condition('item.form_revision', $form_revision);
    }
    if ($langcode !== NULL) {
      $query->condition('item.langcode', $langcode);
    }

    $executed = $query->execute();
    // Execute returns null if query is invalid, this is just extra safety check.
    if ($executed instanceof StatementInterface) {
      // We only want the first result.
      $result = $executed->fetch();
    }

    if (!empty($result)) {
      return new CustomFormItem(
        $result->form,
        $result->form_revision,
        $result->created,
        $result->changed,
        $result->langcode,
        $result->type,
        $result->plugin,
        $result->machine_name,
        $result->settings,
        $result->states,
        $result->mapping,
        $result->weight,
        $result->pid,
        $result->revision,
        $result->id
      );
    }

    return FALSE;
  }

  /**
   * Gets the items associated with a form.
   *
   * @param \Drupal\custom_forms\CustomFormInterface $form
   *   The form to get the items belonging to.
   * @param bool $sort_by_weight
   *   (Optional) Whether to sort by weight or not, default = FALSE.
   * @param bool $nested
   *   (Optional) Whether to nest the items according to their parent-child
   *   relationships.
   *
   * @return \Drupal\custom_forms\CustomFormItem[]
   *   Returns an array of CustomFormItem instances that belong to the specified
   *   CustomForm.
   */
  public function getFormItems(CustomFormInterface $form, $sort_by_weight = FALSE, $nested = FALSE): array {
    if ($sort_by_weight) {
      $items = $this->loadMultipleItems(
        [
          'form' => $form->id(),
          'form_revision' => $form->getLoadedRevisionId(),
          'langcode' => $form->language()->getId(),
        ],
        'weight',
        'ASC'
      );
    }
    else {
      $items = $this->loadMultipleItems(
        [
          'form' => $form->id(),
          'form_revision' => $form->getLoadedRevisionId(),
          'langcode' => $form->language()->getId(),
        ]
      );
    }

    // If no items, return an empty array.
    if (empty($items)) {
      return [];
    }

    /** @var CustomFormItem $item */
    foreach ($items as $id => $item) {
      if ($item->getParentId() > 0) {
        $items[$item->getParentId()]->addChild($item);
      }
    }

    // Remove the nested items from the root array.
    if ($nested) {
      foreach ($items as $id => $item) {
        if ($item->getParentId() > 0) {
          unset($items[$id]);
        }
      }
    }

    return $items;
  }

  /**
   * Gets the items as a sorted tree based on child->parent relationship and
   * optional weight.
   *
   * @param \Drupal\custom_forms\CustomFormInterface $form
   *   The form to get the items belonging to.
   * @param bool $sort_by_weight
   *   (Optional) Whether to sort by weight or not, default = FALSE.
   * @param bool $nested
   *   (Optional) Whether to nest the items according to their parent-child
   *   relationships.
   *
   * @return CustomFormItem[]
   *   An array of CustomFormItem instances formatted for use as a tree.
   */
  public function getFormItemsAsTree(CustomFormInterface $form, $sort_by_weight = FALSE, $nested = FALSE): array {
    if ($sort_by_weight) {
      $root_items = $this->loadMultipleItems(
        [
          'form' => $form->id(),
          'form_revision' => $form->getLoadedRevisionId(),
          'pid' => '0',
          'langcode' => $form->language()->getId(),
        ],
        'weight'
      );
    }
    else {
      $root_items = $this->loadMultipleItems(
        [
          'form' => $form->id(),
          'form_revision' => $form->getLoadedRevisionId(),
          'pid' => '0',
          'langcode' => $form->language()->getId(),
        ]
      );
    }

    // If no items, return an empty array.
    if (empty($root_items)) {
      return [];
    }

    // Initialize a variable to store our ordered tree structure.
    $tree = [];

    // Depth will be incremented in our getTree()
    // function for the first parent item, so we start it at -1.
    $depth = -1;

    // Loop through the root item, and add their trees to the array.
    foreach ($root_items as $root_item) {
      $this->getFormItemTree($form, $root_item, $tree, $depth, $sort_by_weight);
    }

    if ($nested) {
      /** @var CustomFormItem $item */
      foreach ($tree as $id => $item) {
        if ($item->getParentId() > 0) {
          $tree[$item->getParentId()]->addChild($item);
        }
      }
      foreach ($tree as $id => $item) {
        if ($item->getParentId() > 0) {
          unset($tree[$id]);
        }
      }
    }

    return $tree;
  }

  /**
   * Recursively adds custom form items to the tree, ordered by
   * parent/child/weight.
   *
   * @param \Drupal\custom_forms\CustomFormInterface $form
   *   The form to get the items belonging to.
   * @param CustomFormItem $item
   *   The item.
   * @param array $tree
   *   (Optional) The item tree.
   * @param int $depth
   *   (Optional) The depth of the item.
   * @param bool $sorted_by_weight
   *   (Optional) Whether to sort by weight or not, default = FALSE.
   */
  private function getFormItemTree(CustomFormInterface $form, CustomFormItem $item, array &$tree = [], $depth = 0, $sorted_by_weight = FALSE): void {
    // Increase our $depth value by one.
    $depth++;

    // Set the current tree 'depth' for this item, used to calculate
    // indentation.
    $item->setDepth($depth);

    // Add the item to the tree.
    $tree[$item->id()] = $item;

    // Retrieve each of the children belonging to this nested demo.
    if ($sorted_by_weight) {
      $children = $this->loadMultipleItems(
        [
          'form' => $form->id(),
          'form_revision' => $form->getLoadedRevisionId(),
          'pid' => $item->id(),
          'langcode' => $form->language()->getId(),
        ],
        'weight',
        'ASC'
      );
    }
    else {
      $children = $this->loadMultipleItems(
        [
          'form' => $form->id(),
          'form_revision' => $form->getLoadedRevisionId(),
          'pid' => $item->id(),
          'langcode' => $form->language()->getId(),
        ]
      );
    }

    if (!empty($children)) {
      foreach ($children as $child) {
        // Make sure this child does not already exist in the tree, to
        // avoid loops.
        if (!array_key_exists($child->id(), $tree)) {
          // Add this child's tree to the item tree array.
          $this->getFormItemTree($form, $child, $tree, $depth);
        }
      }
    }

    // Finished processing this tree branch.  Decrease our $depth value by one
    // to represent moving to the next branch.
    $depth--;
  }

  /**
   * Gets all the compatible field plugins for a specific field type.
   *
   * @param \Drupal\custom_forms\CustomFormInterface $custom_form
   *   The custom form to use for checking against field limits.
   *
   * @return array
   *   Returns an array of all compatible field plugin definitions keyed by
   *   their id.
   */
  public function getCompatibleFields(CustomFormInterface $custom_form) : array {
    $field_types = [];
    $custom_form_type = CustomFormType::load($custom_form->bundle());
    $available_types = $this->getItemPluginManager('field')->getDefinitions();
    $enabled_types = $custom_form_type->getEnabledFieldTypes();

    foreach ($available_types as $id => $type) {
      // Make sure the field type is enabled
      if (!isset($enabled_types[$id]) || $enabled_types[$id] !== $id) {
        continue;
      }
      // We only allow adding fields that has a UI.
      if (!isset($type['no_ui']) || !$type['no_ui']) {
        $enable_field = TRUE;

        // If the definition limits the amount of this field that can exist on the form, check if there are any existing fields with the id.
        if (isset($type['limit'])) {
          /** @var CustomFormItem[] $custom_form_fields */
          $custom_form_fields = $this->getFormItems($custom_form);
          $count = 0;

          foreach ($custom_form_fields as $field) {
            if ($field->getPluginId() === $id) {
              $count++;
            }
          }

          // Disable the field if it's limit has been reached or exceeded.
          if ($count >= $type['limit']) {
            $enable_field = FALSE;
          }
        }

        if ($enable_field) {
          $field_types[$id] = $type['label'];
        }
      }
    }
    asort($field_types);
    return $field_types;
  }

  /**
   * Gets all the compatible group plugins for a specific field type.
   *
   * @param \Drupal\custom_forms\CustomFormInterface $custom_form
   *   The custom form to use for checking against group limits.
   *
   * @return array
   *   Returns an array of all compatible group plugin definitions keyed by
   *   their id.
   */
  public function getCompatibleGroups(CustomFormInterface $custom_form) : array {
    $group_types = [];
    $custom_form_type = CustomFormType::load($custom_form->bundle());
    $available_types = $this->getItemPluginManager('group')->getDefinitions();
    $enabled_types = $custom_form_type->getEnabledGroupTypes();

    foreach ($available_types as $id => $type) {
      // Make sure the field type is enabled
      if (!isset($enabled_types[$id]) || $enabled_types[$id] !== $id) {
        continue;
      }
      // We only allow adding fields that has a UI.
      if (!isset($type['no_ui']) || !$type['no_ui']) {
        $enable_group = TRUE;

        // If the definition limits the amount of this field that can exist on the form, check if there are any existing fields with the id.
        if (isset($type['limit'])) {
          /** @var CustomFormItem[] $custom_form_fields */
          $custom_form_fields = $this->getFormItems($custom_form);
          $count = 0;

          foreach ($custom_form_fields as $field) {
            if ($field->getPluginId() === $id) {
              $count++;
            }
          }

          // Disable the field if it's limit has been reached or exceeded.
          if ($count >= $type['limit']) {
            $enable_group = FALSE;
          }
        }

        if ($enable_group) {
          $group_types[$id] = $type['label'];
        }
      }
    }
    asort($group_types);
    return $group_types;
  }

  /**
   * Gets all the compatible mapping plugins for a specific field type.
   *
   * @param \Drupal\custom_forms\CustomFormInterface $custom_form
   *   The custom form to use for checking against mapping limits.
   * @param string $field_type_id
   *   The field type id used for checking if a mapping plugin is compatible
   *   with.
   * @param \Drupal\custom_forms\CustomFormItem|null $item
   *   The custom form item checking for compatible mappings for.
   *   This is only used when editing an item, to ensure we don't block
   *   limited mappings if the item is already mapped.
   *
   * @return array
   *   Returns an array of all compatible mapping plugin definitions keyed by
   *   their id.
   */
  public function getCompatibleMappings(CustomFormInterface $custom_form, $field_type_id, CustomFormItem $item = NULL) : array {
    $custom_form_type = CustomFormType::load($custom_form->bundle());
    $available_mappings = $this->getItemPluginManager('mapping')->getDefinitions();
    $enabled_mappings = $custom_form_type->getEnabledMappingTypes();
    $mapping_options = [];

    // Loop through all available mapping plugins
    foreach ($available_mappings as $key => $mapping) {
      // Make sure the field type is enabled
      if (!isset($enabled_mappings[$key]) || $enabled_mappings[$key] !== $key) {
        continue;
      }

      $enable_mapping = TRUE;

      // If the definition limits the amount of this field that can exist on the form, check if there are any existing fields with the id.
      if (isset($mapping['limit'])) {
        /** @var \Drupal\custom_forms\CustomFormItem[] $custom_form_fields */
        $custom_form_fields = $this->getFormItems($custom_form);
        $count = 0;

        foreach ($custom_form_fields as $field) {
          if ($field->getMappingId() === $key) {
            $count++;
          }
        }

        // Disable the field if it's limit has been reached or exceeded.
        if ($count >= $mapping['limit']) {
          $enable_mapping = FALSE;
        }
      }

      if ($enable_mapping) {
        // If the field has no specified compatible fields, allow all fields.
        if (empty($mapping['compatible_fields'])) {
          // If the field is compatible, add the mapping to the select.
          $mapping_options[$key] = $mapping;
        } else {
          // Loop through each plugins compatibility list
          foreach ($mapping['compatible_fields'] as $field) {
            if ($field === $field_type_id) {
              // If the field is compatible, add the mapping to the select.
              $mapping_options[$key] = $mapping;
            }
          }
        }
      }
    }

    return $mapping_options;
  }

}