diff --git a/entityreference.info b/entityreference.info index 0a4d62d..a56e215 100644 --- a/entityreference.info +++ b/entityreference.info @@ -19,4 +19,5 @@ files[] = views/entityreference_plugin_row_fields.inc ; Tests. files[] = tests/entityreference.handlers.test +files[] = tests/entityreference.taxonomy.test files[] = tests/entityreference.admin.test diff --git a/entityreference.module b/entityreference.module index 14e45a4..c8aafc6 100644 --- a/entityreference.module +++ b/entityreference.module @@ -331,6 +331,49 @@ function entityreference_field_attach_delete($entity_type, $entity) { } /** + * Implements hook_entity_insert(). + */ +function entityreference_entity_insert($entity, $entity_type) { + entityreference_entity_crud($entity, $entity_type, 'entityPostInsert'); +} + +/** + * Implements hook_entity_update(). + */ +function entityreference_entity_update($entity, $entity_type) { + entityreference_entity_crud($entity, $entity_type, 'entityPostUpdate'); +} + +/** + * Implements hook_entity_delete(). + */ +function entityreference_entity_delete($entity, $entity_type) { + entityreference_entity_crud($entity, $entity_type, 'entityPostDelete'); +} + +/** + * Invoke a behavior based on entity CRUD. + * + * @param $entity + * The entity object. + * @param $entity_type + * The entity type. + * @param $method_name + * The method to invoke. + */ +function entityreference_entity_crud($entity, $entity_type, $method_name) { + list(, , $bundle) = entity_extract_ids($entity_type, $entity); + foreach (field_info_instances($entity_type, $bundle) as $field_name => $instance) { + $field = field_info_field($field_name); + if ($field['type'] == 'entityreference') { + foreach (entityreference_get_behavior_handlers($field, $instance) as $handler) { + $handler->{$method_name}($entity_type, $entity, $field, $instance); + } + } + } +} + +/** * Implements hook_field_settings_form(). */ function entityreference_field_settings_form($field, $instance, $has_data) { diff --git a/plugins/behavior/EntityReferenceBehavior_TaxonomyIndex.class.php b/plugins/behavior/EntityReferenceBehavior_TaxonomyIndex.class.php new file mode 100644 index 0000000..43ac693 --- /dev/null +++ b/plugins/behavior/EntityReferenceBehavior_TaxonomyIndex.class.php @@ -0,0 +1,189 @@ +buildNodeIndex($entity); + } + + /** + * Overrides EntityReference_BehaviorHandler_Abstract::entityPostUpdate(). + * + * Runs after hook_node_update() used by taxonomy module. + */ + public function entityPostUpdate($entity_type, $entity, $field, $instance) { + if ($entity_type != 'node') { + return; + } + + $this->buildNodeIndex($entity); + } + + /** + * Builds and inserts taxonomy index entries for a given node. + * + * The index lists all terms that are related to a given node entity, and is + * therefore maintained at the entity level. + * + * @param $node + * The node object. + * + * @see taxonomy_build_node_index() + */ + protected function buildNodeIndex($node) { + // We maintain a denormalized table of term/node relationships, containing + // only data for current, published nodes. + $status = NULL; + if (variable_get('taxonomy_maintain_index_table', TRUE)) { + // If a node property is not set in the node object when node_save() is + // called, the old value from $node->original is used. + if (!empty($node->original)) { + $status = (int)(!empty($node->status) || (!isset($node->status) && !empty($node->original->status))); + $sticky = (int)(!empty($node->sticky) || (!isset($node->sticky) && !empty($node->original->sticky))); + } + else { + $status = (int)(!empty($node->status)); + $sticky = (int)(!empty($node->sticky)); + } + } + // We only maintain the taxonomy index for published nodes. + if ($status) { + // Collect a unique list of all the term IDs from all node fields. + $tid_all = array(); + foreach (field_info_instances('node', $node->type) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); + if (!empty($field['settings']['target_type']) && $field['settings']['target_type'] == 'taxonomy_term' && $field['storage']['type'] == 'field_sql_storage') { + // If a field value is not set in the node object when node_save() is + // called, the old value from $node->original is used. + if (isset($node->{$field_name})) { + $items = $node->{$field_name}; + } + elseif (isset($node->original->{$field_name})) { + $items = $node->original->{$field_name}; + } + else { + continue; + } + foreach (field_available_languages('node', $field) as $langcode) { + if (!empty($items[$langcode])) { + foreach ($items[$langcode] as $item) { + $tid_all[$item['target_id']] = $item['target_id']; + } + } + } + } + + // Re-calculate the terms added in taxonomy_build_node_index() so + // we can optimize database queries. + $original_tid_all = array(); + if ($field['module'] == 'taxonomy' && $field['storage']['type'] == 'field_sql_storage') { + // If a field value is not set in the node object when node_save() is + // called, the old value from $node->original is used. + if (isset($node->{$field_name})) { + $items = $node->{$field_name}; + } + elseif (isset($node->original->{$field_name})) { + $items = $node->original->{$field_name}; + } + else { + continue; + } + foreach (field_available_languages('node', $field) as $langcode) { + if (!empty($items[$langcode])) { + foreach ($items[$langcode] as $item) { + $original_tid_all[$item['tid']] = $item['tid']; + } + } + } + } + } + + // Insert index entries for all the node's terms, that were not + // already inserted in taxonomy_build_node_index(). + $tid_all = array_diff($tid_all, $original_tid_all); + + // Insert index entries for all the node's terms. + if (!empty($tid_all)) { + $query = db_insert('taxonomy_index')->fields(array('nid', 'tid', 'sticky', 'created')); + foreach ($tid_all as $tid) { + $query->values(array( + 'nid' => $node->nid, + 'tid' => $tid, + 'sticky' => $sticky, + 'created' => $node->created, + )); + } + $query->execute(); + } + } + } + + /** + * Overrides EntityReference_BehaviorHandler_Abstract::settingsForm(). + */ + public function settingsForm($field, $instance) { + $form = array(); + $target = $field['settings']['target_type']; + if ($target != 'taxonomy_term') { + $form['ti-on-terms'] = array( + '#markup' => t('This behavior can only be set when the target type is taxonomy_term, but the target of this field is %target.', array('%target' => $target)), + ); + } + + $entity_type = $instance['entity_type']; + if ($entity_type != 'node') { + $form['ti-on-nodes'] = array( + '#markup' => t('This behavior can only be set when the entity type is node, but the entity type of this instance is %type.', array('%type' => $entity_type)), + ); + } + + if (!variable_get('taxonomy_maintain_index_table', TRUE)) { + $form['ti-disabled'] = array( + '#markup' => t('This core variable "taxonomy_maintain_index_table" is disabled.'), + ); + } + return $form; + } +} diff --git a/plugins/behavior/abstract.inc b/plugins/behavior/abstract.inc index 43b2b36..de827bc 100644 --- a/plugins/behavior/abstract.inc +++ b/plugins/behavior/abstract.inc @@ -108,6 +108,27 @@ interface EntityReference_BehaviorHandler { public function postDelete($entity_type, $entity, $field, $instance); /** + * Act after inserting an entity. + * + * @see hook_entity_insert() + */ + public function entityPostInsert($entity_type, $entity, $field, $instance); + + /** + * Act after updating an entity. + * + * @see hook_entity_update() + */ + public function entityPostUpdate($entity_type, $entity, $field, $instance); + + /** + * Act after deleting an entity. + * + * @see hook_entity_delete() + */ + public function entityPostDelete($entity_type, $entity, $field, $instance); + + /** * Generate a settings form for this handler. */ public function settingsForm($field, $instance); @@ -167,6 +188,12 @@ abstract class EntityReference_BehaviorHandler_Abstract implements EntityReferen public function postDelete($entity_type, $entity, $field, $instance) {} + public function entityPostInsert($entity_type, $entity, $field, $instance) {} + + public function entityPostUpdate($entity_type, $entity, $field, $instance) {} + + public function entityPostDelete($entity_type, $entity, $field, $instance) {} + public function settingsForm($field, $instance) {} public function access($field, $instance) { diff --git a/plugins/behavior/taxonomy-index.inc b/plugins/behavior/taxonomy-index.inc new file mode 100644 index 0000000..1a29d6e --- /dev/null +++ b/plugins/behavior/taxonomy-index.inc @@ -0,0 +1,16 @@ + t('Taxonomy index'), + 'description' => t('Include the term references created by instances of this field carried by node entities in the core {taxonomy_index} table. This will allow various modules to handle them like core term_reference fields.'), + 'class' => 'EntityReferenceBehavior_TaxonomyIndex', + 'behavior type' => 'instance', + 'force enabled' => TRUE, + ); +} diff --git a/tests/entityreference.taxonomy.test b/tests/entityreference.taxonomy.test new file mode 100644 index 0000000..6e4afb7 --- /dev/null +++ b/tests/entityreference.taxonomy.test @@ -0,0 +1,115 @@ + 'Entity Reference Taxonomy', + 'description' => 'Tests nodes with reference to terms as indexed.', + 'group' => 'Entity Reference', + ); + } + + public function setUp() { + parent::setUp('entityreference', 'taxonomy'); + + // Create an entity reference field. + $field = array( + 'entity_types' => array('node'), + 'settings' => array( + 'handler' => 'base', + 'target_type' => 'taxonomy_term', + 'handler_settings' => array( + 'target_bundles' => array(), + ), + ), + 'field_name' => 'field_entityreference_term', + 'type' => 'entityreference', + ); + $field = field_create_field($field); + $instance = array( + 'field_name' => 'field_entityreference_term', + 'bundle' => 'article', + 'entity_type' => 'node', + ); + + // Enable the taxonomy-index behavior. + $instance['settings']['behaviors']['taxonomy-index']['status'] = TRUE; + field_create_instance($instance); + + // Create a term reference field. + $field = array( + 'translatable' => FALSE, + 'entity_types' => array('node'), + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => 'terms', + 'parent' => 0, + ), + ), + ), + 'field_name' => 'field_taxonomy_term', + 'type' => 'taxonomy_term_reference', + ); + $field = field_create_field($field); + $instance = array( + 'field_name' => 'field_taxonomy_term', + 'bundle' => 'article', + 'entity_type' => 'node', + ); + field_create_instance($instance); + + // Create a terms vocobulary. + $vocabulary = new stdClass(); + $vocabulary->name = 'Terms'; + $vocabulary->machine_name = 'terms'; + taxonomy_vocabulary_save($vocabulary); + + // Create term. + for ($i = 1; $i <= 2; $i++) { + $term = new stdClass(); + $term->name = "term $i"; + $term->vid = 1; + taxonomy_term_save($term); + } + } + + /** + * Test referencing a term using entity reference field. + */ + public function testNodeIndex() { + // Asert node insert with reference to term. + $settings = array(); + $settings['type'] = 'article'; + $settings['field_entityreference_term'][LANGUAGE_NONE][0]['target_id'] = 1; + $node = $this->drupalCreateNode($settings); + + $this->assertEqual(taxonomy_select_nodes(1), array($node->nid)); + + // Asert node update with reference to term. + node_save($node); + $this->assertEqual(taxonomy_select_nodes(1), array($node->nid)); + + // Assert node update with reference to term and taxonomy reference to + // another term. + $wrapper = entity_metadata_wrapper('node', $node); + $wrapper->field_taxonomy_term->set(2); + $wrapper->save(); + + $this->assertEqual(taxonomy_select_nodes(1), array($node->nid)); + $this->assertEqual(taxonomy_select_nodes(2), array($node->nid)); + + // Assert node update with reference to term and taxonomy reference to + // same term. + $wrapper->field_taxonomy_term->set(1); + $wrapper->save(); + $this->assertEqual(taxonomy_select_nodes(1), array($node->nid)); + + $wrapper->delete(); + $this->assertFalse(taxonomy_select_nodes(1)); + } + +}