From 58a5b82f90d0d92bd58ca6eb18bd936b5b7eb083 Mon Sep 17 00:00:00 2001 From: Dries Buytaert Date: Wed, 21 Sep 2011 06:09:49 -0400 Subject: [PATCH] - Patch #1018602 by fago, catch, aspilicious: move entity system to a module. --- includes/common.inc | 407 ---------------- includes/install.core.inc | 5 +- includes/module.inc | 8 +- includes/update.inc | 30 ++ modules/comment/comment.info | 1 + modules/entity/entity.api.php | 414 ++++++++++++++++ modules/entity/entity.controller.inc | 390 ++++++++++++++++ modules/entity/entity.info | 8 + modules/entity/entity.module | 442 ++++++++++++++++++ .../entity/entity.query.inc | 393 +--------------- .../tests/entity_cache_test.info | 0 .../tests/entity_cache_test.module | 0 .../tests/entity_cache_test_dependency.info | 0 .../tests/entity_cache_test_dependency.module | 0 .../tests/entity_crud_hook_test.info | 0 .../tests/entity_crud_hook_test.module | 0 .../tests/entity_crud_hook_test.test | 0 .../tests/entity_query.test | 1 - modules/field/field.info | 1 + modules/node/node.info | 1 + modules/system/system.api.php | 434 +---------------- modules/system/system.install | 4 +- modules/taxonomy/taxonomy.info | 1 + modules/user/user.entity.inc | 52 +++ modules/user/user.info | 2 +- modules/user/user.module | 47 -- update.php | 1 - 27 files changed, 1381 insertions(+), 1261 deletions(-) create mode 100644 modules/entity/entity.api.php create mode 100644 modules/entity/entity.controller.inc create mode 100644 modules/entity/entity.info create mode 100644 modules/entity/entity.module rename includes/entity.inc => modules/entity/entity.query.inc (71%) rename modules/{simpletest => entity}/tests/entity_cache_test.info (100%) rename modules/{simpletest => entity}/tests/entity_cache_test.module (100%) rename modules/{simpletest => entity}/tests/entity_cache_test_dependency.info (100%) rename modules/{simpletest => entity}/tests/entity_cache_test_dependency.module (100%) rename modules/{simpletest => entity}/tests/entity_crud_hook_test.info (100%) rename modules/{simpletest => entity}/tests/entity_crud_hook_test.module (100%) rename modules/{simpletest => entity}/tests/entity_crud_hook_test.test (100%) rename modules/{simpletest => entity}/tests/entity_query.test (99%) create mode 100644 modules/user/user.entity.inc diff --git a/includes/common.inc b/includes/common.inc index 0c6c9eb2be8f..268e36bfbd2b 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -7277,413 +7277,6 @@ function drupal_check_incompatibility($v, $current_version) { } } -/** - * Get the entity info array of an entity type. - * - * @see hook_entity_info() - * @see hook_entity_info_alter() - * - * @param $entity_type - * The entity type, e.g. node, for which the info shall be returned, or NULL - * to return an array with info about all types. - */ -function entity_get_info($entity_type = NULL) { - global $language; - - // Use the advanced drupal_static() pattern, since this is called very often. - static $drupal_static_fast; - if (!isset($drupal_static_fast)) { - $drupal_static_fast['entity_info'] = &drupal_static(__FUNCTION__); - } - $entity_info = &$drupal_static_fast['entity_info']; - - // hook_entity_info() includes translated strings, so each language is cached - // separately. - $langcode = $language->language; - - if (empty($entity_info)) { - if ($cache = cache()->get("entity_info:$langcode")) { - $entity_info = $cache->data; - } - else { - $entity_info = module_invoke_all('entity_info'); - // Merge in default values. - foreach ($entity_info as $name => $data) { - $entity_info[$name] += array( - 'fieldable' => FALSE, - 'controller class' => 'DrupalDefaultEntityController', - 'static cache' => TRUE, - 'field cache' => TRUE, - 'load hook' => $name . '_load', - 'bundles' => array(), - 'view modes' => array(), - 'entity keys' => array(), - 'translation' => array(), - ); - $entity_info[$name]['entity keys'] += array( - 'revision' => '', - 'bundle' => '', - ); - foreach ($entity_info[$name]['view modes'] as $view_mode => $view_mode_info) { - $entity_info[$name]['view modes'][$view_mode] += array( - 'custom settings' => FALSE, - ); - } - // If no bundle key is provided, assume a single bundle, named after - // the entity type. - if (empty($entity_info[$name]['entity keys']['bundle']) && empty($entity_info[$name]['bundles'])) { - $entity_info[$name]['bundles'] = array($name => array('label' => $entity_info[$name]['label'])); - } - // Prepare entity schema fields SQL info for - // DrupalEntityControllerInterface::buildQuery(). - if (isset($entity_info[$name]['base table'])) { - $entity_info[$name]['schema_fields_sql']['base table'] = drupal_schema_fields_sql($entity_info[$name]['base table']); - if (isset($entity_info[$name]['revision table'])) { - $entity_info[$name]['schema_fields_sql']['revision table'] = drupal_schema_fields_sql($entity_info[$name]['revision table']); - } - } - } - // Let other modules alter the entity info. - drupal_alter('entity_info', $entity_info); - cache()->set("entity_info:$langcode", $entity_info); - } - } - - if (empty($entity_type)) { - return $entity_info; - } - elseif (isset($entity_info[$entity_type])) { - return $entity_info[$entity_type]; - } -} - -/** - * Resets the cached information about entity types. - */ -function entity_info_cache_clear() { - drupal_static_reset('entity_get_info'); - // Clear all languages. - cache()->deletePrefix('entity_info:'); -} - -/** - * Helper function to extract id, vid, and bundle name from an entity. - * - * @param $entity_type - * The entity type; e.g. 'node' or 'user'. - * @param $entity - * The entity from which to extract values. - * @return - * A numerically indexed array (not a hash table) containing these - * elements: - * 0: primary id of the entity - * 1: revision id of the entity, or NULL if $entity_type is not versioned - * 2: bundle name of the entity - */ -function entity_extract_ids($entity_type, $entity) { - $info = entity_get_info($entity_type); - - // Objects being created might not have id/vid yet. - $id = isset($entity->{$info['entity keys']['id']}) ? $entity->{$info['entity keys']['id']} : NULL; - $vid = ($info['entity keys']['revision'] && isset($entity->{$info['entity keys']['revision']})) ? $entity->{$info['entity keys']['revision']} : NULL; - - if (!empty($info['entity keys']['bundle'])) { - // Explicitly fail for malformed entities missing the bundle property. - if (!isset($entity->{$info['entity keys']['bundle']}) || $entity->{$info['entity keys']['bundle']} === '') { - throw new EntityMalformedException(t('Missing bundle property on entity of type @entity_type.', array('@entity_type' => $entity_type))); - } - $bundle = $entity->{$info['entity keys']['bundle']}; - } - else { - // The entity type provides no bundle key: assume a single bundle, named - // after the entity type. - $bundle = $entity_type; - } - - return array($id, $vid, $bundle); -} - -/** - * Helper function to assemble an object structure with initial ids. - * - * This function can be seen as reciprocal to entity_extract_ids(). - * - * @param $entity_type - * The entity type; e.g. 'node' or 'user'. - * @param $ids - * A numerically indexed array, as returned by entity_extract_ids(), - * containing these elements: - * 0: primary id of the entity - * 1: revision id of the entity, or NULL if $entity_type is not versioned - * 2: bundle name of the entity, or NULL if $entity_type has no bundles - * @return - * An entity structure, initialized with the ids provided. - */ -function entity_create_stub_entity($entity_type, $ids) { - $entity = new stdClass(); - $info = entity_get_info($entity_type); - $entity->{$info['entity keys']['id']} = $ids[0]; - if (!empty($info['entity keys']['revision']) && isset($ids[1])) { - $entity->{$info['entity keys']['revision']} = $ids[1]; - } - if (!empty($info['entity keys']['bundle']) && isset($ids[2])) { - $entity->{$info['entity keys']['bundle']} = $ids[2]; - } - return $entity; -} - -/** - * Load entities from the database. - * - * The entities are stored in a static memory cache, and will not require - * database access if loaded again during the same page request. - * - * The actual loading is done through a class that has to implement the - * DrupalEntityControllerInterface interface. By default, - * DrupalDefaultEntityController is used. Entity types can specify that a - * different class should be used by setting the 'controller class' key in - * hook_entity_info(). These classes can either implement the - * DrupalEntityControllerInterface interface, or, most commonly, extend the - * DrupalDefaultEntityController class. See node_entity_info() and the - * NodeController in node.module as an example. - * - * @see hook_entity_info() - * @see DrupalEntityControllerInterface - * @see DrupalDefaultEntityController - * @see EntityFieldQuery - * - * @param $entity_type - * The entity type to load, e.g. node or user. - * @param $ids - * An array of entity 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. - * @param $reset - * Whether to reset the internal cache for the requested entity type. - * - * @return - * An array of entity objects indexed by their ids. When no results are - * found, an empty array is returned. - * - * @todo Remove $conditions in Drupal 8. - */ -function entity_load($entity_type, $ids = FALSE, $conditions = array(), $reset = FALSE) { - if ($reset) { - entity_get_controller($entity_type)->resetCache(); - } - return entity_get_controller($entity_type)->load($ids, $conditions); -} - -/** - * Loads the unchanged, i.e. not modified, entity from the database. - * - * Unlike entity_load() this function ensures the entity is directly loaded from - * the database, thus bypassing any static cache. In particular, this function - * is useful to determine changes by comparing the entity being saved to the - * stored entity. - * - * @param $entity_type - * The entity type to load, e.g. node or user. - * @param $id - * The id of the entity to load. - * - * @return - * The unchanged entity, or FALSE if the entity cannot be loaded. - */ -function entity_load_unchanged($entity_type, $id) { - entity_get_controller($entity_type)->resetCache(array($id)); - $result = entity_get_controller($entity_type)->load(array($id)); - return reset($result); -} - -/** - * Get the entity controller class for an entity type. - */ -function entity_get_controller($entity_type) { - $controllers = &drupal_static(__FUNCTION__, array()); - if (!isset($controllers[$entity_type])) { - $type_info = entity_get_info($entity_type); - $class = $type_info['controller class']; - $controllers[$entity_type] = new $class($entity_type); - } - return $controllers[$entity_type]; -} - -/** - * Invoke hook_entity_prepare_view(). - * - * If adding a new entity similar to nodes, comments or users, you should - * invoke this function during the ENTITY_build_content() or - * ENTITY_view_multiple() phases of rendering to allow other modules to alter - * the objects during this phase. This is needed for situations where - * information needs to be loaded outside of ENTITY_load() - particularly - * when loading entities into one another - i.e. a user object into a node, due - * to the potential for unwanted side-effects such as caching and infinite - * recursion. By convention, entity_prepare_view() is called after - * field_attach_prepare_view() to allow entity level hooks to act on content - * loaded by field API. - * @see hook_entity_prepare_view() - * - * @param $entity_type - * The type of entity, i.e. 'node', 'user'. - * @param $entities - * The entity objects which are being prepared for view, keyed by object ID. - * @param $langcode - * (optional) A language code to be used for rendering. Defaults to the global - * content language of the current request. - */ -function entity_prepare_view($entity_type, $entities, $langcode = NULL) { - if (!isset($langcode)) { - $langcode = $GLOBALS['language_content']->language; - } - - // To ensure hooks are only run once per entity, check for an - // entity_view_prepared flag and only process items without it. - // @todo: resolve this more generally for both entity and field level hooks. - $prepare = array(); - foreach ($entities as $id => $entity) { - if (empty($entity->entity_view_prepared)) { - // Add this entity to the items to be prepared. - $prepare[$id] = $entity; - - // Mark this item as prepared. - $entity->entity_view_prepared = TRUE; - } - } - - if (!empty($prepare)) { - module_invoke_all('entity_prepare_view', $prepare, $entity_type, $langcode); - } -} - -/** - * Returns the uri elements of an entity. - * - * @param $entity_type - * The entity type; e.g. 'node' or 'user'. - * @param $entity - * The entity for which to generate a path. - * @return - * An array containing the 'path' and 'options' keys used to build the uri of - * the entity, and matching the signature of url(). NULL if the entity has no - * uri of its own. - */ -function entity_uri($entity_type, $entity) { - $info = entity_get_info($entity_type); - list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); - - // A bundle-specific callback takes precedence over the generic one for the - // entity type. - if (isset($info['bundles'][$bundle]['uri callback'])) { - $uri_callback = $info['bundles'][$bundle]['uri callback']; - } - elseif (isset($info['uri callback'])) { - $uri_callback = $info['uri callback']; - } - else { - return NULL; - } - - // Invoke the callback to get the URI. If there is no callback, return NULL. - if (isset($uri_callback) && function_exists($uri_callback)) { - $uri = $uri_callback($entity); - // Pass the entity data to url() so that alter functions do not need to - // lookup this entity again. - $uri['options']['entity_type'] = $entity_type; - $uri['options']['entity'] = $entity; - return $uri; - } -} - -/** - * Returns the label of an entity. - * - * See the 'label callback' component of the hook_entity_info() return value - * for more information. - * - * @param $entity_type - * The entity type; e.g., 'node' or 'user'. - * @param $entity - * The entity for which to generate the label. - * - * @return - * The entity label, or FALSE if not found. - */ -function entity_label($entity_type, $entity) { - $label = FALSE; - $info = entity_get_info($entity_type); - if (isset($info['label callback']) && function_exists($info['label callback'])) { - $label = $info['label callback']($entity_type, $entity); - } - elseif (!empty($info['entity keys']['label']) && isset($entity->{$info['entity keys']['label']})) { - $label = $entity->{$info['entity keys']['label']}; - } - - return $label; -} - -/** - * Helper function for attaching field API validation to entity forms. - */ -function entity_form_field_validate($entity_type, $form, &$form_state) { - // All field attach API functions act on an entity object, but during form - // validation, we don't have one. $form_state contains the entity as it was - // prior to processing the current form submission, and we must not update it - // until we have fully validated the submitted input. Therefore, for - // validation, act on a pseudo entity created out of the form values. - $pseudo_entity = (object) $form_state['values']; - field_attach_form_validate($entity_type, $pseudo_entity, $form, $form_state); -} - -/** - * Helper function for copying submitted values to entity properties for simple entity forms. - * - * During the submission handling of an entity form's "Save", "Preview", and - * possibly other buttons, the form state's entity needs to be updated with the - * submitted form values. Each entity form implements its own builder function - * for doing this, appropriate for the particular entity and form, whereas - * modules may specify additional builder functions in $form['#entity_builders'] - * for copying the form values of added form elements to entity properties. - * Many of the main entity builder functions can call this helper function to - * re-use its logic of copying $form_state['values'][PROPERTY] values to - * $entity->PROPERTY for all entries in $form_state['values'] that are not field - * data, and calling field_attach_submit() to copy field data. Apart from that - * this helper invokes any additional builder functions that have been specified - * in $form['#entity_builders']. - * - * For some entity forms (e.g., forms with complex non-field data and forms that - * simultaneously edit multiple entities), this behavior may be inappropriate, - * so the builder function for such forms needs to implement the required - * functionality instead of calling this function. - */ -function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_state) { - $info = entity_get_info($entity_type); - list(, , $bundle) = entity_extract_ids($entity_type, $entity); - - // Copy top-level form values that are not for fields to entity properties, - // without changing existing entity properties that are not being edited by - // this form. Copying field values must be done using field_attach_submit(). - $values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $bundle)) : $form_state['values']; - foreach ($values_excluding_fields as $key => $value) { - $entity->$key = $value; - } - - // Invoke all specified builders for copying form values to entity properties. - if (isset($form['#entity_builders'])) { - foreach ($form['#entity_builders'] as $function) { - $function($entity_type, $entity, $form, $form_state); - } - } - - // Copy field values to the entity. - if ($info['fieldable']) { - field_attach_submit($entity_type, $entity, $form, $form_state); - } -} - /** * Performs one or more XML-RPC request(s). * diff --git a/includes/install.core.inc b/includes/install.core.inc index 1040bf3e550d..3791d71b56ce 100644 --- a/includes/install.core.inc +++ b/includes/install.core.inc @@ -253,12 +253,13 @@ function install_begin_request(&$install_state) { // Set up $language, so t() caller functions will still work. drupal_language_initialize(); - include_once DRUPAL_ROOT . '/includes/entity.inc'; require_once DRUPAL_ROOT . '/includes/ajax.inc'; $module_list['system']['filename'] = 'modules/system/system.module'; - $module_list['user']['filename'] = 'modules/user/user.module'; + $module_list['entity']['filename'] = 'modules/entity/entity.module'; + $module_list['user']['filename'] = 'modules/user/user.module'; module_list(TRUE, FALSE, FALSE, $module_list); drupal_load('module', 'system'); + drupal_load('module', 'entity'); drupal_load('module', 'user'); // Load the cache infrastructure using a "fake" cache implementation that diff --git a/includes/module.inc b/includes/module.inc index cc3aa8eecb14..77fc98ac87e2 100644 --- a/includes/module.inc +++ b/includes/module.inc @@ -424,8 +424,9 @@ function module_enable($module_list, $enable_dependencies = TRUE) { registry_update(); // Refresh the schema to include it. drupal_get_schema(NULL, TRUE); - // Clear entity cache. - entity_info_cache_clear(); + + // Allow modules to react prior to the installation of a module. + module_invoke_all('modules_preinstall', array($module)); // Now install the module if necessary. if (drupal_get_installed_schema_version($module, TRUE) == SCHEMA_UNINSTALLED) { @@ -450,6 +451,9 @@ function module_enable($module_list, $enable_dependencies = TRUE) { watchdog('system', '%module module installed.', array('%module' => $module), WATCHDOG_INFO); } + // Allow modules to react prior to the enabling of a module. + module_invoke_all('modules_preenable', array($module)); + // Enable the module. module_invoke($module, 'enable'); diff --git a/includes/update.inc b/includes/update.inc index cbee34e0dc0e..416d2033a09e 100644 --- a/includes/update.inc +++ b/includes/update.inc @@ -82,6 +82,7 @@ function update_prepare_d8_bootstrap() { // Allow the database system to work even if the registry has not been // created yet. include_once DRUPAL_ROOT . '/includes/install.inc'; + include_once DRUPAL_ROOT . '/modules/entity/entity.controller.inc'; drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE); // If the site has not updated to Drupal 8 yet, check to make sure that it is @@ -120,6 +121,35 @@ function update_fix_d8_requirements() { } } +/** + * Helper function to install a new module in Drupal 8 via hook_update_N(). + */ +function update_module_enable(array $modules) { + foreach ($modules as $module) { + // Check for initial schema and install it. The schema version of a newly + // installed module is always 0. Using 8000 here would be inconsistent + // since $module_update_8000() may involve a schema change, and we want + // to install the schema as it was before any updates were added. + $function = $module . '_schema_0'; + if (function_exists($function)) { + $schema = $function(); + foreach ($schema as $table => $spec) { + db_create_table($table, $spec); + } + } + // Change the schema version from SCHEMA_UNINSTALLED to 0, so any module + // updates since the module's inception are executed in a core upgrade. + db_update('system') + ->condition('type', 'module') + ->condition('name', $module) + ->fields(array('schema_version' => 0)) + ->execute(); + + system_list_reset(); + // @todo: figure out what to do about hook_install() and hook_enable(). + } +} + /** * Perform one update and store the results for display on finished page. * diff --git a/modules/comment/comment.info b/modules/comment/comment.info index a5837af83ab1..949ffc2a830b 100644 --- a/modules/comment/comment.info +++ b/modules/comment/comment.info @@ -4,6 +4,7 @@ package = Core version = VERSION core = 8.x dependencies[] = text +dependencies[] = entity files[] = comment.module files[] = comment.test configure = admin/content/comment diff --git a/modules/entity/entity.api.php b/modules/entity/entity.api.php new file mode 100644 index 000000000000..9b19477163bf --- /dev/null +++ b/modules/entity/entity.api.php @@ -0,0 +1,414 @@ +subject, then + * 'subject' should be specified here. If complex logic is required to + * build the label, a 'label callback' should be defined instead (see + * the 'label callback' section above for details). + * - bundle keys: An array describing how the Field API can extract the + * information it needs from the bundle objects for this type (e.g + * $vocabulary objects for terms; not applicable for nodes). This entry can + * be omitted if this type's bundles do not exist as standalone objects. + * Elements: + * - bundle: The name of the property that contains the name of the bundle + * object. + * - bundles: An array describing all bundles for this object type. Keys are + * bundles machine names, as found in the objects' 'bundle' property + * (defined in the 'entity keys' entry above). Elements: + * - label: The human-readable name of the bundle. + * - uri callback: Same as the 'uri callback' key documented above for the + * entity type, but for the bundle only. When determining the URI of an + * entity, if a 'uri callback' is defined for both the entity type and + * the bundle, the one for the bundle is used. + * - admin: An array of information that allows Field UI pages to attach + * themselves to the existing administration pages for the bundle. + * Elements: + * - path: the path of the bundle's main administration page, as defined + * in hook_menu(). If the path includes a placeholder for the bundle, + * the 'bundle argument', 'bundle helper' and 'real path' keys below + * are required. + * - bundle argument: The position of the placeholder in 'path', if any. + * - real path: The actual path (no placeholder) of the bundle's main + * administration page. This will be used to generate links. + * - access callback: As in hook_menu(). 'user_access' will be assumed if + * no value is provided. + * - access arguments: As in hook_menu(). + * - view modes: An array describing the view modes for the entity type. View + * modes let entities be displayed differently depending on the context. + * For instance, a node can be displayed differently on its own page + * ('full' mode), on the home page or taxonomy listings ('teaser' mode), or + * in an RSS feed ('rss' mode). Modules taking part in the display of the + * entity (notably the Field API) can adjust their behavior depending on + * the requested view mode. An additional 'default' view mode is available + * for all entity types. This view mode is not intended for actual entity + * display, but holds default display settings. For each available view + * mode, administrators can configure whether it should use its own set of + * field display settings, or just replicate the settings of the 'default' + * view mode, thus reducing the amount of display configurations to keep + * track of. Keys of the array are view mode names. Each view mode is + * described by an array with the following key/value pairs: + * - label: The human-readable name of the view mode + * - custom settings: A boolean specifying whether the view mode should by + * default use its own custom field display settings. If FALSE, entities + * displayed in this view mode will reuse the 'default' display settings + * by default (e.g. right after the module exposing the view mode is + * enabled), but administrators can later use the Field UI to apply custom + * display settings specific to the view mode. + * + * @see entity_load() + * @see hook_entity_info_alter() + */ +function hook_entity_info() { + $return = array( + 'node' => array( + 'label' => t('Node'), + 'controller class' => 'NodeController', + 'base table' => 'node', + 'revision table' => 'node_revision', + 'uri callback' => 'node_uri', + 'fieldable' => TRUE, + 'translation' => array( + 'locale' => TRUE, + ), + 'entity keys' => array( + 'id' => 'nid', + 'revision' => 'vid', + 'bundle' => 'type', + ), + 'bundle keys' => array( + 'bundle' => 'type', + ), + 'bundles' => array(), + 'view modes' => array( + 'full' => array( + 'label' => t('Full content'), + 'custom settings' => FALSE, + ), + 'teaser' => array( + 'label' => t('Teaser'), + 'custom settings' => TRUE, + ), + 'rss' => array( + 'label' => t('RSS'), + 'custom settings' => FALSE, + ), + ), + ), + ); + + // Search integration is provided by node.module, so search-related + // view modes for nodes are defined here and not in search.module. + if (module_exists('search')) { + $return['node']['view modes'] += array( + 'search_index' => array( + 'label' => t('Search index'), + 'custom settings' => FALSE, + ), + 'search_result' => array( + 'label' => t('Search result'), + 'custom settings' => FALSE, + ), + ); + } + + // Bundles must provide a human readable name so we can create help and error + // messages, and the path to attach Field admin pages to. + foreach (node_type_get_names() as $type => $name) { + $return['node']['bundles'][$type] = array( + 'label' => $name, + 'admin' => array( + 'path' => 'admin/structure/types/manage/%node_type', + 'real path' => 'admin/structure/types/manage/' . str_replace('_', '-', $type), + 'bundle argument' => 4, + 'access arguments' => array('administer content types'), + ), + ); + } + + return $return; +} + +/** + * Alter the entity info. + * + * Modules may implement this hook to alter the information that defines an + * entity. All properties that are available in hook_entity_info() can be + * altered here. + * + * @param $entity_info + * The entity info array, keyed by entity name. + * + * @see hook_entity_info() + */ +function hook_entity_info_alter(&$entity_info) { + // Set the controller class for nodes to an alternate implementation of the + // DrupalEntityController interface. + $entity_info['node']['controller class'] = 'MyCustomNodeController'; +} + +/** + * Act on entities when loaded. + * + * This is a generic load hook called for all entity types loaded via the + * entity API. + * + * @param $entities + * The entities keyed by entity ID. + * @param $type + * The type of entities being loaded (i.e. node, user, comment). + */ +function hook_entity_load($entities, $type) { + foreach ($entities as $entity) { + $entity->foo = mymodule_add_something($entity, $type); + } +} + +/** + * Act on an entity before it is about to be created or updated. + * + * @param $entity + * The entity object. + * @param $type + * The type of entity being saved (i.e. node, user, comment). + */ +function hook_entity_presave($entity, $type) { + $entity->changed = REQUEST_TIME; +} + +/** + * Act on entities when inserted. + * + * @param $entity + * The entity object. + * @param $type + * The type of entity being inserted (i.e. node, user, comment). + */ +function hook_entity_insert($entity, $type) { + // Insert the new entity into a fictional table of all entities. + $info = entity_get_info($type); + list($id) = entity_extract_ids($type, $entity); + db_insert('example_entity') + ->fields(array( + 'type' => $type, + 'id' => $id, + 'created' => REQUEST_TIME, + 'updated' => REQUEST_TIME, + )) + ->execute(); +} + +/** + * Act on entities when updated. + * + * @param $entity + * The entity object. + * @param $type + * The type of entity being updated (i.e. node, user, comment). + */ +function hook_entity_update($entity, $type) { + // Update the entity's entry in a fictional table of all entities. + $info = entity_get_info($type); + list($id) = entity_extract_ids($type, $entity); + db_update('example_entity') + ->fields(array( + 'updated' => REQUEST_TIME, + )) + ->condition('type', $type) + ->condition('id', $id) + ->execute(); +} + +/** + * Act on entities when deleted. + * + * @param $entity + * The entity object. + * @param $type + * The type of entity being deleted (i.e. node, user, comment). + */ +function hook_entity_delete($entity, $type) { + // Delete the entity's entry from a fictional table of all entities. + $info = entity_get_info($type); + list($id) = entity_extract_ids($type, $entity); + db_delete('example_entity') + ->condition('type', $type) + ->condition('id', $id) + ->execute(); +} + +/** + * Alter or execute an EntityFieldQuery. + * + * @param EntityFieldQuery $query + * An EntityFieldQuery. One of the most important properties to be changed is + * EntityFieldQuery::executeCallback. If this is set to an existing function, + * this function will get the query as its single argument and its result + * will be the returned as the result of EntityFieldQuery::execute(). This can + * be used to change the behavior of EntityFieldQuery entirely. For example, + * the default implementation can only deal with one field storage engine, but + * it is possible to write a module that can query across field storage + * engines. Also, the default implementation presumes entities are stored in + * SQL, but the execute callback could instead query any other entity storage, + * local or remote. + * + * Note the $query->altered attribute which is TRUE in case the query has + * already been altered once. This happens with cloned queries. + * If there is a pager, then such a cloned query will be executed to count + * all elements. This query can be detected by checking for + * ($query->pager && $query->count), allowing the driver to return 0 from + * the count query and disable the pager. + */ +function hook_entity_query_alter($query) { + $query->executeCallback = 'my_module_query_callback'; +} + +/** + * Act on entities being assembled before rendering. + * + * @param $entity + * The entity object. + * @param $type + * The type of entity being rendered (i.e. node, user, comment). + * @param $view_mode + * The view mode the entity is rendered in. + * @param $langcode + * The language code used for rendering. + * + * The module may add elements to $entity->content prior to rendering. The + * structure of $entity->content is a renderable array as expected by + * drupal_render(). + * + * @see hook_entity_view_alter() + * @see hook_comment_view() + * @see hook_node_view() + * @see hook_user_view() + */ +function hook_entity_view($entity, $type, $view_mode, $langcode) { + $entity->content['my_additional_field'] = array( + '#markup' => $additional_field, + '#weight' => 10, + '#theme' => 'mymodule_my_additional_field', + ); +} + +/** + * Alter the results of ENTITY_view(). + * + * This hook is called after the content has been assembled in a structured + * array and may be used for doing processing which requires that the complete + * entity content structure has been built. + * + * If a module wishes to act on the rendered HTML of the entity rather than the + * structured content array, it may use this hook to add a #post_render + * callback. Alternatively, it could also implement hook_preprocess_ENTITY(). + * See drupal_render() and theme() for details. + * + * @param $build + * A renderable array representing the entity content. + * @param $type + * The type of entity being rendered (i.e. node, user, comment). + * + * @see hook_entity_view() + * @see hook_comment_view_alter() + * @see hook_node_view_alter() + * @see hook_taxonomy_term_view_alter() + * @see hook_user_view_alter() + */ +function hook_entity_view_alter(&$build, $type) { + if ($build['#view_mode'] == 'full' && isset($build['an_additional_field'])) { + // Change its weight. + $build['an_additional_field']['#weight'] = -10; + + // Add a #post_render callback to act on the rendered HTML of the entity. + $build['#post_render'][] = 'my_module_node_post_render'; + } +} + +/** + * Act on entities as they are being prepared for view. + * + * Allows you to operate on multiple entities as they are being prepared for + * view. Only use this if attaching the data during the entity_load() phase + * is not appropriate, for example when attaching other 'entity' style objects. + * + * @param $entities + * The entities keyed by entity ID. + * @param $type + * The type of entities being loaded (i.e. node, user, comment). + */ +function hook_entity_prepare_view($entities, $type) { + // Load a specific node into the user object for later theming. + if ($type == 'user') { + $nodes = mymodule_get_user_nodes(array_keys($entities)); + foreach ($entities as $uid => $entity) { + $entity->user_node = $nodes[$uid]; + } + } +} diff --git a/modules/entity/entity.controller.inc b/modules/entity/entity.controller.inc new file mode 100644 index 000000000000..8327bc6d0682 --- /dev/null +++ b/modules/entity/entity.controller.inc @@ -0,0 +1,390 @@ + $value. + * + * @return + * An array of entity objects indexed by their ids. + */ + public function load($ids = array(), $conditions = array()); +} + +/** + * Default implementation of DrupalEntityControllerInterface. + * + * This class can be used as-is by most simple entity types. Entity types + * requiring special handling can extend the class. + */ +class DrupalDefaultEntityController implements DrupalEntityControllerInterface { + + /** + * Static cache of entities. + * + * @var array + */ + protected $entityCache; + + /** + * Entity type for this controller instance. + * + * @var string + */ + protected $entityType; + + /** + * Array of information about the entity. + * + * @var array + * + * @see entity_get_info() + */ + protected $entityInfo; + + /** + * Additional arguments to pass to hook_TYPE_load(). + * + * Set before calling DrupalDefaultEntityController::attachLoad(). + * + * @var array + */ + protected $hookLoadArguments; + + /** + * Name of the entity's ID field in the entity database table. + * + * @var string + */ + protected $idKey; + + /** + * Name of entity's revision database table field, if it supports revisions. + * + * Has the value FALSE if this entity does not use revisions. + * + * @var string + */ + protected $revisionKey; + + /** + * The table that stores revisions, if the entity supports revisions. + * + * @var string + */ + protected $revisionTable; + + /** + * Whether this entity type should use the static cache. + * + * Set by entity info. + * + * @var boolean + */ + protected $cache; + + /** + * Constructor: sets basic variables. + */ + public function __construct($entityType) { + $this->entityType = $entityType; + $this->entityInfo = entity_get_info($entityType); + $this->entityCache = array(); + $this->hookLoadArguments = array(); + $this->idKey = $this->entityInfo['entity keys']['id']; + + // Check if the entity type supports revisions. + if (!empty($this->entityInfo['entity keys']['revision'])) { + $this->revisionKey = $this->entityInfo['entity keys']['revision']; + $this->revisionTable = $this->entityInfo['revision table']; + } + else { + $this->revisionKey = FALSE; + } + + // Check if the entity type supports static caching of loaded entities. + $this->cache = !empty($this->entityInfo['static cache']); + } + + /** + * Implements DrupalEntityControllerInterface::resetCache(). + */ + public function resetCache(array $ids = NULL) { + if (isset($ids)) { + foreach ($ids as $id) { + unset($this->entityCache[$id]); + } + } + else { + $this->entityCache = array(); + } + } + + /** + * Implements DrupalEntityControllerInterface::load(). + */ + public function load($ids = array(), $conditions = array()) { + $entities = array(); + + // 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; + } + + // Create a new variable which is either a prepared version of the $ids + // array for later comparison with the entity cache, or FALSE if no $ids + // were passed. The $ids array is reduced as items are loaded from cache, + // and we need to know if it's empty for this reason to avoid querying the + // database when all requested entities are loaded from cache. + $passed_ids = !empty($ids) ? array_flip($ids) : FALSE; + // Try to load entities from the static cache, if the entity type supports + // static caching. + 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) { + $ids = array_keys(array_diff_key($passed_ids, $entities)); + } + } + + // Load any remaining entities from the database. This is the case if $ids + // is set to FALSE (so we load all entities), if there are any ids left to + // load, if loading a revision, or if $conditions was passed without $ids. + if ($ids === FALSE || $ids || $revision_id || ($conditions && !$passed_ids)) { + // Build the query. + $query = $this->buildQuery($ids, $conditions, $revision_id); + $queried_entities = $query + ->execute() + ->fetchAllAssoc($this->idKey); + } + + // Pass all entities loaded from the database through $this->attachLoad(), + // which attaches fields (if supported by the entity type) and calls the + // entity type specific load callback, for example hook_node_load(). + if (!empty($queried_entities)) { + $this->attachLoad($queried_entities, $revision_id); + $entities += $queried_entities; + } + + if ($this->cache) { + // Add entities to the cache if we are not loading a revision. + if (!empty($queried_entities) && !$revision_id) { + $this->cacheSet($queried_entities); + } + } + + // Ensure that the returned array is ordered the same as the original + // $ids array if this was passed in and remove any invalid ids. + if ($passed_ids) { + // Remove any invalid ids from the array. + $passed_ids = array_intersect_key($passed_ids, $entities); + foreach ($entities as $entity) { + $passed_ids[$entity->{$this->idKey}] = $entity; + } + $entities = $passed_ids; + } + + return $entities; + } + + /** + * Builds the query to load the entity. + * + * This has full revision support. For entities requiring special queries, + * the class can be extended, and the default query can be constructed by + * calling parent::buildQuery(). This is usually necessary when the object + * being loaded needs to be augmented with additional data from another + * table, such as loading node type into comments or vocabulary machine name + * into terms, however it can also support $conditions on different tables. + * See CommentController::buildQuery() or TaxonomyTermController::buildQuery() + * for examples. + * + * @param $ids + * An array of entity IDs, or FALSE to load all entities. + * @param $conditions + * An array of conditions in the form 'field' => $value. + * @param $revision_id + * The ID of the revision to load, or FALSE if this query is asking for the + * most current revision(s). + * + * @return SelectQuery + * A SelectQuery object for loading the entity. + */ + protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { + $query = db_select($this->entityInfo['base table'], 'base'); + + $query->addTag($this->entityType . '_load_multiple'); + + if ($revision_id) { + $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", array(':revisionId' => $revision_id)); + } + elseif ($this->revisionKey) { + $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}"); + } + + // Add fields from the {entity} table. + $entity_fields = $this->entityInfo['schema_fields_sql']['base table']; + + if ($this->revisionKey) { + // Add all fields from the {entity_revision} table. + $entity_revision_fields = drupal_map_assoc($this->entityInfo['schema_fields_sql']['revision table']); + // The id field is provided by entity, so remove it. + unset($entity_revision_fields[$this->idKey]); + + // Remove all fields from the base table that are also fields by the same + // name in the revision table. + $entity_field_keys = array_flip($entity_fields); + foreach ($entity_revision_fields as $key => $name) { + if (isset($entity_field_keys[$name])) { + unset($entity_fields[$entity_field_keys[$name]]); + } + } + $query->fields('revision', $entity_revision_fields); + } + + $query->fields('base', $entity_fields); + + if ($ids) { + $query->condition("base.{$this->idKey}", $ids, 'IN'); + } + if ($conditions) { + foreach ($conditions as $field => $value) { + $query->condition('base.' . $field, $value); + } + } + return $query; + } + + /** + * Attaches data to entities upon loading. + * + * This will attach fields, if the entity is fieldable. It calls + * hook_entity_load() for modules which need to add data to all entities. + * It also calls hook_TYPE_load() on the loaded entities. For example + * hook_node_load() or hook_user_load(). If your hook_TYPE_load() + * expects special parameters apart from the queried entities, you can set + * $this->hookLoadArguments prior to calling the method. + * See NodeController::attachLoad() for an example. + * + * @param $queried_entities + * Associative array of query results, keyed on the entity ID. + * @param $revision_id + * ID of the revision that was loaded, or FALSE if teh most current revision + * was loaded. + */ + 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. + $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); + } + } + + /** + * Gets entities from the static cache. + * + * @param $ids + * If not empty, return entities that match these IDs. + * @param $conditions + * If set, return entities that match all of these conditions. + * + * @return + * Array of entities from the entity cache. + */ + protected function cacheGet($ids, $conditions = array()) { + $entities = array(); + // Load any available entities from the internal cache. + if (!empty($this->entityCache)) { + if ($ids) { + $entities += array_intersect_key($this->entityCache, array_flip($ids)); + } + // If loading entities only by conditions, fetch all available entities + // from the cache. Entities which don't match are removed later. + elseif ($conditions) { + $entities = $this->entityCache; + } + } + + // Exclude any entities loaded from cache if they don't match $conditions. + // This ensures the same behavior whether loading from memory or database. + if ($conditions) { + foreach ($entities as $entity) { + $entity_values = (array) $entity; + if (array_diff_assoc($conditions, $entity_values)) { + unset($entities[$entity->{$this->idKey}]); + } + } + } + return $entities; + } + + /** + * Stores entities in the static entity cache. + * + * @param $entities + * Entities to store in the cache. + */ + protected function cacheSet($entities) { + $this->entityCache += $entities; + } +} diff --git a/modules/entity/entity.info b/modules/entity/entity.info new file mode 100644 index 000000000000..31eb720d0a7c --- /dev/null +++ b/modules/entity/entity.info @@ -0,0 +1,8 @@ +name = Entity +description = API for managing entities like nodes and users. +package = Core +version = VERSION +core = 8.x +required = TRUE +files[] = entity.query.inc +files[] = entity.controller.inc diff --git a/modules/entity/entity.module b/modules/entity/entity.module new file mode 100644 index 000000000000..02611e3c4910 --- /dev/null +++ b/modules/entity/entity.module @@ -0,0 +1,442 @@ +' . t('About') . ''; + $output .= '

' . t('The Entity module provides an API for managing entities like nodes and users, i.e. an API for loading and identifying entities. For more information, see the online handbook entry for Entity module', array('!url' => 'http://drupal.org/handbook/modules/entity')) . '

'; + return $output; + } +} + +/** + * Implements hook_modules_preenable(). + */ +function entity_modules_preenable() { + entity_info_cache_clear(); +} + +/** + * Implements hook_modules_disabled(). + */ +function entity_modules_disabled() { + entity_info_cache_clear(); +} + +/** + * Gets the entity info array of an entity type. + * + * @see hook_entity_info() + * @see hook_entity_info_alter() + * + * @param $entity_type + * The entity type, e.g. node, for which the info shall be returned, or NULL + * to return an array with info about all types. + */ +function entity_get_info($entity_type = NULL) { + global $language; + + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['entity_info'] = &drupal_static(__FUNCTION__); + } + $entity_info = &$drupal_static_fast['entity_info']; + + // hook_entity_info() includes translated strings, so each language is cached + // separately. + $langcode = $language->language; + + if (empty($entity_info)) { + if ($cache = cache()->get("entity_info:$langcode")) { + $entity_info = $cache->data; + } + else { + $entity_info = module_invoke_all('entity_info'); + // Merge in default values. + foreach ($entity_info as $name => $data) { + $entity_info[$name] += array( + 'fieldable' => FALSE, + 'controller class' => 'DrupalDefaultEntityController', + 'static cache' => TRUE, + 'field cache' => TRUE, + 'load hook' => $name . '_load', + 'bundles' => array(), + 'view modes' => array(), + 'entity keys' => array(), + 'translation' => array(), + ); + $entity_info[$name]['entity keys'] += array( + 'revision' => '', + 'bundle' => '', + ); + foreach ($entity_info[$name]['view modes'] as $view_mode => $view_mode_info) { + $entity_info[$name]['view modes'][$view_mode] += array( + 'custom settings' => FALSE, + ); + } + // If no bundle key is provided, assume a single bundle, named after + // the entity type. + if (empty($entity_info[$name]['entity keys']['bundle']) && empty($entity_info[$name]['bundles'])) { + $entity_info[$name]['bundles'] = array($name => array('label' => $entity_info[$name]['label'])); + } + // Prepare entity schema fields SQL info for + // DrupalEntityControllerInterface::buildQuery(). + if (isset($entity_info[$name]['base table'])) { + $entity_info[$name]['schema_fields_sql']['base table'] = drupal_schema_fields_sql($entity_info[$name]['base table']); + if (isset($entity_info[$name]['revision table'])) { + $entity_info[$name]['schema_fields_sql']['revision table'] = drupal_schema_fields_sql($entity_info[$name]['revision table']); + } + } + } + // Let other modules alter the entity info. + drupal_alter('entity_info', $entity_info); + cache()->set("entity_info:$langcode", $entity_info); + } + } + + if (empty($entity_type)) { + return $entity_info; + } + elseif (isset($entity_info[$entity_type])) { + return $entity_info[$entity_type]; + } +} + +/** + * Resets the cached information about entity types. + */ +function entity_info_cache_clear() { + drupal_static_reset('entity_get_info'); + // Clear all languages. + cache()->deletePrefix('entity_info:'); +} + +/** + * Helper function to extract id, vid, and bundle name from an entity. + * + * @param $entity_type + * The entity type; e.g. 'node' or 'user'. + * @param $entity + * The entity from which to extract values. + * @return + * A numerically indexed array (not a hash table) containing these + * elements: + * 0: primary id of the entity + * 1: revision id of the entity, or NULL if $entity_type is not versioned + * 2: bundle name of the entity + */ +function entity_extract_ids($entity_type, $entity) { + $info = entity_get_info($entity_type); + + // Objects being created might not have id/vid yet. + $id = isset($entity->{$info['entity keys']['id']}) ? $entity->{$info['entity keys']['id']} : NULL; + $vid = ($info['entity keys']['revision'] && isset($entity->{$info['entity keys']['revision']})) ? $entity->{$info['entity keys']['revision']} : NULL; + + if (!empty($info['entity keys']['bundle'])) { + // Explicitly fail for malformed entities missing the bundle property. + if (!isset($entity->{$info['entity keys']['bundle']}) || $entity->{$info['entity keys']['bundle']} === '') { + throw new EntityMalformedException(t('Missing bundle property on entity of type @entity_type.', array('@entity_type' => $entity_type))); + } + $bundle = $entity->{$info['entity keys']['bundle']}; + } + else { + // The entity type provides no bundle key: assume a single bundle, named + // after the entity type. + $bundle = $entity_type; + } + + return array($id, $vid, $bundle); +} + +/** + * Helper function to assemble an object structure with initial ids. + * + * This function can be seen as reciprocal to entity_extract_ids(). + * + * @param $entity_type + * The entity type; e.g. 'node' or 'user'. + * @param $ids + * A numerically indexed array, as returned by entity_extract_ids(), + * containing these elements: + * 0: primary id of the entity + * 1: revision id of the entity, or NULL if $entity_type is not versioned + * 2: bundle name of the entity, or NULL if $entity_type has no bundles + * + * @return + * An entity structure, initialized with the ids provided. + */ +function entity_create_stub_entity($entity_type, $ids) { + $entity = new stdClass(); + $info = entity_get_info($entity_type); + $entity->{$info['entity keys']['id']} = $ids[0]; + if (!empty($info['entity keys']['revision']) && isset($ids[1])) { + $entity->{$info['entity keys']['revision']} = $ids[1]; + } + if (!empty($info['entity keys']['bundle']) && isset($ids[2])) { + $entity->{$info['entity keys']['bundle']} = $ids[2]; + } + return $entity; +} + +/** + * Loads entities from the database. + * + * This function should be used whenever you need to load more than one entity + * from the database. The entities are loaded into memory and will not require + * database access if loaded again during the same page request. + * + * The actual loading is done through a class that has to implement the + * DrupalEntityControllerInterface interface. By default, + * DrupalDefaultEntityController is used. Entity types can specify that a + * different class should be used by setting the 'controller class' key in + * hook_entity_info(). These classes can either implement the + * DrupalEntityControllerInterface interface, or, most commonly, extend the + * DrupalDefaultEntityController class. See node_entity_info() and the + * NodeController in node.module as an example. + * + * @see hook_entity_info() + * @see DrupalEntityControllerInterface + * @see DrupalDefaultEntityController + * @see EntityFieldQuery + * + * @param $entity_type + * The entity type to load, e.g. node or user. + * @param $ids + * An array of entity 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. + * @param $reset + * Whether to reset the internal cache for the requested entity type. + * + * @return + * An array of entity objects indexed by their ids. + * + * @todo Remove $conditions in Drupal 8. + */ +function entity_load($entity_type, $ids = FALSE, $conditions = array(), $reset = FALSE) { + if ($reset) { + entity_get_controller($entity_type)->resetCache(); + } + return entity_get_controller($entity_type)->load($ids, $conditions); +} + +/** + * Loads the unchanged, i.e. not modified, entity from the database. + * + * Unlike entity_load() this function ensures the entity is directly loaded from + * the database, thus bypassing any static cache. In particular, this function + * is useful to determine changes by comparing the entity being saved to the + * stored entity. + * + * @param $entity_type + * The entity type to load, e.g. node or user. + * @param $id + * The id of the entity to load. + * + * @return + * The unchanged entity, or FALSE if the entity cannot be loaded. + */ +function entity_load_unchanged($entity_type, $id) { + entity_get_controller($entity_type)->resetCache(array($id)); + $result = entity_get_controller($entity_type)->load(array($id)); + return reset($result); +} + +/** + * Gets the entity controller class for an entity type. + */ +function entity_get_controller($entity_type) { + $controllers = &drupal_static(__FUNCTION__, array()); + if (!isset($controllers[$entity_type])) { + $type_info = entity_get_info($entity_type); + $class = $type_info['controller class']; + $controllers[$entity_type] = new $class($entity_type); + } + return $controllers[$entity_type]; +} + +/** + * Invokes hook_entity_prepare_view(). + * + * If adding a new entity similar to nodes, comments or users, you should + * invoke this function during the ENTITY_build_content() or + * ENTITY_view_multiple() phases of rendering to allow other modules to alter + * the objects during this phase. This is needed for situations where + * information needs to be loaded outside of ENTITY_load() - particularly + * when loading entities into one another - i.e. a user object into a node, due + * to the potential for unwanted side-effects such as caching and infinite + * recursion. By convention, entity_prepare_view() is called after + * field_attach_prepare_view() to allow entity level hooks to act on content + * loaded by field API. + * + * @see hook_entity_prepare_view() + * + * @param $entity_type + * The type of entity, i.e. 'node', 'user'. + * @param $entities + * The entity objects which are being prepared for view, keyed by object ID. + */ +function entity_prepare_view($entity_type, $entities) { + // To ensure hooks are only run once per entity, check for an + // entity_view_prepared flag and only process items without it. + // @todo: resolve this more generally for both entity and field level hooks. + $prepare = array(); + foreach ($entities as $id => $entity) { + if (empty($entity->entity_view_prepared)) { + // Add this entity to the items to be prepared. + $prepare[$id] = $entity; + + // Mark this item as prepared. + $entity->entity_view_prepared = TRUE; + } + } + + if (!empty($prepare)) { + module_invoke_all('entity_prepare_view', $prepare, $entity_type); + } +} + +/** + * Returns the uri elements of an entity. + * + * @param $entity_type + * The entity type; e.g. 'node' or 'user'. + * @param $entity + * The entity for which to generate a path. + * + * @return + * An array containing the 'path' and 'options' keys used to build the uri of + * the entity, and matching the signature of url(). NULL if the entity has no + * uri of its own. + */ +function entity_uri($entity_type, $entity) { + $info = entity_get_info($entity_type); + list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); + + // A bundle-specific callback takes precedence over the generic one for the + // entity type. + if (isset($info['bundles'][$bundle]['uri callback'])) { + $uri_callback = $info['bundles'][$bundle]['uri callback']; + } + elseif (isset($info['uri callback'])) { + $uri_callback = $info['uri callback']; + } + else { + return NULL; + } + + // Invoke the callback to get the URI. If there is no callback, return NULL. + if (isset($uri_callback) && function_exists($uri_callback)) { + $uri = $uri_callback($entity); + // Pass the entity data to url() so that alter functions do not need to + // lookup this entity again. + $uri['options']['entity_type'] = $entity_type; + $uri['options']['entity'] = $entity; + return $uri; + } +} + +/** + * Returns the label of an entity. + * + * See the 'label callback' component of the hook_entity_info() return value + * for more information. + * + * @param $entity_type + * The entity type; e.g., 'node' or 'user'. + * @param $entity + * The entity for which to generate the label. + * + * @return + * The entity label, or FALSE if not found. + */ +function entity_label($entity_type, $entity) { + $label = FALSE; + $info = entity_get_info($entity_type); + if (isset($info['label callback']) && function_exists($info['label callback'])) { + $label = $info['label callback']($entity_type, $entity); + } + elseif (!empty($info['entity keys']['label']) && isset($entity->{$info['entity keys']['label']})) { + $label = $entity->{$info['entity keys']['label']}; + } + + return $label; +} + +/** + * Helper function for attaching field API validation to entity forms. + */ +function entity_form_field_validate($entity_type, $form, &$form_state) { + // All field attach API functions act on an entity object, but during form + // validation, we don't have one. $form_state contains the entity as it was + // prior to processing the current form submission, and we must not update it + // until we have fully validated the submitted input. Therefore, for + // validation, act on a pseudo entity created out of the form values. + $pseudo_entity = (object) $form_state['values']; + field_attach_form_validate($entity_type, $pseudo_entity, $form, $form_state); +} + +/** + * Helper function for copying submitted values to entity properties for simple entity forms. + * + * During the submission handling of an entity form's "Save", "Preview", and + * possibly other buttons, the form state's entity needs to be updated with the + * submitted form values. Each entity form implements its own builder function + * for doing this, appropriate for the particular entity and form, whereas + * modules may specify additional builder functions in $form['#entity_builders'] + * for copying the form values of added form elements to entity properties. + * Many of the main entity builder functions can call this helper function to + * re-use its logic of copying $form_state['values'][PROPERTY] values to + * $entity->PROPERTY for all entries in $form_state['values'] that are not field + * data, and calling field_attach_submit() to copy field data. Apart from that + * this helper invokes any additional builder functions that have been specified + * in $form['#entity_builders']. + * + * For some entity forms (e.g., forms with complex non-field data and forms that + * simultaneously edit multiple entities), this behavior may be inappropriate, + * so the builder function for such forms needs to implement the required + * functionality instead of calling this function. + */ +function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_state) { + $info = entity_get_info($entity_type); + list(, , $bundle) = entity_extract_ids($entity_type, $entity); + + // Copy top-level form values that are not for fields to entity properties, + // without changing existing entity properties that are not being edited by + // this form. Copying field values must be done using field_attach_submit(). + $values_excluding_fields = $info['fieldable'] ? array_diff_key($form_state['values'], field_info_instances($entity_type, $bundle)) : $form_state['values']; + foreach ($values_excluding_fields as $key => $value) { + $entity->$key = $value; + } + + // Invoke all specified builders for copying form values to entity properties. + if (isset($form['#entity_builders'])) { + foreach ($form['#entity_builders'] as $function) { + $function($entity_type, $entity, $form, $form_state); + } + } + + // Copy field values to the entity. + if ($info['fieldable']) { + field_attach_submit($entity_type, $entity, $form, $form_state); + } +} + +/** + * Exception thrown when a malformed entity is passed. + */ +class EntityMalformedException extends Exception { } + diff --git a/includes/entity.inc b/modules/entity/entity.query.inc similarity index 71% rename from includes/entity.inc rename to modules/entity/entity.query.inc index 99baf4992b47..b4e91a2f9f40 100644 --- a/includes/entity.inc +++ b/modules/entity/entity.query.inc @@ -1,388 +1,9 @@ $value. - * - * @return - * An array of entity objects indexed by their ids. When no results are - * found, an empty array is returned. - */ - public function load($ids = array(), $conditions = array()); -} - -/** - * Default implementation of DrupalEntityControllerInterface. - * - * This class can be used as-is by most simple entity types. Entity types - * requiring special handling can extend the class. - */ -class DrupalDefaultEntityController implements DrupalEntityControllerInterface { - - /** - * Static cache of entities. - * - * @var array - */ - protected $entityCache; - - /** - * Entity type for this controller instance. - * - * @var string - */ - protected $entityType; - - /** - * Array of information about the entity. - * - * @var array - * - * @see entity_get_info() - */ - protected $entityInfo; - - /** - * Additional arguments to pass to hook_TYPE_load(). - * - * Set before calling DrupalDefaultEntityController::attachLoad(). - * - * @var array - */ - protected $hookLoadArguments; - - /** - * Name of the entity's ID field in the entity database table. - * - * @var string - */ - protected $idKey; - - /** - * Name of entity's revision database table field, if it supports revisions. - * - * Has the value FALSE if this entity does not use revisions. - * - * @var string - */ - protected $revisionKey; - - /** - * The table that stores revisions, if the entity supports revisions. - * - * @var string - */ - protected $revisionTable; - - /** - * Whether this entity type should use the static cache. - * - * Set by entity info. - * - * @var boolean - */ - protected $cache; - - /** - * Constructor: sets basic variables. - */ - public function __construct($entityType) { - $this->entityType = $entityType; - $this->entityInfo = entity_get_info($entityType); - $this->entityCache = array(); - $this->hookLoadArguments = array(); - $this->idKey = $this->entityInfo['entity keys']['id']; - - // Check if the entity type supports revisions. - if (!empty($this->entityInfo['entity keys']['revision'])) { - $this->revisionKey = $this->entityInfo['entity keys']['revision']; - $this->revisionTable = $this->entityInfo['revision table']; - } - else { - $this->revisionKey = FALSE; - } - - // Check if the entity type supports static caching of loaded entities. - $this->cache = !empty($this->entityInfo['static cache']); - } - - /** - * Implements DrupalEntityControllerInterface::resetCache(). - */ - public function resetCache(array $ids = NULL) { - if (isset($ids)) { - foreach ($ids as $id) { - unset($this->entityCache[$id]); - } - } - else { - $this->entityCache = array(); - } - } - - /** - * Implements DrupalEntityControllerInterface::load(). - */ - public function load($ids = array(), $conditions = array()) { - $entities = array(); - - // 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; - } - - // Create a new variable which is either a prepared version of the $ids - // array for later comparison with the entity cache, or FALSE if no $ids - // were passed. The $ids array is reduced as items are loaded from cache, - // and we need to know if it's empty for this reason to avoid querying the - // database when all requested entities are loaded from cache. - $passed_ids = !empty($ids) ? array_flip($ids) : FALSE; - // Try to load entities from the static cache, if the entity type supports - // static caching. - 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) { - $ids = array_keys(array_diff_key($passed_ids, $entities)); - } - } - - // Load any remaining entities from the database. This is the case if $ids - // is set to FALSE (so we load all entities), if there are any ids left to - // load, if loading a revision, or if $conditions was passed without $ids. - if ($ids === FALSE || $ids || $revision_id || ($conditions && !$passed_ids)) { - // Build the query. - $query = $this->buildQuery($ids, $conditions, $revision_id); - $queried_entities = $query - ->execute() - ->fetchAllAssoc($this->idKey); - } - - // Pass all entities loaded from the database through $this->attachLoad(), - // which attaches fields (if supported by the entity type) and calls the - // entity type specific load callback, for example hook_node_load(). - if (!empty($queried_entities)) { - $this->attachLoad($queried_entities, $revision_id); - $entities += $queried_entities; - } - - if ($this->cache) { - // Add entities to the cache if we are not loading a revision. - if (!empty($queried_entities) && !$revision_id) { - $this->cacheSet($queried_entities); - } - } - - // Ensure that the returned array is ordered the same as the original - // $ids array if this was passed in and remove any invalid ids. - if ($passed_ids) { - // Remove any invalid ids from the array. - $passed_ids = array_intersect_key($passed_ids, $entities); - foreach ($entities as $entity) { - $passed_ids[$entity->{$this->idKey}] = $entity; - } - $entities = $passed_ids; - } - - return $entities; - } - - /** - * Builds the query to load the entity. - * - * This has full revision support. For entities requiring special queries, - * the class can be extended, and the default query can be constructed by - * calling parent::buildQuery(). This is usually necessary when the object - * being loaded needs to be augmented with additional data from another - * table, such as loading node type into comments or vocabulary machine name - * into terms, however it can also support $conditions on different tables. - * See CommentController::buildQuery() or TaxonomyTermController::buildQuery() - * for examples. - * - * @param $ids - * An array of entity IDs, or FALSE to load all entities. - * @param $conditions - * An array of conditions in the form 'field' => $value. - * @param $revision_id - * The ID of the revision to load, or FALSE if this query is asking for the - * most current revision(s). - * - * @return SelectQuery - * A SelectQuery object for loading the entity. - */ - protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { - $query = db_select($this->entityInfo['base table'], 'base'); - - $query->addTag($this->entityType . '_load_multiple'); - - if ($revision_id) { - $query->join($this->revisionTable, 'revision', "revision.{$this->idKey} = base.{$this->idKey} AND revision.{$this->revisionKey} = :revisionId", array(':revisionId' => $revision_id)); - } - elseif ($this->revisionKey) { - $query->join($this->revisionTable, 'revision', "revision.{$this->revisionKey} = base.{$this->revisionKey}"); - } - - // Add fields from the {entity} table. - $entity_fields = $this->entityInfo['schema_fields_sql']['base table']; - - if ($this->revisionKey) { - // Add all fields from the {entity_revision} table. - $entity_revision_fields = drupal_map_assoc($this->entityInfo['schema_fields_sql']['revision table']); - // The id field is provided by entity, so remove it. - unset($entity_revision_fields[$this->idKey]); - - // Remove all fields from the base table that are also fields by the same - // name in the revision table. - $entity_field_keys = array_flip($entity_fields); - foreach ($entity_revision_fields as $key => $name) { - if (isset($entity_field_keys[$name])) { - unset($entity_fields[$entity_field_keys[$name]]); - } - } - $query->fields('revision', $entity_revision_fields); - } - - $query->fields('base', $entity_fields); - - if ($ids) { - $query->condition("base.{$this->idKey}", $ids, 'IN'); - } - if ($conditions) { - foreach ($conditions as $field => $value) { - $query->condition('base.' . $field, $value); - } - } - return $query; - } - - /** - * Attaches data to entities upon loading. - * This will attach fields, if the entity is fieldable. It calls - * hook_entity_load() for modules which need to add data to all entities. - * It also calls hook_TYPE_load() on the loaded entities. For example - * hook_node_load() or hook_user_load(). If your hook_TYPE_load() - * expects special parameters apart from the queried entities, you can set - * $this->hookLoadArguments prior to calling the method. - * See NodeController::attachLoad() for an example. - * - * @param $queried_entities - * Associative array of query results, keyed on the entity ID. - * @param $revision_id - * ID of the revision that was loaded, or FALSE if teh most current revision - * was loaded. - */ - 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. - $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); - } - } - - /** - * Gets entities from the static cache. - * - * @param $ids - * If not empty, return entities that match these IDs. - * @param $conditions - * If set, return entities that match all of these conditions. - * - * @return - * Array of entities from the entity cache. - */ - protected function cacheGet($ids, $conditions = array()) { - $entities = array(); - // Load any available entities from the internal cache. - if (!empty($this->entityCache)) { - if ($ids) { - $entities += array_intersect_key($this->entityCache, array_flip($ids)); - } - // If loading entities only by conditions, fetch all available entities - // from the cache. Entities which don't match are removed later. - elseif ($conditions) { - $entities = $this->entityCache; - } - } - - // Exclude any entities loaded from cache if they don't match $conditions. - // This ensures the same behavior whether loading from memory or database. - if ($conditions) { - foreach ($entities as $entity) { - $entity_values = (array) $entity; - if (array_diff_assoc($conditions, $entity_values)) { - unset($entities[$entity->{$this->idKey}]); - } - } - } - return $entities; - } - - /** - * Stores entities in the static entity cache. - * - * @param $entities - * Entities to store in the cache. - */ - protected function cacheSet($entities) { - $this->entityCache += $entities; - } -} /** * Exception thrown by EntityFieldQuery() on unsupported query syntax. @@ -938,7 +559,7 @@ class EntityFieldQuery { } /** - * Enable a pager for the query. + * Enables a pager for the query. * * @param $limit * An integer specifying the number of elements per page. If passed a false @@ -966,7 +587,7 @@ class EntityFieldQuery { } /** - * Enable sortable tables for this query. + * Enables sortable tables for this query. * * @param $headers * An EFQ Header array based on which the order clause is added to the query. @@ -1243,7 +864,7 @@ class EntityFieldQuery { } /** - * Get the total number of results and initialize a pager for the query. + * Gets the total number of results and initialize a pager for the query. * * This query can be detected by checking for ($this->pager && $this->count), * which allows a driver to return 0 from the count query and disable @@ -1330,7 +951,3 @@ class EntityFieldQuery { } -/** - * Exception thrown when a malformed entity is passed. - */ -class EntityMalformedException extends Exception { } diff --git a/modules/simpletest/tests/entity_cache_test.info b/modules/entity/tests/entity_cache_test.info similarity index 100% rename from modules/simpletest/tests/entity_cache_test.info rename to modules/entity/tests/entity_cache_test.info diff --git a/modules/simpletest/tests/entity_cache_test.module b/modules/entity/tests/entity_cache_test.module similarity index 100% rename from modules/simpletest/tests/entity_cache_test.module rename to modules/entity/tests/entity_cache_test.module diff --git a/modules/simpletest/tests/entity_cache_test_dependency.info b/modules/entity/tests/entity_cache_test_dependency.info similarity index 100% rename from modules/simpletest/tests/entity_cache_test_dependency.info rename to modules/entity/tests/entity_cache_test_dependency.info diff --git a/modules/simpletest/tests/entity_cache_test_dependency.module b/modules/entity/tests/entity_cache_test_dependency.module similarity index 100% rename from modules/simpletest/tests/entity_cache_test_dependency.module rename to modules/entity/tests/entity_cache_test_dependency.module diff --git a/modules/simpletest/tests/entity_crud_hook_test.info b/modules/entity/tests/entity_crud_hook_test.info similarity index 100% rename from modules/simpletest/tests/entity_crud_hook_test.info rename to modules/entity/tests/entity_crud_hook_test.info diff --git a/modules/simpletest/tests/entity_crud_hook_test.module b/modules/entity/tests/entity_crud_hook_test.module similarity index 100% rename from modules/simpletest/tests/entity_crud_hook_test.module rename to modules/entity/tests/entity_crud_hook_test.module diff --git a/modules/simpletest/tests/entity_crud_hook_test.test b/modules/entity/tests/entity_crud_hook_test.test similarity index 100% rename from modules/simpletest/tests/entity_crud_hook_test.test rename to modules/entity/tests/entity_crud_hook_test.test diff --git a/modules/simpletest/tests/entity_query.test b/modules/entity/tests/entity_query.test similarity index 99% rename from modules/simpletest/tests/entity_query.test rename to modules/entity/tests/entity_query.test index fb95518d168e..e540a90d99f8 100644 --- a/modules/simpletest/tests/entity_query.test +++ b/modules/entity/tests/entity_query.test @@ -1,6 +1,5 @@ subject, then - * 'subject' should be specified here. If complex logic is required to - * build the label, a 'label callback' should be defined instead (see - * the 'label callback' section above for details). - * - bundle keys: An array describing how the Field API can extract the - * information it needs from the bundle objects for this type. This entry - * is required if the 'path' provided in the 'bundles'/'admin' section - * identifies the bundle using a named menu placeholder whose loader - * callback returns an object (e.g., $vocabulary for taxonomy terms, or - * $node_type for nodes). If the path does not include the bundle, or the - * bundle is just a string rather than an automatically loaded object, then - * this can be omitted. Elements: - * - bundle: The name of the property of the bundle object that contains - * the name of the bundle object. - * - bundles: An array describing all bundles for this object type. Keys are - * bundles machine names, as found in the objects' 'bundle' property - * (defined in the 'entity keys' entry above). Elements: - * - label: The human-readable name of the bundle. - * - uri callback: Same as the 'uri callback' key documented above for the - * entity type, but for the bundle only. When determining the URI of an - * entity, if a 'uri callback' is defined for both the entity type and - * the bundle, the one for the bundle is used. - * - admin: An array of information that allows Field UI pages to attach - * themselves to the existing administration pages for the bundle. - * Elements: - * - path: the path of the bundle's main administration page, as defined - * in hook_menu(). If the path includes a placeholder for the bundle, - * the 'bundle argument' and 'real path' keys below are required. - * - bundle argument: The position of the bundle placeholder in 'path', if - * any. - * - real path: The actual path (no placeholder) of the bundle's main - * administration page. This will be used to generate links. - * - access callback: As in hook_menu(). 'user_access' will be assumed if - * no value is provided. - * - access arguments: As in hook_menu(). - * - view modes: An array describing the view modes for the entity type. View - * modes let entities be displayed differently depending on the context. - * For instance, a node can be displayed differently on its own page - * ('full' mode), on the home page or taxonomy listings ('teaser' mode), or - * in an RSS feed ('rss' mode). Modules taking part in the display of the - * entity (notably the Field API) can adjust their behavior depending on - * the requested view mode. An additional 'default' view mode is available - * for all entity types. This view mode is not intended for actual entity - * display, but holds default display settings. For each available view - * mode, administrators can configure whether it should use its own set of - * field display settings, or just replicate the settings of the 'default' - * view mode, thus reducing the amount of display configurations to keep - * track of. Keys of the array are view mode names. Each view mode is - * described by an array with the following key/value pairs: - * - label: The human-readable name of the view mode - * - custom settings: A boolean specifying whether the view mode should by - * default use its own custom field display settings. If FALSE, entities - * displayed in this view mode will reuse the 'default' display settings - * by default (e.g. right after the module exposing the view mode is - * enabled), but administrators can later use the Field UI to apply custom - * display settings specific to the view mode. - * - * @see entity_load() - * @see hook_entity_info_alter() - */ -function hook_entity_info() { - $return = array( - 'node' => array( - 'label' => t('Node'), - 'controller class' => 'NodeController', - 'base table' => 'node', - 'revision table' => 'node_revision', - 'uri callback' => 'node_uri', - 'fieldable' => TRUE, - 'translation' => array( - 'locale' => TRUE, - ), - 'entity keys' => array( - 'id' => 'nid', - 'revision' => 'vid', - 'bundle' => 'type', - ), - 'bundle keys' => array( - 'bundle' => 'type', - ), - 'bundles' => array(), - 'view modes' => array( - 'full' => array( - 'label' => t('Full content'), - 'custom settings' => FALSE, - ), - 'teaser' => array( - 'label' => t('Teaser'), - 'custom settings' => TRUE, - ), - 'rss' => array( - 'label' => t('RSS'), - 'custom settings' => FALSE, - ), - ), - ), - ); - - // Search integration is provided by node.module, so search-related - // view modes for nodes are defined here and not in search.module. - if (module_exists('search')) { - $return['node']['view modes'] += array( - 'search_index' => array( - 'label' => t('Search index'), - 'custom settings' => FALSE, - ), - 'search_result' => array( - 'label' => t('Search result'), - 'custom settings' => FALSE, - ), - ); - } - - // Bundles must provide a human readable name so we can create help and error - // messages, and the path to attach Field admin pages to. - foreach (node_type_get_names() as $type => $name) { - $return['node']['bundles'][$type] = array( - 'label' => $name, - 'admin' => array( - 'path' => 'admin/structure/types/manage/%node_type', - 'real path' => 'admin/structure/types/manage/' . str_replace('_', '-', $type), - 'bundle argument' => 4, - 'access arguments' => array('administer content types'), - ), - ); - } - - return $return; -} - -/** - * Alter the entity info. - * - * Modules may implement this hook to alter the information that defines an - * entity. All properties that are available in hook_entity_info() can be - * altered here. - * - * @param $entity_info - * The entity info array, keyed by entity name. - * - * @see hook_entity_info() - */ -function hook_entity_info_alter(&$entity_info) { - // Set the controller class for nodes to an alternate implementation of the - // DrupalEntityController interface. - $entity_info['node']['controller class'] = 'MyCustomNodeController'; -} - -/** - * Act on entities when loaded. - * - * This is a generic load hook called for all entity types loaded via the - * entity API. - * - * @param $entities - * The entities keyed by entity ID. - * @param $type - * The type of entities being loaded (i.e. node, user, comment). - */ -function hook_entity_load($entities, $type) { - foreach ($entities as $entity) { - $entity->foo = mymodule_add_something($entity, $type); - } -} - -/** - * Act on an entity before it is about to be created or updated. - * - * @param $entity - * The entity object. - * @param $type - * The type of entity being saved (i.e. node, user, comment). - */ -function hook_entity_presave($entity, $type) { - $entity->changed = REQUEST_TIME; -} - -/** - * Act on entities when inserted. - * - * @param $entity - * The entity object. - * @param $type - * The type of entity being inserted (i.e. node, user, comment). - */ -function hook_entity_insert($entity, $type) { - // Insert the new entity into a fictional table of all entities. - $info = entity_get_info($type); - list($id) = entity_extract_ids($type, $entity); - db_insert('example_entity') - ->fields(array( - 'type' => $type, - 'id' => $id, - 'created' => REQUEST_TIME, - 'updated' => REQUEST_TIME, - )) - ->execute(); -} - -/** - * Act on entities when updated. - * - * @param $entity - * The entity object. - * @param $type - * The type of entity being updated (i.e. node, user, comment). - */ -function hook_entity_update($entity, $type) { - // Update the entity's entry in a fictional table of all entities. - $info = entity_get_info($type); - list($id) = entity_extract_ids($type, $entity); - db_update('example_entity') - ->fields(array( - 'updated' => REQUEST_TIME, - )) - ->condition('type', $type) - ->condition('id', $id) - ->execute(); -} - -/** - * Act on entities when deleted. - * - * @param $entity - * The entity object. - * @param $type - * The type of entity being deleted (i.e. node, user, comment). - */ -function hook_entity_delete($entity, $type) { - // Delete the entity's entry from a fictional table of all entities. - $info = entity_get_info($type); - list($id) = entity_extract_ids($type, $entity); - db_delete('example_entity') - ->condition('type', $type) - ->condition('id', $id) - ->execute(); -} - -/** - * Alter or execute an EntityFieldQuery. - * - * @param EntityFieldQuery $query - * An EntityFieldQuery. One of the most important properties to be changed is - * EntityFieldQuery::executeCallback. If this is set to an existing function, - * this function will get the query as its single argument and its result - * will be the returned as the result of EntityFieldQuery::execute(). This can - * be used to change the behavior of EntityFieldQuery entirely. For example, - * the default implementation can only deal with one field storage engine, but - * it is possible to write a module that can query across field storage - * engines. Also, the default implementation presumes entities are stored in - * SQL, but the execute callback could instead query any other entity storage, - * local or remote. - * - * Note the $query->altered attribute which is TRUE in case the query has - * already been altered once. This happens with cloned queries. - * If there is a pager, then such a cloned query will be executed to count - * all elements. This query can be detected by checking for - * ($query->pager && $query->count), allowing the driver to return 0 from - * the count query and disable the pager. - */ -function hook_entity_query_alter($query) { - $query->executeCallback = 'my_module_query_callback'; -} - -/** - * Act on entities being assembled before rendering. - * - * @param $entity - * The entity object. - * @param $type - * The type of entity being rendered (i.e. node, user, comment). - * @param $view_mode - * The view mode the entity is rendered in. - * @param $langcode - * The language code used for rendering. - * - * The module may add elements to $entity->content prior to rendering. The - * structure of $entity->content is a renderable array as expected by - * drupal_render(). - * - * @see hook_entity_view_alter() - * @see hook_comment_view() - * @see hook_node_view() - * @see hook_user_view() - */ -function hook_entity_view($entity, $type, $view_mode, $langcode) { - $entity->content['my_additional_field'] = array( - '#markup' => $additional_field, - '#weight' => 10, - '#theme' => 'mymodule_my_additional_field', - ); -} - -/** - * Alter the results of ENTITY_view(). - * - * This hook is called after the content has been assembled in a structured - * array and may be used for doing processing which requires that the complete - * entity content structure has been built. - * - * If a module wishes to act on the rendered HTML of the entity rather than the - * structured content array, it may use this hook to add a #post_render - * callback. Alternatively, it could also implement hook_preprocess_ENTITY(). - * See drupal_render() and theme() for details. - * - * @param $build - * A renderable array representing the entity content. - * @param $type - * The type of entity being rendered (i.e. node, user, comment). - * - * @see hook_entity_view() - * @see hook_comment_view_alter() - * @see hook_node_view_alter() - * @see hook_taxonomy_term_view_alter() - * @see hook_user_view_alter() - */ -function hook_entity_view_alter(&$build, $type) { - if ($build['#view_mode'] == 'full' && isset($build['an_additional_field'])) { - // Change its weight. - $build['an_additional_field']['#weight'] = -10; - - // Add a #post_render callback to act on the rendered HTML of the entity. - $build['#post_render'][] = 'my_module_node_post_render'; - } -} - /** * Define administrative paths. * @@ -491,30 +105,6 @@ function hook_admin_paths_alter(&$paths) { $paths['node/add/forum'] = FALSE; } -/** - * Act on entities as they are being prepared for view. - * - * Allows you to operate on multiple entities as they are being prepared for - * view. Only use this if attaching the data during the entity_load() phase - * is not appropriate, for example when attaching other 'entity' style objects. - * - * @param $entities - * The entities keyed by entity ID. - * @param $type - * The type of entities being loaded (i.e. node, user, comment). - * @param $langcode - * The language to display the entity in. - */ -function hook_entity_prepare_view($entities, $type, $langcode) { - // Load a specific node into the user object for later theming. - if ($type == 'user') { - $nodes = mymodule_get_user_nodes(array_keys($entities)); - foreach ($entities as $uid => $entity) { - $entity->user_node = $nodes[$uid]; - } - } -} - /** * Perform periodic actions. * @@ -2394,6 +1984,30 @@ function hook_flush_caches() { return array('example'); } +/** + * Perform necessary actions before modules are installed. + * + * This function allows all modules to react prior to a module being installed. + * + * @param $modules + * An array of modules about to be installed. + */ +function hook_modules_preinstall($modules) { + mymodule_cache_clear(); +} + +/** + * Perform necessary actions before modules are enabled. + * + * This function allows all modules to react prior to a module being enabled. + * + * @param $module + * An array of modules about to be enabled. + */ +function hook_modules_preenable($modules) { + mymodule_cache_clear(); +} + /** * Perform necessary actions after modules are installed. * diff --git a/modules/system/system.install b/modules/system/system.install index 6d80c43a0057..5c4250aec7bd 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -1623,10 +1623,10 @@ function system_update_last_removed() { */ /** - * Placeholder update to set the schema version to 8000. + * Enable entity module. */ function system_update_8000() { - // Fill in the first update to Drupal 8 when needed. + update_module_enable(array('entity')); } /** diff --git a/modules/taxonomy/taxonomy.info b/modules/taxonomy/taxonomy.info index cba3869820ff..6a13f81db0af 100644 --- a/modules/taxonomy/taxonomy.info +++ b/modules/taxonomy/taxonomy.info @@ -4,6 +4,7 @@ package = Core version = VERSION core = 8.x dependencies[] = options +dependencies[] = entity files[] = taxonomy.module files[] = taxonomy.test configure = admin/structure/taxonomy diff --git a/modules/user/user.entity.inc b/modules/user/user.entity.inc new file mode 100644 index 000000000000..5549c7707188 --- /dev/null +++ b/modules/user/user.entity.inc @@ -0,0 +1,52 @@ + $record) { + $picture_fids[] = $record->picture; + $queried_users[$key]->data = unserialize($record->data); + $queried_users[$key]->roles = array(); + if ($record->uid) { + $queried_users[$record->uid]->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user'; + } + else { + $queried_users[$record->uid]->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user'; + } + } + + // Add any additional roles from the database. + $result = db_query('SELECT r.rid, r.name, ur.uid FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid IN (:uids)', array(':uids' => array_keys($queried_users))); + foreach ($result as $record) { + $queried_users[$record->uid]->roles[$record->rid] = $record->name; + } + + // Add the full file objects for user pictures if enabled. + if (!empty($picture_fids) && variable_get('user_pictures', 1) == 1) { + $pictures = file_load_multiple($picture_fids); + foreach ($queried_users as $account) { + if (!empty($account->picture) && isset($pictures[$account->picture])) { + $account->picture = $pictures[$account->picture]; + } + else { + $account->picture = NULL; + } + } + } + // Call the default attachLoad() method. This will add fields and call + // hook_user_load(). + parent::attachLoad($queried_users, $revision_id); + } +} diff --git a/modules/user/user.info b/modules/user/user.info index a4d18d634085..d887352760e4 100644 --- a/modules/user/user.info +++ b/modules/user/user.info @@ -3,7 +3,7 @@ description = Manages the user registration and login system. package = Core version = VERSION core = 8.x -files[] = user.module +files[] = user.entity.inc files[] = user.test required = TRUE configure = admin/config/people diff --git a/modules/user/user.module b/modules/user/user.module index 1355159a90c3..47942584e9a8 100644 --- a/modules/user/user.module +++ b/modules/user/user.module @@ -287,53 +287,6 @@ function user_load_multiple($uids = array(), $conditions = array(), $reset = FAL return entity_load('user', $uids, $conditions, $reset); } -/** - * Controller class for users. - * - * This extends the DrupalDefaultEntityController class, adding required - * special handling for user objects. - */ -class UserController extends DrupalDefaultEntityController { - - function attachLoad(&$queried_users, $revision_id = FALSE) { - // Build an array of user picture IDs so that these can be fetched later. - $picture_fids = array(); - foreach ($queried_users as $key => $record) { - $picture_fids[] = $record->picture; - $queried_users[$key]->data = unserialize($record->data); - $queried_users[$key]->roles = array(); - if ($record->uid) { - $queried_users[$record->uid]->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user'; - } - else { - $queried_users[$record->uid]->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user'; - } - } - - // Add any additional roles from the database. - $result = db_query('SELECT r.rid, r.name, ur.uid FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid IN (:uids)', array(':uids' => array_keys($queried_users))); - foreach ($result as $record) { - $queried_users[$record->uid]->roles[$record->rid] = $record->name; - } - - // Add the full file objects for user pictures if enabled. - if (!empty($picture_fids) && variable_get('user_pictures', 1) == 1) { - $pictures = file_load_multiple($picture_fids); - foreach ($queried_users as $account) { - if (!empty($account->picture) && isset($pictures[$account->picture])) { - $account->picture = $pictures[$account->picture]; - } - else { - $account->picture = NULL; - } - } - } - // Call the default attachLoad() method. This will add fields and call - // hook_user_load(). - parent::attachLoad($queried_users, $revision_id); - } -} - /** * Loads a user object. * diff --git a/update.php b/update.php index b7594366b3db..e1ad8c0e760e 100644 --- a/update.php +++ b/update.php @@ -345,7 +345,6 @@ require_once DRUPAL_ROOT . '/includes/bootstrap.inc'; require_once DRUPAL_ROOT . '/includes/update.inc'; require_once DRUPAL_ROOT . '/includes/common.inc'; require_once DRUPAL_ROOT . '/includes/file.inc'; -require_once DRUPAL_ROOT . '/includes/entity.inc'; require_once DRUPAL_ROOT . '/includes/unicode.inc'; update_prepare_d8_bootstrap();