diff --git a/modules/comment/comment.module b/modules/comment/comment.module index 60a9ca4..074006a 100644 --- a/modules/comment/comment.module +++ b/modules/comment/comment.module @@ -99,6 +99,7 @@ function comment_entity_info() { 'uri callback' => 'comment_uri', 'fieldable' => TRUE, 'controller class' => 'CommentController', + 'entity class' => 'Entity', 'entity keys' => array( 'id' => 'cid', 'bundle' => 'node_type', @@ -732,7 +733,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)) { - $build = drupal_get_form("comment_node_{$node->type}_form", (object) array('nid' => $node->nid)); + $build = drupal_get_form("comment_node_{$node->type}_form", entity_create('comment', array('nid' => $node->nid))); $additions['comment_form'] = $build; } @@ -1432,147 +1433,7 @@ function comment_access($op, $comment) { * A comment object. */ function comment_save($comment) { - global $user; - - $transaction = db_transaction(); - try { - $defaults = array( - 'mail' => '', - 'homepage' => '', - 'name' => '', - 'status' => user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED, - ); - foreach ($defaults as $key => $default) { - if (!isset($comment->$key)) { - $comment->$key = $default; - } - } - // Make sure we have a bundle name. - if (!isset($comment->node_type)) { - $node = node_load($comment->nid); - $comment->node_type = 'comment_node_' . $node->type; - } - - // Load the stored entity, if any. - if (!empty($comment->cid) && !isset($comment->original)) { - $comment->original = entity_load_unchanged('comment', $comment->cid); - } - - field_attach_presave('comment', $comment); - - // Allow modules to alter the comment before saving. - module_invoke_all('comment_presave', $comment); - module_invoke_all('entity_presave', $comment, 'comment'); - - if ($comment->cid) { - - drupal_write_record('comment', $comment, 'cid'); - - // Ignore slave server temporarily to give time for the - // saved comment to be propagated to the slave. - db_ignore_slave(); - - // Update the {node_comment_statistics} table prior to executing hooks. - _comment_update_node_statistics($comment->nid); - - field_attach_update('comment', $comment); - // Allow modules to respond to the updating of a comment. - module_invoke_all('comment_update', $comment); - module_invoke_all('entity_update', $comment, 'comment'); - } - else { - // Add the comment to database. This next section builds the thread field. - // Also see the documentation for comment_view(). - if (!empty($comment->thread)) { - // Allow calling code to set thread itself. - $thread = $comment->thread; - } - elseif ($comment->pid == 0) { - // This is a comment with no parent comment (depth 0): we start - // by retrieving the maximum thread level. - $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField(); - // Strip the "/" from the end of the thread. - $max = rtrim($max, '/'); - // Finally, build the thread field for this new comment. - $thread = int2vancode(vancode2int($max) + 1) . '/'; - } - else { - // This is a comment with a parent comment, so increase the part of the - // thread value at the proper depth. - - // Get the parent comment: - $parent = comment_load($comment->pid); - // Strip the "/" from the end of the parent thread. - $parent->thread = (string) rtrim((string) $parent->thread, '/'); - // Get the max value in *this* thread. - $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array( - ':thread' => $parent->thread . '.%', - ':nid' => $comment->nid, - ))->fetchField(); - - if ($max == '') { - // First child of this parent. - $thread = $parent->thread . '.' . int2vancode(0) . '/'; - } - else { - // Strip the "/" at the end of the thread. - $max = rtrim($max, '/'); - // Get the value at the correct depth. - $parts = explode('.', $max); - $parent_depth = count(explode('.', $parent->thread)); - $last = $parts[$parent_depth]; - // Finally, build the thread field for this new comment. - $thread = $parent->thread . '.' . int2vancode(vancode2int($last) + 1) . '/'; - } - } - - if (empty($comment->created)) { - $comment->created = REQUEST_TIME; - } - - if (empty($comment->changed)) { - $comment->changed = $comment->created; - } - - if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well. - $comment->name = $user->name; - } - - // Ensure the parent id (pid) has a value set. - if (empty($comment->pid)) { - $comment->pid = 0; - } - - // Add the values which aren't passed into the function. - $comment->thread = $thread; - $comment->hostname = ip_address(); - - drupal_write_record('comment', $comment); - - // Ignore slave server temporarily to give time for the - // created comment to be propagated to the slave. - db_ignore_slave(); - - // Update the {node_comment_statistics} table prior to executing hooks. - _comment_update_node_statistics($comment->nid); - - field_attach_insert('comment', $comment); - - // Tell the other modules a new comment has been submitted. - module_invoke_all('comment_insert', $comment); - module_invoke_all('entity_insert', $comment, 'comment'); - } - if ($comment->status == COMMENT_PUBLISHED) { - module_invoke_all('comment_publish', $comment); - } - unset($comment->original); - } - catch (Exception $e) { - $transaction->rollback('comment'); - watchdog_exception('comment', $e); - throw $e; - } - + entity_save('comment', $comment); } /** @@ -1592,31 +1453,7 @@ function comment_delete($cid) { * The comment to delete. */ function comment_delete_multiple($cids) { - $comments = comment_load_multiple($cids); - if ($comments) { - $transaction = db_transaction(); - try { - // Delete the comments. - db_delete('comment') - ->condition('cid', array_keys($comments), 'IN') - ->execute(); - foreach ($comments as $comment) { - field_attach_delete('comment', $comment); - module_invoke_all('comment_delete', $comment); - module_invoke_all('entity_delete', $comment, 'comment'); - - // Delete the comment's replies. - $child_cids = db_query('SELECT cid FROM {comment} WHERE pid = :cid', array(':cid' => $comment->cid))->fetchCol(); - comment_delete_multiple($child_cids); - _comment_update_node_statistics($comment->nid); - } - } - catch (Exception $e) { - $transaction->rollback(); - watchdog_exception('comment', $e); - throw $e; - } - } + entity_delete_multiple('comment', $cids); } /** @@ -1659,10 +1496,10 @@ function comment_load($cid) { /** * Controller class for comments. * - * This extends the DrupalDefaultEntityController class, adding required - * special handling for comment objects. + * This extends the EntityAPIController class, adding required pecial handling + * for comment objects. */ -class CommentController extends DrupalDefaultEntityController { +class CommentController extends EntityAPIController { protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) { $query = parent::buildQuery($ids, $conditions, $revision_id); @@ -1685,6 +1522,115 @@ class CommentController extends DrupalDefaultEntityController { } parent::attachLoad($comments, $revision_id); } + + public function create(array $values = array()) { + $defaults = array( + 'mail' => '', + 'homepage' => '', + 'name' => '', + 'status' => user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED, + 'hostname' => ip_address(), + 'created' => REQUEST_TIME, + 'changed' => REQUEST_TIME, + 'subject' => '', + 'comment' => '', + 'cid' => NULL, + 'pid' => NULL, + 'language' => LANGUAGE_NONE, + 'uid' => 0, + ); + return parent::create($values + $defaults); + } + + public function save($comment, DatabaseTransaction $transaction = NULL) { + global $user; + $transaction = isset($transaction) ? $transaction : db_transaction(); + + try { + // Make sure we have a bundle name. + if (!isset($comment->node_type)) { + $node = node_load($comment->nid); + $comment->node_type = 'comment_node_' . $node->type; + } + if ($comment->uid === $user->uid && isset($user->name)) { // '===' Need to modify anonymous users as well. + $comment->name = $user->name; + } + if (!$comment->cid) { + // Add the comment to database. This next section builds the thread field. + // Also see the documentation for comment_view(). + if (!empty($comment->thread)) { + // Allow calling code to set thread itself. + $thread = $comment->thread; + } + elseif ($comment->pid == 0) { + // This is a comment with no parent comment (depth 0): we start + // by retrieving the maximum thread level. + $max = db_query('SELECT MAX(thread) FROM {comment} WHERE nid = :nid', array(':nid' => $comment->nid))->fetchField(); + // Strip the "/" from the end of the thread. + $max = rtrim($max, '/'); + // Finally, build the thread field for this new comment. + $thread = int2vancode(vancode2int($max) + 1) . '/'; + } + else { + // This is a comment with a parent comment, so increase the part of + // the thread value at the proper depth. + + // Get the parent comment: + $parent = comment_load($comment->pid); + // Strip the "/" from the end of the parent thread. + $parent->thread = (string) rtrim((string) $parent->thread, '/'); + // Get the max value in *this* thread. + $max = db_query("SELECT MAX(thread) FROM {comment} WHERE thread LIKE :thread AND nid = :nid", array( + ':thread' => $parent->thread . '.%', + ':nid' => $comment->nid, + ))->fetchField(); + + if ($max == '') { + // First child of this parent. + $thread = $parent->thread . '.' . int2vancode(0) . '/'; + } + else { + // Strip the "/" at the end of the thread. + $max = rtrim($max, '/'); + // Get the value at the correct depth. + $parts = explode('.', $max); + $parent_depth = count(explode('.', $parent->thread)); + $last = $parts[$parent_depth]; + // Finally, build the thread field for this new comment. + $thread = $parent->thread . '.' . int2vancode(vancode2int($last) + 1) . '/'; + } + } + + // Add the values which aren't passed into the function. + $comment->thread = $thread; + } + + parent::save($comment, $transaction); + + if ($comment->status == COMMENT_PUBLISHED) { + module_invoke_all('comment_publish', $comment); + } + } + catch (Exception $e) { + $transaction->rollback('comment'); + watchdog_exception('comment', $e); + throw $e; + } + } + + public function invoke($hook, $comment) { + if ($hook == 'insert' || $hook == 'update') { + // Update the {node_comment_statistics} table prior to executing hooks. + _comment_update_node_statistics($comment->nid); + } + parent::invoke($hook, $comment); + if ($hook == 'delete') { + // Delete the comment's replies. + $child_cids = db_query('SELECT cid FROM {comment} WHERE pid = :cid', array(':cid' => $comment->cid))->fetchCol(); + comment_delete_multiple($child_cids); + _comment_update_node_statistics($comment->nid); + } + } } /** @@ -1816,22 +1762,6 @@ function comment_form($form, &$form_state, $comment) { // use during form building and processing. During a rebuild, use what is in // the form state. if (!isset($form_state['comment'])) { - $defaults = array( - 'name' => '', - 'mail' => '', - 'homepage' => '', - 'subject' => '', - 'comment' => '', - 'cid' => NULL, - 'pid' => NULL, - 'language' => LANGUAGE_NONE, - 'uid' => 0, - ); - foreach ($defaults as $key => $value) { - if (!isset($comment->$key)) { - $comment->$key = $value; - } - } $form_state['comment'] = $comment; } else { @@ -2129,12 +2059,6 @@ function comment_form_validate($form, &$form_state) { * Prepare a comment for submission. */ function comment_submit($comment) { - // @todo Legacy support. Remove in Drupal 8. - if (is_array($comment)) { - $comment += array('subject' => ''); - $comment = (object) $comment; - } - if (empty($comment->date)) { $comment->date = 'now'; } diff --git a/modules/comment/comment.pages.inc b/modules/comment/comment.pages.inc index 7e88bff..b176105 100644 --- a/modules/comment/comment.pages.inc +++ b/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')) { - $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", (object) array('pid' => $pid, 'nid' => $node->nid)); + $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", entity_create('comment', array('pid' => $pid, 'nid' => $node->nid))); } else { drupal_set_message(t('You are not authorized to post comments.'), 'error'); @@ -86,8 +86,8 @@ function comment_reply($node, $pid = NULL) { drupal_goto("node/$node->nid"); } elseif (user_access('post comments')) { - $edit = array('nid' => $node->nid, 'pid' => $pid); - $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", (object) $edit); + $comment = entity_create('comment', array('nid' => $node->nid, 'pid' => $pid)); + $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment); } else { drupal_set_message(t('You are not authorized to post comments.'), 'error'); diff --git a/modules/comment/comment.test b/modules/comment/comment.test index 770e01d..04c071d 100644 --- a/modules/comment/comment.test +++ b/modules/comment/comment.test @@ -1,7 +1,7 @@ $match[1], 'subject' => $subject, 'comment' => $comment); + return entity_create('comment', array('id' => $match[1], 'subject' => $subject, 'comment' => $comment)); } } @@ -604,7 +604,7 @@ class CommentInterfaceTest extends CommentHelperCase { if ($info['comment count']) { // Create a comment via CRUD API functionality, since // $this->postComment() relies on actual user permissions. - $comment = (object) array( + $comment = entity_create('comment', array( 'cid' => NULL, 'nid' => $this->node->nid, 'node_type' => $this->node->type, @@ -615,7 +615,7 @@ class CommentInterfaceTest extends CommentHelperCase { 'hostname' => ip_address(), 'language' => LANGUAGE_NONE, 'comment_body' => array(LANGUAGE_NONE => array($this->randomName())), - ); + )); comment_save($comment); $this->comment = $comment; @@ -1404,7 +1404,7 @@ class CommentApprovalTest extends CommentHelperCase { // Get unapproved comment id. $this->drupalLogin($this->admin_user); $anonymous_comment4 = $this->getUnapprovedComment($subject); - $anonymous_comment4 = (object) array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body); + $anonymous_comment4 = entity_create('comment', array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body)); $this->drupalLogout(); $this->assertFalse($this->commentExists($anonymous_comment4), t('Anonymous comment was not published.')); @@ -1468,7 +1468,7 @@ class CommentApprovalTest extends CommentHelperCase { // Get unapproved comment id. $this->drupalLogin($this->admin_user); $anonymous_comment4 = $this->getUnapprovedComment($subject); - $anonymous_comment4 = (object) array('id' => $anonymous_comment4, 'subject' => $subject, 'comment' => $body); + $anonymous_comment4 = entity_create('comment', 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/modules/entity/entity.class.inc b/modules/entity/entity.class.inc new file mode 100644 index 0000000..7ab6cfd --- /dev/null +++ b/modules/entity/entity.class.inc @@ -0,0 +1,127 @@ +entityType = $entityType; + $this->entityInfo = entity_get_info($entityType); + $this->idKey = $this->entityInfo['entity keys']['id']; + + // Set initial values. + foreach ($values as $key => $value) { + $this->$key = $value; + } + } + + /** + * Returns the entity identifier. + * + * @return + * The identifier of the entity. + */ + public function identifier() { + return isset($this->{$this->idKey}) ? $this->{$this->idKey} : NULL; + } + + /** + * Returns the info of the type of the entity. + * + * @see entity_get_info() + */ + public function entityInfo() { + return $this->entityInfo; + } + + /** + * Returns the type of the entity. + */ + public function entityType() { + return $this->entityType; + } + + /** + * Returns the label of the entity. + * + * @see entity_label() + */ + public function label() { + return entity_label($this->entityType, $this); + } + + /** + * Returns the uri of the entity just as entity_uri(). + * + * @see entity_uri() + */ + public function uri() { + return entity_uri($this->entityType, $this); + } + + /** + * Permanently saves the entity. + * + * @see entity_save() + */ + public function save() { + return entity_get_controller($this->entityType)->save($this); + } + + /** + * Permanently deletes the entity. + * + * @see entity_delete() + */ + public function delete() { + $id = $this->identifier(); + if (isset($id)) { + entity_get_controller($this->entityType)->delete(array($id)); + } + } + + /** + * Exports the entity. + * + * @see entity_export() + */ + public function export($prefix = '') { + return entity_get_controller($this->entityType)->export($this, $prefix); + } + + /** + * Generate an array for rendering the entity. + * + * @see entity_view() + */ + public function view($view_mode = 'full', $langcode = NULL) { + return entity_get_controller($this->entityType)->view(array($this), $view_mode, $langcode); + } + + /** + * Builds a structured array representing the entity's content. + * + * @see entity_build_content() + */ + public function buildContent($view_mode = 'full', $langcode = NULL) { + return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode); + } +} diff --git a/modules/entity/entity.controller.inc b/modules/entity/entity.controller.inc index c57b9e0..ac3a98c 100644 --- a/modules/entity/entity.controller.inc +++ b/modules/entity/entity.controller.inc @@ -387,3 +387,436 @@ class DrupalDefaultEntityController implements DrupalEntityControllerInterface { $this->entityCache += $entities; } } + +/** + * Interface for EntityControllers compatible with the entity API. + */ +interface EntityAPIControllerInterface extends DrupalEntityControllerInterface { + + /** + * Delete permanently saved entities. + * + * In case of failures, an exception is thrown. + * + * @param $ids + * An array of entity IDs. + */ + public function delete($ids); + + /** + * Invokes a hook on behalf of the entity. For hooks that have a respective + * field API attacher like insert/update/.. the attacher is called too. + */ + public function invoke($hook, $entity); + + /** + * Permanently saves the given entity. + * + * In case of failures, an exception is thrown. + * + * @param $entity + * The entity to save. + * + * @return + * SAVED_NEW or SAVED_UPDATED is returned depending on the operation + * performed. + */ + public function save($entity); + + /** + * Create a new entity. + * + * @param array $values + * An array of values to set, keyed by property name. + * @return + * A new instance of the entity type. + */ + public function create(array $values = array()); + + /** + * Exports an entity as serialized string. + * + * @param $entity + * The entity to export. + * @param $prefix + * An optional prefix for each line. + * + * @return + * The exported entity as serialized string. The format is determined by + * the controller and has to be compatible with the format that is accepted + * by the import() method. + */ + public function export($entity, $prefix = ''); + + /** + * Imports an entity from a string. + * + * @param string $export + * An exported entity as serialized string. + * + * @return + * An entity object not yet saved. + */ + public function import($export); + + /** + * Builds a structured array representing the entity's content. + * + * The content built for the entity will vary depending on the $view_mode + * parameter. + * + * @param $entity + * An entity object. + * @param $view_mode + * View mode, e.g. 'full', 'teaser'... + * @param $langcode + * (optional) A language code to use for rendering. Defaults to the global + * content language of the current request. + * @return + * The renderable array. + */ + public function buildContent($entity, $view_mode = 'full', $langcode = NULL); + + /** + * Generate an array for rendering the given entities. + * + * @param $entities + * An array of entities to render. + * @param $view_mode + * View mode, e.g. 'full', 'teaser'... + * @param $langcode + * (optional) A language code to use for rendering. Defaults to the global + * content language of the current request. + * @return + * The renderable array, keyed by entity name or numeric id. + */ + public function view($entities, $view_mode = 'full', $langcode = NULL); +} + +/** + * A controller implementing EntityAPIControllerInterface for the database. + */ +class EntityAPIController extends DrupalDefaultEntityController implements EntityAPIControllerInterface { + + protected $cacheComplete = FALSE; + protected $bundleKey; + + /** + * Overridden. + * @see DrupalDefaultEntityController#__construct() + */ + public function __construct($entityType) { + parent::__construct($entityType); + // If this is the bundle of another entity, set the bundle key. + if (isset($this->entityInfo['bundle of'])) { + $info = entity_get_info($this->entityInfo['bundle of']); + $this->bundleKey = $info['bundle keys']['bundle']; + } + } + + /** + * Builds and executes the query for loading. + * + * @return The results in a Traversable object. + */ + public function query($ids, $conditions, $revision_id = FALSE) { + // Build the query. + $query = $this->buildQuery($ids, $conditions, $revision_id); + $result = $query->execute(); + if (!empty($this->entityInfo['entity class'])) { + $result->setFetchMode(PDO::FETCH_CLASS, $this->entityInfo['entity class'], array(array(), $this->entityType)); + } + return $result; + } + + /** + * Overridden. + * @see DrupalDefaultEntityController#load($ids, $conditions) + * + * In contrast to the parent implementation we factor out query execution, so + * fetching can be further customized easily. + */ + 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 ($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 or if loading a revision. + if (!($this->cacheComplete && $ids === FALSE && !$conditions) && ($ids === FALSE || $ids || $revision_id)) { + $queried_entities = array(); + foreach ($this->query($ids, $conditions, $revision_id) as $record) { + // Skip entities already retrieved from cache. + if (isset($entities[$record->{$this->idKey}])) { + continue; + } + + // Take care of serialized columns. + $schema = drupal_get_schema($this->entityInfo['base table']); + + foreach ($schema['fields'] as $field => $info) { + if (!empty($info['serialize']) && isset($record->$field)) { + $record->$field = unserialize($record->$field); + } + } + $queried_entities[$record->{$this->idKey}] = $record; + } + } + + // 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); + + // Remember if we have cached all entities now. + if (!$conditions && $ids === FALSE) { + $this->cacheComplete = TRUE; + } + } + } + // 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 && $passed_ids = array_intersect_key($passed_ids, $entities)) { + foreach ($passed_ids as $id => $value) { + $passed_ids[$id] = $entities[$id]; + } + $entities = $passed_ids; + } + return $entities; + } + + public function resetCache(array $ids = NULL) { + $this->cacheComplete = FALSE; + parent::resetCache($ids); + } + + /** + * Implements EntityAPIControllerInterface. + */ + public function invoke($hook, $entity) { + if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) { + $function($this->entityType, $entity); + } + module_invoke_all($this->entityType . '_' . $hook, $entity); + + // Invoke the respective entity level hook. + if ($hook == 'presave' || $hook == 'insert' || $hook == 'update' || $hook == 'delete') { + module_invoke_all('entity_' . $hook, $entity, $this->entityType); + } + } + + /** + * Implements EntityAPIControllerInterface. + * + * @param $transaction + * Optionally a DatabaseTransaction object to use. Allows overrides to pass + * in their transaction object. + */ + public function delete($ids, DatabaseTransaction $transaction = NULL) { + $entities = $ids ? $this->load($ids) : FALSE; + if (!$entities) { + // Do nothing, in case invalid or no ids have been passed. + return; + } + $transaction = isset($transaction) ? $transaction : db_transaction(); + + try { + $ids = array_keys($entities); + + db_delete($this->entityInfo['base table']) + ->condition($this->idKey, $ids, 'IN') + ->execute(); + // Reset the cache as soon as the changes have been applied. + $this->resetCache($ids); + + foreach ($entities as $id => $entity) { + $this->invoke('delete', $entity); + } + // Ignore slave server temporarily. + db_ignore_slave(); + } + catch (Exception $e) { + if (isset($transaction)) { + $transaction->rollback(); + } + watchdog_exception($this->entityType, $e); + throw $e; + } + } + + /** + * Implements EntityAPIControllerInterface. + * + * @param $transaction + * Optionally a DatabaseTransaction object to use. Allows overrides to pass + * in their transaction object. + */ + public function save($entity, DatabaseTransaction $transaction = NULL) { + $transaction = isset($transaction) ? $transaction : db_transaction(); + try { + // Load the stored entity, if any. + if (!empty($entity->{$this->idKey}) && !isset($entity->original)) { + // In order to properly work in case of name changes, load the original + // entity using the id key if it is available. + $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->idKey}); + } + + $this->invoke('presave', $entity); + + if (!empty($entity->{$this->idKey}) && empty($entity->is_new)) { + $return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey); + $this->resetCache(array($entity->{$this->idKey})); + $this->invoke('update', $entity); + } + else { + $return = drupal_write_record($this->entityInfo['base table'], $entity); + $this->invoke('insert', $entity); + } + // Ignore slave server temporarily. + db_ignore_slave(); + unset($entity->is_new); + unset($entity->original); + + return $return; + } + catch (Exception $e) { + $transaction->rollback(); + watchdog_exception($this->entityType, $e); + throw $e; + } + } + + /** + * Implements EntityAPIControllerInterface. + */ + public function create(array $values = array()) { + // Add is_new property if it is not set. + $values += array('is_new' => TRUE); + if (isset($this->entityInfo['entity class']) && $class = $this->entityInfo['entity class']) { + return new $class($values, $this->entityType); + } + return (object) $values; + } + + /** + * Implements EntityAPIControllerInterface. + * + * @return + * A serialized string in JSON format suitable for the import() method. + */ + public function export($entity, $prefix = '') { + $vars = get_object_vars($entity); + unset($vars['is_new']); + return entity_var_json_export($vars, $prefix); + } + + /** + * Implements EntityAPIControllerInterface. + * + * @param $export + * A serialized string in JSON format as produced by the export() method. + */ + public function import($export) { + $vars = drupal_json_decode($export); + if (is_array($vars)) { + return $this->create($vars); + } + return FALSE; + } + + /** + * Implements EntityAPIControllerInterface. + * + * @param $content + * Optionally. Allows pre-populating the built content to ease overridding + * this method. + */ + public function buildContent($entity, $view_mode = 'full', $langcode = NULL, $content = array()) { + // Remove previously built content, if exists. + $entity->content = $content; + $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language; + + // Add in fields. + if (!empty($this->entityInfo['fieldable'])) { + $entity->content += field_attach_view($this->entityType, $entity, $view_mode, $langcode); + } + // Invoke hook_ENTITY_view() to allow modules to add their additions. + if (module_exists('rules')) { + rules_invoke_all($this->entityType . '_view', $entity, $view_mode, $langcode); + } + else { + module_invoke_all($this->entityType . '_view', $entity, $view_mode, $langcode); + } + module_invoke_all('entity_view', $entity, $this->entityType, $view_mode, $langcode); + $build = $entity->content; + unset($entity->content); + return $build; + } + + /** + * Implements EntityAPIControllerInterface. + */ + public function view($entities, $view_mode = 'full', $langcode = NULL) { + // For Field API and entity_prepare_view, the entities have to be keyed by + // (numeric) id. + $entities = entity_key_array_by_property($entities, $this->idKey); + if (!empty($this->entityInfo['fieldable'])) { + field_attach_prepare_view($this->entityType, $entities, $view_mode); + } + entity_prepare_view($this->entityType, $entities); + $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language; + + $view = array(); + foreach ($entities as $entity) { + $build = entity_build_content($this->entityType, $entity, $view_mode, $langcode); + $build += array( + // If the entity type provides an implementation, use this instead the + // generic one. + // @see template_preprocess_entity() + '#theme' => 'entity', + '#entity_type' => $this->entityType, + '#entity' => $entity, + '#view_mode' => $view_mode, + '#language' => $langcode, + ); + // Allow modules to modify the structured entity. + drupal_alter(array($this->entityType . '_view', 'entity_view'), $build, $this->entityType); + $key = isset($entity->{$this->idKey}) ? $entity->{$this->idKey} : NULL; + $view[$this->entityType][$key] = $build; + } + return $view; + } +} \ No newline at end of file diff --git a/modules/entity/entity.info b/modules/entity/entity.info index 3f4591e..233e857 100644 --- a/modules/entity/entity.info +++ b/modules/entity/entity.info @@ -4,5 +4,9 @@ package = Core version = VERSION core = 8.x required = TRUE +files[] = entity.class.inc files[] = entity.query.inc files[] = entity.controller.inc +files[] = tests/entity_crud_hook_test.test +files[] = tests/entity_query.test +files[] = tests/entity.test \ No newline at end of file diff --git a/modules/entity/entity.module b/modules/entity/entity.module index 52d8667..81eb2db 100644 --- a/modules/entity/entity.module +++ b/modules/entity/entity.module @@ -164,19 +164,20 @@ function entity_extract_ids($entity_type, $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. + * An entity object, initialized with the ids provided. */ function entity_create_stub_entity($entity_type, $ids) { - $entity = new stdClass(); + $values = array(); $info = entity_get_info($entity_type); - $entity->{$info['entity keys']['id']} = $ids[0]; + $values[$info['entity keys']['id']] = $ids[0]; if (!empty($info['entity keys']['revision']) && isset($ids[1])) { - $entity->{$info['entity keys']['revision']} = $ids[1]; + $values[$info['entity keys']['revision']] = $ids[1]; } if (!empty($info['entity keys']['bundle']) && isset($ids[2])) { - $entity->{$info['entity keys']['bundle']} = $ids[2]; + $values[$info['entity keys']['bundle']] = $ids[2]; } - return $entity; + // @todo: Once all entities are converted, just rely on entity_create(). + return isset($info['entity class']) ? entity_create($entity_type, $values) : (object) $values; } /** @@ -247,6 +248,145 @@ function entity_load_unchanged($entity_type, $id) { } /** + * Permanently save an entity. + * + * In case of failures, an exception is thrown. + * + * @param $entity_type + * The type of the entity. + * @param $entity + * The entity to save. + * @return + * Either SAVED_NEW or SAVED_UPDATED is returned, depending on the operation + * performed. + */ +function entity_save($entity_type, $entity) { + return entity_get_controller($entity_type)->save($entity); +} + +/** + * Permanently delete the given entity. + * + * In case of failures, an exception is thrown. + * + * @param $entity_type + * The type of the entity. + * @param $id + * The identifier of the entity to delete. + */ +function entity_delete($entity_type, $id) { + entity_get_controller($entity_type)->delete(array($id)); +} + +/** + * Permanently delete multiple entities. + * + * @param $entity_type + * The type of the entity. + * @param $ids + * An array of entity ids of the entities to delete. + */ +function entity_delete_multiple($entity_type, $ids) { + entity_get_controller($entity_type)->delete($ids); +} + +/** + * Create a new entity object. + * + * @param $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. + * @return Entity + * A new entity object. + */ +function entity_create($entity_type, array $values) { + return entity_get_controller($entity_type)->create($values); +} + +/** + * Exports an entity. + * + * @param $entity_type + * The type of the entity. + * @param $entity + * The entity to export. + * @param $prefix + * (optional) A prefix for each line. + * @return + * The exported entity as serialized string. The format is determined by the + * respective entity controller, e.g. it is JSON for the EntityAPIController. + * The output is suitable for entity_import(). + */ +function entity_export($entity_type, $entity, $prefix = '') { + return entity_get_controller($entity_type)->export($entity, $prefix); +} + +/** + * Imports an entity. + * + * For persisting a newly imported entity use entity_save(). + * + * @param $entity_type + * The type of the entity. + * @param string $export + * The string containing the serialized entity as produced by + * entity_export(). + * @return Entity + * The imported entity object. + */ +function entity_import($entity_type, $export) { + return entity_get_controller($entity_type)->import($export); +} + +/** + * Builds a structured array representing the entity's content. + * + * The content built for the entity will vary depending on the $view_mode + * parameter. + * + * @param $entity_type + * The type of the entity. + * @param $entity + * An entity object. + * @param $view_mode + * A view mode as used by this entity type, e.g. 'full', 'teaser'... + * @param $langcode + * (optional) A language code to use for rendering. Defaults to the global + * content language of the current request. + * @return array + * The renderable array. + */ +function entity_build_content($entity_type, $entity, $view_mode = 'full', $langcode = NULL) { + return entity_get_controller($entity_type)->buildContent($entity, $view_mode, $langcode); +} + +/** + * Generate an array for rendering the given entities. + * + * Entities being viewed, are generally expected to be fully-loaded entity + * objects as returned by entity_load(). However, it is possible to view a + * single entity without an id, e.g. for generating a preview during + * creation. + * + * @param $entity_type + * The type of the entity. + * @param $entities + * An array of entities to render. + * @param $view_mode + * A view mode as used by this entity type, e.g. 'full', 'teaser'... + * @param $langcode + * (optional) A language code to use for rendering. Defaults to the global + * content language of the current request. + * @return + * The renderable array, keyed by id. + */ +function entity_view($entity_type, $entities, $view_mode = 'full', $langcode = NULL) { + return entity_get_controller($entity_type)->view($entities, $view_mode, $langcode); +} + +/** * Get the entity controller class for an entity type. */ function entity_get_controller($entity_type) { @@ -437,3 +577,120 @@ function entity_form_submit_build_entity($entity_type, $entity, $form, &$form_st field_attach_submit($entity_type, $entity, $form, $form_state); } } + +/** + * Converts an array of entities to be keyed by the values of a given property. + * + * @param array $entities + * The array of entities to convert. + * @param $property + * The name of entity property, by which the array should be keyed. To get + * reasonable results, the property has to have unique values. + * + * @return array + * The same entities in the same order, but keyed by their $property values. + */ +function entity_key_array_by_property(array $entities, $property) { + $ret = array(); + foreach ($entities as $entity) { + $key = isset($entity->$property) ? $entity->$property : NULL; + $ret[$key] = $entity; + } + return $ret; +} + +/** + * Export a variable in pretty formatted JSON. + * + * @param $var + * The variable to export. + * @param $prefix + * (optional) A prefix that will be added at the begining of every lines of + * the output. + * + * @return + * The variable exported in JSON. + */ +function entity_var_json_export($var, $prefix = '') { + if (is_array($var) && $var) { + // Defines whether we use a JSON array or object. + $use_array = ($var == array_values($var)); + $output = $use_array ? "[" : "{"; + + foreach ($var as $key => $value) { + if ($use_array) { + $values[] = entity_var_json_export($value, ' '); + } + else { + $values[] = entity_var_json_export((string) $key, ' ') . ' : ' . entity_var_json_export($value, ' '); + } + } + // Use several lines for long content. However for objects with a single + // entry keep the key in the first line. + if (strlen($content = implode(", ", $values)) > 70 && ($use_array || count($values) > 1)) { + $output .= "\n " . implode(",\n ", $values) . "\n"; + } + elseif (strpos($content, "\n") !== FALSE) { + $output .= " " . $content . "\n"; + } + else { + $output .= " " . $content . " "; + } + $output .= $use_array ? "]" : "}"; + } + else { + $output = drupal_json_encode($var); + } + + if ($prefix) { + $output = str_replace("\n", "\n$prefix", $output); + } + return $output; +} + +/** + * Process variables for entity.tpl.php. + */ +function template_preprocess_entity(&$variables) { + $variables['view_mode'] = $variables['elements']['#view_mode']; + $entity_type = $variables['elements']['#entity_type']; + $variables['entity_type'] = $entity_type; + $entity = $variables['elements']['#entity']; + $variables[$variables['elements']['#entity_type']] = $entity; + $info = entity_get_info($entity_type); + + $variables['title'] = check_plain(entity_label($entity_type, $entity)); + $uri = entity_uri($entity_type, $entity); + $variables['url'] = $uri ? url($uri['path'], $uri['options']) : FALSE; + $variables['page'] = $uri && $uri['path'] == $_GET['q']; + + // Helpful $content variable for templates. + $variables['content'] = array(); + foreach (element_children($variables['elements']) as $key) { + $variables['content'][$key] = $variables['elements'][$key]; + } + + if (!empty($info['fieldable'])) { + // Make the field variables available with the appropriate language. + field_attach_preprocess($entity_type, $entity, $variables['content'], $variables); + } + list(, , $bundle) = entity_extract_ids($entity_type, $entity); + + // Gather css classes. + $variables['classes_array'][] = drupal_html_class('entity-' . $entity_type); + $variables['classes_array'][] = drupal_html_class($entity_type . '-' . $bundle); + + // Add RDF type and about URI. + if (module_exists('rdf')) { + $variables['attributes_array']['about'] = empty($uri['path']) ? NULL: url($uri['path']); + $variables['attributes_array']['typeof'] = empty($entity->rdf_mapping['rdftype']) ? NULL : $entity->rdf_mapping['rdftype']; + } + + // Add suggestions. + $variables['theme_hook_suggestions'][] = $entity_type; + $variables['theme_hook_suggestions'][] = $entity_type . '__' . $bundle; + $variables['theme_hook_suggestions'][] = $entity_type . '__' . $bundle . '__' . $variables['view_mode']; + if ($id = $entity->identifier()) { + $variables['theme_hook_suggestions'][] = $entity_type . '__' . $id; + } +} diff --git a/modules/entity/tests/entity.test b/modules/entity/tests/entity.test new file mode 100644 index 0000000..1a9f045 --- /dev/null +++ b/modules/entity/tests/entity.test @@ -0,0 +1,123 @@ + 'Entity CRUD', + 'description' => 'Tests basic CRUD API functionality.', + 'group' => 'Entity API', + ); + } + + function setUp() { + parent::setUp('entity', 'entity_test'); + } + + /** + * Tests CRUD by making use of the Entity class methods. + */ + function testCRUD() { + + $user1 = $this->drupalCreateUser(); + // Create test entities for the user1 and unrelated to a user. + $entity = entity_create('entity_test', array('name' => 'test', 'uid' => $user1->uid)); + $entity->save(); + $entity = entity_create('entity_test', array('name' => 'test2', 'uid' => $user1->uid)); + $entity->save(); + $entity = entity_create('entity_test', array('name' => 'test', 'uid' => NULL)); + $entity->save(); + + $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test'))); + + $this->assertEqual($entities[0]->name, 'test', 'Created and loaded entity.'); + $this->assertEqual($entities[1]->name, 'test', 'Created and loaded entity.'); + + $results = entity_test_load_multiple(array($entity->pid)); + $loaded = array_pop($results); + $this->assertEqual($loaded->pid, $entity->pid, 'Loaded a single entity by id.'); + + $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test2'))); + $entities[0]->delete(); + $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test2'))); + $this->assertEqual($entities, array(), 'Entity deleted.'); + + $entity->save(); + $this->assertEqual($entity->pid, $loaded->pid, 'Entity updated.'); + } + + /** + * Tests CRUD API functions: entity_(create|delete|delete_multiple|save)(). + */ + function testCRUDAPIfunctions() { + $user1 = $this->drupalCreateUser(); + // Create test entities for the user1 and unrelated to a user. + $entity = entity_create('entity_test', array('name' => 'test', 'uid' => $user1->uid)); + entity_save('entity_test', $entity); + $entity = entity_create('entity_test', array('name' => 'test2', 'uid' => $user1->uid)); + entity_save('entity_test', $entity); + $entity = entity_create('entity_test', array('name' => 'test', 'uid' => NULL)); + entity_save('entity_test', $entity); + + $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test'))); + $this->assertEqual($entities[0]->name, 'test', 'Created and loaded entity.'); + $this->assertEqual($entities[1]->name, 'test', 'Created and loaded entity.'); + + $results = entity_test_load_multiple(array($entity->pid)); + $loaded = array_pop($results); + $this->assertEqual($loaded->pid, $entity->pid, 'Loaded a single entity by id.'); + + $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test2'))); + + entity_delete('entity_test', $entities[0]->pid); + $entities = array_values(entity_test_load_multiple(FALSE, array('name' => 'test2'))); + $this->assertEqual($entities, array(), 'Entity deleted.'); + + entity_save('entity_test', $entity); + $this->assertEqual($entity->pid, $loaded->pid, 'Entity updated.'); + + // Try deleting multiple test entities by deleting all. + $pids = array_keys(entity_test_load_multiple(FALSE)); + entity_test_delete_multiple($pids); + + $all = entity_test_load_multiple(FALSE); + $this->assertTrue(empty($all), 'Deleted all entities.'); + } + + /** + * Tests viewing entites. + */ + function testEntityView() { + $user1 = $this->drupalCreateUser(); + // Create test entities for the user1 and unrelated to a user. + $entity = entity_create('entity_test', array('name' => 'test', 'uid' => $user1->uid)); + + $render = $entity->view(); + $output = drupal_render($render); + // The entity class adds the user name to the output. Verify it is there. + $this->assertTrue(strpos($output, format_username($user1)) !== FALSE, 'Entity has been rendered'); + } + + /** + * Tests exporting and importing an entity. + */ + function testExport() { + // Test exporting an entity to JSON. + $entity = entity_create('entity_test', array('name' => 'test', 'uid' => $GLOBALS['user']->uid)); + $serialized_string = $entity->export(); + $data = drupal_json_decode($serialized_string); + $this->assertNotNull($data, 'Exported entity is valid JSON.'); + + $import = entity_import('entity_test', $serialized_string); + $this->assertTrue(get_class($import) == get_class($entity) && $entity->name == $import->name, 'Imported entity.'); + } +} + diff --git a/modules/entity/tests/entity_test.info b/modules/entity/tests/entity_test.info new file mode 100644 index 0000000..ba5c3eb --- /dev/null +++ b/modules/entity/tests/entity_test.info @@ -0,0 +1,7 @@ +name = Entity CRUD test module +description = Provides entity types based upon the CRUD API. +package = Testing +version = VERSION +core = 8.x +dependencies[] = entity +hidden = TRUE \ No newline at end of file diff --git a/modules/entity/tests/entity_test.install b/modules/entity/tests/entity_test.install new file mode 100644 index 0000000..860b3d3 --- /dev/null +++ b/modules/entity/tests/entity_test.install @@ -0,0 +1,71 @@ + 'entity_test_fullname', + 'type' => 'text', + 'cardinality' => 1, + 'translatable' => FALSE, + ); + field_create_field($field); + + $instance = array( + 'entity_type' => 'entity_test', + 'field_name' => 'entity_test_fullname', + 'bundle' => 'entity_test', + 'label' => 'Full name', + 'description' => 'Specify your first and last name.', + 'widget' => array( + 'type' => 'text_textfield', + 'weight' => 0, + ), + ); + field_create_instance($instance); +} + +/** + * Implement hook_schema(). + */ +function entity_test_schema() { + $schema['entity_test'] = array( + 'description' => 'Stores entity_test items.', + 'fields' => array( + 'pid' => array( + 'type' => 'serial', + 'not null' => TRUE, + 'description' => 'Primary Key: Unique entity_test item ID.', + ), + 'name' => array( + 'description' => 'The name of the test entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'uid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + 'description' => "The {users}.uid of the associated user.", + ), + ), + 'indexes' => array( + 'uid' => array('uid'), + ), + 'foreign keys' => array( + 'uid' => array('users' => 'uid'), + ), + 'primary key' => array('pid'), + ); + return $schema; +} diff --git a/modules/entity/tests/entity_test.module b/modules/entity/tests/entity_test.module new file mode 100644 index 0000000..747bba8 --- /dev/null +++ b/modules/entity/tests/entity_test.module @@ -0,0 +1,67 @@ + array( + 'label' => t('Test entity'), + 'entity class' => 'Entity', + 'controller class' => 'EntityTestController', + 'base table' => 'entity_test', + 'fieldable' => TRUE, + 'entity keys' => array( + 'id' => 'pid', + ), + ), + ); + return $return; +} + +/** + * Load multiple test entities based on certain conditions. + * + * @param $pids + * An array of entity IDs. + * @param $conditions + * An array of conditions to match against the {entity} table. + * @param $reset + * A boolean indicating that the internal cache should be reset. + * @return + * An array of test entity objects, indexed by pid. + */ +function entity_test_load_multiple($pids = array(), $conditions = array(), $reset = FALSE) { + return entity_load('entity_test', $pids, $conditions, $reset); +} + +/** + * Delete multiple test entities. + * + * @param $pids + * An array of test entity IDs. + */ +function entity_test_delete_multiple(array $pids) { + entity_get_controller('entity_test')->delete($pids); +} + +/** + * Controller class for test entities. + */ +class EntityTestController extends EntityAPIController { + + /** + * Override buildContent() to add the username to the output. + */ + public function buildContent($entity, $view_mode = 'full', $langcode = NULL) { + $content['user'] = array( + '#markup' => "User: ". format_username(user_load($entity->uid)), + ); + return parent::buildContent($entity, $view_mode, $langcode, $content); + } +} diff --git a/modules/simpletest/simpletest.info b/modules/simpletest/simpletest.info index 54b020d..926ade6 100644 --- a/modules/simpletest/simpletest.info +++ b/modules/simpletest/simpletest.info @@ -15,8 +15,6 @@ files[] = tests/bootstrap.test files[] = tests/cache.test files[] = tests/common.test files[] = tests/database_test.test -files[] = tests/entity_crud_hook_test.test -files[] = tests/entity_query.test files[] = tests/error.test files[] = tests/file.test files[] = tests/filetransfer.test