diff --git a/README.txt b/README.txt index fd21715..d3c524b 100644 --- a/README.txt +++ b/README.txt @@ -47,8 +47,9 @@ you may stop reading now. "Entity" class is provided. In particular, it is useful to extend this class in order to easily customize the entity type, e.g. saving. - * The controller supports fieldable entities as well as exportable entities, - however it does not yet support revisions. + * The controller supports fieldable entities, however it does not yet support + revisions. There is also a controller which supports implementing exportable + entities. * The Entity CRUD API helps with providing additional module integration too, e.g. exportable entities are automatically integrate with the Features @@ -75,7 +76,8 @@ you may stop reading now. the "entity_test.module". * Implement hook_entity_info() for your entity. At least specifiy the - controller class of this API, your db table and your entity's keys. + controller class (EntityAPIController, EntityAPIControllerExportable or your + own), your db table and your entity's keys. Again just look at "entity_test.module"'s hook_entity_info() for guidance. * If you want your entity to be fieldable just set 'fieldable' in @@ -87,7 +89,7 @@ you may stop reading now. and set the 'bundle of' property for it. Again just look at "entity_test.module"'s hook_entity_info() for guidance. - * Schema fields marked as 'serialzed' are automatically unserialized upon + * Schema fields marked as 'serialized' are automatically unserialized upon loading as well as serialized on saving. If the 'merge' attribute is also set to TRUE the unserialized data is automatically "merged" into the entity. diff --git a/entity.api.php b/entity.api.php index dff6cdc..2dec177 100644 --- a/entity.api.php +++ b/entity.api.php @@ -15,7 +15,11 @@ * * This is a placeholder for describing further keys for hook_entity_info(), * which are introduced the entity API for providing a new entity type with the - * entity CRUD API. + * entity CRUD API. For that the entity API provides two controllers: + * - EntityAPIController: A regular CRUD controller. + * - EntityAPIControllerExportable: Extends the regular controller to + * additionally support exportable entities and/or entities making use of a + * name key. * See entity_metadata_hook_entity_info() for the documentation of additional * keys for hook_entity_info() as introduced by the entity API and supported for * any entity type. @@ -34,19 +38,30 @@ * If enabled, a name key should be specified and db columns for the module * and status key as defined by entity_exportable_schema_fields() have to * exist in the entity's base table. Also see 'entity keys' below. + * This option requires the EntityAPIControllerExportable to work. * - entity keys: An array of keys as defined by Drupal core. The following * additional keys are used by the entity CRUD API: * - name: (optional) The key of the entity property containing the unique, * machine readable name of the entity. If specified, this is used as - * uniform identifier of the entity, while the usual 'id' key is still - * required. If a name key is given, the name is used as identifier for all - * API functions like entity_load(), but the numeric id as specified by the - * 'id' key is still used to refer to the entity internally, i.e. in the - * database. - * For exportable entities, it's strongly recommended to use a machine name - * here as those are more portable across systems. + * identifier of the entity, while the usual 'id' key is still required and + * may be used when modules deal with entities generically, or to refer to + * the entity internally, i.e. in the database. + * If a name key is given, the name is used as entity identifier for most + * API functions and hooks. However note that for consistency all generic + * entity hooks like hook_entity_load() are invoked with the entities keyed + * by numeric id, while entity-type specific hooks like + * hook_{entity_type}_load() are invoked with the entities keyed by name. + * Also, just as entity_load_single() entity_load() may be called + * with names passed as the $ids parameter, while the results of + * entity_load() are always keyed by numeric id. Thus, it is suggested to + * make use of entity_load_multiple_by_name() to implement entity-type + * specific loading functions like {entity_type}_load_multiple(), as this + * function returns the entities keyed by name. + * For exportable entities, it is strongly recommended to make use of a + * machine name as names are portable across systems. + * This option requires the EntityAPIControllerExportable to work. * - module: (optional) A key for the module property used by the entity CRUD - * API to save the source module name for exportable entities, which are + * API to save the source module name for exportable entities that have been * provided in code. Defaults to 'module'. * - status: (optional) The name of the entity property used by the entity * CRUD API to save the exportable entity status using defined bit flags. diff --git a/entity.module b/entity.module index 432d469..252c2ec 100644 --- a/entity.module +++ b/entity.module @@ -71,6 +71,57 @@ function entity_type_supports($entity_type, $op) { } /** + * A wrapper around entity_load() to load a single entity by name or numeric id. + * + * @todo: Re-name entity_load() to entity_load_multiple() in d8 core and this + * to entity_load(). + * + * @param $entity_type + * The entity type to load, e.g. node or user. + * @param $id + * The entity id, either the numeric id or the entity name. In case the entity + * type has specified a name key, both the numeric id and the name may be + * passed. + * + * @return + * The entity object, or FALSE. + * + * @see entity_load() + */ +function entity_load_single($entity_type, $id) { + $entities = entity_load($entity_type, array($id)); + return reset($entities); +} + +/** + * A wrapper around entity_load() to return entities keyed by name key if existing. + * + * @param $entity_type + * The entity type to load, e.g. node or user. + * @param $names + * An array of entity names or ids, or FALSE to load all entities. + * @param $conditions + * (deprecated) An associative array of conditions on the base table, where + * the keys are the database fields and the values are the values those + * fields must have. Instead, it is preferable to use EntityFieldQuery to + * retrieve a list of entity IDs loadable by this function. + * + * @return + * An array of entity objects indexed by their names (or ids if the entity + * type has no name key). + * + * @see entity_load() + */ +function entity_load_multiple_by_name($entity_type, $names = FALSE, $conditions = array()) { + $entities = entity_load($entity_type, $names, $conditions); + $info = entity_get_info($entity_type); + if (!isset($info['entity keys']['name'])) { + return $entities; + } + return entity_key_array_by_property($entities, $info['entity keys']['name']); +} + +/** * Permanently save an entity. * * In case of failures, an exception is thrown. @@ -127,7 +178,8 @@ function entity_delete($entity_type, $id) { * @param $entity_type * The type of the entity. * @param $ids - * An array of uniform identifiers of the entities to delete. + * An array of entity ids of the entities to delete. In case the entity makes + * use of a name key, both the names or numeric ids may be passed. * @return * FALSE if the given entity type isn't compatible to the CRUD API. */ @@ -251,11 +303,10 @@ function entity_build_content($entity_type, $entity, $view_mode = 'full', $langc } /** - * Returns the entity identifier. + * Returns the entity identifier, i.e. the entities name or numeric id. * - * Unlike entity_extract_ids() this function returns the uniform identifier, - * thus if an exportable entity makes use of a name key, the name of the entity - * is returned. Otherwise the usual numeric id is returned. + * Unlike entity_extract_ids() this function returns the name of the entity + * instead of the numeric id, in case the entity type has specified a name key. * * @param $entity_type * The type of the entity. @@ -276,19 +327,23 @@ function entity_id($entity_type, $entity) { /** * Generate an array for rendering the given entities. * + * Entities being viewed, are generally expected to be fully-loaded entity + * objects, thus have their name or id key set. However, it is possible to + * view a single entity without any id, e.g. for generating a preview during + * creation. + * * @param $entity_type * The type of the entity. * @param $entities - * An array of entities to render, keyed by their ids. E.g. as returned from - * entity_load(). + * An array of entities to render. * @param $view_mode * A view mode as used by this entity type, e.g. 'full', 'teaser'... * @param $langcode * (optional) A language code to use for rendering. Defaults to the global * content language of the current request. * @return - * The renderable array, or FALSE if there is no information how to view an - * entity. + * The renderable array, keyed by name key if existing or by numeric id else. + * If there is no information on how to view an entity, FALSE is returned. */ function entity_view($entity_type, $entities, $view_mode = 'full', $langcode = NULL) { $info = entity_get_info($entity_type); @@ -298,6 +353,7 @@ function entity_view($entity_type, $entities, $view_mode = 'full', $langcode = N elseif (in_array('EntityAPIControllerInterface', class_implements($info['controller class']))) { return entity_get_controller($entity_type)->view($entities, $view_mode, $langcode); } + return FALSE; } /** @@ -327,6 +383,27 @@ function entity_access($op, $entity_type, $entity = NULL, $account = NULL) { } /** + * Converts an array of entities to be keyed by the values of a given property. + * + * @param array $entities + * The array of entities to convert. + * @param $property + * The name of entity property, by which the array should be keyed. To get + * reasonable results, the property has to have unique values. + * + * @return array + * The same entities in the same order, but keyed by their $property values. + */ +function entity_key_array_by_property(array $entities, $property) { + $ret = array(); + foreach ($entities as $entity) { + $key = isset($entity->$property) ? $entity->$property : NULL; + $ret[$key] = $entity; + } + return $ret; +} + +/** * Returns an array of entity info for the entity types provided via the entity CRUD API. */ function entity_crud_get_info() { @@ -492,11 +569,11 @@ function _entity_defaults_rebuild($entity_type) { drupal_alter($hook, $entities); // Remove defaults which are already overridden. - $overridden_entities = entity_load($entity_type, array_keys($entities), array($keys['status'] => ENTITY_OVERRIDDEN)); + $overridden_entities = entity_load_multiple_by_name($entity_type, array_keys($entities), array($keys['status'] => ENTITY_OVERRIDDEN)); $entities = array_diff_key($entities, $overridden_entities); // Determine already existing, custom entities and mark them as overridden. - $existing_entities = entity_load($entity_type, array_keys($entities), array($keys['status'] => ENTITY_CUSTOM)); + $existing_entities = entity_load_multiple_by_name($entity_type, array_keys($entities), array($keys['status'] => ENTITY_CUSTOM)); foreach ($existing_entities as $name => $entity) { $entity->{$keys['status']} |= ENTITY_OVERRIDDEN; $entity->{$keys['module']} = $entities[$name]->{$keys['module']}; @@ -524,8 +601,20 @@ function _entity_defaults_rebuild($entity_type) { * Implements hook_modules_enabled(). */ function entity_modules_enabled($modules) { + $built_types = variable_get('entity_defaults_built', array()); if ($entity_types = _entity_modules_get_default_types($modules)) { - entity_defaults_rebuild($entity_types); + // Determining the entity types might have triggered rebuilding already, in + // which case we do not need to rebuild again. So we make sure to only + // add entity-types for rebuilding that were not marked for rebuilding + // *before our check* anyway. + foreach ($entity_types as $key => $type) { + if (!isset($built_types[$type])) { + unset($entity_types[$key]); + } + } + if ($entity_types) { + entity_defaults_rebuild($entity_types); + } } } diff --git a/entity.test b/entity.test index 09a2551..38dd990 100644 --- a/entity.test +++ b/entity.test @@ -106,19 +106,21 @@ class EntityAPITestCase extends DrupalWebTestCase { function testExportables() { module_enable(array('entity_feature')); - $types = entity_load('entity_test_type', array('test', 'test2')); + $types = entity_load_multiple_by_name('entity_test_type', array('test2', 'test')); + + $this->assertEqual(array_keys($types), array('test2', 'test'), 'Entities have been loaded in the order as specified.'); $this->assertEqual($types['test']->label, 'label', 'Default type loaded.'); $this->assertTrue($types['test']->status & ENTITY_IN_CODE && !($types['test']->status & ENTITY_CUSTOM), 'Default type status is correct.'); // Test using a condition, which has to be applied on the defaults. - $types = entity_load('entity_test_type', FALSE, array('label' => 'label')); + $types = entity_load_multiple_by_name('entity_test_type', FALSE, array('label' => 'label')); $this->assertEqual($types['test']->label, 'label', 'Condition to default type applied.'); $types['test']->label = 'modified'; $types['test']->save(); // Ensure loading the changed entity works. - $types = entity_load('entity_test_type', FALSE, array('label' => 'modified')); + $types = entity_load_multiple_by_name('entity_test_type', FALSE, array('label' => 'modified')); $this->assertEqual($types['test']->label, 'modified', 'Modified type loaded.'); // Clear the cache to simulate a new page load. @@ -126,30 +128,30 @@ class EntityAPITestCase extends DrupalWebTestCase { // Test loading using a condition again, now they default may not appear any // more as it's overridden by an entity with another label. - $types = entity_load('entity_test_type', FALSE, array('label' => 'label')); + $types = entity_load_multiple_by_name('entity_test_type', FALSE, array('label' => 'label')); $this->assertTrue(empty($types), 'Conditions are applied to the overridden entity only.'); // But the overridden entity has to appear with another condition. - $types = entity_load('entity_test_type', FALSE, array('label' => 'modified')); + $types = entity_load_multiple_by_name('entity_test_type', FALSE, array('label' => 'modified')); $this->assertEqual($types['test']->label, 'modified', 'Modified default type loaded by condition.'); - $types = entity_load('entity_test_type', array('test', 'test2')); + $types = entity_load_multiple_by_name('entity_test_type', array('test', 'test2')); $this->assertEqual($types['test']->label, 'modified', 'Modified default type loaded by id.'); $this->assertTrue(entity_has_status('entity_test_type', $types['test'], ENTITY_OVERRIDDEN), 'Status of overridden type is correct.'); // Test rebuilding the defaults and make sure overridden entities stay. entity_defaults_rebuild(); - $types = entity_load('entity_test_type', array('test', 'test2')); + $types = entity_load_multiple_by_name('entity_test_type', array('test', 'test2')); $this->assertEqual($types['test']->label, 'modified', 'Overridden entity is still overridden.'); $this->assertTrue(entity_has_status('entity_test_type', $types['test'], ENTITY_OVERRIDDEN), 'Status of overridden type is correct.'); // Test reverting. $types['test']->delete(); - $types = entity_load('entity_test_type', array('test', 'test2')); + $types = entity_load_multiple_by_name('entity_test_type', array('test', 'test2')); $this->assertEqual($types['test']->label, 'label', 'Entity has been reverted.'); // Test loading an exportable by its numeric id. - $result = entity_load('entity_test_type', array($types['test']->id)); + $result = entity_load_multiple_by_name('entity_test_type', array($types['test']->id)); $this->assertTrue(isset($result['test']), 'Exportable entity loaded by the numeric id.'); // Test exporting an entity to JSON. @@ -174,6 +176,10 @@ class EntityAPITestCase extends DrupalWebTestCase { $this->assertTrue($_SESSION['entity_hook_test']['entity_insert'] == $insert, 'Hook entity_insert has been invoked.'); $this->assertTrue($_SESSION['entity_hook_test']['entity_test_type_insert'] == $insert, 'Hook entity_test_type_insert has been invoked.'); + // Load a default entity and make sure the rebuilt logic only ran once. + entity_load_single('entity_test_type', 'test'); + $this->assertTrue(!isset($_SESSION['entity_hook_test']['entity_test_type_update']), '"Entity-test-type" defaults have been rebuilt only once.'); + // Add a new test entity in DB and make sure the hook is invoked too. $test3 = entity_create('entity_test_type', array( 'name' => 'test3', @@ -187,7 +193,7 @@ class EntityAPITestCase extends DrupalWebTestCase { $this->assertTrue($_SESSION['entity_hook_test']['entity_test_type_insert'] == $insert, 'Hook entity_test_type_insert has been invoked.'); // Now override the 'test' entity and make sure it invokes the update hook. - $result = entity_load('entity_test_type', array('test')); + $result = entity_load_multiple_by_name('entity_test_type', array('test')); $result['test']->label = 'modified'; $result['test']->save(); @@ -202,7 +208,7 @@ class EntityAPITestCase extends DrupalWebTestCase { $this->assertTrue($_SESSION['entity_hook_test']['entity_test_type_delete'] == $delete, 'Hook entity_test_type_deleted has been invoked.'); // Now make sure 'test' is not overridden any more, but custom. - $result = entity_load('entity_test_type', array('test')); + $result = entity_load_multiple_by_name('entity_test_type', array('test')); $this->assertTrue(!$result['test']->hasStatus(ENTITY_OVERRIDDEN), 'Entity is not marked as overridden any more.'); $this->assertTrue(entity_has_status('entity_test_type', $result['test'], ENTITY_CUSTOM), 'Entity is marked as custom.'); @@ -219,7 +225,7 @@ class EntityAPITestCase extends DrupalWebTestCase { */ function testChanges() { module_enable(array('entity_feature')); - $types = entity_load('entity_test_type'); + $types = entity_load_multiple_by_name('entity_test_type'); // Override the default entity, such it gets saved in the DB. $types['test']->label ='test_changes'; @@ -239,7 +245,7 @@ class EntityAPITestCase extends DrupalWebTestCase { $this->assertEqual($types['test']->label, 'updated_presave_update', 'Changes have been determined.'); // Test the static load cache to be cleared. - $types = entity_load('entity_test_type'); + $types = entity_load_multiple_by_name('entity_test_type'); $this->assertEqual($types['test']->label, 'updated_presave', 'Static cache has been cleared.'); } @@ -294,6 +300,8 @@ class EntityAPIRulesIntegrationTestCase extends DrupalWebTestCase { // Let the events occur. $user1 = $this->drupalCreateUser(); + RulesLog::logger()->clear(); + $entity = entity_create('entity_test', array('name' => 'test', 'uid' => $user1->uid)); $entity->save(); $entity->name = 'update'; @@ -302,7 +310,6 @@ class EntityAPIRulesIntegrationTestCase extends DrupalWebTestCase { // Now there should have been 5 events, 2 times presave and once insert, // update and delete. - $count = substr_count(RulesLog::logger()->render(), '0 ms Reacting on event'); $this->assertTrue($count == 5, 'Events have been properly invoked.'); RulesLog::logger()->checkLog(); @@ -933,7 +940,7 @@ class EntityMetadataIntegrationTestCase extends DrupalWebTestCase { if ($return === FALSE) { continue; // No support for deleting. } - $return = entity_load($entity_type, array($id)); + $return = entity_load_single($entity_type, $id); $this->assertFalse($return, "$entity_type has been successfully deleted."); } } diff --git a/includes/entity.controller.inc b/includes/entity.controller.inc index 83fe55e..20bd3b8 100644 --- a/includes/entity.controller.inc +++ b/includes/entity.controller.inc @@ -106,35 +106,25 @@ interface EntityAPIControllerInterface extends DrupalEntityControllerInterface { * (optional) A language code to use for rendering. Defaults to the global * content language of the current request. * @return - * The renderable array. + * The renderable array, keyed by entity name or numeric id. */ public function view($entities, $view_mode = 'full', $langcode = NULL); } /** - * A controller implementing EntityAPIControllerInterface for the database and - * being able to deal with exportable entities. + * A controller implementing EntityAPIControllerInterface for the database. */ class EntityAPIController extends DrupalDefaultEntityController implements EntityAPIControllerInterface { protected $cacheComplete = FALSE; - protected $nameKey, $statusKey, $moduleKey, $bundleKey; + protected $bundleKey; /** * Overridden. * @see DrupalDefaultEntityController#__construct() - * - * Allows specifying a name key serving as uniform identifier for this entity - * type while still internally we are using numeric identifieres. */ public function __construct($entityType) { parent::__construct($entityType); - // Use the name key as primary identifier. - $this->nameKey = isset($this->entityInfo['entity keys']['name']) ? $this->entityInfo['entity keys']['name'] : $this->idKey; - if (!empty($this->entityInfo['exportable'])) { - $this->statusKey = isset($this->entityInfo['entity keys']['status']) ? $this->entityInfo['entity keys']['status'] : 'status'; - $this->moduleKey = isset($this->entityInfo['entity keys']['module']) ? $this->entityInfo['entity keys']['module'] : 'module'; - } // If this is the bundle of another entity, set the bundle key. if (isset($this->entityInfo['bundle of'])) { $info = entity_get_info($this->entityInfo['bundle of']); @@ -157,33 +147,16 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit return $result; } - protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { - // Add the id condition ourself, as we might have a separate name key. - $query = parent::buildQuery(array(), $conditions, $revision_id); - if ($ids) { - // Support loading by numeric ids as well as by machine names. - $key = is_numeric(reset($ids)) ? $this->idKey : $this->nameKey; - $query->condition("base.$key", $ids, 'IN'); - } - return $query; - } - /** * Overridden. * @see DrupalDefaultEntityController#load($ids, $conditions) * * In contrast to the parent implementation we factor out query execution, so - * fetching can be further customized easily. Also we add any objects defined - * in code. + * fetching can be further customized easily. */ public function load($ids = array(), $conditions = array()) { $entities = array(); - // For exportable entities, make sure the defaults have been built. - if (!empty($this->entityInfo['exportable']) && empty($GLOBALS['conf']['entity_defaults_built'][$this->entityType])) { - _entity_defaults_rebuild($this->entityType); - } - // Revisions are not statically cached, and require a different query to // other conditions, so separate the revision id into its own variable. if ($this->revisionKey && isset($conditions[$this->revisionKey])) { @@ -202,7 +175,7 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit $passed_ids = !empty($ids) ? array_flip($ids) : FALSE; // Try to load entities from the static cache. - if (!$revision_id) { + if ($this->cache && !$revision_id) { $entities = $this->cacheGet($ids, $conditions); // If any entities were loaded, remove them from the ids still to load. if ($passed_ids) { @@ -217,7 +190,7 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit $queried_entities = array(); foreach ($this->query($ids, $conditions, $revision_id) as $record) { // Skip entities already retrieved from cache. - if (isset($entities[$record->{$this->nameKey}])) { + if (isset($entities[$record->{$this->idKey}])) { continue; } @@ -237,7 +210,7 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit } } - $queried_entities[$record->{$this->nameKey}] = $record; + $queried_entities[$record->{$this->idKey}] = $record; } } @@ -254,7 +227,7 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit if (!empty($queried_entities) && !$revision_id) { $this->cacheSet($queried_entities); - // Remember we have cached all entities now. + // Remember if we have cached all entities now. if (!$conditions && $ids === FALSE) { $this->cacheComplete = TRUE; } @@ -271,82 +244,6 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit return $entities; } - protected function applyConditions($entities, $conditions = array()) { - if ($conditions) { - foreach ($entities as $key => $entity) { - $entity_values = (array) $entity; - if (array_diff_assoc($conditions, $entity_values)) { - unset($entities[$key]); - } - } - } - return $entities; - } - - /** - * Overridden. - * @see includes/DrupalDefaultEntityController#cacheGet($ids, $conditions) - * - * If there is nameKey given, we index our entities by this key. This - * overrides cacheGet() to respect that when applying $conditions. - */ - protected function cacheGet($ids, $conditions = array()) { - if (!empty($this->entityCache)) { - // First get the entities by ids, then apply the conditions. - $entities = is_array($ids) ? array_intersect_key($this->entityCache, array_flip($ids)) : $this->entityCache; - return $this->applyConditions($entities, $conditions); - } - return array(); - } - - /** - * Overridden. - * @see DrupalDefaultEntityController::attachLoad() - * - * Fixed to make attaching fields to entities having a name key work. - */ - protected function attachLoad(&$queried_entities, $revision_id = FALSE) { - // Attach fields. - if ($this->entityInfo['fieldable']) { - // Field API assumes queried entities are keyed by the idkey, thus - // adapt the array accordingly for it. - $entities = $this->getEntitiesKeyedByNumericID($queried_entities); - if ($revision_id) { - field_attach_load_revision($this->entityType, $entities); - } - else { - field_attach_load($this->entityType, $entities); - } - } - - // Call hook_entity_load(). - foreach (module_implements('entity_load') as $module) { - $function = $module . '_entity_load'; - $function($queried_entities, $this->entityType); - } - // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are - // always the queried entities, followed by additional arguments set in - // $this->hookLoadArguments. - $args = array_merge(array($queried_entities), $this->hookLoadArguments); - foreach (module_implements($this->entityInfo['load hook']) as $module) { - call_user_func_array($module . '_' . $this->entityInfo['load hook'], $args); - } - } - - /** - * Converts an array of entities in an array of entities keyed by numeric id, i.e. by id key. - */ - protected function getEntitiesKeyedByNumericID($entities) { - if ($this->nameKey != $this->idKey) { - $result = array(); - foreach ($entities as $entity) { - $result[$entity->{$this->idKey}] = $entity; - } - return $result; - } - return $entities; - } - public function resetCache(array $ids = NULL) { $this->cacheComplete = FALSE; parent::resetCache($ids); @@ -401,36 +298,33 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit // Do nothing, in case invalid or no ids have been passed. return; } - $transaction = isset($transaction) ? $transaction : db_transaction(); + // This transaction causes troubles on MySQL, see + // http://drupal.org/node/1007830. So we deactivate this by default until + // is shipped in a point release. + // $transaction = isset($transaction) ? $transaction : db_transaction(); try { + $ids = array_keys($entities); + db_delete($this->entityInfo['base table']) - ->condition($this->nameKey, array_keys($entities), 'IN') + ->condition($this->idKey, $ids, 'IN') ->execute(); // Reset the cache as soon as the changes have been applied. $this->resetCache($ids); foreach ($entities as $id => $entity) { $this->invoke('delete', $entity); - if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE)) { - $rebuild = TRUE; - } } // Ignore slave server temporarily. db_ignore_slave(); } catch (Exception $e) { - $transaction->rollback(); + if (isset($transaction)) { + $transaction->rollback(); + } watchdog_exception($this->entityType, $e); throw $e; } - // We have to postpone rebuilding defaults until the transaction has been - // committed. This is as variable_set() triggers a merge query and dies with - // a savepoint exception if it is inside a transaction. - if (!empty($rebuild)) { - $transaction = NULL; - entity_defaults_rebuild(array($this->entityType)); - } } /** @@ -444,21 +338,17 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit $transaction = isset($transaction) ? $transaction : db_transaction(); try { // Load the stored entity, if any. - if (!empty($entity->{$this->nameKey}) && !isset($entity->original)) { + if (!empty($entity->{$this->idKey}) && !isset($entity->original)) { // In order to properly work in case of name changes, load the original // entity using the id key if it is available. - $entity->original = entity_load_unchanged($this->entityType, !empty($entity->{$this->idKey}) ? $entity->{$this->idKey} : $entity->{$this->nameKey}); - } - // Update the status for entities getting overridden. - if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE) && empty($entity->is_rebuild)) { - $entity->{$this->statusKey} |= ENTITY_CUSTOM; + $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->idKey}); } $this->invoke('presave', $entity); if (!empty($entity->{$this->idKey}) && empty($entity->is_new)) { $return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey); - $this->resetCache(array($entity->{$this->nameKey})); + $this->resetCache(array($entity->{$this->idKey})); $this->invoke('update', $entity); } else { @@ -499,10 +389,7 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit */ public function export($entity, $prefix = '') { $vars = get_object_vars($entity); - unset($vars[$this->statusKey], $vars[$this->moduleKey], $vars['is_new']); - if ($this->nameKey != $this->idKey) { - unset($vars[$this->idKey]); - } + unset($vars['is_new']); return entity_var_json_export($vars, $prefix); } @@ -553,20 +440,22 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit * Implements EntityAPIControllerInterface. */ public function view($entities, $view_mode = 'full', $langcode = NULL) { + // For Field API and entity_prepare_view, the entities have to be keyed by + // (numeric) id. + $entities = entity_key_array_by_property($entities, $this->idKey); if (!empty($this->entityInfo['fieldable'])) { - // Field API assumes queried entities are keyed by the idkey, thus - // adapt the array accordingly for it. - field_attach_prepare_view($this->entityType, $this->getEntitiesKeyedByNumericID($entities), $view_mode); + field_attach_prepare_view($this->entityType, $entities, $view_mode); } entity_prepare_view($this->entityType, $entities); $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language; $view = array(); - foreach ($entities as $key => $entity) { + foreach ($entities as $entity) { $build = entity_build_content($this->entityType, $entity, $view_mode, $langcode); $build += array( // If the entity type provides an implementation, use this instead the // generic one. + // @see template_preprocess_entity() '#theme' => 'entity', '#entity_type' => $this->entityType, '#entity' => $entity, @@ -575,9 +464,257 @@ class EntityAPIController extends DrupalDefaultEntityController implements Entit ); // Allow modules to modify the structured entity. drupal_alter(array($this->entityType . '_view', 'entity_view'), $build, $this->entityType); + $key = isset($entity->{$this->idKey}) ? $entity->{$this->idKey} : NULL; $view[$this->entityType][$key] = $build; } return $view; } +} + +/** + * A controller implementing exportables stored in the database. + */ +class EntityAPIControllerExportable extends EntityAPIController { + + protected $entityCacheByName = array(); + protected $nameKey, $statusKey, $moduleKey; + + /** + * Overridden. + * + * Allows specifying a name key serving as uniform identifier for this entity + * type while still internally we are using numeric identifieres. + */ + public function __construct($entityType) { + parent::__construct($entityType); + // Use the name key as primary identifier. + $this->nameKey = isset($this->entityInfo['entity keys']['name']) ? $this->entityInfo['entity keys']['name'] : $this->idKey; + if (!empty($this->entityInfo['exportable'])) { + $this->statusKey = isset($this->entityInfo['entity keys']['status']) ? $this->entityInfo['entity keys']['status'] : 'status'; + $this->moduleKey = isset($this->entityInfo['entity keys']['module']) ? $this->entityInfo['entity keys']['module'] : 'module'; + } + } + + /** + * Support loading by name key. + */ + protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { + // Add the id condition ourself, as we might have a separate name key. + $query = parent::buildQuery(array(), $conditions, $revision_id); + if ($ids) { + // Support loading by numeric ids as well as by machine names. + $key = is_numeric(reset($ids)) ? $this->idKey : $this->nameKey; + $query->condition("base.$key", $ids, 'IN'); + } + return $query; + } + /** + * Overridden to support passing numeric ids as well as names as $ids. + */ + public function load($ids = array(), $conditions = array()) { + $entities = array(); + // For exportable entities, make sure the defaults have been built. + if (!empty($this->entityInfo['exportable']) && empty($GLOBALS['conf']['entity_defaults_built'][$this->entityType])) { + _entity_defaults_rebuild($this->entityType); + } + + // Only do something if loaded by names. + if (!$ids || $this->nameKey == $this->idKey || is_numeric(reset($ids))) { + return parent::load($ids, $conditions); + } + + // Revisions are not statically cached, and require a different query to + // other conditions, so separate the revision id into its own variable. + if ($this->revisionKey && isset($conditions[$this->revisionKey])) { + $revision_id = $conditions[$this->revisionKey]; + unset($conditions[$this->revisionKey]); + } + else { + $revision_id = FALSE; + } + $passed_ids = !empty($ids) ? array_flip($ids) : FALSE; + + // Care about the static cache. + if ($this->cache && !$revision_id) { + $entities = $this->cacheGetByName($ids, $conditions); + } + // If any entities were loaded, remove them from the ids still to load. + if ($entities) { + $ids = array_keys(array_diff_key($passed_ids, $entities)); + } + + $entities_by_id = parent::load($ids, $conditions); + $entities += entity_key_array_by_property($entities_by_id, $this->nameKey); + + // Ensure that the returned array is keyed by numeric id and ordered the + // same as the original $ids array and remove any invalid ids. + $return = array(); + foreach ($passed_ids as $name => $value) { + if (isset($entities[$name])) { + $return[$entities[$name]->{$this->idKey}] = $entities[$name]; + } + } + return $return; + } + + /** + * Overridden. + * @see DrupalDefaultEntityController::cacheGet() + */ + protected function cacheGet($ids, $conditions = array()) { + if (!empty($this->entityCache) && $ids !== array()) { + $entities = $ids ? array_intersect_key($this->entityCache, array_flip($ids)) : $this->entityCache; + return $this->applyConditions($entities, $conditions); + } + return array(); + } + + /** + * Like cacheGet() but keyed by name. + */ + protected function cacheGetByName($names, $conditions = array()) { + if (!empty($this->entityCacheByName) && $names !== array() && $names) { + // First get the entities by ids, then apply the conditions. + // Generally, we make use of $this->entityCache, but if we are loading by + // name, we have to use $this->entityCacheByName. + $entities = array_intersect_key($this->entityCacheByName, array_flip($names)); + return $this->applyConditions($entities, $conditions); + } + return array(); + } + + protected function applyConditions($entities, $conditions = array()) { + if ($conditions) { + foreach ($entities as $key => $entity) { + $entity_values = (array) $entity; + if (array_diff_assoc($conditions, $entity_values)) { + unset($entities[$key]); + } + } + } + return $entities; + } + + /** + * Overridden. + * @see DrupalDefaultEntityController::cacheSet() + */ + protected function cacheSet($entities) { + $this->entityCache += $entities; + // If we have a name key, also support static caching when loading by name. + if ($this->nameKey != $this->idKey) { + $this->entityCacheByName += entity_key_array_by_property($entities, $this->nameKey); + } + } + + /** + * Overridden. + * @see DrupalDefaultEntityController::attachLoad() + * + * Changed to call type-specific hook with the entities keyed by name if they + * have one. + */ + protected function attachLoad(&$queried_entities, $revision_id = FALSE) { + // Attach fields. + if ($this->entityInfo['fieldable']) { + if ($revision_id) { + field_attach_load_revision($this->entityType, $queried_entities); + } + else { + field_attach_load($this->entityType, $queried_entities); + } + } + + // Call hook_entity_load(). + foreach (module_implements('entity_load') as $module) { + $function = $module . '_entity_load'; + $function($queried_entities, $this->entityType); + } + // Call hook_TYPE_load(). The first argument for hook_TYPE_load() are + // always the queried entities, followed by additional arguments set in + // $this->hookLoadArguments. + // For entities with a name key, pass the entities keyed by name to the + // specific load hook. + if ($this->nameKey != $this->idKey) { + $entities_by_name = entity_key_array_by_property($queried_entities, $this->nameKey); + } + else { + $entities_by_name = $queried_entities; + } + $args = array_merge(array($entities_by_name), $this->hookLoadArguments); + foreach (module_implements($this->entityInfo['load hook']) as $module) { + call_user_func_array($module . '_' . $this->entityInfo['load hook'], $args); + } + } + + public function resetCache(array $ids = NULL) { + $this->cacheComplete = FALSE; + if (isset($ids)) { + foreach (array_intersect_key($this->entityCache, array_flip($ids)) as $id => $entity) { + unset($this->entityCacheByName[$this->entityCache[$id]->{$this->nameKey}]); + unset($this->entityCache[$id]); + } + } + else { + $this->entityCache = array(); + $this->entityCacheByName = array(); + } + } + + /** + * Overridden to care about reverted entities. + */ + public function delete($ids, DatabaseTransaction $transaction = NULL) { + $entities = $ids ? $this->load($ids) : FALSE; + if ($entities) { + parent::delete($ids, $transaction); + + foreach ($entities as $id => $entity) { + if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE)) { + entity_defaults_rebuild(array($this->entityType)); + break; + } + } + } + } + + /** + * Overridden to care exportables that are overridden. + */ + public function save($entity, DatabaseTransaction $transaction = NULL) { + // Preload $entity->original by name key if necessary. + if (!empty($entity->{$this->nameKey}) && empty($entity->{$this->idKey}) && !isset($entity->original)) { + $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->nameKey}); + } + // Update the status for entities getting overridden. + if (entity_has_status($this->entityType, $entity, ENTITY_IN_CODE) && empty($entity->is_rebuild)) { + $entity->{$this->statusKey} |= ENTITY_CUSTOM; + } + parent::save($entity, $transaction); + } + + /** + * Overridden. + */ + public function export($entity, $prefix = '') { + $vars = get_object_vars($entity); + unset($vars[$this->statusKey], $vars[$this->moduleKey], $vars['is_new']); + if ($this->nameKey != $this->idKey) { + unset($vars[$this->idKey]); + } + return entity_var_json_export($vars, $prefix); + } + + /** + * Implements EntityAPIControllerInterface. + */ + public function view($entities, $view_mode = 'full', $langcode = NULL) { + $view = parent::view($entities, $view_mode, $langcode); + + // Re-key the view array to be keyed by name. + $key = isset($entity->{$this->nameKey}) ? $entity->{$this->nameKey} : NULL; + $view[$this->entityType][$key] = reset($view[$this->entityType]); + return $view; + } } diff --git a/includes/entity.inc b/includes/entity.inc index 89903cd..5bbbaf7 100644 --- a/includes/entity.inc +++ b/includes/entity.inc @@ -48,21 +48,21 @@ class Entity { /** * Returns the internal, numeric identifier. * - * For exportable entities, this differs to the uniform identifier returned - * by Entity::identifier(). The internal identifier is supposed to be only - * used internally to refer to an entity, i.e. in the database. If unsure, use - * Entity:identifier(). + * Returns the numeric identifier, even if the entity type has specified a + * name key. In the latter case, the numeric identifier is supposed to be used + * when dealing generically with entities or internally to refer to an entity, + * i.e. in a relational database. If unsure, use Entity:identifier(). */ public function internalIdentifier() { return isset($this->{$this->idKey}) ? $this->{$this->idKey} : NULL; } /** - * Returns the unified identifer. + * Returns the entity identifier, i.e. the entities name or numeric id. * * @return - * The identifier of the entity. For exportable entities, this is their - * machine readable name. + * The identifier of the entity. If the entity type makes use of a name key, + * the name is returned, else the numeric id. * * @see entity_id() */ @@ -188,7 +188,7 @@ class Entity { * @see entity_view() */ public function view($view_mode = 'full', $langcode = NULL) { - return entity_get_controller($this->entityType)->view(array($this->identifier() => $this), $view_mode, $langcode); + return entity_get_controller($this->entityType)->view(array($this), $view_mode, $langcode); } /** diff --git a/includes/entity.ui.inc b/includes/entity.ui.inc index 14d97a4..d33a357 100644 --- a/includes/entity.ui.inc +++ b/includes/entity.ui.inc @@ -179,8 +179,8 @@ class EntityDefaultUIController { ksort($entities); $rows = array(); - foreach ($entities as $id => $entity) { - $rows[] = $this->overviewTableRow($conditions, $id, $entity); + foreach ($entities as $entity) { + $rows[] = $this->overviewTableRow($conditions, entity_id($this->entityType, $entity), $entity); } // Assemble the right table header. $header = array(t('Label')); diff --git a/tests/entity_test.module b/tests/entity_test.module index c1cf2d5..7e0878b 100644 --- a/tests/entity_test.module +++ b/tests/entity_test.module @@ -31,7 +31,7 @@ function entity_test_entity_info() { 'entity_test_type' => array( 'label' => t('Test entity type'), 'entity class' => 'Entity', - 'controller class' => 'EntityAPIController', + 'controller class' => 'EntityAPIControllerExportable', 'base table' => 'entity_test_type', 'fieldable' => FALSE, 'bundle of' => 'entity_test', @@ -66,11 +66,8 @@ function entity_test_entity_info_alter(&$entity_info) { * If set, the type with the given name is returned. */ function entity_test_get_types($name = NULL) { - $types = entity_load('entity_test_type', isset($name) ? array($name) : FALSE); - if (isset($name)) { - return isset($types[$name]) ? $types[$name] : FALSE; - } - return $types; + $types = entity_load_multiple_by_name('entity_test_type', isset($name) ? array($name) : FALSE); + return isset($name) ? reset($types) : $types; } /**