danielebarchiesi@4: entityInfo['bundle of'])) { danielebarchiesi@4: $info = entity_get_info($this->entityInfo['bundle of']); danielebarchiesi@4: $this->bundleKey = $info['bundle keys']['bundle']; danielebarchiesi@4: } danielebarchiesi@4: $this->defaultRevisionKey = !empty($this->entityInfo['entity keys']['default revision']) ? $this->entityInfo['entity keys']['default revision'] : 'default_revision'; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Overrides DrupalDefaultEntityController::buildQuery(). danielebarchiesi@4: */ danielebarchiesi@4: protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { danielebarchiesi@4: $query = parent::buildQuery($ids, $conditions, $revision_id); danielebarchiesi@4: if ($this->revisionKey) { danielebarchiesi@4: // Compare revision id of the base and revision table, if equal then this danielebarchiesi@4: // is the default revision. danielebarchiesi@4: $query->addExpression('base.' . $this->revisionKey . ' = revision.' . $this->revisionKey, $this->defaultRevisionKey); danielebarchiesi@4: } danielebarchiesi@4: return $query; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Builds and executes the query for loading. danielebarchiesi@4: * danielebarchiesi@4: * @return The results in a Traversable object. danielebarchiesi@4: */ danielebarchiesi@4: public function query($ids, $conditions, $revision_id = FALSE) { danielebarchiesi@4: // Build the query. danielebarchiesi@4: $query = $this->buildQuery($ids, $conditions, $revision_id); danielebarchiesi@4: $result = $query->execute(); danielebarchiesi@4: if (!empty($this->entityInfo['entity class'])) { danielebarchiesi@4: $result->setFetchMode(PDO::FETCH_CLASS, $this->entityInfo['entity class'], array(array(), $this->entityType)); danielebarchiesi@4: } danielebarchiesi@4: return $result; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Overridden. danielebarchiesi@4: * @see DrupalDefaultEntityController#load($ids, $conditions) danielebarchiesi@4: * danielebarchiesi@4: * In contrast to the parent implementation we factor out query execution, so danielebarchiesi@4: * fetching can be further customized easily. danielebarchiesi@4: */ danielebarchiesi@4: public function load($ids = array(), $conditions = array()) { danielebarchiesi@4: $entities = array(); danielebarchiesi@4: danielebarchiesi@4: // Revisions are not statically cached, and require a different query to danielebarchiesi@4: // other conditions, so separate the revision id into its own variable. danielebarchiesi@4: if ($this->revisionKey && isset($conditions[$this->revisionKey])) { danielebarchiesi@4: $revision_id = $conditions[$this->revisionKey]; danielebarchiesi@4: unset($conditions[$this->revisionKey]); danielebarchiesi@4: } danielebarchiesi@4: else { danielebarchiesi@4: $revision_id = FALSE; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: // Create a new variable which is either a prepared version of the $ids danielebarchiesi@4: // array for later comparison with the entity cache, or FALSE if no $ids danielebarchiesi@4: // were passed. The $ids array is reduced as items are loaded from cache, danielebarchiesi@4: // and we need to know if it's empty for this reason to avoid querying the danielebarchiesi@4: // database when all requested entities are loaded from cache. danielebarchiesi@4: $passed_ids = !empty($ids) ? array_flip($ids) : FALSE; danielebarchiesi@4: danielebarchiesi@4: // Try to load entities from the static cache. danielebarchiesi@4: if ($this->cache && !$revision_id) { danielebarchiesi@4: $entities = $this->cacheGet($ids, $conditions); danielebarchiesi@4: // If any entities were loaded, remove them from the ids still to load. danielebarchiesi@4: if ($passed_ids) { danielebarchiesi@4: $ids = array_keys(array_diff_key($passed_ids, $entities)); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: // Support the entitycache module if activated. danielebarchiesi@4: if (!empty($this->entityInfo['entity cache']) && !$revision_id && $ids && !$conditions) { danielebarchiesi@4: $cached_entities = EntityCacheControllerHelper::entityCacheGet($this, $ids, $conditions); danielebarchiesi@4: // If any entities were loaded, remove them from the ids still to load. danielebarchiesi@4: $ids = array_diff($ids, array_keys($cached_entities)); danielebarchiesi@4: $entities += $cached_entities; danielebarchiesi@4: danielebarchiesi@4: // Add loaded entities to the static cache if we are not loading a danielebarchiesi@4: // revision. danielebarchiesi@4: if ($this->cache && !empty($cached_entities) && !$revision_id) { danielebarchiesi@4: $this->cacheSet($cached_entities); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: // Load any remaining entities from the database. This is the case if $ids danielebarchiesi@4: // is set to FALSE (so we load all entities), if there are any ids left to danielebarchiesi@4: // load or if loading a revision. danielebarchiesi@4: if (!($this->cacheComplete && $ids === FALSE && !$conditions) && ($ids === FALSE || $ids || $revision_id)) { danielebarchiesi@4: $queried_entities = array(); danielebarchiesi@4: foreach ($this->query($ids, $conditions, $revision_id) as $record) { danielebarchiesi@4: // Skip entities already retrieved from cache. danielebarchiesi@4: if (isset($entities[$record->{$this->idKey}])) { danielebarchiesi@4: continue; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: // For DB-based entities take care of serialized columns. danielebarchiesi@4: if (!empty($this->entityInfo['base table'])) { danielebarchiesi@4: $schema = drupal_get_schema($this->entityInfo['base table']); danielebarchiesi@4: danielebarchiesi@4: foreach ($schema['fields'] as $field => $info) { danielebarchiesi@4: if (!empty($info['serialize']) && isset($record->$field)) { danielebarchiesi@4: $record->$field = unserialize($record->$field); danielebarchiesi@4: // Support automatic merging of 'data' fields into the entity. danielebarchiesi@4: if (!empty($info['merge']) && is_array($record->$field)) { danielebarchiesi@4: foreach ($record->$field as $key => $value) { danielebarchiesi@4: $record->$key = $value; danielebarchiesi@4: } danielebarchiesi@4: unset($record->$field); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: $queried_entities[$record->{$this->idKey}] = $record; danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: // Pass all entities loaded from the database through $this->attachLoad(), danielebarchiesi@4: // which attaches fields (if supported by the entity type) and calls the danielebarchiesi@4: // entity type specific load callback, for example hook_node_load(). danielebarchiesi@4: if (!empty($queried_entities)) { danielebarchiesi@4: $this->attachLoad($queried_entities, $revision_id); danielebarchiesi@4: $entities += $queried_entities; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: // Entitycache module support: Add entities to the entity cache if we are danielebarchiesi@4: // not loading a revision. danielebarchiesi@4: if (!empty($this->entityInfo['entity cache']) && !empty($queried_entities) && !$revision_id) { danielebarchiesi@4: EntityCacheControllerHelper::entityCacheSet($this, $queried_entities); danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: if ($this->cache) { danielebarchiesi@4: // Add entities to the cache if we are not loading a revision. danielebarchiesi@4: if (!empty($queried_entities) && !$revision_id) { danielebarchiesi@4: $this->cacheSet($queried_entities); danielebarchiesi@4: danielebarchiesi@4: // Remember if we have cached all entities now. danielebarchiesi@4: if (!$conditions && $ids === FALSE) { danielebarchiesi@4: $this->cacheComplete = TRUE; danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: // Ensure that the returned array is ordered the same as the original danielebarchiesi@4: // $ids array if this was passed in and remove any invalid ids. danielebarchiesi@4: if ($passed_ids && $passed_ids = array_intersect_key($passed_ids, $entities)) { danielebarchiesi@4: foreach ($passed_ids as $id => $value) { danielebarchiesi@4: $passed_ids[$id] = $entities[$id]; danielebarchiesi@4: } danielebarchiesi@4: $entities = $passed_ids; danielebarchiesi@4: } danielebarchiesi@4: return $entities; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Overrides DrupalDefaultEntityController::resetCache(). danielebarchiesi@4: */ danielebarchiesi@4: public function resetCache(array $ids = NULL) { danielebarchiesi@4: $this->cacheComplete = FALSE; danielebarchiesi@4: parent::resetCache($ids); danielebarchiesi@4: // Support the entitycache module. danielebarchiesi@4: if (!empty($this->entityInfo['entity cache'])) { danielebarchiesi@4: EntityCacheControllerHelper::resetEntityCache($this, $ids); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Implements EntityAPIControllerInterface. danielebarchiesi@4: */ danielebarchiesi@4: public function invoke($hook, $entity) { danielebarchiesi@4: // entity_revision_delete() invokes hook_entity_revision_delete() and danielebarchiesi@4: // hook_field_attach_delete_revision() just as node module does. So we need danielebarchiesi@4: // to adjust the name of our revision deletion field attach hook in order to danielebarchiesi@4: // stick to this pattern. danielebarchiesi@4: $field_attach_hook = ($hook == 'revision_delete' ? 'delete_revision' : $hook); danielebarchiesi@4: if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $field_attach_hook)) { danielebarchiesi@4: $function($this->entityType, $entity); danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: if (!empty($this->entityInfo['bundle of']) && entity_type_is_fieldable($this->entityInfo['bundle of'])) { danielebarchiesi@4: $type = $this->entityInfo['bundle of']; danielebarchiesi@4: // Call field API bundle attachers for the entity we are a bundle of. danielebarchiesi@4: if ($hook == 'insert') { danielebarchiesi@4: field_attach_create_bundle($type, $entity->{$this->bundleKey}); danielebarchiesi@4: } danielebarchiesi@4: elseif ($hook == 'delete') { danielebarchiesi@4: field_attach_delete_bundle($type, $entity->{$this->bundleKey}); danielebarchiesi@4: } danielebarchiesi@4: elseif ($hook == 'update' && $entity->original->{$this->bundleKey} != $entity->{$this->bundleKey}) { danielebarchiesi@4: field_attach_rename_bundle($type, $entity->original->{$this->bundleKey}, $entity->{$this->bundleKey}); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: // Invoke the hook. danielebarchiesi@4: module_invoke_all($this->entityType . '_' . $hook, $entity); danielebarchiesi@4: // Invoke the respective entity level hook. danielebarchiesi@4: if ($hook == 'presave' || $hook == 'insert' || $hook == 'update' || $hook == 'delete') { danielebarchiesi@4: module_invoke_all('entity_' . $hook, $entity, $this->entityType); danielebarchiesi@4: } danielebarchiesi@4: // Invoke rules. danielebarchiesi@4: if (module_exists('rules')) { danielebarchiesi@4: rules_invoke_event($this->entityType . '_' . $hook, $entity); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Implements EntityAPIControllerInterface. danielebarchiesi@4: * danielebarchiesi@4: * @param $transaction danielebarchiesi@4: * Optionally a DatabaseTransaction object to use. Allows overrides to pass danielebarchiesi@4: * in their transaction object. danielebarchiesi@4: */ danielebarchiesi@4: public function delete($ids, DatabaseTransaction $transaction = NULL) { danielebarchiesi@4: $entities = $ids ? $this->load($ids) : FALSE; danielebarchiesi@4: if (!$entities) { danielebarchiesi@4: // Do nothing, in case invalid or no ids have been passed. danielebarchiesi@4: return; danielebarchiesi@4: } danielebarchiesi@4: // This transaction causes troubles on MySQL, see danielebarchiesi@4: // http://drupal.org/node/1007830. So we deactivate this by default until danielebarchiesi@4: // is shipped in a point release. danielebarchiesi@4: // $transaction = isset($transaction) ? $transaction : db_transaction(); danielebarchiesi@4: danielebarchiesi@4: try { danielebarchiesi@4: $ids = array_keys($entities); danielebarchiesi@4: danielebarchiesi@4: db_delete($this->entityInfo['base table']) danielebarchiesi@4: ->condition($this->idKey, $ids, 'IN') danielebarchiesi@4: ->execute(); danielebarchiesi@4: danielebarchiesi@4: if (isset($this->revisionTable)) { danielebarchiesi@4: db_delete($this->revisionTable) danielebarchiesi@4: ->condition($this->idKey, $ids, 'IN') danielebarchiesi@4: ->execute(); danielebarchiesi@4: } danielebarchiesi@4: // Reset the cache as soon as the changes have been applied. danielebarchiesi@4: $this->resetCache($ids); danielebarchiesi@4: danielebarchiesi@4: foreach ($entities as $id => $entity) { danielebarchiesi@4: $this->invoke('delete', $entity); danielebarchiesi@4: } danielebarchiesi@4: // Ignore slave server temporarily. danielebarchiesi@4: db_ignore_slave(); danielebarchiesi@4: } danielebarchiesi@4: catch (Exception $e) { danielebarchiesi@4: if (isset($transaction)) { danielebarchiesi@4: $transaction->rollback(); danielebarchiesi@4: } danielebarchiesi@4: watchdog_exception($this->entityType, $e); danielebarchiesi@4: throw $e; danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Implements EntityAPIControllerRevisionableInterface::deleteRevision(). danielebarchiesi@4: */ danielebarchiesi@4: public function deleteRevision($revision_id) { danielebarchiesi@4: if ($entity_revision = entity_revision_load($this->entityType, $revision_id)) { danielebarchiesi@4: // Prevent deleting the default revision. danielebarchiesi@4: if (entity_revision_is_default($this->entityType, $entity_revision)) { danielebarchiesi@4: return FALSE; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: db_delete($this->revisionTable) danielebarchiesi@4: ->condition($this->revisionKey, $revision_id) danielebarchiesi@4: ->execute(); danielebarchiesi@4: danielebarchiesi@4: $this->invoke('revision_delete', $entity_revision); danielebarchiesi@4: return TRUE; danielebarchiesi@4: } danielebarchiesi@4: return FALSE; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Implements EntityAPIControllerInterface. danielebarchiesi@4: * danielebarchiesi@4: * @param $transaction danielebarchiesi@4: * Optionally a DatabaseTransaction object to use. Allows overrides to pass danielebarchiesi@4: * in their transaction object. danielebarchiesi@4: */ danielebarchiesi@4: public function save($entity, DatabaseTransaction $transaction = NULL) { danielebarchiesi@4: $transaction = isset($transaction) ? $transaction : db_transaction(); danielebarchiesi@4: try { danielebarchiesi@4: // Load the stored entity, if any. danielebarchiesi@4: if (!empty($entity->{$this->idKey}) && !isset($entity->original)) { danielebarchiesi@4: // In order to properly work in case of name changes, load the original danielebarchiesi@4: // entity using the id key if it is available. danielebarchiesi@4: $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->idKey}); danielebarchiesi@4: } danielebarchiesi@4: $entity->is_new = !empty($entity->is_new) || empty($entity->{$this->idKey}); danielebarchiesi@4: $this->invoke('presave', $entity); danielebarchiesi@4: danielebarchiesi@4: if ($entity->is_new) { danielebarchiesi@4: $return = drupal_write_record($this->entityInfo['base table'], $entity); danielebarchiesi@4: if ($this->revisionKey) { danielebarchiesi@4: $this->saveRevision($entity); danielebarchiesi@4: } danielebarchiesi@4: $this->invoke('insert', $entity); danielebarchiesi@4: } danielebarchiesi@4: else { danielebarchiesi@4: // Update the base table if the entity doesn't have revisions or danielebarchiesi@4: // we are updating the default revision. danielebarchiesi@4: if (!$this->revisionKey || !empty($entity->{$this->defaultRevisionKey})) { danielebarchiesi@4: $return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey); danielebarchiesi@4: } danielebarchiesi@4: if ($this->revisionKey) { danielebarchiesi@4: $return = $this->saveRevision($entity); danielebarchiesi@4: } danielebarchiesi@4: $this->resetCache(array($entity->{$this->idKey})); danielebarchiesi@4: $this->invoke('update', $entity); danielebarchiesi@4: danielebarchiesi@4: // Field API always saves as default revision, so if the revision saved danielebarchiesi@4: // is not default we have to restore the field values of the default danielebarchiesi@4: // revision now by invoking field_attach_update() once again. danielebarchiesi@4: if ($this->revisionKey && !$entity->{$this->defaultRevisionKey} && !empty($this->entityInfo['fieldable'])) { danielebarchiesi@4: field_attach_update($this->entityType, $entity->original); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: // Ignore slave server temporarily. danielebarchiesi@4: db_ignore_slave(); danielebarchiesi@4: unset($entity->is_new); danielebarchiesi@4: unset($entity->is_new_revision); danielebarchiesi@4: unset($entity->original); danielebarchiesi@4: danielebarchiesi@4: return $return; danielebarchiesi@4: } danielebarchiesi@4: catch (Exception $e) { danielebarchiesi@4: $transaction->rollback(); danielebarchiesi@4: watchdog_exception($this->entityType, $e); danielebarchiesi@4: throw $e; danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Saves an entity revision. danielebarchiesi@4: * danielebarchiesi@4: * @param Entity $entity danielebarchiesi@4: * Entity revision to save. danielebarchiesi@4: */ danielebarchiesi@4: protected function saveRevision($entity) { danielebarchiesi@4: // Convert the entity into an array as it might not have the same properties danielebarchiesi@4: // as the entity, it is just a raw structure. danielebarchiesi@4: $record = (array) $entity; danielebarchiesi@4: // File fields assumes we are using $entity->revision instead of danielebarchiesi@4: // $entity->is_new_revision, so we also support it and make sure it's set to danielebarchiesi@4: // the same value. danielebarchiesi@4: $entity->is_new_revision = !empty($entity->is_new_revision) || !empty($entity->revision) || $entity->is_new; danielebarchiesi@4: $entity->revision = &$entity->is_new_revision; danielebarchiesi@4: $entity->{$this->defaultRevisionKey} = !empty($entity->{$this->defaultRevisionKey}) || $entity->is_new; danielebarchiesi@4: danielebarchiesi@4: danielebarchiesi@4: danielebarchiesi@4: // When saving a new revision, set any existing revision ID to NULL so as to danielebarchiesi@4: // ensure that a new revision will actually be created. danielebarchiesi@4: if ($entity->is_new_revision && isset($record[$this->revisionKey])) { danielebarchiesi@4: $record[$this->revisionKey] = NULL; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: if ($entity->is_new_revision) { danielebarchiesi@4: drupal_write_record($this->revisionTable, $record); danielebarchiesi@4: $update_default_revision = $entity->{$this->defaultRevisionKey}; danielebarchiesi@4: } danielebarchiesi@4: else { danielebarchiesi@4: drupal_write_record($this->revisionTable, $record, $this->revisionKey); danielebarchiesi@4: // @todo: Fix original entity to be of the same revision and check whether danielebarchiesi@4: // the default revision key has been set. danielebarchiesi@4: $update_default_revision = $entity->{$this->defaultRevisionKey} && $entity->{$this->revisionKey} != $entity->original->{$this->revisionKey}; danielebarchiesi@4: } danielebarchiesi@4: // Make sure to update the new revision key for the entity. danielebarchiesi@4: $entity->{$this->revisionKey} = $record[$this->revisionKey]; danielebarchiesi@4: danielebarchiesi@4: // Mark this revision as the default one. danielebarchiesi@4: if ($update_default_revision) { danielebarchiesi@4: db_update($this->entityInfo['base table']) danielebarchiesi@4: ->fields(array($this->revisionKey => $record[$this->revisionKey])) danielebarchiesi@4: ->condition($this->idKey, $entity->{$this->idKey}) danielebarchiesi@4: ->execute(); danielebarchiesi@4: } danielebarchiesi@4: return $entity->is_new_revision ? SAVED_NEW : SAVED_UPDATED; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Implements EntityAPIControllerInterface. danielebarchiesi@4: */ danielebarchiesi@4: public function create(array $values = array()) { danielebarchiesi@4: // Add is_new property if it is not set. danielebarchiesi@4: $values += array('is_new' => TRUE); danielebarchiesi@4: if (isset($this->entityInfo['entity class']) && $class = $this->entityInfo['entity class']) { danielebarchiesi@4: return new $class($values, $this->entityType); danielebarchiesi@4: } danielebarchiesi@4: return (object) $values; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Implements EntityAPIControllerInterface. danielebarchiesi@4: * danielebarchiesi@4: * @return danielebarchiesi@4: * A serialized string in JSON format suitable for the import() method. danielebarchiesi@4: */ danielebarchiesi@4: public function export($entity, $prefix = '') { danielebarchiesi@4: $vars = get_object_vars($entity); danielebarchiesi@4: unset($vars['is_new']); danielebarchiesi@4: return entity_var_json_export($vars, $prefix); danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Implements EntityAPIControllerInterface. danielebarchiesi@4: * danielebarchiesi@4: * @param $export danielebarchiesi@4: * A serialized string in JSON format as produced by the export() method. danielebarchiesi@4: */ danielebarchiesi@4: public function import($export) { danielebarchiesi@4: $vars = drupal_json_decode($export); danielebarchiesi@4: if (is_array($vars)) { danielebarchiesi@4: return $this->create($vars); danielebarchiesi@4: } danielebarchiesi@4: return FALSE; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Implements EntityAPIControllerInterface. danielebarchiesi@4: * danielebarchiesi@4: * @param $content danielebarchiesi@4: * Optionally. Allows pre-populating the built content to ease overridding danielebarchiesi@4: * this method. danielebarchiesi@4: */ danielebarchiesi@4: public function buildContent($entity, $view_mode = 'full', $langcode = NULL, $content = array()) { danielebarchiesi@4: // Remove previously built content, if exists. danielebarchiesi@4: $entity->content = $content; danielebarchiesi@4: $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language; danielebarchiesi@4: danielebarchiesi@4: // By default add in properties for all defined extra fields. danielebarchiesi@4: if ($extra_field_controller = entity_get_extra_fields_controller($this->entityType)) { danielebarchiesi@4: $wrapper = entity_metadata_wrapper($this->entityType, $entity); danielebarchiesi@4: $extra = $extra_field_controller->fieldExtraFields(); danielebarchiesi@4: $type_extra = &$extra[$this->entityType][$this->entityType]['display']; danielebarchiesi@4: $bundle_extra = &$extra[$this->entityType][$wrapper->getBundle()]['display']; danielebarchiesi@4: danielebarchiesi@4: foreach ($wrapper as $name => $property) { danielebarchiesi@4: if (isset($type_extra[$name]) || isset($bundle_extra[$name])) { danielebarchiesi@4: $this->renderEntityProperty($wrapper, $name, $property, $view_mode, $langcode, $entity->content); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: // Add in fields. danielebarchiesi@4: if (!empty($this->entityInfo['fieldable'])) { danielebarchiesi@4: // Perform the preparation tasks if they have not been performed yet. danielebarchiesi@4: // An internal flag prevents the operation from running twice. danielebarchiesi@4: $key = isset($entity->{$this->idKey}) ? $entity->{$this->idKey} : NULL; danielebarchiesi@4: field_attach_prepare_view($this->entityType, array($key => $entity), $view_mode); danielebarchiesi@4: $entity->content += field_attach_view($this->entityType, $entity, $view_mode, $langcode); danielebarchiesi@4: } danielebarchiesi@4: // Invoke hook_ENTITY_view() to allow modules to add their additions. danielebarchiesi@4: if (module_exists('rules')) { danielebarchiesi@4: rules_invoke_all($this->entityType . '_view', $entity, $view_mode, $langcode); danielebarchiesi@4: } danielebarchiesi@4: else { danielebarchiesi@4: module_invoke_all($this->entityType . '_view', $entity, $view_mode, $langcode); danielebarchiesi@4: } danielebarchiesi@4: module_invoke_all('entity_view', $entity, $this->entityType, $view_mode, $langcode); danielebarchiesi@4: $build = $entity->content; danielebarchiesi@4: unset($entity->content); danielebarchiesi@4: return $build; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Renders a single entity property. danielebarchiesi@4: */ danielebarchiesi@4: protected function renderEntityProperty($wrapper, $name, $property, $view_mode, $langcode, &$content) { danielebarchiesi@4: $info = $property->info(); danielebarchiesi@4: danielebarchiesi@4: $content[$name] = array( danielebarchiesi@4: '#label_hidden' => FALSE, danielebarchiesi@4: '#label' => $info['label'], danielebarchiesi@4: '#entity_wrapped' => $wrapper, danielebarchiesi@4: '#theme' => 'entity_property', danielebarchiesi@4: '#property_name' => $name, danielebarchiesi@4: '#access' => $property->access('view'), danielebarchiesi@4: '#entity_type' => $this->entityType, danielebarchiesi@4: ); danielebarchiesi@4: $content['#attached']['css']['entity.theme'] = drupal_get_path('module', 'entity') . '/theme/entity.theme.css'; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Implements EntityAPIControllerInterface. danielebarchiesi@4: */ danielebarchiesi@4: public function view($entities, $view_mode = 'full', $langcode = NULL, $page = NULL) { danielebarchiesi@4: // For Field API and entity_prepare_view, the entities have to be keyed by danielebarchiesi@4: // (numeric) id. danielebarchiesi@4: $entities = entity_key_array_by_property($entities, $this->idKey); danielebarchiesi@4: if (!empty($this->entityInfo['fieldable'])) { danielebarchiesi@4: field_attach_prepare_view($this->entityType, $entities, $view_mode); danielebarchiesi@4: } danielebarchiesi@4: entity_prepare_view($this->entityType, $entities); danielebarchiesi@4: $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language; danielebarchiesi@4: danielebarchiesi@4: $view = array(); danielebarchiesi@4: foreach ($entities as $entity) { danielebarchiesi@4: $build = entity_build_content($this->entityType, $entity, $view_mode, $langcode); danielebarchiesi@4: $build += array( danielebarchiesi@4: // If the entity type provides an implementation, use this instead the danielebarchiesi@4: // generic one. danielebarchiesi@4: // @see template_preprocess_entity() danielebarchiesi@4: '#theme' => 'entity', danielebarchiesi@4: '#entity_type' => $this->entityType, danielebarchiesi@4: '#entity' => $entity, danielebarchiesi@4: '#view_mode' => $view_mode, danielebarchiesi@4: '#language' => $langcode, danielebarchiesi@4: '#page' => $page, danielebarchiesi@4: ); danielebarchiesi@4: // Allow modules to modify the structured entity. danielebarchiesi@4: drupal_alter(array($this->entityType . '_view', 'entity_view'), $build, $this->entityType); danielebarchiesi@4: $key = isset($entity->{$this->idKey}) ? $entity->{$this->idKey} : NULL; danielebarchiesi@4: $view[$this->entityType][$key] = $build; danielebarchiesi@4: } danielebarchiesi@4: return $view; danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * A controller implementing exportables stored in the database. danielebarchiesi@4: */ danielebarchiesi@4: class EntityAPIControllerExportable extends EntityAPIController { danielebarchiesi@4: danielebarchiesi@4: protected $entityCacheByName = array(); danielebarchiesi@4: protected $nameKey, $statusKey, $moduleKey; danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Overridden. danielebarchiesi@4: * danielebarchiesi@4: * Allows specifying a name key serving as uniform identifier for this entity danielebarchiesi@4: * type while still internally we are using numeric identifieres. danielebarchiesi@4: */ danielebarchiesi@4: public function __construct($entityType) { danielebarchiesi@4: parent::__construct($entityType); danielebarchiesi@4: // Use the name key as primary identifier. danielebarchiesi@4: $this->nameKey = isset($this->entityInfo['entity keys']['name']) ? $this->entityInfo['entity keys']['name'] : $this->idKey; danielebarchiesi@4: if (!empty($this->entityInfo['exportable'])) { danielebarchiesi@4: $this->statusKey = isset($this->entityInfo['entity keys']['status']) ? $this->entityInfo['entity keys']['status'] : 'status'; danielebarchiesi@4: $this->moduleKey = isset($this->entityInfo['entity keys']['module']) ? $this->entityInfo['entity keys']['module'] : 'module'; danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Support loading by name key. danielebarchiesi@4: */ danielebarchiesi@4: protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { danielebarchiesi@4: // Add the id condition ourself, as we might have a separate name key. danielebarchiesi@4: $query = parent::buildQuery(array(), $conditions, $revision_id); danielebarchiesi@4: if ($ids) { danielebarchiesi@4: // Support loading by numeric ids as well as by machine names. danielebarchiesi@4: $key = is_numeric(reset($ids)) ? $this->idKey : $this->nameKey; danielebarchiesi@4: $query->condition("base.$key", $ids, 'IN'); danielebarchiesi@4: } danielebarchiesi@4: return $query; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Overridden to support passing numeric ids as well as names as $ids. danielebarchiesi@4: */ danielebarchiesi@4: public function load($ids = array(), $conditions = array()) { danielebarchiesi@4: $entities = array(); danielebarchiesi@4: danielebarchiesi@4: // Only do something if loaded by names. danielebarchiesi@4: if (!$ids || $this->nameKey == $this->idKey || is_numeric(reset($ids))) { danielebarchiesi@4: return parent::load($ids, $conditions); danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: // Revisions are not statically cached, and require a different query to danielebarchiesi@4: // other conditions, so separate the revision id into its own variable. danielebarchiesi@4: if ($this->revisionKey && isset($conditions[$this->revisionKey])) { danielebarchiesi@4: $revision_id = $conditions[$this->revisionKey]; danielebarchiesi@4: unset($conditions[$this->revisionKey]); danielebarchiesi@4: } danielebarchiesi@4: else { danielebarchiesi@4: $revision_id = FALSE; danielebarchiesi@4: } danielebarchiesi@4: $passed_ids = !empty($ids) ? array_flip($ids) : FALSE; danielebarchiesi@4: danielebarchiesi@4: // Care about the static cache. danielebarchiesi@4: if ($this->cache && !$revision_id) { danielebarchiesi@4: $entities = $this->cacheGetByName($ids, $conditions); danielebarchiesi@4: } danielebarchiesi@4: // If any entities were loaded, remove them from the ids still to load. danielebarchiesi@4: if ($entities) { danielebarchiesi@4: $ids = array_keys(array_diff_key($passed_ids, $entities)); danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: $entities_by_id = parent::load($ids, $conditions); danielebarchiesi@4: $entities += entity_key_array_by_property($entities_by_id, $this->nameKey); danielebarchiesi@4: danielebarchiesi@4: // Ensure that the returned array is keyed by numeric id and ordered the danielebarchiesi@4: // same as the original $ids array and remove any invalid ids. danielebarchiesi@4: $return = array(); danielebarchiesi@4: foreach ($passed_ids as $name => $value) { danielebarchiesi@4: if (isset($entities[$name])) { danielebarchiesi@4: $return[$entities[$name]->{$this->idKey}] = $entities[$name]; danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: return $return; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Overridden. danielebarchiesi@4: * @see DrupalDefaultEntityController::cacheGet() danielebarchiesi@4: */ danielebarchiesi@4: protected function cacheGet($ids, $conditions = array()) { danielebarchiesi@4: if (!empty($this->entityCache) && $ids !== array()) { danielebarchiesi@4: $entities = $ids ? array_intersect_key($this->entityCache, array_flip($ids)) : $this->entityCache; danielebarchiesi@4: return $this->applyConditions($entities, $conditions); danielebarchiesi@4: } danielebarchiesi@4: return array(); danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Like cacheGet() but keyed by name. danielebarchiesi@4: */ danielebarchiesi@4: protected function cacheGetByName($names, $conditions = array()) { danielebarchiesi@4: if (!empty($this->entityCacheByName) && $names !== array() && $names) { danielebarchiesi@4: // First get the entities by ids, then apply the conditions. danielebarchiesi@4: // Generally, we make use of $this->entityCache, but if we are loading by danielebarchiesi@4: // name, we have to use $this->entityCacheByName. danielebarchiesi@4: $entities = array_intersect_key($this->entityCacheByName, array_flip($names)); danielebarchiesi@4: return $this->applyConditions($entities, $conditions); danielebarchiesi@4: } danielebarchiesi@4: return array(); danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: protected function applyConditions($entities, $conditions = array()) { danielebarchiesi@4: if ($conditions) { danielebarchiesi@4: foreach ($entities as $key => $entity) { danielebarchiesi@4: $entity_values = (array) $entity; danielebarchiesi@4: // We cannot use array_diff_assoc() here because condition values can danielebarchiesi@4: // also be arrays, e.g. '$conditions = array('status' => array(1, 2))' danielebarchiesi@4: foreach ($conditions as $condition_key => $condition_value) { danielebarchiesi@4: if (is_array($condition_value)) { danielebarchiesi@4: if (!isset($entity_values[$condition_key]) || !in_array($entity_values[$condition_key], $condition_value)) { danielebarchiesi@4: unset($entities[$key]); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: elseif (!isset($entity_values[$condition_key]) || $entity_values[$condition_key] != $condition_value) { danielebarchiesi@4: unset($entities[$key]); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: return $entities; danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Overridden. danielebarchiesi@4: * @see DrupalDefaultEntityController::cacheSet() danielebarchiesi@4: */ danielebarchiesi@4: protected function cacheSet($entities) { danielebarchiesi@4: $this->entityCache += $entities; danielebarchiesi@4: // If we have a name key, also support static caching when loading by name. danielebarchiesi@4: if ($this->nameKey != $this->idKey) { danielebarchiesi@4: $this->entityCacheByName += entity_key_array_by_property($entities, $this->nameKey); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Overridden. danielebarchiesi@4: * @see DrupalDefaultEntityController::attachLoad() danielebarchiesi@4: * danielebarchiesi@4: * Changed to call type-specific hook with the entities keyed by name if they danielebarchiesi@4: * have one. danielebarchiesi@4: */ danielebarchiesi@4: protected function attachLoad(&$queried_entities, $revision_id = FALSE) { danielebarchiesi@4: // Attach fields. danielebarchiesi@4: if ($this->entityInfo['fieldable']) { danielebarchiesi@4: if ($revision_id) { danielebarchiesi@4: field_attach_load_revision($this->entityType, $queried_entities); danielebarchiesi@4: } danielebarchiesi@4: else { danielebarchiesi@4: field_attach_load($this->entityType, $queried_entities); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: // Call hook_entity_load(). danielebarchiesi@4: foreach (module_implements('entity_load') as $module) { danielebarchiesi@4: $function = $module . '_entity_load'; danielebarchiesi@4: $function($queried_entities, $this->entityType); danielebarchiesi@4: } danielebarchiesi@4: // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are danielebarchiesi@4: // always the queried entities, followed by additional arguments set in danielebarchiesi@4: // $this->hookLoadArguments. danielebarchiesi@4: // For entities with a name key, pass the entities keyed by name to the danielebarchiesi@4: // specific load hook. danielebarchiesi@4: if ($this->nameKey != $this->idKey) { danielebarchiesi@4: $entities_by_name = entity_key_array_by_property($queried_entities, $this->nameKey); danielebarchiesi@4: } danielebarchiesi@4: else { danielebarchiesi@4: $entities_by_name = $queried_entities; danielebarchiesi@4: } danielebarchiesi@4: $args = array_merge(array($entities_by_name), $this->hookLoadArguments); danielebarchiesi@4: foreach (module_implements($this->entityInfo['load hook']) as $module) { danielebarchiesi@4: call_user_func_array($module . '_' . $this->entityInfo['load hook'], $args); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: public function resetCache(array $ids = NULL) { danielebarchiesi@4: $this->cacheComplete = FALSE; danielebarchiesi@4: if (isset($ids)) { danielebarchiesi@4: foreach (array_intersect_key($this->entityCache, array_flip($ids)) as $id => $entity) { danielebarchiesi@4: unset($this->entityCacheByName[$this->entityCache[$id]->{$this->nameKey}]); danielebarchiesi@4: unset($this->entityCache[$id]); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: else { danielebarchiesi@4: $this->entityCache = array(); danielebarchiesi@4: $this->entityCacheByName = array(); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Overridden to care about reverted entities. danielebarchiesi@4: */ danielebarchiesi@4: public function delete($ids, DatabaseTransaction $transaction = NULL) { danielebarchiesi@4: $entities = $ids ? $this->load($ids) : FALSE; danielebarchiesi@4: if ($entities) { danielebarchiesi@4: parent::delete($ids, $transaction); danielebarchiesi@4: danielebarchiesi@4: foreach ($entities as $id => $entity) { danielebarchiesi@4: if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE)) { danielebarchiesi@4: entity_defaults_rebuild(array($this->entityType)); danielebarchiesi@4: break; danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Overridden to care about reverted bundle entities and to skip Rules. danielebarchiesi@4: */ danielebarchiesi@4: public function invoke($hook, $entity) { danielebarchiesi@4: if ($hook == 'delete') { danielebarchiesi@4: // To ease figuring out whether this is a revert, make sure that the danielebarchiesi@4: // entity status is updated in case the providing module has been danielebarchiesi@4: // disabled. danielebarchiesi@4: if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE) && !module_exists($entity->{$this->moduleKey})) { danielebarchiesi@4: $entity->{$this->statusKey} = ENTITY_CUSTOM; danielebarchiesi@4: } danielebarchiesi@4: $is_revert = entity_has_status($this->entityType, $entity, ENTITY_IN_CODE); danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) { danielebarchiesi@4: $function($this->entityType, $entity); danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: if (isset($this->entityInfo['bundle of']) && $type = $this->entityInfo['bundle of']) { danielebarchiesi@4: // Call field API bundle attachers for the entity we are a bundle of. danielebarchiesi@4: if ($hook == 'insert') { danielebarchiesi@4: field_attach_create_bundle($type, $entity->{$this->bundleKey}); danielebarchiesi@4: } danielebarchiesi@4: elseif ($hook == 'delete' && !$is_revert) { danielebarchiesi@4: field_attach_delete_bundle($type, $entity->{$this->bundleKey}); danielebarchiesi@4: } danielebarchiesi@4: elseif ($hook == 'update' && $id = $entity->{$this->nameKey}) { danielebarchiesi@4: if ($entity->original->{$this->bundleKey} != $entity->{$this->bundleKey}) { danielebarchiesi@4: field_attach_rename_bundle($type, $entity->original->{$this->bundleKey}, $entity->{$this->bundleKey}); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: // Invoke the hook. danielebarchiesi@4: module_invoke_all($this->entityType . '_' . $hook, $entity); danielebarchiesi@4: // Invoke the respective entity level hook. danielebarchiesi@4: if ($hook == 'presave' || $hook == 'insert' || $hook == 'update' || $hook == 'delete') { danielebarchiesi@4: module_invoke_all('entity_' . $hook, $entity, $this->entityType); danielebarchiesi@4: } danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Overridden to care exportables that are overridden. danielebarchiesi@4: */ danielebarchiesi@4: public function save($entity, DatabaseTransaction $transaction = NULL) { danielebarchiesi@4: // Preload $entity->original by name key if necessary. danielebarchiesi@4: if (!empty($entity->{$this->nameKey}) && empty($entity->{$this->idKey}) && !isset($entity->original)) { danielebarchiesi@4: $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->nameKey}); danielebarchiesi@4: } danielebarchiesi@4: // Update the status for entities getting overridden. danielebarchiesi@4: if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE) && empty($entity->is_rebuild)) { danielebarchiesi@4: $entity->{$this->statusKey} |= ENTITY_CUSTOM; danielebarchiesi@4: } danielebarchiesi@4: return parent::save($entity, $transaction); danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Overridden. danielebarchiesi@4: */ danielebarchiesi@4: public function export($entity, $prefix = '') { danielebarchiesi@4: $vars = get_object_vars($entity); danielebarchiesi@4: unset($vars[$this->statusKey], $vars[$this->moduleKey], $vars['is_new']); danielebarchiesi@4: if ($this->nameKey != $this->idKey) { danielebarchiesi@4: unset($vars[$this->idKey]); danielebarchiesi@4: } danielebarchiesi@4: return entity_var_json_export($vars, $prefix); danielebarchiesi@4: } danielebarchiesi@4: danielebarchiesi@4: /** danielebarchiesi@4: * Implements EntityAPIControllerInterface. danielebarchiesi@4: */ danielebarchiesi@4: public function view($entities, $view_mode = 'full', $langcode = NULL, $page = NULL) { danielebarchiesi@4: $view = parent::view($entities, $view_mode, $langcode, $page); danielebarchiesi@4: danielebarchiesi@4: if ($this->nameKey != $this->idKey) { danielebarchiesi@4: // Re-key the view array to be keyed by name. danielebarchiesi@4: $return = array(); danielebarchiesi@4: foreach ($view[$this->entityType] as $id => $content) { danielebarchiesi@4: $key = isset($content['#entity']->{$this->nameKey}) ? $content['#entity']->{$this->nameKey} : NULL; danielebarchiesi@4: $return[$this->entityType][$key] = $content; danielebarchiesi@4: } danielebarchiesi@4: $view = $return; danielebarchiesi@4: } danielebarchiesi@4: return $view; danielebarchiesi@4: } danielebarchiesi@4: }