diff --git a/core/modules/comment/comment.entity.inc b/core/modules/comment/comment.entity.inc index bce8b56..479f25a 100644 --- a/core/modules/comment/comment.entity.inc +++ b/core/modules/comment/comment.entity.inc @@ -6,6 +6,91 @@ */ /** + * Implements hook_entity_property_info(). + */ +function comment_entity_property_info() { + $info = array(); + + $info['comment']['properties']['cid'] = array( + 'label' => t('Comment ID'), + 'description' => t('The unique ID of the comment.'), + 'type' => 'integer', + ); + $info['comment']['properties']['hostname'] = array( + 'label' => t('IP Address'), + 'description' => t('The IP address of the computer the comment was posted from.'), + 'type' => 'text', + ); + $info['comment']['properties']['name'] = array( + 'label' => t('Name'), + 'description' => t('The name left by the comment author.'), + 'type' => 'text', + ); + $info['comment']['properties']['mail'] = array( + 'label' => t('Email address'), + 'description' => t('The email address left by the comment author.'), + 'type' => 'text', + ); + $info['comment']['properties']['homepage'] = array( + 'label' => t('Home page'), + 'description' => t('The home page URL left by the comment author.'), + 'type' => 'text', + ); + $info['comment']['properties']['subject'] = array( + 'label' => t('Subject'), + 'description' => t('The subject of the comment.'), + 'type' => 'text', + 'required' => TRUE, + ); + $info['comment']['properties']['created'] = array( + 'label' => t('Date created'), + 'description' => t('The date the comment was posted.'), + 'type' => 'date', + ); + $info['comment']['properties']['parent'] = array( + 'label' => t('Parent'), + 'description' => t("The comment's parent, if comment threading is active."), + 'type' => 'entity', + 'entity type' => 'comment', + 'storage field' => 'pid', + ); + $info['comment']['properties']['node_type'] = array( + 'label' => t('Comment node type'), + 'description' => t('The type of the node the comment was posted to.'), + 'type' => 'token', + 'storage field' => FALSE, + ); + $info['comment']['properties']['node'] = array( + 'label' => t('Node'), + 'description' => t('The node the comment was posted to.'), + 'type' => 'entity', + 'entity type' => 'node', + 'required' => TRUE, + 'storage field' => 'nid', + ); + $info['comment']['properties']['user'] = array( + 'label' => t('User'), + 'description' => t('The author of the comment.'), + 'type' => 'entity', + 'entity type' => 'user', + 'required' => TRUE, + 'storage field' => 'uid', + ); + $info['comment']['properties']['status'] = array( + 'label' => t('Status'), + 'description' => t('Whether the comment is published or unpublished.'), + 'type' => 'boolean', + ); + $info['comment']['properties']['language'] = array( + 'label' => t('Language'), + 'description' => t('The language the comment was posted in.'), + 'type' => 'token', + 'storage field' => 'langcode', + ); + return $info; +} + +/** * Defines the comment entity class. */ class Comment extends Entity { @@ -84,6 +169,23 @@ class Comment extends Entity { class CommentStorageController extends EntityDatabaseStorageController { /** + * Overrides EntityDatabaseStorageController::create(). + */ + public function create(array $values) { + // Care about setting the bundle key first. + if (!is_object($values['node'])) { + $values['node'] = node_load($values['node']); + } + $entity = parent::create(array('node_type' => $values['node']->type)); + + // Set the remaining values. + foreach ($values as $key => $value) { + $entity->set($key, $value); + } + return $entity; + } + + /** * Overrides EntityDatabaseStorageController::buildQuery(). */ protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { @@ -123,11 +225,6 @@ class CommentStorageController extends EntityDatabaseStorageController { if (!isset($comment->status)) { $comment->status = user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED; } - // Make sure we have a proper bundle name. - if (!isset($comment->node_type)) { - $node = node_load($comment->nid); - $comment->node_type = 'comment_node_' . $node->type; - } if (!$comment->cid) { // Add the comment to database. This next section builds the thread field. // Also see the documentation for comment_view(). diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index 1a3580f..0d04725 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -764,7 +764,7 @@ function comment_node_page_additions($node) { // Append comment form if needed. if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) { - $comment = entity_create('comment', array('nid' => $node->nid)); + $comment = entity_create('comment', array('node' => $node)); $additions['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment); } diff --git a/core/modules/comment/comment.pages.inc b/core/modules/comment/comment.pages.inc index 344e757..895aa6d 100644 --- a/core/modules/comment/comment.pages.inc +++ b/core/modules/comment/comment.pages.inc @@ -35,7 +35,7 @@ function comment_reply($node, $pid = NULL) { // The user is previewing a comment prior to submitting it. if ($op == t('Preview')) { if (user_access('post comments')) { - $comment = entity_create('comment', array('nid' => $node->nid, 'pid' => $pid)); + $comment = entity_create('comment', array('node' => $node, 'parent' => $pid)); $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment); } else { @@ -87,7 +87,7 @@ function comment_reply($node, $pid = NULL) { drupal_goto("node/$node->nid"); } elseif (user_access('post comments')) { - $comment = entity_create('comment', array('nid' => $node->nid, 'pid' => $pid)); + $comment = entity_create('comment', array('node' => $node, 'parent' => $pid)); $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment); } else { diff --git a/core/modules/comment/comment.test b/core/modules/comment/comment.test index 9257513..259b9e3 100644 --- a/core/modules/comment/comment.test +++ b/core/modules/comment/comment.test @@ -89,15 +89,15 @@ class CommentHelperCase extends DrupalWebTestCase { } if (isset($match[1])) { - return entity_create('comment', array('id' => $match[1], 'subject' => $subject, 'comment' => $comment)); + return (object) array('id' => $match[1], 'subject' => $subject, 'comment' => $comment); } } /** * Checks current page for specified comment. * - * @param object $comment - * The comment object. + * @param stdClass $comment + * A comment object (not a full entity). * @param boolean $reply * Boolean indicating whether the comment is a reply to another comment. * @@ -445,11 +445,9 @@ class CommentInterfaceTest extends CommentHelperCase { // Create a new comment. This helper function may be run with different // comment settings so use comment_save() to avoid complex setup. $comment = entity_create('comment', array( - 'cid' => NULL, - 'nid' => $this->node->nid, - 'node_type' => $this->node->type, - 'pid' => 0, - 'uid' => $this->loggedInUser->uid, + 'node' => $this->node, + 'parent' => 0, + 'user' => $this->loggedInUser->uid, 'status' => COMMENT_PUBLISHED, 'subject' => $this->randomName(), 'hostname' => ip_address(), @@ -496,8 +494,8 @@ class CommentInterfaceTest extends CommentHelperCase { // Add a comment. $comment = entity_create('comment', array( - 'nid' => $node->nid, - 'uid' => $case['comment_uid'], + 'node' => $node, + 'user' => $case['comment_uid'], 'status' => $case['comment_status'], 'subject' => $this->randomName(), 'language' => LANGUAGE_NOT_SPECIFIED, @@ -772,11 +770,9 @@ class CommentInterfaceTest extends CommentHelperCase { // Create a comment via CRUD API functionality, since // $this->postComment() relies on actual user permissions. $comment = entity_create('comment', array( - 'cid' => NULL, - 'nid' => $this->node->nid, - 'node_type' => $this->node->type, - 'pid' => 0, - 'uid' => 0, + 'node' => $this->node, + 'parent' => 0, + 'user' => 0, 'status' => COMMENT_PUBLISHED, 'subject' => $this->randomName(), 'hostname' => ip_address(), @@ -1602,7 +1598,7 @@ class CommentApprovalTest extends CommentHelperCase { // Get unapproved comment id. $this->drupalLogin($this->admin_user); $anonymous_comment4 = $this->getUnapprovedComment($subject); - $anonymous_comment4 = entity_create('comment', array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body)); + $anonymous_comment4 = (object) array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body); $this->drupalLogout(); $this->assertFalse($this->commentExists($anonymous_comment4), t('Anonymous comment was not published.')); @@ -1666,7 +1662,7 @@ class CommentApprovalTest extends CommentHelperCase { // Get unapproved comment id. $this->drupalLogin($this->admin_user); $anonymous_comment4 = $this->getUnapprovedComment($subject); - $anonymous_comment4 = entity_create('comment', array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body)); + $anonymous_comment4 = (object) array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body); $this->drupalLogout(); $this->assertFalse($this->commentExists($anonymous_comment4), t('Anonymous comment was not published.')); diff --git a/core/modules/entity/entity.api.php b/core/modules/entity/entity.api.php index e983778..b85bda2 100644 --- a/core/modules/entity/entity.api.php +++ b/core/modules/entity/entity.api.php @@ -214,6 +214,86 @@ function hook_entity_info_alter(&$entity_info) { } /** + * Provide information about entity properties. + * + * All entity properties and fields are defined using this hook. For example, + * this information is leveraged by the EntityInterface::get() and + * EntityInterface::set() methods, as well as by many modules. + * + * @return + * An array whose keys are entity type names and whose values are arrays + * containing the keys: + * - properties: The array describing all properties for this entity. Entries + * are keyed by the property name and contain an array of metadata for each + * property. The name may only contain alphanumeric lowercase characters + * and underscores. Known keys are: + * - label: A human readable, translated label for the property. + * - description: (optional) A human readable, translated description for + * the property. + * - type: The data type of the property. This may either be a primitive + * data type, an entity type or a data type as registered in + * hook_data_type_info(). The following primitive data types are valid: + * - text: Any text. + * - token: A string containing only lowercase letters, numbers, and + * underscores starting with a letter; e.g. as used for machine + * readable names. + * - integer: An integer value. + * - decimal: A float or integer value. + * - date: A full date and time, represented as timestamp. + * - duration: A duration, represented as number of seconds. + * - boolean: A boolean value, represented either as TRUE and FALSE or + * 1 and 0. + * - uri: An absolute URI or URL. + * For specifying an entity type use type 'entity' and optionally specify + * the type of the entity using the 'entity type' and 'bundle' keys. + * - multiple: (optional) A boolean specifying whether the property contains + * a list of values, which is represented as numerically indexed array + * containing the values in their usual representation. Defaults to FALSE. + * - entity type: (optional) If the data type is 'entity', the type of the + * entity. + * - bundle: (optional) If the data type is 'entity', the bundle of the + * entity. + * - storage field: (optional) The storage field which stores the value of + * this property, or FALSE if the property is not stored. Defaults to the + * name of the property. + * - required: (optional) Whether a non-NULL value is required for this + * property for the entity being able to be created or saved. Defaults to + * FALSE. + * - module: (optional) The module providing the property. No value means + * the property is provided by the entity type providing module. + * - bundles: An array keyed by bundle name containing information about + * further properties related to the bundles only. This array may contain + * the key 'properties' with an array of info about the bundle specific + * properties, structured in the same way as the entity properties array. + * + * @see hook_entity_property_info_alter() + * @see EntityInterface::propertyInfo() + */ +function hook_entity_property_info() { + $info['node']['properties']['nid'] = array( + 'label' => t('Node ID'), + 'type' => 'integer', + 'description' => t('The unique content ID.'), + ); + return $info; +} + +/** + * Alter entity property information. + * + * @see hook_entity_property_info() + */ +function hook_entity_property_info_alter(&$info) { + $properties = &$info['node']['bundles']['poll']['properties']; + + $properties['poll-votes'] = array( + 'label' => t('Poll votes'), + 'description' => t("The number of votes that have been cast on a poll node."), + 'type' => 'integer', + ); +} + +/** * Act on entities when loaded. * * This is a generic load hook called for all entity types loaded via the diff --git a/core/modules/entity/entity.class.inc b/core/modules/entity/entity.class.inc index 7ffd496..85a594d 100644 --- a/core/modules/entity/entity.class.inc +++ b/core/modules/entity/entity.class.inc @@ -69,8 +69,8 @@ interface EntityInterface { * Returns the bundle of the entity. * * @return - * The bundle of the entity. Defaults to the entity type if the entity type - * does not make use of different bundles. + * The bundle of the entity. Defaults to FALSE if the entity type does not + * make use of different bundles. */ public function bundle(); @@ -93,6 +93,134 @@ interface EntityInterface { public function uri(); /** + * Returns the language code associated with the entity. + * + * If the entity is not specific to a certain language, the language code of + * an entity is one of the system language constants, e.g. + * LANGUAGE_NOT_SPECIFIED, LANGUAGE_NOT_APPLICABLE or LANGUAGE_MULTIPLE. + * Otherwise, for language-specific entities the language code refers to + * the default language as returned by EntityInterface::language(). + * + * @return + * A valid language code. + * + * @see EntityInterface::language() + */ + public function languageCode(); + + /** + * Returns the default language of a language-specific entity. + * + * @return + * The language object of the entity's default language, or FALSE if the + * entity is not language-specific. + * + * @see EntityInterface::translations() + * @see EntityInterface::languageCode() + */ + public function language(); + + /** + * Returns the languages the entity is translated to. + * + * @return + * An array of language objects, keyed by language codes. + * + * @see EntityInterface::language() + */ + public function translations(); + + /** + * Returns information about all properties of the entity. + * + * @param string $property_name + * (optional) If given, the entry of the specified property is returned. + * Otherwise, an array of information of all properties is returned. + * + * @return array|false + * If no property name is given, an array of property information containing + * entries for properties keyed by the property name. Each entry contains + * keys describing the property as defined in hook_entity_property_info(), + * e.g. 'label' and 'type'. + * If a property name is given, the entry for this property is returned, or + * FALSE if the property has no entry. + * + * @see hook_entity_property_info() + * @see entity_get_property_info() + */ + public function propertyInfo($property_name = NULL); + + /** + * Returns the value of an entity property. + * + * Returned property values match their property information as declared in + * hook_entity_property_info(). For entity references the loaded entity + * objects will be returned, e.g. a user entity will be returned for the node + * author property. + * + * @param $property_name + * The name of the property to return, as described in + * EntityInterface::propertyInfo(); e.g. 'title'. + * @param $langcode + * (optional) If the property is translatable, the language code of the + * language that should be used for getting the property. If set to NULL, + * the entity's default language is being used. + * + * @return + * The property value, or NULL if it is not defined. + * + * @see EntityInterface::getRawValue() + * @see EntityInterface::set() + * @see EntityInterface::language() + */ + public function get($property_name, $langcode = NULL); + + /** + * Returns the raw value of an entity property. + * + * Returned property values match their property information as declared in + * hook_entity_property_info(). However for entity references just the IDs + * of the entities will be returned, e.g. a user ID will be returned for the + * node author property. + * + * @param $property_name + * The name of the property to return, as described in + * EntityInterface::propertyInfo(); e.g. 'title'. + * @param $langcode + * (optional) If the property is translatable, the language code of the + * language that should be used for getting the property. If set to NULL, + * the entity's default language is being used. + * + * @return + * The raw property value, or NULL if it is not defined. + * + * @see EntityInterface::set() + * @see EntityInterface::get() + * @see EntityInterface::language() + */ + public function getRawValue($property_name, $langcode = NULL); + + /** + * Sets the value of an entity property. + * + * @param $property_name + * The name of the property to set; e.g., 'title'. + * @param $value + * The value to set, or NULL to unset the property. For entity references + * the entity object as well as the entity ID may be passed as value, e.g. + * for the node author property a user entity or a user ID may be passed. + * @param $langcode + * (optional) If the property is translatable, the language code of the + * language that should be used for getting the property. If set to NULL, + * the entity's default language is being used. + * + * @see EntityInterface::get() + * @see EntityInterface::getRawValue() + * @see EntityInterface::language() + */ + public function set($property_name, $value, $langcode = NULL); + + /** * Saves an entity permanently. * * @return @@ -139,6 +267,13 @@ interface EntityInterface { class Entity implements EntityInterface { /** + * The language code of the entity's default language. + * + * @var string + */ + public $langcode = LANGUAGE_NOT_SPECIFIED; + + /** * The entity type. * * @var string @@ -179,9 +314,15 @@ class Entity implements EntityInterface { public function __construct(array $values = array(), $entity_type) { $this->entityType = $entity_type; $this->setUp(); - // Set initial values. + + // Set initial values. But start with setting the bundle property as it is + // necessary for being able to set bundle-specific properties. + if ($this->bundleKey && !empty($values[$this->bundleKey])) { + $this->set($this->bundleKey, $values[$this->bundleKey]); + unset($values[$this->bundleKey]); + } foreach ($values as $key => $value) { - $this->$key = $value; + $this->set($key, $value); } } @@ -191,7 +332,7 @@ class Entity implements EntityInterface { protected function setUp() { $this->entityInfo = entity_get_info($this->entityType); $this->idKey = $this->entityInfo['entity keys']['id']; - $this->bundleKey = isset($this->entityInfo['entity keys']['bundle']) ? $this->entityInfo['entity keys']['bundle'] : NULL; + $this->bundleKey = !empty($this->entityInfo['entity keys']['bundle']) ? $this->entityInfo['entity keys']['bundle'] : NULL; } /** @@ -226,7 +367,7 @@ class Entity implements EntityInterface { * Implements EntityInterface::bundle(). */ public function bundle() { - return isset($this->bundleKey) ? $this->{$this->bundleKey} : $this->entityType; + return isset($this->bundleKey) && !empty($this->{$this->bundleKey}) ? $this->{$this->bundleKey} : FALSE; } /** @@ -276,6 +417,174 @@ class Entity implements EntityInterface { } /** + * Implements EntityInterface::languageCode(). + */ + public function languageCode() { + return $this->langcode; + } + + /** + * Implements EntityInterface::language(). + */ + public function language() { + // @todo: Check for language.module instead, once Field API language + // handling depends upon it too. + return module_exists('locale') ? language_load($this->languageCode()) : FALSE; + } + + /** + * Implements EntityInterface::translations(). + */ + public function translations() { + $languages = array(); + if ($this->entityInfo['fieldable'] && ($default_language = $this->language())) { + // Go through translatable properties and determine all languages for + // which translated values are available. + $instances = field_info_instances($this->entityType, $this->getFieldBundle()); + + foreach ($instances as $field_name => $instance) { + $field = field_info_field($field_name); + if (field_is_translatable($this->entityType, $field) && isset($this->$field_name)) { + foreach ($this->$field_name as $langcode => $value) { + $languages[$langcode] = TRUE; + } + } + } + // Remove the default language from the translations. + unset($languages[$default_language->langcode]); + $languages = array_intersect_key(language_list(), $languages); + } + return $languages; + } + + /** + * Implements EntityInterface::propertyInfo(). + */ + public function propertyInfo($property_name = NULL) { + $info = entity_get_property_info($this->entityType); + $properties = isset($info['properties']) ? $info['properties'] : array(); + + // Add in bundle-specific properties. + $bundle = $this->bundle(); + if ($bundle && !empty($info['bundles'][$bundle]['properties'])) { + $properties = $info['bundles'][$bundle]['properties'] + $properties; + } + + if (isset($property_name)) { + return isset($properties[$property_name]) ? $properties[$property_name] : FALSE; + } + return $properties; + } + + /** + * Implements EntityInterface::get(). + */ + public function get($property_name, $langcode = NULL) { + $value = $this->getRawValue($property_name, $langcode); + + // Load entities. + $info = $this->propertyInfo($property_name); + if (isset($value) && $info['type'] == 'entity') { + + if (!empty($info['multiple'])) { + $value = entity_load($info['entity type'], $value); + } + else { + $result = entity_load($info['entity type'], array($value)); + $value = reset($result); + } + } + + return $value; + } + + /** + * Implements EntityInterface::getRawValue(). + */ + public function getRawValue($property_name, $langcode = NULL) { + $info = $this->propertyInfo($property_name); + $name = !empty($info['storage field']) ? $info['storage field'] : $property_name; + + // Handle fields. + $is_field = !empty($info['module']) && $info['module'] == 'field'; + if ($is_field && field_info_instance($this->entityType, $name, $this->getFieldBundle())) { + $field = field_info_field($name); + $langcode = $this->getFieldLangcode($field, $langcode); + $value = isset($this->{$name}[$langcode]) ? $this->{$name}[$langcode] : NULL; + } + else { + // Handle properties being not fields. + // @todo: Add support for translatable properties being not fields. + $value = isset($this->{$name}) ? $this->{$name} : NULL; + } + return $value; + } + + /** + * Implements EntityInterface::set(). + */ + public function set($property_name, $value, $langcode = NULL) { + $info = $this->propertyInfo($property_name); + $name = !empty($info['storage field']) ? $info['storage field'] : $property_name; + + // For entity references deal with entity objects passed as values. + if (isset($value) && $info['type'] == 'entity') { + if (!empty($info['multiple'])) { + foreach ($value as $delta => $entity) { + if ($entity instanceof EntityInterface) { + $value[$delta] = $entity->id(); + } + } + } + elseif ($value instanceof EntityInterface) { + $value = $value->id(); + } + // @todo: Remove once all entities have been converted. + elseif (is_object($value)) { + list($id, ,) = entity_extract_ids($info['entity type'], $value); + $value = $id; + } + } + + // Handle fields. + if ($this->entityInfo['fieldable'] && field_info_instance($this->entityType, $name, $this->getFieldBundle())) { + $field = field_info_field($name); + $langcode = $this->getFieldLangcode($field, $langcode); + $this->{$name}[$langcode] = $value; + } + else { + // Handle properties being not fields. + // @todo: Add support for translatable properties being not fields. + $this->{$name} = $value; + } + } + + /** + * Determines the language code to use for accessing a field value in a certain language. + */ + protected function getFieldLangcode($field, $langcode = NULL) { + // Only apply the given langcode if the entity is language-specific. + // Otherwise translatable fields are handled as non-translatable fields. + if (field_is_translatable($this->entityType, $field) && ($default_language = $this->language())) { + // For translatable fields the values in default language are stored using + // the language code of the default language. + return isset($langcode) ? $langcode : $default_language->langcode; + } + else { + // Non-translatable fields always use LANGUAGE_NOT_SPECIFIED. + return LANGUAGE_NOT_SPECIFIED; + } + } + + /** + * Gets the bundle as used by the field API, thus defaulting to the entity type. + */ + protected function getFieldBundle() { + $bundle = $this->bundle(); + return $bundle ? $bundle : $this->entityType; + } + + /** * Implements EntityInterface::save(). */ public function save() { diff --git a/core/modules/entity/entity.module b/core/modules/entity/entity.module index 9b2b6f2..718ef26 100644 --- a/core/modules/entity/entity.module +++ b/core/modules/entity/entity.module @@ -272,14 +272,18 @@ function entity_delete_multiple($entity_type, $ids) { /** * Constructs a new entity object, without permanently saving it. * - * @param $entity_type + * @param string $entity_type * The type of the entity. - * @param $values - * An array of values to set, keyed by property name. If the entity type has - * bundles the bundle key has to be specified. + * @param array $values + * An array of values to set, keyed by property name. Non-NULL values for all + * properties which are marked as required via hook_entity_property_info() + * have to be provided. * * @return EntityInterface * A new entity object. + * + * @see hook_entity_property_info() + * @see EntityInterface::set() */ function entity_create($entity_type, array $values) { return entity_get_controller($entity_type)->create($values); @@ -415,6 +419,55 @@ function entity_label($entity_type, $entity) { } /** + * Implements hook_hook_info(). + */ +function entity_hook_info() { + $hook_info['entity_property_info'] = array( + 'group' => 'entity', + ); + $hook_info['entity_property_info_alter'] = array( + 'group' => 'entity', + ); + return $hook_info; +} + +/** + * Gets the entity property information of an entity type. + * + * @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. + * + * @see hook_entity_property_info() + * @see hook_entity_property_info_alter() + */ +function entity_get_property_info($entity_type = NULL) { + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['info'] = &drupal_static(__FUNCTION__); + } + $info = &$drupal_static_fast['info']; + + // hook_entity_property_info() includes translated strings, so each language + // is cached separately. + $langcode = $GLOBALS['language_interface']->langcode; + + if (empty($info)) { + if ($cache = cache()->get("entity_property_info:$langcode")) { + $info = $cache->data; + } + else { + $info = module_invoke_all('entity_property_info'); + // Let other modules alter the entity info. + drupal_alter('entity_property_info', $info); + cache()->set("entity_property_info:$langcode", $info); + } + } + return empty($entity_type) ? $info : (isset($info[$entity_type]) ? $info[$entity_type] : array()); +} + +/** * Attaches field API validation to entity forms. */ function entity_form_field_validate($entity_type, $form, &$form_state) { diff --git a/core/modules/entity/tests/entity.test b/core/modules/entity/tests/entity.test index 35b6ab1..21efc71 100644 --- a/core/modules/entity/tests/entity.test +++ b/core/modules/entity/tests/entity.test @@ -19,7 +19,7 @@ class EntityAPITestCase extends DrupalWebTestCase { } function setUp() { - parent::setUp('entity', 'entity_test'); + parent::setUp('entity', 'entity_test', 'comment'); } /** @@ -29,11 +29,11 @@ class EntityAPITestCase extends DrupalWebTestCase { $user1 = $this->drupalCreateUser(); // Create some test entities. - $entity = entity_create('entity_test', array('name' => 'test', 'uid' => $user1->uid)); + $entity = entity_create('entity_test', array('name' => 'test', 'user' => $user1->uid)); $entity->save(); - $entity = entity_create('entity_test', array('name' => 'test2', 'uid' => $user1->uid)); + $entity = entity_create('entity_test', array('name' => 'test2', 'user' => $user1->uid)); $entity->save(); - $entity = entity_create('entity_test', array('name' => 'test', 'uid' => NULL)); + $entity = entity_create('entity_test', array('name' => 'test', 'user' => NULL)); $entity->save(); $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test'))); @@ -65,6 +65,156 @@ class EntityAPITestCase extends DrupalWebTestCase { $all = entity_test_load_multiple(FALSE); $this->assertTrue(empty($all), 'Deleted all entities.'); } + + /** + * Tests Entity getters/setters. + */ + function testEntityGettersSetters() { + $entity = entity_create('entity_test', array('name' => 'test')); + $this->assertNull($entity->get('description'), 'Property is not set.'); + + $entity->set('description', 'Example.'); + $this->assertEqual($entity->description, 'Example.', 'Property has been set.'); + + $value = $entity->get('description'); + $this->assertEqual($value, $entity->description, 'Property has been retrieved.'); + + // Make sure setting/getting translations boils down to setting/getting the + // regular value as the entity and property are not translatable. + $entity->set('description', NULL, 'en'); + $this->assertNull($entity->description, 'Language neutral property has been set.'); + + $value = $entity->get('description', 'en'); + $this->assertNull($value, 'Language neutral property has been retrieved.'); + + // Test an entity reference. + $this->assertNull($entity->get('comment'), 'Entity reference is empty.'); + + $node = $this->drupalCreateNode(); + $comment = entity_create('comment', array('node' => $node)); + $comment->save(); + $this->assertEqual($comment->nid, $node->nid, 'Entity reference has been instantiated on entity creation.'); + + // Test setting the comment referency by object and ID. + $entity->set('comment', $comment); + $this->assertEqual($entity->comment_id, $comment->cid, 'Comment reference has been set by entity object.'); + + $entity->set('comment', $comment->cid); + $this->assertEqual($entity->comment_id, $comment->cid, 'Comment reference has been set by ID.'); + + $entity->set('comment', NULL); + $this->assertNull($entity->comment_id, 'Comment reference has been set by ID.'); + } +} + +/** + * Tests entity translation. + */ +class EntityTranslationTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Entity Translation', + 'description' => 'Tests entity translation functionality.', + 'group' => 'Entity API', + ); + } + + function setUp() { + // Enable translations for the test entity type. We cannot use + // variable_set() here as variables are cleared by parent::setUp(); + $GLOBALS['entity_test_translation'] = TRUE; + parent::setUp('entity_test', 'language', 'locale'); + + // Create a translatable test field. + $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); + $field = array( + 'field_name' => $this->field_name, + 'type' => 'text', + 'cardinality' => 4, + 'translatable' => TRUE, + ); + field_create_field($field); + $this->field = field_read_field($this->field_name); + + $instance = array( + 'field_name' => $this->field_name, + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + ); + field_create_instance($instance); + $this->instance = field_read_instance('entity_test', $this->field_name, 'entity_test'); + + // Create test languages. + $this->langcodes = array(); + for ($i = 0; $i < 3; ++$i) { + $language = (object) array( + 'langcode' => 'l' . $i, + 'name' => $this->randomString(), + ); + $this->langcodes[$i] = $language->langcode; + language_save($language); + } + } + + /** + * Tests language related methods of the Entity class. + */ + function testEntityLanguageMethods() { + $entity = entity_create('entity_test', array( + 'name' => 'test', + 'user' => $GLOBALS['user']->uid, + )); + $this->assertFalse($entity->language(), 'No entity language has been specified.'); + $this->assertFalse($entity->translations(), 'No translations are available'); + + // Set the value in default language. + $entity->set($this->field_name, array(0 => array('value' => 'default value'))); + // Get the value. + $value = $entity->get($this->field_name); + $this->assertEqual($value, array(0 => array('value' => 'default value')), 'Untranslated value retrieved.'); + + // Set the value in a certain language. As the entity is not + // language-specific it should use the default language and so ignore the + // specified language. + $entity->set($this->field_name, array(0 => array('value' => 'default value2')), $this->langcodes[1]); + $value = $entity->get($this->field_name); + $this->assertEqual($value, array(0 => array('value' => 'default value2')), 'Untranslated value updated.'); + $this->assertFalse($entity->translations(), 'No translations are available'); + + // Test getting a field value using the default language for a not + // language-specific entity. + $value = $entity->get($this->field_name, $this->langcodes[1]); + $this->assertEqual($value, array(0 => array('value' => 'default value2')), 'Untranslated value retrieved.'); + + // Now, make the entity language-specific by assigning a language and test + // translating it. + $entity->langcode = $this->langcodes[0]; + $entity->{$this->field_name} = array(); + $this->assertEqual($entity->language(), language_load($this->langcodes[0]), 'Entity language retrieved.'); + $this->assertFalse($entity->translations(), 'No translations are available'); + + // Set the value in default language. + $entity->set($this->field_name, array(0 => array('value' => 'default value'))); + // Get the value. + $value = $entity->get($this->field_name); + $this->assertEqual($value, array(0 => array('value' => 'default value')), 'Untranslated value retrieved.'); + + // Set a translation. + $entity->set($this->field_name, array(0 => array('value' => 'translation 1')), $this->langcodes[1]); + $value = $entity->get($this->field_name, $this->langcodes[1]); + $this->assertEqual($value, array(0 => array('value' => 'translation 1')), 'Translated value set.'); + // Make sure the untranslated value stays. + $value = $entity->get($this->field_name); + $this->assertEqual($value, array(0 => array('value' => 'default value')), 'Untranslated value stays.'); + + $translations[$this->langcodes[1]] = language_load($this->langcodes[1]); + $this->assertEqual($entity->translations(), $translations, 'Translations retrieved.'); + + // Try to get a not available translation. + $value = $entity->get($this->field_name, $this->langcodes[2]); + $this->assertNull($value, 'A translation that is not available is NULL.'); + } } /** diff --git a/core/modules/entity/tests/entity_crud_hook_test.test b/core/modules/entity/tests/entity_crud_hook_test.test index be59e99..12a311c 100644 --- a/core/modules/entity/tests/entity_crud_hook_test.test +++ b/core/modules/entity/tests/entity_crud_hook_test.test @@ -75,13 +75,11 @@ class EntityCrudHookTestCase extends DrupalWebTestCase { 'changed' => REQUEST_TIME, ); node_save($node); - $nid = $node->nid; $comment = entity_create('comment', array( - 'cid' => NULL, 'pid' => 0, - 'nid' => $nid, - 'uid' => 1, + 'node' => $node, + 'user' => 1, 'subject' => 'Test comment', 'created' => REQUEST_TIME, 'changed' => REQUEST_TIME, diff --git a/core/modules/entity/tests/entity_test.entity.inc b/core/modules/entity/tests/entity_test.entity.inc new file mode 100644 index 0000000..e87c0d2 --- /dev/null +++ b/core/modules/entity/tests/entity_test.entity.inc @@ -0,0 +1,53 @@ + t('Test ID'), + 'description' => t('The unique ID of the test.'), + 'type' => 'integer', + ); + $info['entity_test']['properties']['id'] = array( + 'label' => t('Test ID'), + 'description' => t('The unique ID of the test entity.'), + 'type' => 'integer', + ); + $info['entity_test']['properties']['name'] = array( + 'label' => t('Test name'), + 'description' => t('The name of the test entity.'), + 'type' => 'text', + ); + $info['entity_test']['properties']['description'] = array( + 'label' => t('Test description'), + 'description' => t('The description of the test entity.'), + 'type' => 'text', + ); + $info['entity_test']['properties']['user'] = array( + 'label' => t('Test entity user'), + 'description' => t('The user of the test entity.'), + 'type' => 'entity', + 'entity type' => 'user', + 'storage field' => 'user_id', + ); + $info['entity_test']['properties']['comment'] = array( + 'label' => t('Test entity comment'), + 'description' => t('The comment of the test entity.'), + 'type' => 'entity', + 'entity type' => 'comment', + 'storage field' => 'comment_id', + ); + $info['entity_test']['properties']['langcode'] = array( + 'label' => t('Test entity langcode'), + 'description' => t('The langcode of the test entity.'), + 'type' => 'token', + ); + return $info; +} diff --git a/core/modules/entity/tests/entity_test.install b/core/modules/entity/tests/entity_test.install index ec2e5bd..0cd3378 100644 --- a/core/modules/entity/tests/entity_test.install +++ b/core/modules/entity/tests/entity_test.install @@ -50,20 +50,41 @@ function entity_test_schema() { 'not null' => TRUE, 'default' => '', ), - 'uid' => array( + 'description' => array( + 'description' => 'The description of the test entity.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + 'default' => '', + ), + 'user_id' => array( 'type' => 'int', 'unsigned' => TRUE, 'not null' => FALSE, 'default' => NULL, 'description' => "The {users}.uid of the associated user.", ), - ), - 'indexes' => array( - 'uid' => array('uid'), + 'comment_id' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + 'description' => "The {comment}.cid of the associated comment.", + ), + 'langcode' => array( + 'description' => 'The {language}.langcode of the test entity.', + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + ), ), 'foreign keys' => array( 'uid' => array('users' => 'uid'), ), + 'foreign keys' => array( + 'cid' => array('comment' => 'cid'), + ), 'primary key' => array('id'), ); return $schema; diff --git a/core/modules/entity/tests/entity_test.module b/core/modules/entity/tests/entity_test.module index 6034b06..f7dffa0 100644 --- a/core/modules/entity/tests/entity_test.module +++ b/core/modules/entity/tests/entity_test.module @@ -9,19 +9,21 @@ * Implements hook_entity_info(). */ function entity_test_entity_info() { - $return = array( - 'entity_test' => array( - 'label' => t('Test entity'), - 'entity class' => 'Entity', - 'controller class' => 'EntityDatabaseStorageController', - 'base table' => 'entity_test', - 'fieldable' => TRUE, - 'entity keys' => array( - 'id' => 'id', - ), + $items['entity_test'] = array( + 'label' => t('Test entity'), + 'entity class' => 'Entity', + 'controller class' => 'EntityDatabaseStorageController', + 'base table' => 'entity_test', + 'fieldable' => TRUE, + 'entity keys' => array( + 'id' => 'id', ), ); - return $return; + // Optionally specify a translation handler for testing translations. + if (!empty($GLOBALS['entity_test_translation'])) { + $items['entity_test']['translation']['entity_test'] = TRUE; + } + return $items; } /**