You are here

relation.module in Relation 7

Same filename and directory in other branches
  1. 8.2 relation.module
  2. 8 relation.module

Describes relations between entities.

File

relation.module
View source
<?php

require_once dirname(__FILE__) . '/relation.rules.inc';
require_once dirname(__FILE__) . '/relation.ctools.inc';

/**
 * @file
 * Describes relations between entities.
 */

/**
 * Implements hook_entity_info().
 */
function relation_entity_info() {
  $entities['relation'] = array(
    'label' => t('Relation'),
    'label callback' => 'relation_label',
    'base table' => 'relation',
    'revision table' => 'relation_revision',
    'fieldable' => TRUE,
    'controller class' => 'RelationController',
    'save callback' => 'relation_save',
    'creation callback' => 'relation_entity_create',
    'deletion callback' => 'relation_delete',
    'access callback' => 'relation_rules_access',
    'uri callback' => 'relation_uri',
    'view callback' => 'relation_multiple_view',
    'entity keys' => array(
      'id' => 'rid',
      'revision' => 'vid',
      'bundle' => 'relation_type',
      'label' => 'rid',
    ),
    'bundle keys' => array(
      'bundle' => 'relation_type',
    ),
    'bundles' => array(),
    'view modes' => array(),
  );
  foreach (relation_get_types() as $type => $info) {
    $entities['relation']['bundles'][$type] = (array) $info;
    $entities['relation']['bundles'][$type]['admin'] = array(
      'path' => 'admin/structure/relation/manage/%relation_type',
      'real path' => 'admin/structure/relation/manage/' . $type,
      'bundle argument' => 4,
      'access arguments' => array(
        'administer relation types',
      ),
    );
  }
  return $entities;
}

/**
 * Implements hook_entity_property_info().
 */
function relation_entity_property_info() {
  $info = array();
  $properties =& $info['relation']['properties'];
  $properties = array(
    'rid' => array(
      'label' => t('Relation ID'),
      'description' => t('The internal numeric ID of the relation.'),
      'type' => 'integer',
      'schema field' => 'rid',
    ),
    'relation_type' => array(
      'label' => t('Relation type'),
      'type' => 'token',
      'description' => t('The type of the relation.'),
      'setter callback' => 'entity_property_verbatim_set',
      'setter permission' => 'edit relations',
      'options list' => 'relation_rules_get_type_options',
      'required' => TRUE,
      'schema field' => 'relation_type',
    ),
    'endpoints' => array(
      'label' => t('Endpoints'),
      'type' => 'list<entity>',
      'description' => t('The endpoints of the relation.'),
      'setter callback' => 'relation_rules_set_endpoints',
      'getter callback' => 'relation_rules_get_endpoints',
      'setter permission' => 'edit relations',
      'required' => TRUE,
    ),
  );
  return $info;
}

/**
 * Implements hook_entity_property_info_alter().
 */
function relation_entity_property_info_alter(&$info) {

  // Add relation type-specific information, so that it is easily available
  // for modules that need to introspect the
  // relation structure (ie. Search API).
  foreach (relation_get_types() as $relation_type => $relation) {
    foreach ($relation->source_bundles as $key => $bundles) {
      list($entity_type, $bundle) = explode(':', $bundles, 2);
      $info['relation']['bundles'][$bundle]['properties']['endpoints_source_' . $entity_type] = array(
        'label' => t('@type (source endpoint)', array(
          '@type' => $entity_type,
        )),
        'type' => 'list<' . $entity_type . '>',
        'getter callback' => 'relation_rules_get_specific_endpoints',
        'endpoint_type' => 'source',
        'relation_directional' => $relation->directional,
      );
    }
    foreach ($relation->target_bundles as $key => $bundles) {
      list($entity_type, $bundle) = explode(':', $bundles, 2);
      $info['relation']['bundles'][$bundle]['properties']['endpoints_target_' . $entity_type] = array(
        'label' => t('@type (target endpoint)', array(
          '@type' => $entity_type,
        )),
        'type' => 'list<' . $entity_type . '>',
        'getter callback' => 'relation_rules_get_specific_endpoints',
        'endpoint_type' => 'target',
        'relation_directional' => $relation->directional,
      );
    }
    $source_bundles = $relation->source_bundles;
    $original_sb = array_values($source_bundles);
    $directional = FALSE;

    // If its a directional relation, merge the source and target bundles.
    if (count($relation->target_bundles) >= 1) {
      $original_tb = array_values($relation->target_bundles);
      $target_bundles = array_merge($source_bundles, $relation->target_bundles);
      $source_bundles = $target_bundles;
      $directional = TRUE;
    }
    else {
      $target_bundles = $source_bundles;
    }
    foreach ($target_bundles as $target_key => $target_bundle) {
      list($entity_target) = explode(':', $target_bundle, 2);
      foreach ($source_bundles as $source_key => $source_bundle) {
        $property_reverse = $directional && !in_array($source_bundle, $original_sb) && !in_array($target_bundle, $original_tb);
        $property = $property_reverse ? 'relation_' . $relation_type . '_' . $entity_target . '_reverse' : 'relation_' . $relation_type . '_' . $entity_target;
        list($entity_source) = explode(':', $source_bundle, 2);
        if (!$directional || $property_reverse || in_array($target_bundle, $original_tb) && in_array($source_bundle, $original_sb)) {
          $info[$entity_source]['properties'][$property] = array(
            'label' => t('Relation @relation_type (to @entity' . ($property_reverse ? ' reverse)' : ')'), array(
              '@relation_type' => $relation_type,
              '@entity' => $entity_target,
            )),
            'type' => 'list<' . $entity_target . '>',
            'relation_type' => $relation_type,
            'target_type' => $entity_target,
            'description' => t("A list of entities related."),
            'getter callback' => 'relation_rules_get_related_entities',
          );
        }
      }
    }
  }
}

/**
 * Implements hook_permission().
 */
function relation_permission() {
  return array(
    'administer relation types' => array(
      'title' => t('Administer Relation types'),
      'description' => t('Create, edit, delete, and perform administration tasks for relation types.'),
    ),
    'export relation types' => array(
      'title' => t('Export Relation types'),
    ),
    'use relation import' => array(
      'title' => t('Use Relation import'),
      'restrict access' => TRUE,
    ),
    'access relations' => array(
      'title' => t('View Relations'),
      'description' => t('Grant access to view the endpoints of Relations.'),
    ),
    'create relations' => array(
      'title' => t('Create Relations'),
    ),
    'edit relations' => array(
      'title' => t('Edit Relations'),
    ),
    'delete relations' => array(
      'title' => t('Delete Relations'),
    ),
    'administer relations' => array(
      'title' => t('Administer Relations'),
    ),
  );
}

/**
 * Creates a relation bundle.
 *
 * @param $info
 *   Array of relation type settings. All but relation_type are optional,
 *   although if the bundles arrays are empty, relations might be difficult to
 *   create! Keys are:
 *   - relation_type: Relation type machine name (string).
 *   - label: Relation type human-readable name (string). Defaults to
 *     duplicating relation_type.
 *   - directional: whether relation is directional (boolean). Defaults to
 *     FALSE.
 *   - transitive: whether relation is transitive (boolean). Defaults to FALSE.
 *   - r_unique: whether relations of this type are unique (boolean). Defaults
 *     to FALSE.
 *   - min_arity: minimum number of entities in relations of this type
 *     (int >= 2). Defaults to 2.
 *   - max_arity: maximum number of entities in relations of this type
 *     (int >= min_arity). Defaults to 2.
 *   - source_bundles: array containing allowed bundle keys. This is used for
 *     both directional and non-directional relations. Bundle key arrays are
 *     of the form 'entity:bundle', eg. 'node:article', or 'entity:*' for all
 *     bundles of the type.
 *   - target_bundles: array containing arrays allowed target bundle keys.
 *     This is the same format as source_bundles, but is only used for
 *     directional relations.
 *
 * @return
 *   Relation type object, or FALSE if creation fails.
 */
function relation_type_create($info = array()) {
  $info = (array) $info;
  if (empty($info['relation_type'])) {
    return FALSE;
  }
  $info += array(
    'min_arity' => 2,
    'max_arity' => 2,
    'directional' => FALSE,
    'transitive' => FALSE,
    'r_unique' => FALSE,
    'source_bundles' => array(),
    'target_bundles' => array(),
  );
  if (empty($info['label'])) {
    $info['label'] = $info['relation_type'];
  }
  if (empty($info['reverse_label'])) {

    // Directional relations should have a reverse label, but if they don't,
    // or if they are symmetric:
    $info['reverse_label'] = $info['label'];
  }
  return (object) $info;
}

/**
 * Saves a relation bundle.
 *
 * @param $relation_type
 *   stdClass object with relation type properties. See relation_type_create().
 * @param $write_record_keys
 *   Array containing the primary key of the relation ('relation_type'), if
 *   updating a relation, or an empty array if creating a new relation.
 * @param $rebuild_menu
 *   Boolean indicating whether the the database tables used by various menu
 *   functions should be rebuilt. Setting this to FALSE is useful if multiple
 *   relation types are being created programmatically.
 */
function relation_type_save($relation_type, $write_record_keys = array(), $rebuild_menu = TRUE) {

  // Make sure all keys are populated.
  $relation_type = relation_type_create($relation_type);
  $existing_relation_type = relation_get_types(array(
    $relation_type->relation_type,
  ));
  $type = $relation_type->relation_type;
  $source_bundles = $relation_type->source_bundles;
  $target_bundles = $relation_type->target_bundles;
  unset($relation_type->source_bundles, $relation_type->target_bundles);
  $transaction = db_transaction();
  drupal_write_record('relation_type', $relation_type, $write_record_keys);

  // Remove all existing bundles from the relation type before re-adding.
  db_delete('relation_bundles')
    ->condition('relation_type', $type)
    ->execute();
  $query = db_insert('relation_bundles')
    ->fields(array(
    'relation_type',
    'entity_type',
    'bundle',
    'r_index',
  ));
  foreach ($source_bundles as $entity_bundles) {
    list($entity_type, $bundle) = explode(':', $entity_bundles, 2);
    $query
      ->values(array(
      $type,
      $entity_type,
      $bundle,
      0,
    ));
  }
  if ($relation_type->directional) {
    foreach ($target_bundles as $entity_bundles) {
      list($entity_type, $bundle) = explode(':', $entity_bundles, 2);
      $query
        ->values(array(
        $type,
        $entity_type,
        $bundle,
        1,
      ));
    }
  }
  $query
    ->execute();
  relation_type_ensure_instance($type);
  if ($rebuild_menu) {
    menu_rebuild();
  }
  if (empty($existing_relation_type)) {
    field_attach_create_bundle('relation', $relation_type->relation_type);
  }
}

/**
 * Make sure the instance exists for this type.
 */
function relation_type_ensure_instance($type) {
  if (!drupal_static('relation_install') && !field_info_instance('relation', 'endpoints', $type)) {
    $instance = array(
      'field_name' => 'endpoints',
      'entity_type' => 'relation',
      'bundle' => $type,
    );
    field_create_instance($instance);
    field_info_instance('relation', 'endpoints', $type);
  }
}

/**
 * Loads a relation type (bundle).
 *
 * @param $relation_type
 *   The machine name of the relation type (bundle) to be loaded.
 *
 * @return
 *   A relation type record (as an Object) in the same format as expected by
 *   relation_type_save().
 */
function relation_type_load($relation_type) {
  $types = relation_get_types(array(
    $relation_type,
  ));
  return isset($types[$relation_type]) ? $types[$relation_type] : FALSE;
}

/**
 * Loads a relation type (bundle), or all relation bundles.
 *
 * @param $types
 *   An array of machine names of the relation types to be loaded. If $types
 *   is empty, load all relation types.
 *
 * @return
 *   A an array of relation type records in the same format as expected by
 *   relation_type_save(), keyed by relation_type.
 */
function relation_get_types($types = array()) {
  if (module_exists('ctools')) {
    ctools_include('export');
  }
  if (function_exists('ctools_export_crud_load_multiple')) {
    $relation_types = $types ? ctools_export_crud_load_multiple('relation_type', $types) : ctools_export_crud_load_all('relation_type');
    static $recurse = FALSE;
    if (!$recurse) {
      $recurse = TRUE;
      foreach ($relation_types as $type => $data) {
        if (!empty($data->in_code_only)) {
          relation_type_ensure_instance($type);
        }
      }
    }
    $recurse = FALSE;
  }
  else {
    $query = db_select('relation_type', 'rt')
      ->fields('rt', array(
      'relation_type',
      'label',
      'reverse_label',
      'directional',
      'transitive',
      'r_unique',
      'min_arity',
      'max_arity',
    ));
    if ($types) {
      $query
        ->condition('relation_type', $types);
    }
    $results = $query
      ->execute();
    $relation_types = array();
    foreach ($results as $relation_type) {
      $relation_types[$relation_type->relation_type] = $relation_type;
    }
    _relation_get_types_bundles($relation_types);
  }
  return $relation_types;
}

/**
 * Returns all relation types in a way which can be used on form options.
 */
function relation_get_types_options() {
  $types = relation_get_types();
  $options = array();
  foreach ($types as $type => $relation_type) {
    $options[$type] = $relation_type->label;
  }
  return $options;
}

/**
 * Helper function. Attaches bundles to relation type objects in an array.
 */
function _relation_get_types_bundles(&$relation_types) {
  foreach ($relation_types as &$relation_type) {
    if (empty($relation_type->in_code_only) && empty($relation_type->bundles_loaded)) {

      // If overridden or not exported at all, reset the bundles before
      // loading from the database to avoid duplication.
      $relation_type->source_bundles = array();
      $relation_type->target_bundles = array();
      foreach (db_query('SELECT relation_type, entity_type, bundle, r_index FROM {relation_bundles} WHERE relation_type = :relation_type', array(
        ':relation_type' => $relation_type->relation_type,
      )) as $record) {
        $endpoint = $record->r_index ? 'target_bundles' : 'source_bundles';
        $relation_type->{$endpoint}[] = "{$record->entity_type}:{$record->bundle}";
      }

      // Do not run this twice. ctools static caches the types but runs the
      // subrecord callback on the whole cache, every already loaded relation
      // type.
      $relation_type->bundles_loaded = TRUE;
    }
  }
}

/**
 * Lists all relation types.
 *
 * @return
 *   Array of relation type names in the format "Label (type)", keyed by
 *   relation_type.
 */
function relation_list_types() {
  $results = relation_get_types();
  $relation_types = array();
  foreach ($results as $type => $relation_type) {
    $relation_types[$type] = $relation_type->label . ' (' . $type . ')';
  }
  return $relation_types;
}

/**
 * Deletes a relation type (bundle).
 *
 * @param $relation_type
 *   The machine name of the relation type (bundle) to be deleted.
 */
function relation_type_delete($relation_type) {
  db_delete('relation_type')
    ->condition('relation_type', $relation_type)
    ->execute();
  db_delete('relation_bundles')
    ->condition('relation_type', $relation_type)
    ->execute();
  if (field_info_instance('relation', 'endpoints', $relation_type)) {
    $instance = array(
      'field_name' => 'endpoints',
      'entity_type' => 'relation',
      'bundle' => $relation_type,
    );
    field_delete_instance($instance, FALSE);
  }
}

/**
 * Loads a relation from a relation id.
 *
 * @param $rid
 *   Numerical id of the relation to be loaded.
 *
 * @return
 *   Loaded relation object. Relation objects are stdClass Object of the form:
 *   - rid: numeric relation id.
 *   - relation_type: relation bundle machine name.
 *   - arity: the number of entities in the relation
 *   - rdf_mapping: not yet implemented (empty array)
 *   - endpoints: Field holding the entities that make up the relation.
 *     Field columns are:
 *     - entity_type: The type of the entity (eg. node).
 *     - entity_id: Numeric entity ID.
 */
function relation_load($rid, $vid = NULL, $reset = FALSE) {
  $conditions = isset($vid) ? array(
    'vid' => $vid,
  ) : array();
  $relations = relation_load_multiple(array(
    $rid,
  ), $conditions, $reset);
  return reset($relations);
}

/**
 * Loads a set of relations from an array of relation ids.
 *
 * @param $rids
 *   Array of numerical relation ids of the relations to be loaded.
 *
 * @return
 *   Associative array of loaded relation objects, keyed by relation id.
 *
 * @see relation_load()
 */
function relation_load_multiple($rids, $conditions = array(), $reset = FALSE) {

  // Entity load handles field_attach_load for us.
  return entity_load('relation', $rids, $conditions, $reset);
}
function relation_view($relation, $view_mode = 'full') {
  $entity_type = 'relation';
  $entity = $relation;
  $entities = array(
    $relation->rid => $relation,
  );
  field_attach_prepare_view($entity_type, $entities, $view_mode);
  entity_prepare_view($entity_type, $entities);
  $build = field_attach_view($entity_type, $entity, $view_mode);
  $build += array(
    '#entity' => $relation,
    '#view_mode' => $view_mode,
    '#language' => LANGUAGE_NONE,
  );
  module_invoke_all('entity_view', $entity, $entity_type, $view_mode, LANGUAGE_NONE);
  drupal_alter('entity_view', $build, $entity_type);
  return $build;
}
function relation_multiple_view($relations, $view_mode) {
  $build = array();
  foreach ($relations as $relation) {
    $build['relation'][$relation->rid] = relation_view($relation, $view_mode);
  }
  return $build;
}

/**
 * Checks if a relation exists.
 *
 * The following example demonstrates how to check if a relation of type
 * 'likes' exists between two entities, user 17 and node 253.
 *
 * @code
 *   $entity_keys = array(
 *     array('entity_type' => 'user', 'entity_id' => 17),
 *     array('entity_type' => 'node', 'entity_id' => 253),
 *   );
 *   $relation_type = 'likes';
 *   $results = relation_relation_exists($entity_keys, $relation_type);
 * @endcode
 *
 * @param $entity_keys
 *   The entity keys of the relation to found. Entity_keys are arrays keyed by
 *   'entity_type' and 'entity_id'.
 * @param $relation_type
 *   (Optional) The relation type (bundle) of the relation to be checked.
 * @param $enforce_direction
 *   (Optional) Whether to enforce direction as specified in $entity_keys.
 *
 * @return
 *   Return FALSE if no relation exists, or an array of matching relations.
 */
function relation_relation_exists($entity_keys, $relation_type = NULL, $enforce_direction = FALSE) {
  $query = relation_query();
  foreach ($entity_keys as $r_index => $entity_key) {
    $query
      ->related($entity_key['entity_type'], $entity_key['entity_id'], $enforce_direction ? $r_index : NULL);
  }
  if ($relation_type) {
    $query
      ->propertyCondition('relation_type', $relation_type);
  }
  $query
    ->propertyCondition('arity', count($entity_keys));

  // Avoid Node Access restrictions when checking for a relation
  $query
    ->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT');

  // If direction of the relation is not forced make sure the each endpoint
  // is counted just once.
  if (!$enforce_direction) {
    $query
      ->addTag('enforce_distinct_endpoints');
  }
  $relation_ids = $query
    ->execute();
  return $relation_ids ? $relation_ids : FALSE;
}

/**
 * Implements hook_query_TAG_alter().
 *
 * Adds conditions to query to ensure different r_index for each endpoint.
 */
function relation_query_enforce_distinct_endpoints_alter(QueryAlterableInterface $query) {

  // Get arity of the query
  $relation_query = $query
    ->getMetaData('entity_field_query');
  foreach ($relation_query->propertyConditions as $condition) {
    if ($condition['column'] == 'arity') {
      $arity = $condition['value'];
      break;
    }
  }

  // Add r_index conditions between all endpoints
  for ($i = 0; $i < $arity; $i++) {
    for ($k = $i + 1; $k < $arity; $k++) {
      $column_left = 'field_data_endpoints_delta_' . $i . '.endpoints_r_index';
      $column_right = 'field_data_endpoints_delta_' . $k . '.endpoints_r_index';
      $query
        ->where("{$column_left} != {$column_right}");
    }
  }
}

/**
 * Constructs a relation from a machine name and a list of endpoints.
 *
 * @param $relation_type
 *   The relation type (bundle) of the relation to be created.
 * @param $endpoints
 *   A list of endpoint entities. Each endpoint is defined by an associate
 *   array, with an entity_type and entity_id key. For example:
 *   @code
 *   array(
 *     array('entity_type' => 'node', 'entity_id' => 1),
 *     array('entity_type' => 'user', 'entity_id' => 5),
 *   array);
 *   @endcode
 *
 * @return
 *   The new relation object.
 */
function relation_create($type, $endpoints, $account = NULL) {
  if (!isset($account)) {
    $account = $GLOBALS['user'];
  }
  $relation = new stdClass();
  $relation->is_new = TRUE;
  $relation->relation_type = $type;
  $relation->uid = $account->uid;
  $relation->endpoints[LANGUAGE_NONE] = $endpoints;
  return $relation;
}

/**
 * Saves a relation.
 *
 * @param $relation
 *   The relation entity data object. See relation_create() for the appropriate
 *   format (or just use it).
 *
 * @return
 *   The new relation id.
 */
function relation_save($relation, $account = NULL) {
  if (!isset($account)) {
    $account = $GLOBALS['user'];
  }
  try {
    field_attach_validate('relation', $relation);
  } catch (FieldValidationException $e) {
    foreach ($e->errors as $field_name => $field_errors) {
      foreach ($field_errors as $langcode => $multiple_errors) {
        foreach ($multiple_errors as $delta => $item_errors) {
          foreach ($item_errors as $item_error) {
            watchdog_exception('relation', $e, $item_error['message']);
          }
        }
      }
    }
    return FALSE;
  }

  // Determine if we will be inserting a new relation.
  if (!isset($relation->is_new)) {
    $relation->is_new = empty($relation->rid);
  }
  $transaction = db_transaction();
  $endpoints = field_get_items('relation', $relation, 'endpoints');
  $relation->arity = count($endpoints);

  // use time() instead of REQUEST_TIME, because otherwise tests
  // RelationQuery::order() are impossible.
  $relation->changed = time();
  if (!$relation->is_new) {
    $keys = array(
      'rid',
    );
    $op = 'update';
    $relation->original = relation_load($relation->rid, $relation->vid);
  }
  else {
    $relation->created = $relation->changed;
    $keys = array();
    $op = 'insert';
  }
  field_attach_presave('relation', $relation);
  module_invoke_all('entity_presave', $relation, 'relation');
  unset($relation->vid);
  $temp_uid = $relation->uid;
  $relation->uid = $account->uid;
  drupal_write_record('relation_revision', $relation);
  $relation->uid = $temp_uid;
  drupal_write_record('relation', $relation, $keys);
  if ($relation->is_new) {
    db_update('relation_revision')
      ->fields(array(
      'rid' => $relation->rid,
    ))
      ->condition('vid', $relation->vid)
      ->execute();
  }
  call_user_func("field_attach_{$op}", 'relation', $relation);
  module_invoke_all('entity_' . $op, $relation, 'relation');
  module_invoke('rules', 'invoke_event', 'relation_' . $op, $relation);
  relation_clear_related_entities_cache($endpoints);

  // Clear internal properties.
  unset($relation->is_new);
  unset($relation->original);
  return $relation->rid;
}

/**
 * Clear the cache for a set of endpoints.
 *
 * @param $endpoints
 *   An array of endpoints, with entity_type and entity_id as keys.
 */
function relation_clear_related_entities_cache($endpoints) {
  drupal_static_reset('relation_get_related_entity');
  foreach ($endpoints as $endpoint) {
    cache_clear_all('relation:' . $endpoint['entity_type'] . ':' . $endpoint['entity_id'], 'cache', TRUE);
  }
}

/**
 * Insert a new relation in the database.
 */
function relation_insert($type, $endpoints) {
  $relation = relation_create($type, $endpoints);
  return relation_save($relation);
}

/**
 * Updates a relation.
 */
function relation_update($relation) {
  relation_save($relation);
}

/**
 * Deletes a relation.
 *
 * @param $rid
 *   The numeric id of the relation to be deleted.
 */
function relation_delete($rid) {
  relation_delete_multiple(array(
    $rid,
  ));
}

/**
 * Deletes a relation.
 *
 * @param $rid
 *   An array of numeric ids of the relation to be deleted.
 */
function relation_delete_multiple($rids) {
  $relations = relation_load_multiple($rids);
  foreach ($relations as $rid => $relation) {
    db_delete('relation')
      ->condition('rid', $rid)
      ->execute();
    db_delete('relation_revision')
      ->condition('rid', $rid)
      ->execute();
    module_invoke_all('entity_delete', $relation, 'relation');
    module_invoke('rules', 'invoke_event', 'relation_delete', $relation);
    field_attach_delete('relation', $relation);
    $endpoints = field_get_items('relation', $relation, 'endpoints');
    relation_clear_related_entities_cache($endpoints);
  }
}

/**
 * Gets a relation's label.
 *
 * @see entity_label()
 */
function relation_label($relation) {
  return t('Relation !id', array(
    '!id' => $relation->rid,
  ));
}

/**
 * Gets a relation's URI.
 *
 * @see entity_uri()
 */
function relation_uri($relation) {
  return array(
    'path' => 'relation/' . $relation->rid,
  );
}

/**
 * Returns a query object to find related entities.
 *
 * @param $entity_type
 *   (optional) The entity type of one of the endpoints.
 * @param $entity_id
 *   (optional) The entity id of one of the endpoints. Can also be an array of
 *   entity IDs.
 * @param $r_index
 *   (optional) The index of the search entity in the relation to be found
 *   (0 = source, 1 = target).
 *
 * @return RelationQuery
 *   The query object itself.
 */
function relation_query($entity_type = NULL, $entity_id = NULL, $r_index = NULL) {
  return new RelationQuery($entity_type, $entity_id, $r_index);
}

/**
 * Returns endpoint entities (or entity IDs).
 *
 * @param object $relation
 *   Full relation object containing the endpoints
 * @param string $entity_type
 *   (optional) If set, filter by entity type; else return all endpoints.
 * @param string $endpoint_type
 *   (optional) If set, filter by endpoint type, either 'source' or 'target';
 *   else return all endpoints.
 * @param boolean $load_entities
 *   (optional) Whether to return entities or entity IDs.
 *
 * @return array
 *   The endpoint entities (or entity IDs).
 */
function relation_get_endpoints($relation, $entity_type = NULL, $endpoint_type = NULL, $load_entities = TRUE) {
  $items = array();
  $endpoints = relation_get_endpoints_by_endpoint_type($relation, $endpoint_type);
  foreach ($endpoints as $endpoint) {
    if (empty($entity_type) || $endpoint['entity_type'] == $entity_type) {
      $items[$endpoint['entity_type']][] = $endpoint['entity_id'];
    }
  }
  if ($load_entities) {
    foreach ($items as $entity_type => $ids) {
      $items[$entity_type] = entity_load($entity_type, $ids);
    }
  }
  return $items;
}

/**
 * Returns the endpoints of a specific endpoint type.
 *
 * @param object $relation
 *   A relation object containing the endpoints.
 * @param string $endpoint_type
 *   (optional) The endpoint type, either 'source' or 'target'.
 *
 * @return array
 *   The endpoints.
 */
function relation_get_endpoints_by_endpoint_type($relation, $endpoint_type = 'source') {
  if (empty($endpoint_type)) {
    return $relation->endpoints[LANGUAGE_NONE];
  }
  if (!in_array($endpoint_type, array(
    'source',
    'target',
  ))) {
    return array();
  }
  $relation_type = relation_type_load($relation->relation_type);
  $endpoint_type = $relation_type->directional ? $endpoint_type : 'source';

  // At present, relation only supports a 1-to-many directional relation. In
  // other words, a directional relation may only have one source endpoint; all
  // other endpoints are target endpoints.
  if ($endpoint_type == 'source') {
    if ($relation_type->directional) {
      $endpoints = array(
        $relation->endpoints[LANGUAGE_NONE][0],
      );
    }
    else {
      $endpoints = $relation->endpoints[LANGUAGE_NONE];
    }
  }
  else {
    $endpoints = array_slice($relation->endpoints[LANGUAGE_NONE], 1);
  }
  return $endpoints;
}

/**
 * Returns a related entity.
 *
 * Returns the entity object of the first other entity in the first relation
 * that matches the given conditions. Do not expect to get exactly what you
 * want, especially if you have multiple relations of the same type on the
 * search entity.
 *
 * @param $entity_type
 *   The entity type of one of the endpoints.
 * @param $entity_id
 *   The entity id of one of the endpoints.
 * @param $relation_type
 *   (optional) The relation type of the relation to find.
 * @param $r_index
 *   (optional) The index of the search entity in the relation to be found
 *   (0 = source, 1 = target).
 *
 * @return
 *   The entity object from the other endpoint.
 */
function relation_get_related_entity($entity_type, $entity_id, $relation_type = NULL, $r_index = NULL) {

  // Static cache the results of relation_query() and relation_load() to avoid
  // duplicate queries if this is called multiple times with the same arguments
  // during a request.
  $items =& drupal_static(__FUNCTION__);
  $request_key = "{$entity_type}:{$entity_id}";
  $cache_key = "{$request_key}:{$relation_type}:{$r_index}";
  if (isset($items[$cache_key])) {
    $entities = $items[$cache_key];
  }
  elseif ($cached = cache_get('relation:' . $cache_key)) {
    $entities = $cached->data;
    $items[$cache_key] = $entities;
  }
  else {
    $query = relation_query($entity_type, $entity_id, $r_index)
      ->range(0, 1);
    if ($relation_type) {
      $query
        ->entityCondition('bundle', $relation_type);
    }
    $results = $query
      ->execute();
    $result = reset($results);
    if ($result) {
      $relation = relation_load($result->rid);
      if ($relation->arity == 1) {
        $entities = FALSE;
      }
      else {
        $entities = field_get_items('relation', $relation, 'endpoints');
      }
    }
    else {
      $entities = FALSE;
    }
    cache_set('relation:' . $cache_key, $entities);
    $items[$cache_key] = $entities;
  }
  if ($entities) {
    $first_entity_key = $entities[0]['entity_type'] . ':' . $entities[0]['entity_id'];
    if (isset($r_index)) {
      $request_key = $request_key . ':' . $r_index;
      $first_entity_key = $first_entity_key . ':' . $entities[0]['r_index'];
    }
    if ($request_key == $first_entity_key) {
      $other_endpoints = entity_load($entities[1]['entity_type'], array(
        $entities[1]['entity_id'],
      ));
      return reset($other_endpoints);
    }
    $other_endpoints = entity_load($entities[0]['entity_type'], array(
      $entities[0]['entity_id'],
    ));
    return reset($other_endpoints);
  }
  return FALSE;
}

/**
 * Returns the relation types that can have the given entity as an endpoint.
 *
 * @param $entity_type
 *   The entity type of the endpoint.
 * @param $bundle
 *   The bundle of the endpoint.
 * @param $endpoint
 *   (optional) the type of endpoint. This is only used for directional
 *   relation types. Possible options are 'source', 'target', or 'both'.
 *
 * @return
 *   An array of relation types, keyed by relation_type.
 */
function relation_get_available_types($entity_type, $bundle, $endpoint = 'source') {
  $bundle_key = $entity_type . ':' . $bundle;
  $all_bundle_key = $entity_type . ':*';
  $relation_types = relation_get_types();
  foreach ($relation_types as $type => $relation_type) {
    $available = FALSE;
    if ($endpoint == 'source' || $endpoint == 'both') {
      if (in_array($bundle_key, $relation_type->source_bundles) || in_array($all_bundle_key, $relation_type->source_bundles)) {
        $available = TRUE;
      }
    }
    if ($endpoint == 'target' || $endpoint == 'both') {
      if (in_array($bundle_key, $relation_type->target_bundles) || in_array($all_bundle_key, $relation_type->target_bundles)) {
        $available = TRUE;
      }
    }
    if (!$available) {
      unset($relation_types[$type]);
    }
  }
  return $relation_types;
}

/**
 * Implements hook_entity_delete().
 */
function relation_entity_delete($entity, $entity_type) {
  list($entity_id) = entity_extract_ids($entity_type, $entity);
  $relations = relation_query($entity_type, $entity_id)
    ->execute();
  $relations_to_delete = array();
  foreach ($relations as $row) {

    // Remove any endpoints pointing to entity
    $relation = relation_load($row->rid);
    foreach ($relation->endpoints[LANGUAGE_NONE] as $key => $endpoint) {
      if ($endpoint['entity_id'] == $entity_id && $endpoint['entity_type'] == $entity_type) {
        unset($relation->endpoints[LANGUAGE_NONE][$key]);
      }
    }

    // Check if relation remains valid with regards to arity
    $type = relation_get_types(array(
      $relation->relation_type,
    ));
    $min_arity = $type[$relation->relation_type]->min_arity;
    $arity = count($relation->endpoints[LANGUAGE_NONE]);
    if ($arity < $min_arity) {

      // Not valid - delete
      array_push($relations_to_delete, $relation->rid);
    }
    else {

      // Valid - save
      relation_save($relation);
    }
  }
  if (!empty($relations_to_delete)) {
    relation_delete_multiple($relations_to_delete);
    drupal_set_message(t('Relations @relations have been deleted.', array(
      '@relations' => implode(', ', $relations_to_delete),
    )));
  }
}

/**
 * Implements hook_views_api().
 */
function relation_views_api() {
  return array(
    'api' => 3.0,
    'path' => drupal_get_path('module', 'relation') . '/views',
  );
}

/**
 * Gets the label of the relation type of the given relation
 *
 * @param $relation
 *   A relation object.
 * @param $reverse
 *   optional: whether to get the reverse label (boolean).
 *
 * @return
 *   The label of the relation type.
 */
function relation_get_type_label($relation, $reverse = FALSE) {
  $type = relation_type_load($relation->relation_type);
  if ($type->directional && $reverse) {
    return $type->reverse_label;
  }
  else {
    return $type->label;
  }
}

/**
 * Return the label for a given entity type and bundle.
 *
 * @param string $entity_type
 *   The entity type.
 *
 * @param string $entity_bundle
 *   The entity bundle.
 *
 * @return string
 *   The label of the given entity type and bundle.
 */
function _relation_get_bundle_label($entity_type, $entity_bundle) {
  $entity_info = entity_get_info($entity_type);
  return $entity_info['bundles'][$entity_bundle]['label'];
}

/**
 * Controller class for relation.
 *
 * This extends the DrupalDefaultEntityController class, adding required
 * special handling for relation revisions, very similar to what's being done
 * with nodes.
 */
class RelationController extends DrupalDefaultEntityController {
  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {

    // Ensure that uid is taken from the {relation} table
    $query = parent::buildQuery($ids, $conditions, $revision_id);
    $fields =& $query
      ->getFields();
    $fields['uid']['table'] = 'base';
    $query
      ->addField('revision', 'uid', 'revision_uid');
    $fields['changed']['table'] = 'base';
    $query
      ->addField('revision', 'changed', 'changed');
    return $query;
  }

}

/**
 * Callback to create a relation.
 */
function relation_entity_create($values = array()) {
  $relation = relation_create($values['relation_type'], array());
  foreach ($values as $key => $value) {
    $relation->{$key} = $value;
  }
  return $relation;
}

Functions

Namesort descending Description
relation_clear_related_entities_cache Clear the cache for a set of endpoints.
relation_create Constructs a relation from a machine name and a list of endpoints.
relation_delete Deletes a relation.
relation_delete_multiple Deletes a relation.
relation_entity_create Callback to create a relation.
relation_entity_delete Implements hook_entity_delete().
relation_entity_info Implements hook_entity_info().
relation_entity_property_info Implements hook_entity_property_info().
relation_entity_property_info_alter Implements hook_entity_property_info_alter().
relation_get_available_types Returns the relation types that can have the given entity as an endpoint.
relation_get_endpoints Returns endpoint entities (or entity IDs).
relation_get_endpoints_by_endpoint_type Returns the endpoints of a specific endpoint type.
relation_get_related_entity Returns a related entity.
relation_get_types Loads a relation type (bundle), or all relation bundles.
relation_get_types_options Returns all relation types in a way which can be used on form options.
relation_get_type_label Gets the label of the relation type of the given relation
relation_insert Insert a new relation in the database.
relation_label Gets a relation's label.
relation_list_types Lists all relation types.
relation_load Loads a relation from a relation id.
relation_load_multiple Loads a set of relations from an array of relation ids.
relation_multiple_view
relation_permission Implements hook_permission().
relation_query Returns a query object to find related entities.
relation_query_enforce_distinct_endpoints_alter Implements hook_query_TAG_alter().
relation_relation_exists Checks if a relation exists.
relation_save Saves a relation.
relation_type_create Creates a relation bundle.
relation_type_delete Deletes a relation type (bundle).
relation_type_ensure_instance Make sure the instance exists for this type.
relation_type_load Loads a relation type (bundle).
relation_type_save Saves a relation bundle.
relation_update Updates a relation.
relation_uri Gets a relation's URI.
relation_view
relation_views_api Implements hook_views_api().
_relation_get_bundle_label Return the label for a given entity type and bundle.
_relation_get_types_bundles Helper function. Attaches bundles to relation type objects in an array.

Classes

Namesort descending Description
RelationController Controller class for relation.