tripal/tripal

View on GitHub
tripal/src/Entity/TripalEntity.php

Summary

Maintainability
B
6 hrs
Test Coverage
F
54%
<?php

namespace Drupal\tripal\Entity;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\user\UserInterface;
use Drupal\tripal\TripalField\Interfaces\TripalFieldItemInterface;
use Drupal\field\Entity\FieldConfig;
use Symfony\Component\Routing\Route;
use Drupal\tripal\TripalField\TripalFieldItemBase;

/**
 * Defines the Tripal Content entity.
 *
 * @ingroup tripal
 *
 * @ContentEntityType(
 *   id = "tripal_entity",
 *   label = @Translation("Tripal Content"),
 *   bundle_label = @Translation("Tripal Content type"),
 *   handlers = {
 *     "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "list_builder" = "Drupal\tripal\ListBuilders\TripalEntityListBuilder",
 *     "views_data" = "Drupal\tripal\Entity\TripalEntityViewsData",
 *
 *     "form" = {
 *       "default" = "Drupal\tripal\Form\TripalEntityForm",
 *       "add" = "Drupal\tripal\Form\TripalEntityForm",
 *       "edit" = "Drupal\tripal\Form\TripalEntityForm",
 *       "delete" = "Drupal\tripal\Form\TripalEntityDeleteForm",
 *     },
 *     "access" = "Drupal\tripal\Access\TripalEntityAccessControlHandler",
 *     "route_provider" = {
 *       "html" = "Drupal\tripal\Routing\TripalEntityHtmlRouteProvider",
 *     },
 *   },
 *   base_table = "tripal_entity",
 *   entity_keys = {
 *     "id" = "id",
 *     "bundle" = "type",
 *     "uid" = "user_id",
 *     "status" = "status",
 *   },
 *   links = {
 *     "canonical" = "/bio_data/{tripal_entity}",
 *     "add-page" = "/bio_data/add",
 *     "add-form" = "/bio_data/add/{tripal_entity_type}",
 *     "edit-form" = "/bio_data/{tripal_entity}/edit",
 *     "delete-form" = "/bio_data/{tripal_entity}/delete",
 *     "collection" = "/admin/content/bio_data",
 *   },
 *   bundle_entity_type = "tripal_entity_type",
 *   field_ui_base_route = "entity.tripal_entity_type.edit_form"
 * )
 */
class TripalEntity extends ContentEntityBase implements TripalEntityInterface {

  use EntityChangedTrait;


  /**
   * Constructs a new Tripal entity object, without permanently saving it.
   *
   * @code
      $values = [
        'title' => 'laceytest'.time(),
        'type' => 'organism',
        'uid' => 1,
      ];
      $entity = \Drupal\tripal\Entity\TripalEntity::create($values);
      $entity->save();
   * @endcode
   *
   * @param array $values
   *   - *title: the title of the entity.
   *   - *user_id: the user_id of the user who authored the content.
   *   - *type: the type of tripal entity this is (e.g. organism)
   *   - status: whether the entity is published or not (boolean)
   *   - created: the unix timestamp for when this content was created.
   * @return object
   *  The newly created entity.
   */
  public static function create(array $values = []) {
    return parent::create($values);
  }

  /**
   * {@inheritdoc}
   */
  public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
    parent::preCreate($storage_controller, $values);
    $values += array(
      'uid' => \Drupal::currentUser()->id(),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getID() {
    $entity_id = $this->id();
    return $entity_id;
  }

  /**
   * {@inheritdoc}
   */
  public function label() {
    return $this->getTitle();
  }

  /**
   * {@inheritdoc}
   */
  public function setTitle($title = NULL, $cache = []) {

    if (isset($cache['bundle'])) {
      $bundle = $cache['bundle'];
    }
    else {
      $bundle = \Drupal\tripal\Entity\TripalEntityType::load($this->getType());
    }

    $title = $bundle->getTitleFormat();
    $title = $this->replaceTokens($title, $bundle);

    $this->title = $title;
  }

  /**
   * {@inheritdoc}
   */
  public function getTitle() {
    return $this->title->getString();
  }

  /**
   * Sets the URL alias for the current entity.
   *
   * @param string $alias
   *   The alias to use. It can contain tokens the correspond to field values.
   *   Token should be be compatible with those returned by
   *   tripal_get_entity_tokens().
   */
  public function setAlias($path_alias = NULL) {


    $system_path = "/bio_data/" . $this->getID();

    // If no alias was supplied then we should try to generate one using the
    // default format set by admins.
    if (!$path_alias) {

      $bundle = \Drupal\tripal\Entity\TripalEntityType::load($this->getType());
      $path_alias = $bundle->getURLFormat();
      $path_alias = $this->replaceTokens($path_alias, $bundle);
    }

    // Ensure there is a leading slash.
    if ($path_alias[0] != '/') {
      $path_alias = '/' . $path_alias;
    }

    // Make sure the path alias is URL friendly.
    $path_alias = str_replace(['%2F', '+'], ['/', '-'], urlencode($path_alias));

    // Now finally, set the alias.
    $path = \Drupal::entityTypeManager()->getStorage('path_alias')->create([
      'path' => $system_path,
      'alias' => $path_alias,
    ]);
    $path->save();
  }

  /**
   * {@inheritdoc}
   */
  public function getType() {
    return $this->bundle();
  }

  /**
   * {@inheritdoc}
   */
  public function getCreatedTime() {
    return $this->get('created')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setCreatedTime($timestamp) {
    $this->set('created', $timestamp);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getOwner() {
    return $this->get('uid')->entity;
  }

  /**
   * {@inheritdoc}
   */
  public function getOwnerId() {
    return $this->get('uid')->target_id;
  }

  /**
   * {@inheritdoc}
   */
  public function setOwnerId($uid) {
    $this->set('uid', $uid);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function setOwner(UserInterface $account) {
    $this->set('uid', $account->id());
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function isPublished() {
    return (bool) $this->getEntityKey('status');
  }

  /**
   * {@inheritdoc}
   */
  public function setPublished($published) {
    $this->set('status', $published ? NODE_PUBLISHED : NODE_NOT_PUBLISHED);
    return $this;
  }

  /**
   * Replaces tokens in a given tokens in URL Aliases and Titles.
   *
   * @param string $string
   *   The string to replace.
   * @param array $cache
   */
  protected function replaceTokens($string, $bundle) {

    // Initialize the Tripal token parser service.
    /** @var \Drupal\tripal\Services\TripalTokenParser $token_parser **/
    $token_parser = \Drupal::service('tripal.token_parser');
    $token_parser->initParser($bundle, $this);
    $field_defs = $this->getFieldDefinitions();
    foreach ($field_defs as $field_name => $field_def) {
      /** @var \Drupal\Core\Field\FieldItemList $items **/
      $items = $this->get($field_name);
      if ($items->count() == 1) {
        /** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem  $item **/
        /** @var \Drupal\Core\TypedData\TypedDataInterface $prop **/
        $item = $items->get(0);
        if (! $item instanceof TripalFieldItemBase) {
          continue;
        }
        $props = $item->getProperties();
        foreach ($props as $prop) {
          $token_parser->addFieldValue($field_name, $prop->getName(), $prop->getValue());
        }
      }
    }
    $replaced = $token_parser->replaceTokens([$string]);
    return $replaced[0];
  }

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    $fields = parent::baseFieldDefinitions($entity_type);

    $fields['uid'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Authored by'))
      ->setDescription(t('The user ID of the author of the Tripal Content entity.'))
      ->setRevisionable(TRUE)
      ->setSetting('target_type', 'user')
      ->setSetting('handler', 'default')
      ->setTranslatable(TRUE)
      ->setDisplayOptions('view', array(
        'label' => 'above',
        'type' => 'author',
        'weight' => 0,
      ))
      ->setDisplayOptions('form', array(
        'type' => 'entity_reference_autocomplete',
        'weight' => 5,
        'settings' => array(
          'match_operator' => 'CONTAINS',
          'size' => '60',
          'autocomplete_type' => 'tags',
          'placeholder' => '',
        ),
      ))
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    $fields['title'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Title'))
      ->setDescription(t('The title of this entity.'))
      ->setSettings(array(
        'max_length' => 1024,
        'text_processing' => 0,
      ))
      ->setDefaultValue('')
      ->setDisplayOptions('view', array(
        'label' => 'above',
        'type' => 'string',
        'weight' => -4,
      ))
      ->setDisplayOptions('form', array(
        'type' => 'string_textfield',
        'weight' => -4,
      ))
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    $fields['status'] = BaseFieldDefinition::create('boolean')
      ->setLabel(t('Publishing status'))
      ->setDescription(t('A boolean indicating whether the Tripal Content is published.'))
      ->setDefaultValue(TRUE);

    $fields['created'] = BaseFieldDefinition::create('created')
      ->setLabel(t('Created'))
      ->setDescription(t('The time that the entity was created.'));

    $fields['changed'] = BaseFieldDefinition::create('changed')
      ->setLabel(t('Changed'))
      ->setDescription(t('The time that the entity was last edited.'));

    return $fields;
  }

  /**
   * Returns an associative array of property type value for the entity.
   *
   * The array is keyed in the following levels:
   * - 1st: Tripal Stroage Plugin ID
   * - 2nd: Field name
   * - 3rd: Delta value of the field item.
   * - 4th: the property key.
   * - 5th: One of the following keys:
   *   - 'value': the property value object.
   *   - 'operation': the operation to use when matching this value.
   *
   * This function also returns an array of TripalStorage objects.
   *
   * @param TripalEntity $entity
   *
   * @return array
   *   The returned array has two elements: an array of values as described
   *   above, and an array of TripalStorage objects,
   */
  public static function getValuesArray($entity) {
    $values = [];
    $tripal_storages = [];
    $fields = $entity->getFields();

    // Specifically, for each field...
    foreach ($fields as $field_name => $items) {
      foreach($items as $item) {

        // If it is not a TripalField then skip it.
        if (! $item instanceof TripalFieldItemInterface) {
          continue;
        }

        $delta = $item->getName();
        $tsid = $item->tripalStorageId();


        // If the Tripal Storage Backend is not set on a Tripal-based field,
        // we will log an error and not support the field. If developers want
        // to use Drupal storage for a Tripal-based field then they need to
        // indicate that by using our Drupal SQL Storage option OR by not
        // creating a Tripal-based field at all depending on their needs.
        if (empty($tsid)) {
          \Drupal::logger('tripal')->error('The Tripal-based field :field on
            this content type must indicate a TripalStorage backend and currently does not.',
            [':field' => $field_name]
          );
          continue;
        }

        // Create instance of the storage plugin so we can add the properties
        // to it as we go.
        if (!array_key_exists($tsid, $tripal_storages)) {
          $tripal_storage = \Drupal::service("tripal.storage")->getInstance(['plugin_id' => $tsid]);
          $tripal_storages[$tsid] = $tripal_storage;
        }

        // Add the field definition to the storage for this field.
        $tripal_storages[$tsid]->addFieldDefinition($field_name, $item->getFieldDefinition());

        // Get the empty property values for this field item and the
        // property type objects.
        $prop_values = $item->tripalValuesTemplate($item->getFieldDefinition());
        $prop_types = get_class($item)::tripalTypes($item->getFieldDefinition());

        // Sets the values from the entity on both the property and in entity.
        // Despite the function name, no values are saved to the database.
        $item->tripalSave($item, $field_name, $prop_types, $prop_values, $entity);

        // Clears the values from the entity (does not clear them from the
        // property).
        $item->tripalClear($item, $field_name, $prop_types, $prop_values, $entity);

        // Add the property types to the storage plugin.
        $tripal_storages[$tsid]->addTypes($field_name, $prop_types);

        // Prepare the property values for the storage plugin.
        // Note: We are assuming the key for the value is the
        // same as the key for the type here... This is a temporary assumption
        // as soon the values array will not contain types ;-)
        foreach ($prop_types as $prop_type) {
          $key = $prop_type->getKey();
          $values[$tsid][$field_name][$delta][$key] = [];
        }
        foreach ($prop_values as $prop_value) {
          $key = $prop_value->getKey();
          $values[$tsid][$field_name][$delta][$key]['value'] = $prop_value;
        }
      }
    }
    return [$values, $tripal_storages];
  }

  /**
   * {@inheritdoc}
   */
  public function preSave(EntityStorageInterface $storage) {
    parent::preSave($storage);

    // Create a values array appropriate for `loadValues()`
    list($values, $tripal_storages) = TripalEntity::getValuesArray($this);

    // Perform the Insert or Update of the submitted values to the
    // underlying data store.
    foreach ($values as $tsid => $tsid_values) {

      // Do an insert
      if ($this->isDefaultRevision() and $this->isNewRevision()) {
        try {
          $tripal_storages[$tsid]->insertValues($tsid_values);
        }
        catch (\Exception $e) {
          \Drupal::logger('tripal')->notice($e->getMessage());
          \Drupal::messenger()->addError('Cannot insert this entity. See the recent ' .
              'logs for more details or contact the site administrator if you ' .
              'cannot view the logs.');
        }
        $values[$tsid] = $tsid_values;
      }

      // Do an Update
      else {
        try {
          $tripal_storages[$tsid]->updateValues($tsid_values);
        }
        catch (\Exception $e) {
          \Drupal::logger('tripal')->notice($e->getMessage());
          \Drupal::messenger()->addError('Cannot update this entity. See the recent ' .
              'logs for more details or contact the site administrator if you cannot ' .
              'view the logs.');
        }
      }
    }

    // Set the property values that should be saved in Drupal, everything
    // else will stay in the underlying data store (e.g. Chado).
    $delta_remove = [];
    $fields = $this->getFields();
    foreach ($fields as $field_name => $items) {
      foreach($items as $item) {

        // If it is not a TripalField then skip it.
        if (!($item instanceof TripalFieldItemInterface)) {
          continue;
        }

        $delta = $item->getName();
        $tsid = $item->tripalStorageId();

        // If the Tripal Storage Backend is not set on a Tripal-based field,
        // we will log an error and not support the field. If developers want
        // to use Drupal storage for a Tripal-based field then they need to
        // indicate that by using our Drupal SQL Storage option OR by not
        // creating a Tripal-based field at all depending on their needs.
        if (empty($tsid)) {
          \Drupal::logger('tripal')->error('The Tripal-based field :field on
            this content type must indicate a TripalStorage backend and currently does not.',
            [':field' => $field_name]
          );
          continue;
        }

        // Load into the entity the properties that are to be stored in Drupal.
        $prop_values = [];
        $prop_types = [];
        foreach ($values[$tsid][$field_name][$delta] as $key => $prop_info) {
          $prop_type = $tripal_storages[$tsid]->getPropertyType($field_name, $key);
          $prop_value = $prop_info['value'];
          $settings = $prop_type->getStorageSettings();
          if (array_key_exists('drupal_store', $settings) and $settings['drupal_store'] == TRUE) {
            $prop_values[] = $prop_value;
            $prop_types[] = $prop_type;
          }
        }
        if (count($prop_values) > 0) {
          $item->tripalLoad($item, $field_name, $prop_types, $prop_values, $this);

          // Keep track of elements that have no value.
          foreach ($prop_values as $prop_value) {
            if (!$prop_value->getValue()) {
              // A given delta should only be present once here.
              if (!array_key_exists($field_name, $delta_remove) or !in_array($delta, $delta_remove[$field_name])) {
                $delta_remove[$field_name][] = $delta;
              }
              continue;
            }
          }
        }
      }
    }

    // Now remove any values that shouldn't be there.
    foreach ($delta_remove as $field_name => $deltas) {
      foreach (array_reverse($deltas) as $delta) {
        try {
          $this->get($field_name)->removeItem($delta);
        }
        catch (\Exception $e) {
          \Drupal::logger('tripal')->notice($e->getMessage());
          \Drupal::messenger()->addError('Cannot insert this entity. See the recent ' .
              'logs for more details or contact the site administrator if you ' .
              'cannot view the logs.');
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function postLoad(EntityStorageInterface $storage, array &$entities) {
    parent::postLoad($storage, $entities);


    // IF we are doing a listing of content types there is no way in Drupal 10
    // to specify which fields to load.  By deafult the SqlContentEntityStorage
    // storage system we're using will always attach all fields.  But we can
    // control what fields get attached to entities with this postLoad function.
    // In the TripalEntityListBuilder::load() function we set the
    // `tripal_load_listing` session variable to TRUE.  If it is TRUE then
    // we skip this. @todo: in the future if we want to only attach
    // specific fields we can get more fancy.
    if (\Drupal::request()->hasSession()) {
      $session = \Drupal::request()->getSession();
      $is_listing = $session->get('tripal_load_listing');
      if ($is_listing === TRUE) {
        return;
      }
    }

    $entity_type_id = $storage->getEntityTypeId();
    $field_manager = \Drupal::service('entity_field.manager');
    $field_type_manager = \Drupal::service('plugin.manager.field.field_type');

    // Iterate through the entities provided.
    foreach ($entities as $entity) {
      $bundle = $entity->bundle();

      // Create a values array appropriate for `loadValues()`
      list($values, $tripal_storages) = TripalEntity::getValuesArray($entity);

      // Call the loadValues() function for each storage type.
      $load_success = False;
      foreach ($values as $tsid => $tsid_values) {
        try {
          $load_success = $tripal_storages[$tsid]->loadValues($tsid_values);
          if ($load_success) {
            $values[$tsid] = $tsid_values;
          }
        }
        catch (\Exception $e) {
          \Drupal::logger('tripal')->notice($e->getMessage());
          \Drupal::messenger()->addError('Cannot load the entity. See the recent ' .
              'logs for more details or contact the site administrator if you cannot ' .
              'view the logs.');
        }
      }

      // Update the entity values with the values returned by loadValues().
      $field_defs = $field_manager->getFieldDefinitions($entity_type_id, $bundle);
      foreach ($field_defs as $field_name => $field_def) {

        // Create a fieldItemlist and iterate through it.
        $items = $field_type_manager->createFieldItemList($entity, $field_name, $entity->get($field_name)->getValue());
        foreach($items as $item) {

          // If it is not a TripalField then skip it.
          if (! $item instanceof TripalFieldItemInterface) {
            continue;
          }
          $delta = $item->getName();
          $tsid = $item->tripalStorageId();

          // If the Tripal Storage Backend is not set on a Tripal-based field,
          // we will log an error and not support the field. If developers want
          // to use Drupal storage for a Tripal-based field then they need to
          // indicate that by using our Drupal SQL Storage option OR by not
          // creating a Tripal-based field at all depending on their needs.
          if (empty($tsid)) {
            \Drupal::logger('tripal')->error('The Tripal-based field :field on
              this content type must indicate a TripalStorage backend and currently does not.',
              [':field' => $field_name]
            );
            continue;
          }

          // Create a new properties array for this field item.
          $prop_values = [];
          $prop_types = [];
          foreach ($values[$tsid][$field_name][$delta] as $key => $info) {
            $prop_values[] = $info['value'];
            $prop_types[] = $tripal_storages[$tsid]->getPropertyType($bundle, $field_name, $key);
          }

          // Now set the entity values for this field.
          $item->tripalLoad($item, $field_name, $prop_types, $prop_values, $entity);
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function validate() {
    // Let the parent class do its validations and return the violations list.
    $violations = parent::validate();

    // Create a values array appropriate for `loadValues()`
    list($values, $tripal_storages) = TripalEntity::getValuesArray($this);

    // Iterate through the different Tripal Storage objects and run the
    // validateValues() function for the values that belong to it.
    foreach ($values as $tsid => $tsid_values) {
      $problems = $tripal_storages[$tsid]->validateValues($tsid_values);
      foreach ($problems as $violation) {
        $violations->add($violation);
      }
    }

    return $violations;
  }
}