diff --git a/core/modules/book/book.admin.inc b/core/modules/book/book.admin.inc
index 265b381..3f28651 100644
--- a/core/modules/book/book.admin.inc
+++ b/core/modules/book/book.admin.inc
@@ -66,7 +66,7 @@ function book_admin_settings_validate($form, &$form_state) {
 /**
  * Form constructor for administering a single book's hierarchy.
  *
- * @param $node
+ * @param Node $node
  *   The node of the top-level page in the book.
  *
  * @see book_menu()
@@ -74,7 +74,7 @@ function book_admin_settings_validate($form, &$form_state) {
  * @see book_admin_edit_submit()
  * @ingroup forms
  */
-function book_admin_edit($form, $form_state, $node) {
+function book_admin_edit($form, $form_state, Node $node) {
   drupal_set_title($node->title);
   $form['#node'] = $node;
   _book_admin_table($node, $form);
@@ -135,7 +135,7 @@ function book_admin_edit_submit($form, &$form_state) {
         $node->revision = 1;
         $node->log = t('Title changed from %original to %current.', array('%original' => $node->title, '%current' => $values['title']));
 
-        node_save($node);
+        $node->save();
         watchdog('content', 'book: updated %title.', array('%title' => $node->title), WATCHDOG_NOTICE, l(t('view'), 'node/' . $node->nid));
       }
     }
@@ -147,14 +147,14 @@ function book_admin_edit_submit($form, &$form_state) {
 /**
  * Builds the table portion of the form for the book administration page.
  *
- * @param $node
+ * @param Node $node
  *   The node of the top-level page in the book.
  * @param $form
  *   The form that is being modified.
  *
  * @see book_admin_edit()
  */
-function _book_admin_table($node, &$form) {
+function _book_admin_table(Node $node, &$form) {
   $form['table'] = array(
     '#theme' => 'book_admin_table',
     '#tree' => TRUE,
diff --git a/core/modules/book/book.module b/core/modules/book/book.module
index a6ac5c4..e286144 100644
--- a/core/modules/book/book.module
+++ b/core/modules/book/book.module
@@ -86,12 +86,12 @@ function book_permission() {
 /**
  * Adds relevant book links to the node's links.
  *
- * @param $node
+ * @param Node $node
  *   The book page node to add links to.
  * @param $view_mode
  *   The view mode of the node.
  */
-function book_node_view_link($node, $view_mode) {
+function book_node_view_link(Node $node, $view_mode) {
   $links = array();
 
   if (isset($node->book['depth'])) {
@@ -201,24 +201,24 @@ function book_menu() {
  * - admin/content/book/%node
  * - node/%node/outline
  *
- * @param $node
+ * @param Node $node
  *   The node whose outline tab is to be viewed.
  *
  * @see book_menu()
  */
-function _book_outline_access($node) {
+function _book_outline_access(Node $node) {
   return user_access('administer book outlines') && node_access('view', $node);
 }
 
 /**
  * Access callback: Determines if the user can remove nodes from the outline.
  *
- * @param $node
+ * @param Node $node
  *   The node to remove from the outline.
  *
  * @see book_menu()
  */
-function _book_outline_remove_access($node) {
+function _book_outline_remove_access(Node $node) {
   return _book_node_is_removable($node) && _book_outline_access($node);
 }
 
@@ -520,10 +520,10 @@ function _book_parent_select($book_link) {
 /**
  * Builds the common elements of the book form for the node and outline forms.
  *
- * @param $node
+ * @param Node $node
  *   The node whose form is being viewed.
  */
-function _book_add_form_elements(&$form, &$form_state, $node) {
+function _book_add_form_elements(&$form, &$form_state, Node $node) {
   // If the form is being processed during the Ajax callback of our book bid
   // dropdown, then $form_state will hold the value that was selected.
   if (isset($form_state['values']['book'])) {
@@ -625,13 +625,13 @@ function book_form_update($form, $form_state) {
  * outline through node addition, node editing, node deletion, or the outline
  * tab.
  *
- * @param $node
+ * @param Node $node
  *   The node that is being saved, added, deleted, or moved.
  *
  * @return
  *   TRUE if the menu link was saved; FALSE otherwise.
  */
-function _book_update_outline($node) {
+function _book_update_outline(Node $node) {
   if (empty($node->book['bid'])) {
     return FALSE;
   }
@@ -889,7 +889,7 @@ function book_node_load($nodes, $types) {
 /**
  * Implements hook_node_view().
  */
-function book_node_view($node, $view_mode) {
+function book_node_view(Node $node, $view_mode) {
   if ($view_mode == 'full') {
     if (!empty($node->book['bid']) && empty($node->in_preview)) {
       $node->content['book_navigation'] = array(
@@ -921,7 +921,7 @@ function book_page_alter(&$page) {
 /**
  * Implements hook_node_presave().
  */
-function book_node_presave($node) {
+function book_node_presave(Node $node) {
   // Always save a revision for non-administrators.
   if (!empty($node->book['bid']) && !user_access('administer nodes')) {
     $node->revision = 1;
@@ -939,7 +939,7 @@ function book_node_presave($node) {
 /**
  * Implements hook_node_insert().
  */
-function book_node_insert($node) {
+function book_node_insert(Node $node) {
   if (!empty($node->book['bid'])) {
     if ($node->book['bid'] == 'new') {
       // New nodes that are their own book.
@@ -954,7 +954,7 @@ function book_node_insert($node) {
 /**
  * Implements hook_node_update().
  */
-function book_node_update($node) {
+function book_node_update(Node $node) {
   if (!empty($node->book['bid'])) {
     if ($node->book['bid'] == 'new') {
       // New nodes that are their own book.
@@ -969,7 +969,7 @@ function book_node_update($node) {
 /**
  * Implements hook_node_predelete().
  */
-function book_node_predelete($node) {
+function book_node_predelete(Node $node) {
   if (!empty($node->book['bid'])) {
     if ($node->nid == $node->book['bid']) {
       // Handle deletion of a top-level post.
@@ -993,7 +993,7 @@ function book_node_predelete($node) {
 /**
  * Implements hook_node_prepare().
  */
-function book_node_prepare($node) {
+function book_node_prepare(Node $node) {
   // Prepare defaults for the add/edit form.
   if (empty($node->book) && (user_access('add content to books') || user_access('administer book outlines'))) {
     $node->book = array();
@@ -1291,7 +1291,7 @@ function book_export_traverse($tree, $visit_func) {
 /**
  * Generates printer-friendly HTML for a node.
  *
- * @param $node
+ * @param Node $node
  *   The node that will be output.
  * @param $children
  *   All the rendered child nodes within the current node.
@@ -1301,7 +1301,7 @@ function book_export_traverse($tree, $visit_func) {
  *
  * @see book_export_traverse()
  */
-function book_node_export($node, $children = '') {
+function book_node_export(Node $node, $children = '') {
   $build = node_view($node, 'print');
   unset($build['#theme']);
   // @todo Rendering should happen in the template using render().
diff --git a/core/modules/book/book.pages.inc b/core/modules/book/book.pages.inc
index 4fab6dc..9e9acd6 100644
--- a/core/modules/book/book.pages.inc
+++ b/core/modules/book/book.pages.inc
@@ -93,12 +93,12 @@ function book_export_html($nid) {
 /**
  * Page callback: Shows the outline form for a single node.
  *
- * @param $node
+ * @param Node $node
  *   The book node for which to show the outline.
  *
  * @see book_menu()
  */
-function book_outline($node) {
+function book_outline(Node $node) {
   drupal_set_title($node->title);
   return drupal_get_form('book_outline_form', $node);
 }
@@ -108,14 +108,14 @@ function book_outline($node) {
  *
  * Allows handling of all book outline operations via the outline tab.
  *
- * @param $node
+ * @param Node $node
  *   The book node for which to show the outline.
  *
  * @see book_outline_form_submit()
  * @see book_remove_button_submit()
  * @ingroup forms
  */
-function book_outline_form($form, &$form_state, $node) {
+function book_outline_form($form, &$form_state, Node $node) {
   if (!isset($node->book)) {
     // The node is not part of any book yet - set default options.
     $node->book = _book_link_defaults($node->nid);
@@ -197,14 +197,14 @@ function book_outline_form_submit($form, &$form_state) {
 /**
  * Form constructor to confirm removal of a node from a book.
  *
- * @param $node
+ * @param Node $node
  *   The node to delete.
  *
  * @see book_remove_form_submit()
  * @see book_menu()
  * @ingroup forms
  */
-function book_remove_form($form, &$form_state, $node) {
+function book_remove_form($form, &$form_state, Node $node) {
   $form['#node'] = $node;
   $title = array('%title' => $node->title);
 
diff --git a/core/modules/book/book.test b/core/modules/book/book.test
index 54444b2..214a408 100644
--- a/core/modules/book/book.test
+++ b/core/modules/book/book.test
@@ -108,7 +108,7 @@ class BookTestCase extends DrupalWebTestCase {
   /**
    * Check the outline of sub-pages; previous, up, and next; and printer friendly version.
    *
-   * @param $node
+   * @param Node $node
    *   Node to check.
    * @param $nodes
    *   Nodes that should be in outline.
@@ -121,7 +121,7 @@ class BookTestCase extends DrupalWebTestCase {
    * @param $breadcrumb
    *   The nodes that should be displayed in the breadcrumb.
    */
-  function checkBookNode($node, $nodes, $previous = FALSE, $up = FALSE, $next = FALSE, array $breadcrumb) {
+  function checkBookNode(Node $node, $nodes, $previous = FALSE, $up = FALSE, $next = FALSE, array $breadcrumb) {
     // $number does not use drupal_static as it should not be reset
     // since it uniquely identifies each call to checkBookNode().
     static $number = 0;
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 1a3580f..caad8e2 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -545,13 +545,13 @@ function comment_get_recent($number = 10) {
  *   Number of comments.
  * @param $new_replies
  *   Number of new replies.
- * @param $node
+ * @param Node $node
  *   The first new comment node.
  *
  * @return
  *   "page=X" if the page number is greater than zero; empty string otherwise.
  */
-function comment_new_page_count($num_comments, $new_replies, $node) {
+function comment_new_page_count($num_comments, $new_replies, Node $node) {
   $mode = variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED);
   $comments_per_page = variable_get('comment_default_per_page_' . $node->type, 50);
   $pagenum = NULL;
@@ -629,7 +629,7 @@ function theme_comment_block() {
 /**
  * Implements hook_node_view().
  */
-function comment_node_view($node, $view_mode) {
+function comment_node_view(Node $node, $view_mode) {
   $links = array();
 
   if ($node->comment != COMMENT_NODE_HIDDEN) {
@@ -737,14 +737,14 @@ function comment_node_view($node, $view_mode) {
 /**
  * Builds the comment-related elements for node detail pages.
  *
- * @param $node
+ * @param Node $node
  *   The node object for which to build the comment-related elements.
  *
  * @return
  *   A renderable array representing the comment-related page elements for the
  *   node.
  */
-function comment_node_page_additions($node) {
+function comment_node_page_additions(Node $node) {
   $additions = array();
 
   // Only attempt to render comments if the node has visible comments.
@@ -783,7 +783,7 @@ function comment_node_page_additions($node) {
 /**
  * Retrieves comments for a thread.
  *
- * @param $node
+ * @param Node $node
  *   The node whose comment(s) needs rendering.
  * @param $mode
  *   The comment display mode; COMMENT_MODE_FLAT or COMMENT_MODE_THREADED.
@@ -847,7 +847,7 @@ function comment_node_page_additions($node) {
  * spoil the reverse ordering, "ORDER BY thread ASC" -- here, we do not need
  * to consider the trailing "/" so we use a substring only.
  */
-function comment_get_thread($node, $mode, $comments_per_page) {
+function comment_get_thread(Node $node, $mode, $comments_per_page) {
   $query = db_select('comment', 'c')->extend('PagerDefault');
   $query->addField('c', 'cid');
   $query
@@ -936,7 +936,7 @@ function comment_prepare_thread(&$comments) {
  *
  * @param $comment
  *   The comment object.
- * @param $node
+ * @param Node $node
  *   The node the comment is attached to.
  * @param $view_mode
  *   View mode, e.g. 'full', 'teaser'...
@@ -947,7 +947,7 @@ function comment_prepare_thread(&$comments) {
  * @return
  *   An array as expected by drupal_render().
  */
-function comment_view($comment, $node, $view_mode = 'full', $langcode = NULL) {
+function comment_view($comment, Node $node, $view_mode = 'full', $langcode = NULL) {
   if (!isset($langcode)) {
     $langcode = $GLOBALS['language_content']->langcode;
   }
@@ -1006,7 +1006,7 @@ function comment_view($comment, $node, $view_mode = 'full', $langcode = NULL) {
  *
  * @param $comment
  *   A comment object.
- * @param $node
+ * @param Node $node
  *   The node the comment is attached to.
  * @param $view_mode
  *   View mode, e.g. 'full', 'teaser'...
@@ -1014,7 +1014,7 @@ function comment_view($comment, $node, $view_mode = 'full', $langcode = NULL) {
  *   (optional) A language code to use for rendering. Defaults to the global
  *   content language of the current request.
  */
-function comment_build_content($comment, $node, $view_mode = 'full', $langcode = NULL) {
+function comment_build_content($comment, Node $node, $view_mode = 'full', $langcode = NULL) {
   if (!isset($langcode)) {
     $langcode = $GLOBALS['language_content']->langcode;
   }
@@ -1050,13 +1050,13 @@ function comment_build_content($comment, $node, $view_mode = 'full', $langcode =
  *
  * @param $comment
  *   The comment object.
- * @param $node
+ * @param Node $node
  *   The node the comment is attached to.
  *
  * @return
  *   A structured array of links.
  */
-function comment_links($comment, $node) {
+function comment_links($comment, Node $node) {
   $links = array();
   if ($node->comment == COMMENT_NODE_OPEN) {
     if (user_access('administer comments') && user_access('post comments')) {
@@ -1111,7 +1111,7 @@ function comment_links($comment, $node) {
  *
  * @param $comments
  *   An array of comments as returned by comment_load_multiple().
- * @param $node
+ * @param Node $node
  *   The node the comments are attached to.
  * @param $view_mode
  *   View mode, e.g. 'full', 'teaser'...
@@ -1126,7 +1126,7 @@ function comment_links($comment, $node) {
  *
  * @see drupal_render()
  */
-function comment_view_multiple($comments, $node, $view_mode = 'full', $weight = 0, $langcode = NULL) {
+function comment_view_multiple($comments, Node $node, $view_mode = 'full', $weight = 0, $langcode = NULL) {
   field_attach_prepare_view('comment', $comments, $view_mode, $langcode);
   entity_prepare_view('comment', $comments, $langcode);
 
@@ -1308,7 +1308,7 @@ function comment_node_load($nodes, $types) {
 /**
  * Implements hook_node_prepare().
  */
-function comment_node_prepare($node) {
+function comment_node_prepare(Node $node) {
   if (!isset($node->comment)) {
     $node->comment = variable_get("comment_$node->type", COMMENT_NODE_OPEN);
   }
@@ -1317,7 +1317,7 @@ function comment_node_prepare($node) {
 /**
  * Implements hook_node_insert().
  */
-function comment_node_insert($node) {
+function comment_node_insert(Node $node) {
   // Allow bulk updates and inserts to temporarily disable the
   // maintenance of the {node_comment_statistics} table.
   if (variable_get('comment_maintain_node_statistics', TRUE)) {
@@ -1337,7 +1337,7 @@ function comment_node_insert($node) {
 /**
  * Implements hook_node_predelete().
  */
-function comment_node_predelete($node) {
+function comment_node_predelete(Node $node) {
   $cids = db_query('SELECT cid FROM {comment} WHERE nid = :nid', array(':nid' => $node->nid))->fetchCol();
   comment_delete_multiple($cids);
   db_delete('node_comment_statistics')
@@ -1348,7 +1348,7 @@ function comment_node_predelete($node) {
 /**
  * Implements hook_node_update_index().
  */
-function comment_node_update_index($node) {
+function comment_node_update_index(Node $node) {
   $index_comments = &drupal_static(__FUNCTION__);
 
   if ($index_comments === NULL) {
@@ -1397,7 +1397,7 @@ function comment_update_index() {
  * Formats a comment count string and returns it, for display with search
  * results.
  */
-function comment_node_search_result($node) {
+function comment_node_search_result(Node $node) {
   // Do not make a string if comments are hidden.
   if (user_access('access comments') && $node->comment != COMMENT_NODE_HIDDEN) {
     $comments = db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array('nid' => $node->nid))->fetchField();
diff --git a/core/modules/comment/comment.pages.inc b/core/modules/comment/comment.pages.inc
index 344e757..ad75ca5 100644
--- a/core/modules/comment/comment.pages.inc
+++ b/core/modules/comment/comment.pages.inc
@@ -17,7 +17,7 @@
  * The node or comment that is being replied to must appear above the comment
  * form to provide the user context while authoring the comment.
  *
- * @param $node
+ * @param Node $node
  *   Every comment belongs to a node. This is that node.
  * @param $pid
  *   (optional) Some comments are replies to other comments. In those cases,
@@ -26,7 +26,7 @@
  * @return
  *   The rendered parent node or comment plus the new comment form.
  */
-function comment_reply($node, $pid = NULL) {
+function comment_reply(Node $node, $pid = NULL) {
   // Set the breadcrumb trail.
   drupal_set_breadcrumb(array(l(t('Home'), NULL), l($node->title, 'node/' . $node->nid)));
   $op = isset($_POST['op']) ? $_POST['op'] : '';
diff --git a/core/modules/comment/comment.test b/core/modules/comment/comment.test
index 9257513..464c938 100644
--- a/core/modules/comment/comment.test
+++ b/core/modules/comment/comment.test
@@ -23,8 +23,8 @@ class CommentHelperCase extends DrupalWebTestCase {
   /**
    * Posts a comment.
    *
-   * @param $node
-   *   Node to post comment on.
+   * @param Node|NULL $node
+   *   Node to post comment on or NULL to post to the previusly loaded page.
    * @param $comment
    *   Comment body.
    * @param $subject
diff --git a/core/modules/entity/tests/entity_crud_hook_test.test b/core/modules/entity/tests/entity_crud_hook_test.test
index e7739c9..baa0237 100644
--- a/core/modules/entity/tests/entity_crud_hook_test.test
+++ b/core/modules/entity/tests/entity_crud_hook_test.test
@@ -62,7 +62,7 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
    * Tests hook invocations for CRUD operations on comments.
    */
   public function testCommentHooks() {
-    $node = (object) array(
+    $node = entity_create('node', array(
       'uid' => 1,
       'type' => 'article',
       'title' => 'Test node',
@@ -70,11 +70,11 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
       'comment' => 2,
       'promote' => 0,
       'sticky' => 0,
-      'language' => LANGUAGE_NOT_SPECIFIED,
+      'langcode' => LANGUAGE_NOT_SPECIFIED,
       'created' => REQUEST_TIME,
       'changed' => REQUEST_TIME,
-    );
-    node_save($node);
+    ));
+    $node->save();
     $nid = $node->nid;
 
     $comment = entity_create('comment', array(
@@ -86,7 +86,7 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
       'created' => REQUEST_TIME,
       'changed' => REQUEST_TIME,
       'status' => 1,
-      'language' => LANGUAGE_NOT_SPECIFIED,
+      'langcode' => LANGUAGE_NOT_SPECIFIED,
     ));
 
     $_SESSION['entity_crud_hook_test'] = array();
@@ -189,7 +189,7 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
    * Tests hook invocations for CRUD operations on nodes.
    */
   public function testNodeHooks() {
-    $node = (object) array(
+    $node = entity_create('node', array(
       'uid' => 1,
       'type' => 'article',
       'title' => 'Test node',
@@ -197,12 +197,12 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
       'comment' => 2,
       'promote' => 0,
       'sticky' => 0,
-      'language' => LANGUAGE_NOT_SPECIFIED,
+      'langcode' => LANGUAGE_NOT_SPECIFIED,
       'created' => REQUEST_TIME,
       'changed' => REQUEST_TIME,
-    );
+    ));
     $_SESSION['entity_crud_hook_test'] = array();
-    node_save($node);
+    $node->save();
 
     $this->assertHookMessageOrder(array(
       'entity_crud_hook_test_node_presave called',
@@ -221,7 +221,7 @@ class EntityCrudHookTestCase extends DrupalWebTestCase {
 
     $_SESSION['entity_crud_hook_test'] = array();
     $node->title = 'New title';
-    node_save($node);
+    $node->save();
 
     $this->assertHookMessageOrder(array(
       'entity_crud_hook_test_node_presave called',
diff --git a/core/modules/field_ui/field_ui.test b/core/modules/field_ui/field_ui.test
index 8c202e5..80dab96 100644
--- a/core/modules/field_ui/field_ui.test
+++ b/core/modules/field_ui/field_ui.test
@@ -615,7 +615,7 @@ class FieldUIManageDisplayTestCase extends FieldUITestCase {
   /**
    * Asserts that a string is found in the rendered node in a view mode.
    *
-   * @param $node
+   * @param Node $node
    *   The node.
    * @param $view_mode
    *   The view mode in which the node should be displayed.
@@ -627,14 +627,14 @@ class FieldUIManageDisplayTestCase extends FieldUITestCase {
    * @return
    *   TRUE on pass, FALSE on fail.
    */
-  function assertNodeViewText($node, $view_mode, $text, $message) {
+  function assertNodeViewText(Node $node, $view_mode, $text, $message) {
     return $this->assertNodeViewTextHelper($node, $view_mode, $text, $message, FALSE);
   }
 
   /**
    * Asserts that a string is not found in the rendered node in a view mode.
    *
-   * @param $node
+   * @param Node $node
    *   The node.
    * @param $view_mode
    *   The view mode in which the node should be displayed.
@@ -645,7 +645,7 @@ class FieldUIManageDisplayTestCase extends FieldUITestCase {
    * @return
    *   TRUE on pass, FALSE on fail.
    */
-  function assertNodeViewNoText($node, $view_mode, $text, $message) {
+  function assertNodeViewNoText(Node $node, $view_mode, $text, $message) {
     return $this->assertNodeViewTextHelper($node, $view_mode, $text, $message, TRUE);
   }
 
@@ -655,7 +655,7 @@ class FieldUIManageDisplayTestCase extends FieldUITestCase {
    * This helper function is used by assertNodeViewText() and
    * assertNodeViewNoText().
    *
-   * @param $node
+   * @param Node $node
    *   The node.
    * @param $view_mode
    *   The view mode in which the node should be displayed.
@@ -669,7 +669,7 @@ class FieldUIManageDisplayTestCase extends FieldUITestCase {
    * @return
    *   TRUE on pass, FALSE on fail.
    */
-  function assertNodeViewTextHelper($node, $view_mode, $text, $message, $not_exists) {
+  function assertNodeViewTextHelper(Node $node, $view_mode, $text, $message, $not_exists) {
     // Make sure caches on the tester side are refreshed after changes
     // submitted on the tested side.
     field_info_cache_clear();
@@ -678,7 +678,7 @@ class FieldUIManageDisplayTestCase extends FieldUITestCase {
     $old_content = $this->drupalGetContent();
 
     // Render a cloned node, so that we do not alter the original.
-    $clone = clone $node;
+    $clone = $node->createDuplicate();
     $element = node_view($clone, $view_mode);
     $output = drupal_render($element);
     $this->verbose(t('Rendered node - view mode: @view_mode', array('@view_mode' => $view_mode)) . '<hr />'. $output);
diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module
index 4c12c41..2de8544 100644
--- a/core/modules/forum/forum.module
+++ b/core/modules/forum/forum.module
@@ -244,13 +244,13 @@ function forum_uri($forum) {
 /**
  * Check whether a content type can be used in a forum.
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  *
  * @return
  *   Boolean indicating if the node can be assigned to a forum.
  */
-function _forum_node_check_node_type($node) {
+function _forum_node_check_node_type(Node $node) {
   // Fetch information about the forum field.
   $field = field_info_instance('node', 'taxonomy_forums', $node->type);
 
@@ -260,7 +260,7 @@ function _forum_node_check_node_type($node) {
 /**
  * Implements hook_node_view().
  */
-function forum_node_view($node, $view_mode) {
+function forum_node_view(Node $node, $view_mode) {
   $vid = variable_get('forum_nav_vocabulary', 0);
   $vocabulary = taxonomy_vocabulary_load($vid);
   if (_forum_node_check_node_type($node)) {
@@ -285,7 +285,7 @@ function forum_node_view($node, $view_mode) {
  *
  * Check in particular that only a "leaf" term in the associated taxonomy.
  */
-function forum_node_validate($node, $form) {
+function forum_node_validate(Node $node, $form) {
   if (_forum_node_check_node_type($node)) {
     $langcode = $form['taxonomy_forums']['#language'];
     // vocabulary is selected, not a "container" term.
@@ -321,7 +321,7 @@ function forum_node_validate($node, $form) {
  *
  * Assign forum taxonomy when adding a topic from within a forum.
  */
-function forum_node_presave($node) {
+function forum_node_presave(Node $node) {
   if (_forum_node_check_node_type($node)) {
     // Make sure all fields are set properly:
     $node->icon = !empty($node->icon) ? $node->icon : '';
@@ -341,7 +341,7 @@ function forum_node_presave($node) {
 /**
  * Implements hook_node_update().
  */
-function forum_node_update($node) {
+function forum_node_update(Node $node) {
   if (_forum_node_check_node_type($node)) {
     if (empty($node->revision) && db_query('SELECT tid FROM {forum} WHERE nid=:nid', array(':nid' => $node->nid))->fetchField()) {
       if (!empty($node->forum_tid)) {
@@ -389,7 +389,7 @@ function forum_node_update($node) {
 /**
  * Implements hook_node_insert().
  */
-function forum_node_insert($node) {
+function forum_node_insert(Node $node) {
   if (_forum_node_check_node_type($node)) {
     if (!empty($node->forum_tid)) {
       $nid = db_insert('forum')
@@ -406,7 +406,7 @@ function forum_node_insert($node) {
 /**
  * Implements hook_node_predelete().
  */
-function forum_node_predelete($node) {
+function forum_node_predelete(Node $node) {
   if (_forum_node_check_node_type($node)) {
     db_delete('forum')
       ->condition('nid', $node->nid)
@@ -893,9 +893,10 @@ function forum_get_topics($tid, $sortby, $forum_per_page) {
     $nids[] = $record->nid;
   }
   if ($nids) {
+    $nodes = node_load_multiple($nids);
+
     $query = db_select('node', 'n')->extend('TableSort');
-    $query->fields('n', array('title', 'nid', 'type', 'sticky', 'created', 'uid'));
-    $query->addField('n', 'comment', 'comment_mode');
+    $query->fields('n', array('nid'));
 
     $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
     $query->fields('ncs', array('cid', 'last_comment_uid', 'last_comment_timestamp', 'comment_count'));
@@ -915,7 +916,16 @@ function forum_get_topics($tid, $sortby, $forum_per_page) {
       ->orderByHeader($forum_topic_list_header)
       ->condition('n.nid', $nids);
 
-    $result = $query->execute();
+    $result = array();
+    foreach ($query->execute() as $row) {
+      $topic = $nodes[$row->nid];
+      $topic->comment_mode = $topic->comment;
+
+      foreach ($row as $key => $value) {
+        $topic->{$key} = $value;
+      }
+      $result[] = $topic;
+    }
   }
   else {
     $result = array();
diff --git a/core/modules/forum/forum.test b/core/modules/forum/forum.test
index b5b8ae4..f064f43 100644
--- a/core/modules/forum/forum.test
+++ b/core/modules/forum/forum.test
@@ -422,14 +422,14 @@ class ForumTestCase extends DrupalWebTestCase {
    *
    * @param $node_user
    *   The user who creates the node.
-   * @param $node
+   * @param Node $node
    *   The node being checked.
    * @param $admin
    *   Boolean to indicate whether the user can 'access administration pages'.
    * @param $response
    *   The exptected HTTP response code.
    */
-  private function verifyForums($node_user, $node, $admin, $response = 200) {
+  private function verifyForums($node_user, Node $node, $admin, $response = 200) {
     $response2 = ($admin) ? 200 : 403;
 
     // View forum help node.
diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module
index 6d3ded0..e148487 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -310,9 +310,8 @@ function locale_form_node_form_alter(&$form, &$form_state) {
  */
 function locale_field_node_form_submit($form, &$form_state) {
   if (field_has_translation_handler('node', 'locale')) {
-    $node = (object) $form_state['values'];
-    $available_languages = field_content_languages();
-    list(, , $bundle) = entity_extract_ids('node', $node);
+    $bundle = $form_state['values']['type'];
+    $node_language = $form_state['values']['language'];
 
     foreach (field_info_instances('node', $bundle) as $instance) {
       $field_name = $instance['field_name'];
diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module
index 6862eae..dbe68d8 100644
--- a/core/modules/menu/menu.module
+++ b/core/modules/menu/menu.module
@@ -509,21 +509,21 @@ function menu_block_view_alter(&$data, $block) {
 /**
  * Implements hook_node_insert().
  */
-function menu_node_insert($node) {
+function menu_node_insert(Node $node) {
   menu_node_save($node);
 }
 
 /**
  * Implements hook_node_update().
  */
-function menu_node_update($node) {
+function menu_node_update(Node $node) {
   menu_node_save($node);
 }
 
 /**
  * Helper for hook_node_insert() and hook_node_update().
  */
-function menu_node_save($node) {
+function menu_node_save(Node $node) {
   if (isset($node->menu)) {
     $link = &$node->menu;
     if (empty($link['enabled'])) {
@@ -552,7 +552,7 @@ function menu_node_save($node) {
 /**
  * Implements hook_node_predelete().
  */
-function menu_node_predelete($node) {
+function menu_node_predelete(Node $node) {
   // Delete all menu module links that point to this node.
   $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path AND module = 'menu'", array(':path' => 'node/' . $node->nid), array('fetch' => PDO::FETCH_ASSOC));
   foreach ($result as $m) {
@@ -563,7 +563,7 @@ function menu_node_predelete($node) {
 /**
  * Implements hook_node_prepare().
  */
-function menu_node_prepare($node) {
+function menu_node_prepare(Node $node) {
   if (empty($node->menu)) {
     // Prepare the node for the edit form so that $node->menu always exists.
     $menu_name = strtok(variable_get('menu_parent_' . $node->type, 'main-menu:0'), ':');
@@ -716,7 +716,7 @@ function menu_form_node_form_alter(&$form, $form_state) {
  *
  * @see menu_form_node_form_alter()
  */
-function menu_node_submit($node, $form, $form_state) {
+function menu_node_submit(Node $node, $form, $form_state) {
   // Decompose the selected menu parent option into 'menu_name' and 'plid', if
   // the form used the default parent selection widget.
   if (!empty($form_state['values']['menu']['parent'])) {
diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc
index ac18da7..bba4d27 100644
--- a/core/modules/node/node.admin.inc
+++ b/core/modules/node/node.admin.inc
@@ -315,11 +315,11 @@ function node_mass_update($nodes, $updates) {
 function _node_mass_update_helper($nid, $updates) {
   $node = node_load($nid, NULL, TRUE);
   // For efficiency manually save the original node before applying any changes.
-  $node->original = clone $node;
+  $node->original = $node->createDuplicate();
   foreach ($updates as $name => $value) {
     $node->$name = $value;
   }
-  node_save($node);
+  $node->save();
   return $node;
 }
 
diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php
index 2324b47..0710e31 100644
--- a/core/modules/node/node.api.php
+++ b/core/modules/node/node.api.php
@@ -253,7 +253,7 @@ function hook_node_grants($account, $op) {
  *
  * Note: a deny all grant is not written to the database; denies are implicit.
  *
- * @param $node
+ * @param Node $node
  *   The node that has just been saved.
  *
  * @return
@@ -262,7 +262,7 @@ function hook_node_grants($account, $op) {
  * @see _node_access_write_grants()
  * @ingroup node_access
  */
-function hook_node_access_records($node) {
+function hook_node_access_records(Node $node) {
   // We only care about the node if it has been marked private. If not, it is
   // treated just like any other node and we completely ignore it.
   if ($node->private) {
@@ -313,7 +313,7 @@ function hook_node_access_records($node) {
  *
  * @param $grants
  *   The $grants array returned by hook_node_access_records().
- * @param $node
+ * @param Node $node
  *   The node for which the grants were acquired.
  *
  * The preferred use of this hook is in a module that bridges multiple node
@@ -325,7 +325,7 @@ function hook_node_access_records($node) {
  * @see hook_node_grants_alter()
  * @ingroup node_access
  */
-function hook_node_access_records_alter(&$grants, $node) {
+function hook_node_access_records_alter(&$grants, Node $node) {
   // Our module allows editors to mark specific articles with the 'is_preview'
   // field. If the node being saved has a TRUE value for that field, then only
   // our grants are retained, and other grants are removed. Doing so ensures
@@ -456,14 +456,14 @@ function hook_node_operations() {
  * field_attach_delete() are called, and before the node is removed from the
  * node table in the database.
  *
- * @param $node
+ * @param Node $node
  *   The node that is about to be deleted.
  *
  * @see hook_node_predelete()
  * @see node_delete_multiple()
  * @ingroup node_api_hooks
  */
-function hook_node_predelete($node) {
+function hook_node_predelete(Node $node) {
   db_delete('mytable')
     ->condition('nid', $node->nid)
     ->execute();
@@ -475,14 +475,14 @@ function hook_node_predelete($node) {
  * This hook is invoked from node_delete_multiple() after field_attach_delete()
  * has been called and after the node has been removed from the database.
  *
- * @param $node
+ * @param Node $node
  *   The node that has been deleted.
  *
  * @see hook_node_predelete()
  * @see node_delete_multiple()
  * @ingroup node_api_hooks
  */
-function hook_node_delete($node) {
+function hook_node_delete(Node $node) {
   drupal_set_message(t('Node: @title has been deleted', array('@title' => $node->title)));
 }
 
@@ -493,12 +493,12 @@ function hook_node_delete($node) {
  * removed from the node_revision table, and before
  * field_attach_delete_revision() is called.
  *
- * @param $node
+ * @param Node $node
  *   The node revision (node object) that is being deleted.
  *
  * @ingroup node_api_hooks
  */
-function hook_node_revision_delete($node) {
+function hook_node_revision_delete(Node $node) {
   db_delete('mytable')
     ->condition('vid', $node->vid)
     ->execute();
@@ -511,12 +511,12 @@ function hook_node_revision_delete($node) {
  * node table in the database, after the type-specific hook_insert() is invoked,
  * and after field_attach_insert() is called.
  *
- * @param $node
+ * @param Node $node
  *   The node that is being created.
  *
  * @ingroup node_api_hooks
  */
-function hook_node_insert($node) {
+function hook_node_insert(Node $node) {
   db_insert('mytable')
     ->fields(array(
       'nid' => $node->nid,
@@ -634,12 +634,12 @@ function hook_node_access($node, $op, $account) {
  * This hook is invoked from node_object_prepare() after the type-specific
  * hook_prepare() is invoked.
  *
- * @param $node
+ * @param Node $node
  *   The node that is about to be shown on the add/edit form.
  *
  * @ingroup node_api_hooks
  */
-function hook_node_prepare($node) {
+function hook_node_prepare(Node $node) {
   if (!isset($node->comment)) {
     $node->comment = variable_get("comment_$node->type", COMMENT_NODE_OPEN);
   }
@@ -651,7 +651,7 @@ function hook_node_prepare($node) {
  * This hook is invoked from node_search_execute(), after node_load()
  * and node_view() have been called.
  *
- * @param $node
+ * @param Node $node
  *   The node being displayed in a search result.
  *
  * @return array
@@ -665,7 +665,7 @@ function hook_node_prepare($node) {
  *
  * @ingroup node_api_hooks
  */
-function hook_node_search_result($node) {
+function hook_node_search_result(Node $node) {
   $comments = db_query('SELECT comment_count FROM {node_comment_statistics} WHERE nid = :nid', array('nid' => $node->nid))->fetchField();
   return array('comment' => format_plural($comments, '1 comment', '@count comments'));
 }
@@ -676,12 +676,12 @@ function hook_node_search_result($node) {
  * This hook is invoked from node_save() before the node is saved to the
  * database.
  *
- * @param $node
+ * @param Node $node
  *   The node that is being inserted or updated.
  *
  * @ingroup node_api_hooks
  */
-function hook_node_presave($node) {
+function hook_node_presave(Node $node) {
   if ($node->nid && $node->moderate) {
     // Reset votes when node is updated:
     $node->score = 0;
@@ -697,12 +697,12 @@ function hook_node_presave($node) {
  * table in the database, after the type-specific hook_update() is invoked, and
  * after field_attach_update() is called.
  *
- * @param $node
+ * @param Node $node
  *   The node that is being updated.
  *
  * @ingroup node_api_hooks
  */
-function hook_node_update($node) {
+function hook_node_update(Node $node) {
   db_update('mytable')
     ->fields(array('extra' => $node->extra))
     ->condition('nid', $node->nid)
@@ -715,7 +715,7 @@ function hook_node_update($node) {
  * This hook is invoked during search indexing, after node_load(), and after
  * the result of node_view() is added as $node->rendered to the node object.
  *
- * @param $node
+ * @param Node $node
  *   The node being indexed.
  *
  * @return string
@@ -723,7 +723,7 @@ function hook_node_update($node) {
  *
  * @ingroup node_api_hooks
  */
-function hook_node_update_index($node) {
+function hook_node_update_index(Node $node) {
   $text = '';
   $comments = db_query('SELECT subject, comment, format FROM {comment} WHERE nid = :nid AND status = :status', array(':nid' => $node->nid, ':status' => COMMENT_PUBLISHED));
   foreach ($comments as $comment) {
@@ -747,7 +747,7 @@ function hook_node_update_index($node) {
  * hook_node_presave() instead. If it is really necessary to change
  * the node at the validate stage, you can use form_set_value().
  *
- * @param $node
+ * @param Node $node
  *   The node being validated.
  * @param $form
  *   The form being used to edit the node.
@@ -756,7 +756,7 @@ function hook_node_update_index($node) {
  *
  * @ingroup node_api_hooks
  */
-function hook_node_validate($node, $form, &$form_state) {
+function hook_node_validate(Node $node, $form, &$form_state) {
   if (isset($node->end) && isset($node->start)) {
     if ($node->start > $node->end) {
       form_set_error('time', t('An event may not end before it starts.'));
@@ -775,7 +775,7 @@ function hook_node_validate($node, $form, &$form_state) {
  * properties. See hook_field_attach_submit() for customizing field-related
  * properties.
  *
- * @param $node
+ * @param Node $node
  *   The node object being updated in response to a form submission.
  * @param $form
  *   The form being used to edit the node.
@@ -784,7 +784,7 @@ function hook_node_validate($node, $form, &$form_state) {
  *
  * @ingroup node_api_hooks
  */
-function hook_node_submit($node, $form, &$form_state) {
+function hook_node_submit(Node $node, $form, &$form_state) {
   // Decompose the selected menu parent option into 'menu_name' and 'plid', if
   // the form used the default parent selection widget.
   if (!empty($form_state['values']['menu']['parent'])) {
@@ -804,7 +804,7 @@ function hook_node_submit($node, $form, &$form_state) {
  * the RSS item generated for this node.
  * For details on how this is used, see node_feed().
  *
- * @param $node
+ * @param Node $node
  *   The node that is being assembled for rendering.
  * @param $view_mode
  *   The $view_mode parameter from node_view().
@@ -817,7 +817,7 @@ function hook_node_submit($node, $form, &$form_state) {
  *
  * @ingroup node_api_hooks
  */
-function hook_node_view($node, $view_mode, $langcode) {
+function hook_node_view(Node $node, $view_mode, $langcode) {
   $node->content['my_additional_field'] = array(
     '#markup' => $additional_field,
     '#weight' => 10,
@@ -1027,12 +1027,12 @@ function hook_node_type_delete($info) {
  * removed from the node table in the database, before hook_node_delete() is
  * invoked, and before field_attach_delete() is called.
  *
- * @param $node
+ * @param Node $node
  *   The node that is being deleted.
  *
  * @ingroup node_api_hooks
  */
-function hook_delete($node) {
+function hook_delete(Node $node) {
   db_delete('mytable')
     ->condition('nid', $node->nid)
     ->execute();
@@ -1047,12 +1047,12 @@ function hook_delete($node) {
  * This hook is invoked from node_object_prepare() before the general
  * hook_node_prepare() is invoked.
  *
- * @param $node
+ * @param Node $node
  *   The node that is about to be shown on the add/edit form.
  *
  * @ingroup node_api_hooks
  */
-function hook_prepare($node) {
+function hook_prepare(Node $node) {
   if ($file = file_check_upload($field_name)) {
     $file = file_save_upload($field_name, _image_filename($file->filename, NULL, TRUE));
     if ($file) {
@@ -1083,7 +1083,7 @@ function hook_prepare($node) {
  * displayed automatically by the node module. This hook just needs to
  * return the node title and form editing fields specific to the node type.
  *
- * @param $node
+ * @param Node $node
  *   The node being added or edited.
  * @param $form_state
  *   The form state array.
@@ -1094,7 +1094,7 @@ function hook_prepare($node) {
  *
  * @ingroup node_api_hooks
  */
-function hook_form($node, &$form_state) {
+function hook_form(Node $node, &$form_state) {
   $type = node_type_get_type($node);
 
   $form['title'] = array(
@@ -1135,12 +1135,12 @@ function hook_form($node, &$form_state) {
  * node table in the database, before field_attach_insert() is called, and
  * before hook_node_insert() is invoked.
  *
- * @param $node
+ * @param Node $node
  *   The node that is being created.
  *
  * @ingroup node_api_hooks
  */
-function hook_insert($node) {
+function hook_insert(Node $node) {
   db_insert('mytable')
     ->fields(array(
       'nid' => $node->nid,
@@ -1193,12 +1193,12 @@ function hook_load($nodes) {
  * node table in the database, before field_attach_update() is called, and
  * before hook_node_update() is invoked.
  *
- * @param $node
+ * @param Node $node
  *   The node that is being updated.
  *
  * @ingroup node_api_hooks
  */
-function hook_update($node) {
+function hook_update(Node $node) {
   db_update('mytable')
     ->fields(array('extra' => $node->extra))
     ->condition('nid', $node->nid)
@@ -1222,7 +1222,7 @@ function hook_update($node) {
  * have no effect.  The preferred method to change a node's content is to use
  * hook_node_presave() instead.
  *
- * @param $node
+ * @param Node $node
  *   The node being validated.
  * @param $form
  *   The form being used to edit the node.
@@ -1231,7 +1231,7 @@ function hook_update($node) {
  *
  * @ingroup node_api_hooks
  */
-function hook_validate($node, $form, &$form_state) {
+function hook_validate(Node $node, $form, &$form_state) {
   if (isset($node->end) && isset($node->start)) {
     if ($node->start > $node->end) {
       form_set_error('time', t('An event may not end before it starts.'));
@@ -1249,7 +1249,7 @@ function hook_validate($node, $form, &$form_state) {
  * so that the node type module can define a custom method for display, or
  * add to the default display.
  *
- * @param $node
+ * @param Node $node
  *   The node to be displayed, as returned by node_load().
  * @param $view_mode
  *   View mode, e.g. 'full', 'teaser', ...
@@ -1268,7 +1268,7 @@ function hook_validate($node, $form, &$form_state) {
  *
  * @ingroup node_api_hooks
  */
-function hook_view($node, $view_mode) {
+function hook_view(Node $node, $view_mode) {
   if ($view_mode == 'full' && node_is_page($node)) {
     $breadcrumb = array();
     $breadcrumb[] = l(t('Home'), NULL);
diff --git a/core/modules/node/node.entity.inc b/core/modules/node/node.entity.inc
new file mode 100644
index 0000000..5e4cf0e
--- /dev/null
+++ b/core/modules/node/node.entity.inc
@@ -0,0 +1,394 @@
+<?php
+
+/**
+ * @file
+ * Entity controller and class for nodes.
+ */
+
+/**
+ * Defines the node entity class.
+ */
+class Node extends Entity {
+
+  /**
+   * The node ID.
+   *
+   * @var integer
+   */
+  public $nid;
+
+  /**
+   * The node revision ID.
+   *
+   * @var integer
+   */
+  public $vid;
+
+  /**
+   * The node content_type (bundle).
+   *
+   * @var string
+   */
+  public $type;
+
+  /**
+   * The node language code.
+   *
+   * @var string
+   */
+  public $langcode = LANGUAGE_NOT_SPECIFIED;
+
+  /**
+   * The node title.
+   *
+   * @var string
+   */
+  public $title;
+
+  /**
+   * The node owner's user ID.
+   *
+   * @var integer
+   */
+  public $uid;
+
+  /**
+   * The node published status indicator.
+   *
+   * Unpublished nodes are only visible to their authors and to administrators.
+   * The value is either NODE_PUBLISHED or NODE_NOT_PUBLISHED.
+   *
+   * @var integer
+   */
+  public $status;
+
+  /**
+   * The node creation timestamp.
+   *
+   * @var integer
+   */
+  public $created;
+
+  /**
+   * The node modification timestamp.
+   *
+   * @var integer
+   */
+  public $changed;
+
+  /**
+   * The node comment status indicator.
+   *
+   * 0 => no comments
+   * 1 => comments are read-only
+   * 2 => open (read/write)
+   *
+   * @var integer
+   */
+  public $comment;
+
+  /**
+   * The node promotion status.
+   *
+   * Promoted nodes should be displayed on the front page of the site. The
+   * value is either NODE_PROMOTED or NODE_NOT_PROMOTED.
+   *
+   * @var integer
+   */
+  public $promote;
+
+  /**
+   * The node sticky status.
+   *
+   * Sticky nodes should be displayed at the top of lists in which they appear.
+   * The value is either NODE_STICKY or NODE_NOT_STICKY.
+   *
+   * @var integer
+   */
+  public $sticky;
+
+  /**
+   * The node translation set ID.
+   *
+   * Translations sets are based on the ID of the node containing the source
+   * text for the translation set.
+   */
+  public $tnid;
+
+  /**
+   * The node 'translation needed' status.
+   *
+   * @var integer
+   */
+  public $translate;
+
+  /**
+   * The node revision creation timestamp.
+   */
+  public $revision_timestamp;
+
+  /**
+   * The node revision author's user ID.
+   */
+  public $revision_uid;
+
+  /**
+   * Overrides Entity::createDuplicate().
+   */
+  public function createDuplicate() {
+    $node = parent::createDuplicate();
+    $node->{$this->entityInfo['entity keys']['revision']} = NULL;
+    return $node;
+  }
+}
+
+/**
+ * Controller class for nodes.
+ *
+ * This extends the EntityDatabaseStorageController class, adding required
+ * special handling for node objects.
+ */
+class NodeStorageController extends EntityDatabaseStorageController {
+
+  /**
+   * Overrides EntityDatabaseStorageController::delete().
+   */
+  public function delete($ids) {
+    $entities = $ids ? $this->load($ids) : FALSE;
+    if (!$entities) {
+      // If no IDs or invalid IDs were passed, do nothing.
+      return;
+    }
+    $transaction = db_transaction();
+
+    try {
+      $this->preDelete($entities);
+      foreach ($entities as $id => $entity) {
+        $this->invokeHook('predelete', $entity);
+      }
+      $ids = array_keys($entities);
+
+      db_delete($this->entityInfo['base table'])
+        ->condition($this->idKey, $ids, 'IN')
+        ->execute();
+
+      if ($this->revisionKey) {
+        db_delete($this->revisionTable)
+          ->condition($this->idKey, $ids, 'IN')
+          ->execute();
+      }
+
+      // Reset the cache as soon as the changes have been applied.
+      $this->resetCache($ids);
+
+      $this->postDelete($entities);
+      foreach ($entities as $id => $entity) {
+        $this->invokeHook('delete', $entity);
+      }
+      // Ignore slave server temporarily.
+      db_ignore_slave();
+    }
+    catch (Exception $e) {
+      $transaction->rollback();
+      watchdog_exception($this->entityType, $e);
+      throw new EntityStorageException($e->getMessage, $e->getCode, $e);
+    }
+  }
+
+  /**
+   * Overrides EntityDatabaseStorageController::save().
+   */
+  public function save(EntityInterface $entity) {
+    global $user;
+    $transaction = db_transaction();
+    try {
+      // Load the stored entity, if any.
+      if (!$entity->isNew() && !isset($entity->original)) {
+        $entity->original = entity_load_unchanged($this->entityType, $entity->id());
+      }
+
+      $this->preSave($entity);
+      $this->invokeHook('presave', $entity);
+
+      $base_keys = array();
+      if ($entity->isNew()) {
+        $op = 'insert';
+        $return = drupal_write_record($this->entityInfo['base table'], $entity);
+      }
+      else {
+        $op = 'update';
+        $return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey);
+      }
+
+      if ($this->revisionKey) {
+        $this->saveRevision($entity);
+      }
+
+      $this->resetCache(array($entity->{$this->idKey}));
+
+      $this->postSave($entity);
+      $this->invokeHook($op, $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 new EntityStorageException($e->getMessage(), $e->getCode(), $e);
+    }
+  }
+
+  /**
+   * Saves a node revision.
+   */
+  protected function saveRevision(EntityInterface $entity) {
+    global $user;
+
+    $temp_uid = $entity->uid;
+    $entity->uid = $user->uid;
+
+    if (empty($entity->{$this->revisionKey}) || !empty($entity->revision)) {
+      $result = drupal_write_record($this->revisionTable, $entity);
+      db_update($this->entityInfo['base table'])
+        ->fields(array($this->revisionKey => $entity->{$this->revisionKey}))
+        ->condition($this->idKey, $entity->{$this->idKey})
+        ->execute();
+    }
+    else {
+      $result = drupal_write_record($this->revisionTable, $entity, $this->revisionKey);
+    }
+    $entity->uid = $temp_uid;
+
+    return $result;
+  }
+
+  /**
+   * Overrides DrupalDefaultEntityController::attachLoad().
+   */
+  protected function attachLoad(&$nodes, $revision_id = FALSE) {
+    // Create an array of nodes for each content type and pass this to the
+    // object type specific callback.
+    $typed_nodes = array();
+    foreach ($nodes as $id => $entity) {
+      $typed_nodes[$entity->type][$id] = $entity;
+    }
+
+    // Call object type specific callbacks on each typed array of nodes.
+    foreach ($typed_nodes as $node_type => $nodes_of_type) {
+      if (node_hook($node_type, 'load')) {
+        $function = node_type_get_base($node_type) . '_load';
+        $function($nodes_of_type);
+      }
+    }
+    // Besides the list of nodes, pass one additional argument to
+    // hook_node_load(), containing a list of node types that were loaded.
+    $argument = array_keys($typed_nodes);
+    $this->hookLoadArguments = array($argument);
+    parent::attachLoad($nodes, $revision_id);
+  }
+
+  /**
+   * Overrides DrupalDefaultEntityController::buildQuery().
+   */
+  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
+    // Ensure that uid is taken from the {node} table,
+    // alias timestamp to revision_timestamp and add revision_uid.
+    $query = parent::buildQuery($ids, $conditions, $revision_id);
+    $fields =& $query->getFields();
+    unset($fields['timestamp']);
+    $query->addField('revision', 'timestamp', 'revision_timestamp');
+    $fields['uid']['table'] = 'base';
+    $query->addField('revision', 'uid', 'revision_uid');
+    return $query;
+  }
+
+  /**
+   * Overrides EntityDatabaseStorageController::invokeHook().
+   */
+  protected function invokeHook($hook, EntityInterface $node) {
+    if ($hook == 'insert' || $hook == 'update') {
+      node_invoke($node, $hook);
+    }
+    else if ($hook == 'predelete') {
+      // 'delete' is triggered in 'predelete' is here to preserve hook ordering
+      // from Drupal 7.
+      node_invoke($node, 'delete');
+    }
+
+    parent::invokeHook($hook, $node);
+
+    if ($hook == 'predelete') {
+      if (module_exists('search')) {
+        search_reindex($node->nid, 'node');
+      }
+    }
+    else if ($hook == 'presave') {
+      if ($node->isNew() || !empty($node->revision)) {
+        // When inserting either a new node or a new node revision, $node->log
+        // must be set because {node_revision}.log is a text column and therefore
+        // cannot have a default value. However, it might not be set at this
+        // point (for example, if the user submitting a node form does not have
+        // permission to create revisions), so we ensure that it is at least an
+        // empty string in that case.
+        // @todo: Make the {node_revision}.log column nullable so that we can
+        // remove this check.
+        if (!isset($node->log)) {
+          $node->log = '';
+        }
+      }
+      elseif (!isset($node->log) || $node->log === '') {
+        // If we are updating an existing node without adding a new revision, we
+        // need to make sure $node->log is unset whenever it is empty. As long as
+        // $node->log is unset, drupal_write_record() will not attempt to update
+        // the existing database column when re-saving the revision; therefore,
+        // this code allows us to avoid clobbering an existing log entry with an
+        // empty one.
+        $node->log = $node->original->log;
+      }
+
+      // When saving a new node revision, unset any existing $node->vid so as to
+      // ensure that a new revision will actually be created, then store the old
+      // revision ID in a separate property for use by node hook implementations.
+      if (!$node->isNew() && !empty($node->revision) && $node->vid) {
+        $node->old_vid = $node->vid;
+        $node->vid = NULL;
+      }
+    }
+    else if ($hook == 'insert' || $hook == 'update') {
+      node_access_acquire_grants($node, $hook === 'update');
+    }
+  }
+
+  /**
+   * Overrides EntityDatabaseStorageController::preSave().
+   */
+  protected function preSave(EntityInterface $node) {
+    // Before saving the node, set created, changed and revision times.
+    if (empty($node->created)) {
+      $node->created = REQUEST_TIME;
+    }
+
+    $node->changed = REQUEST_TIME;
+    $node->timestamp = REQUEST_TIME;
+  }
+
+  /**
+   * Overrides EntityDatabaseStorageController::postDelete().
+   */
+  protected function postDelete($nodes) {
+    //  Delete values from other tables also referencing this node.
+    $ids = array_keys($nodes);
+
+    db_delete('history')
+      ->condition('nid', $ids, 'IN')
+      ->execute();
+    db_delete('node_access')
+      ->condition('nid', $ids, 'IN')
+      ->execute();
+  }
+}
diff --git a/core/modules/node/node.info b/core/modules/node/node.info
index 0e9a3af..2b56c62 100644
--- a/core/modules/node/node.info
+++ b/core/modules/node/node.info
@@ -4,6 +4,7 @@ package = Core
 version = VERSION
 core = 8.x
 files[] = node.module
+files[] = node.entity.inc
 files[] = node.test
 dependencies[] = entity
 configure = admin/structure/types
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index be60e48..09294b1 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -183,7 +183,8 @@ function node_entity_info() {
   $return = array(
     'node' => array(
       'label' => t('Node'),
-      'controller class' => 'NodeController',
+      'controller class' => 'NodeStorageController',
+      'entity class' => 'Node',
       'base table' => 'node',
       'revision table' => 'node_revision',
       'uri callback' => 'node_uri',
@@ -260,8 +261,11 @@ function node_field_display_node_alter(&$display, $context) {
 
 /**
  * Entity uri callback.
+ *
+ * @param Node $node
+ *   A node entity.
  */
-function node_uri($node) {
+function node_uri(Node $node) {
   return array(
     'path' => 'node/' . $node->nid,
   );
@@ -314,10 +318,10 @@ function node_title_list($result, $title = NULL) {
 /**
  * Updates the 'last viewed' timestamp of the specified node for current user.
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  */
-function node_tag_new($node) {
+function node_tag_new(Node $node) {
   global $user;
   if ($user->uid) {
     db_merge('history')
@@ -377,7 +381,7 @@ function node_mark($nid, $timestamp) {
 /**
  * Extracts the type name.
  *
- * @param $node
+ * @param Node|string $node
  *   Either a string or object, containing the node type information.
  *
  * @return
@@ -405,7 +409,7 @@ function node_type_get_types() {
 /**
  * Returns the node type of the passed node or node type string.
  *
- * @param $node
+ * @param Node|string $node
  *   A node object or string that indicates the node type to return.
  *
  * @return
@@ -428,7 +432,7 @@ function node_type_get_type($node) {
  * execute node-type-specific hooks. For types defined in the user interface
  * and managed by node.module, the base is 'node_content'.
  *
- * @param $node
+ * @param Node|string $node
  *   A node object or string that indicates the node type to return.
  *
  * @return
@@ -458,7 +462,7 @@ function node_type_get_names() {
 /**
  * Returns the node type name of the passed node or node type string.
  *
- * @param $node
+ * @param Node|string $node
  *   A node object or string that indicates the node type to return.
  *
  * @return
@@ -872,7 +876,7 @@ function node_rdf_mapping() {
 /**
  * Determines whether a node hook exists.
  *
- * @param $node
+ * @param Node|string $node
  *   A node object or a string containing the node type.
  * @param $hook
  *   A string containing the name of the hook.
@@ -888,7 +892,7 @@ function node_hook($node, $hook) {
 /**
  * Invokes a node hook.
  *
- * @param $node
+ * @param Node|string $node
  *   A node object or a string containing the node type.
  * @param $hook
  *   A string containing the name of the hook.
@@ -962,10 +966,10 @@ function node_load($nid = NULL, $vid = NULL, $reset = FALSE) {
  * Fills in a few default values, and then invokes hook_prepare() on the node
  * type module, and hook_node_prepare() on all modules.
  *
- * @param $node
+ * @param Node $node
  *   The node object.
  */
-function node_object_prepare($node) {
+function node_object_prepare(Node $node) {
   // Set up default values, if required.
   $node_options = variable_get('node_options_' . $node->type, array('status', 'promote'));
   // If this is a new node, fill in the default values.
@@ -1057,166 +1061,12 @@ function node_submit($node) {
 /**
  * Saves changes to a node or adds a new node.
  *
- * @param $node
+ * @param Node $node
  *   The $node object to be saved. If $node->nid is
  *   omitted (or $node->is_new is TRUE), a new node will be added.
  */
-function node_save($node) {
-  $transaction = db_transaction();
-
-  try {
-    // Load the stored entity, if any.
-    if (!empty($node->nid) && !isset($node->original)) {
-      $node->original = entity_load_unchanged('node', $node->nid);
-    }
-
-    field_attach_presave('node', $node);
-    global $user;
-
-    // Determine if we will be inserting a new node.
-    if (!isset($node->is_new)) {
-      $node->is_new = empty($node->nid);
-    }
-
-    // Set the timestamp fields.
-    if (empty($node->created)) {
-      $node->created = REQUEST_TIME;
-    }
-    // The changed timestamp is always updated for bookkeeping purposes,
-    // for example: revisions, searching, etc.
-    $node->changed = REQUEST_TIME;
-
-    $node->timestamp = REQUEST_TIME;
-    $update_node = TRUE;
-
-    // Let modules modify the node before it is saved to the database.
-    module_invoke_all('node_presave', $node);
-    module_invoke_all('entity_presave', $node, 'node');
-
-    if ($node->is_new || !empty($node->revision)) {
-      // When inserting either a new node or a new node revision, $node->log
-      // must be set because {node_revision}.log is a text column and therefore
-      // cannot have a default value. However, it might not be set at this
-      // point (for example, if the user submitting a node form does not have
-      // permission to create revisions), so we ensure that it is at least an
-      // empty string in that case.
-      // @todo: Make the {node_revision}.log column nullable so that we can
-      // remove this check.
-      if (!isset($node->log)) {
-        $node->log = '';
-      }
-    }
-    elseif (!isset($node->log) || $node->log === '') {
-      // If we are updating an existing node without adding a new revision, we
-      // need to make sure $node->log is unset whenever it is empty. As long as
-      // $node->log is unset, drupal_write_record() will not attempt to update
-      // the existing database column when re-saving the revision; therefore,
-      // this code allows us to avoid clobbering an existing log entry with an
-      // empty one.
-      unset($node->log);
-    }
-
-    // When saving a new node revision, unset any existing $node->vid so as to
-    // ensure that a new revision will actually be created, then store the old
-    // revision ID in a separate property for use by node hook implementations.
-    if (!$node->is_new && !empty($node->revision) && $node->vid) {
-      $node->old_vid = $node->vid;
-      unset($node->vid);
-    }
-
-    // Save the node and node revision.
-    if ($node->is_new) {
-      // For new nodes, save new records for both the node itself and the node
-      // revision.
-      drupal_write_record('node', $node);
-      _node_save_revision($node, $user->uid);
-      $op = 'insert';
-    }
-    else {
-      // For existing nodes, update the node record which matches the value of
-      // $node->nid.
-      drupal_write_record('node', $node, 'nid');
-      // Then, if a new node revision was requested, save a new record for
-      // that; otherwise, update the node revision record which matches the
-      // value of $node->vid.
-      if (!empty($node->revision)) {
-        _node_save_revision($node, $user->uid);
-      }
-      else {
-        _node_save_revision($node, $user->uid, 'vid');
-        $update_node = FALSE;
-      }
-      $op = 'update';
-    }
-    if ($update_node) {
-      db_update('node')
-        ->fields(array('vid' => $node->vid))
-        ->condition('nid', $node->nid)
-        ->execute();
-    }
-
-    // Call the node specific callback (if any). This can be
-    // node_invoke($node, 'insert') or
-    // node_invoke($node, 'update').
-    node_invoke($node, $op);
-
-    // Save fields.
-    $function = "field_attach_$op";
-    $function('node', $node);
-
-    module_invoke_all('node_' . $op, $node);
-    module_invoke_all('entity_' . $op, $node, 'node');
-
-    // Update the node access table for this node. There's no need to delete
-    // existing records if the node is new.
-    $delete = $op == 'update';
-    node_access_acquire_grants($node, $delete);
-
-    // Clear internal properties.
-    unset($node->is_new);
-    unset($node->original);
-    // Clear the static loading cache.
-    entity_get_controller('node')->resetCache(array($node->nid));
-
-    // Ignore slave server temporarily to give time for the
-    // saved node to be propagated to the slave.
-    db_ignore_slave();
-  }
-  catch (Exception $e) {
-    $transaction->rollback();
-    watchdog_exception('node', $e);
-    throw $e;
-  }
-}
-
-/**
- * Saves a revision with the ID of the current user.
- *
- * The resulting revision ID is available afterward in $node->vid.
- *
- * @param $node
- *   The node object to be processed.
- * @param $uid
- *   User ID of the current user.
- * @param $update
- *   (optional) To indicate that this is a new record to be inserted, omit this
- *   argument. If this is an update, this argument specifies the primary keys'
- *   field names. If there is only 1 field in the key, you may pass in a string;
- *   if there are multiple fields in the key, pass in an array.
- *
- * @see node_save()
- */
-function _node_save_revision($node, $uid, $update = NULL) {
-  $temp_uid = $node->uid;
-  $node->uid = $uid;
-  if (isset($update)) {
-    drupal_write_record('node_revision', $node, $update);
-  }
-  else {
-    drupal_write_record('node_revision', $node);
-  }
-  // Have node object still show node owner's uid, not revision author's.
-  $node->uid = $temp_uid;
+function node_save(Node $node) {
+  $node->save();
 }
 
 /**
@@ -1239,59 +1089,7 @@ function node_delete($nid) {
  * @see hook_node_delete()
  */
 function node_delete_multiple($nids) {
-  $transaction = db_transaction();
-  if (!empty($nids)) {
-    $nodes = node_load_multiple($nids, array());
-
-    try {
-      foreach ($nodes as $nid => $node) {
-        // Call the node-specific callback (if any):
-        node_invoke($node, 'delete');
-
-        // Allow modules to act prior to node deletion.
-        module_invoke_all('node_predelete', $node);
-        module_invoke_all('entity_predelete', $node, 'node');
-
-        field_attach_delete('node', $node);
-
-        // Remove this node from the search index if needed.
-        // This code is implemented in node module rather than in search module,
-        // because node module is implementing search module's API, not the other
-        // way around.
-        if (module_exists('search')) {
-          search_reindex($nid, 'node');
-        }
-      }
-
-      // Delete after calling hooks so that they can query node tables as needed.
-      db_delete('node')
-        ->condition('nid', $nids, 'IN')
-        ->execute();
-      db_delete('node_revision')
-        ->condition('nid', $nids, 'IN')
-        ->execute();
-      db_delete('history')
-        ->condition('nid', $nids, 'IN')
-        ->execute();
-      db_delete('node_access')
-       ->condition('nid', $nids, 'IN')
-       ->execute();
-
-      foreach ($nodes as $nid => $node) {
-        // Allow modules to respond to node deletion.
-        module_invoke_all('node_delete', $node);
-        module_invoke_all('entity_delete', $node, 'node');
-      }
-    }
-    catch (Exception $e) {
-      $transaction->rollback();
-      watchdog_exception('node', $e);
-      throw $e;
-    }
-
-    // Clear the page and block and node_load_multiple caches.
-    entity_get_controller('node')->resetCache();
-  }
+  entity_delete_multiple('node', $nids);
 }
 
 /**
@@ -1325,7 +1123,7 @@ function node_revision_delete($revision_id) {
 /**
  * Generates an array for rendering the given node.
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  * @param $view_mode
  *   (optional) View mode, e.g., 'full', 'teaser'... Defaults to 'full.'
@@ -1336,7 +1134,7 @@ function node_revision_delete($revision_id) {
  * @return
  *   An array as expected by drupal_render().
  */
-function node_view($node, $view_mode = 'full', $langcode = NULL) {
+function node_view(Node $node, $view_mode = 'full', $langcode = NULL) {
   if (!isset($langcode)) {
     $langcode = $GLOBALS['language_content']->langcode;
   }
@@ -1390,7 +1188,7 @@ function node_view($node, $view_mode = 'full', $langcode = NULL) {
  * Contributed modules might define additional view modes, or use existing
  * view modes in additional contexts.
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  * @param $view_mode
  *   (optional) View mode, e.g., 'full', 'teaser'... Defaults to 'full.'
@@ -1398,7 +1196,7 @@ function node_view($node, $view_mode = 'full', $langcode = NULL) {
  *   (optional) A language code to use for rendering. Defaults to the global
  *   content language of the current request.
  */
-function node_build_content($node, $view_mode = 'full', $langcode = NULL) {
+function node_build_content(Node $node, $view_mode = 'full', $langcode = NULL) {
   if (!isset($langcode)) {
     $langcode = $GLOBALS['language_content']->langcode;
   }
@@ -1451,7 +1249,7 @@ function node_build_content($node, $view_mode = 'full', $langcode = NULL) {
 /**
  * Page callback: Generates an array which displays a node detail page.
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  * @param $message
  *   (optional) A flag which sets a page title relevant to the revision being
@@ -1462,7 +1260,7 @@ function node_build_content($node, $view_mode = 'full', $langcode = NULL) {
  *
  * @see node_menu()
  */
-function node_show($node, $message = FALSE) {
+function node_show(Node $node, $message = FALSE) {
   if ($message) {
     drupal_set_title(t('Revision of %title from %date', array('%title' => $node->title, '%date' => format_date($node->revision_timestamp))), PASS_THROUGH);
   }
@@ -1479,13 +1277,13 @@ function node_show($node, $message = FALSE) {
 /**
  * Checks whether the current page is the full page view of the passed-in node.
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  *
  * @return
  *   The ID of the node if this is a full page view, otherwise FALSE.
  */
-function node_is_page($node) {
+function node_is_page(Node $node) {
   $page_node = menu_get_object();
   return (!empty($page_node) ? $page_node->nid == $node->nid : FALSE);
 }
@@ -1908,7 +1706,7 @@ function theme_node_search_admin($variables) {
 /**
  * Access callback: Checks node revision access.
  *
- * @param $node
+ * @param Node $node
  *   The node to check.
  * @param $op
  *   (optional) The specific operation being checked. Defaults to 'view.'
@@ -1921,7 +1719,7 @@ function theme_node_search_admin($variables) {
  *
  * @see node_menu()
  */
-function _node_revision_access($node, $op = 'view', $account = NULL) {
+function _node_revision_access(Node $node, $op = 'view', $account = NULL) {
   $access = &drupal_static(__FUNCTION__, array());
 
   $map = array(
@@ -2209,12 +2007,12 @@ function node_type_page_title($type) {
 /**
  * Title callback: Displays the node's title.
  *
- * @param $node
+ * @param Node $node
  *   The node object.
  *
  * @see node_menu()
  */
-function node_page_title($node) {
+function node_page_title(Node $node) {
   return $node->title;
 }
 
@@ -2233,8 +2031,11 @@ function node_last_changed($nid) {
 
 /**
  * Returns a list of all the existing revision numbers for the node passed in.
+ *
+ * @param Node $node
+ *   The node object.
  */
-function node_revision_list($node) {
+function node_revision_list(Node $node) {
   $revisions = array();
   $result = db_query('SELECT r.vid, r.title, r.log, r.uid, n.vid AS current_vid, r.timestamp, u.name FROM {node_revision} r LEFT JOIN {node} n ON n.vid = r.vid INNER JOIN {users} u ON u.uid = r.uid WHERE r.nid = :nid ORDER BY r.vid DESC', array(':nid' => $node->nid));
   foreach ($result as $revision) {
@@ -2743,12 +2544,12 @@ function node_page_default() {
 /**
  * Page callback: Displays a single node.
  *
- * @param $node
+ * @param Node $node
  *   The node object.
  *
  * @see node_menu()
  */
-function node_page_view($node) {
+function node_page_view(Node $node) {
   // If there is a menu link to this node, the link becomes the last part
   // of the active trail, and the link name becomes the page title.
   // Thus, we must explicitly set the page title to be the node title.
@@ -2768,8 +2569,12 @@ function node_update_index() {
   $limit = (int)variable_get('search_cron_limit', 100);
 
   $result = db_query_range("SELECT n.nid FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0 ORDER BY d.reindex ASC, n.nid ASC", 0, $limit, array(), array('target' => 'slave'));
+  $nids = $result->fetchCol();
+  if (!$nids) {
+    return;
+  }
 
-  foreach ($result as $node) {
+  foreach (node_load_multiple($nids) as $node) {
     _node_index_node($node);
   }
 }
@@ -2777,11 +2582,10 @@ function node_update_index() {
 /**
  * Indexes a single node.
  *
- * @param $node
+ * @param Node $node
  *   The node to index.
  */
-function _node_index_node($node) {
-  $node = node_load($node->nid);
+function _node_index_node(Node $node) {
 
   // Save the changed time of the most recent indexed node, for the search
   // results half-life calculation.
@@ -2999,7 +2803,7 @@ function node_form_system_themes_admin_form_submit($form, &$form_state) {
  *   - "update"
  *   - "delete"
  *   - "create"
- * @param $node
+ * @param Node|string $node
  *   The node object on which the operation is to be performed, or the node type
  *   (e.g., 'forum') for the 'create' operation.
  * @param $account
@@ -3489,13 +3293,13 @@ function _node_query_node_access_alter($query, $type) {
  * via hook_node_access_records_alter() implementations, and saves the collected
  * and altered grants to the database.
  *
- * @param $node
+ * @param Node $node
  *   The $node to acquire grants for.
  * @param $delete
  *   (optional) Whether to delete existing node access records before inserting
  *   new ones. Defaults to TRUE.
  */
-function node_access_acquire_grants($node, $delete = TRUE) {
+function node_access_acquire_grants(Node $node, $delete = TRUE) {
   $grants = module_invoke_all('node_access_records', $node);
   // Let modules alter the grants.
   drupal_alter('node_access_records', $grants, $node);
@@ -3517,7 +3321,7 @@ function node_access_acquire_grants($node, $delete = TRUE) {
  * Note: Don't call this function directly from a contributed module. Call
  * node_access_acquire_grants() instead.
  *
- * @param $node
+ * @param Node $node
  *   The $node being written to. All that is necessary is that it contains a
  *   nid.
  * @param $grants
@@ -3533,7 +3337,7 @@ function node_access_acquire_grants($node, $delete = TRUE) {
  *   purposes, and assumes the caller has already performed a mass delete of
  *   some form.
  */
-function _node_access_write_grants($node, $grants, $realm = NULL, $delete = TRUE) {
+function _node_access_write_grants(Node $node, $grants, $realm = NULL, $delete = TRUE) {
   if ($delete) {
     $query = db_delete('node_access')->condition('nid', $node->nid);
     if ($realm) {
@@ -3722,7 +3526,7 @@ function _node_access_rebuild_batch_finished($success, $results, $operations) {
 /**
  * Implements hook_form().
  */
-function node_content_form($node, $form_state) {
+function node_content_form(Node $node, $form_state) {
   // It is impossible to define a content type without implementing hook_form()
   // @todo: remove this requirement.
   $form = array();
@@ -3833,7 +3637,7 @@ function node_action_info() {
 /**
  * Sets the status of a node to 1 (published).
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  * @param $context
  *   (optional) Array of additional information about what triggered the action.
@@ -3841,7 +3645,7 @@ function node_action_info() {
  *
  * @ingroup actions
  */
-function node_publish_action($node, $context = array()) {
+function node_publish_action(Node $node, $context = array()) {
   $node->status = NODE_PUBLISHED;
   watchdog('action', 'Set @type %title to published.', array('@type' => node_type_get_name($node), '%title' => $node->title));
 }
@@ -3849,7 +3653,7 @@ function node_publish_action($node, $context = array()) {
 /**
  * Sets the status of a node to 0 (unpublished).
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  * @param $context
  *   (optional) Array of additional information about what triggered the action.
@@ -3857,7 +3661,7 @@ function node_publish_action($node, $context = array()) {
  *
  * @ingroup actions
  */
-function node_unpublish_action($node, $context = array()) {
+function node_unpublish_action(Node $node, $context = array()) {
   $node->status = NODE_NOT_PUBLISHED;
   watchdog('action', 'Set @type %title to unpublished.', array('@type' => node_type_get_name($node), '%title' => $node->title));
 }
@@ -3865,7 +3669,7 @@ function node_unpublish_action($node, $context = array()) {
 /**
  * Sets the sticky-at-top-of-list property of a node to 1.
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  * @param $context
  *   (optional) Array of additional information about what triggered the action.
@@ -3873,7 +3677,7 @@ function node_unpublish_action($node, $context = array()) {
  *
  * @ingroup actions
  */
-function node_make_sticky_action($node, $context = array()) {
+function node_make_sticky_action(Node $node, $context = array()) {
   $node->sticky = NODE_STICKY;
   watchdog('action', 'Set @type %title to sticky.', array('@type' => node_type_get_name($node), '%title' => $node->title));
 }
@@ -3881,7 +3685,7 @@ function node_make_sticky_action($node, $context = array()) {
 /**
  * Sets the sticky-at-top-of-list property of a node to 0.
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  * @param $context
  *   (optional) Array of additional information about what triggered the action.
@@ -3889,7 +3693,7 @@ function node_make_sticky_action($node, $context = array()) {
  *
  * @ingroup actions
  */
-function node_make_unsticky_action($node, $context = array()) {
+function node_make_unsticky_action(Node $node, $context = array()) {
   $node->sticky = NODE_NOT_STICKY;
   watchdog('action', 'Set @type %title to unsticky.', array('@type' => node_type_get_name($node), '%title' => $node->title));
 }
@@ -3897,7 +3701,7 @@ function node_make_unsticky_action($node, $context = array()) {
 /**
  * Sets the promote property of a node to 1.
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  * @param $context
  *   (optional) Array of additional information about what triggered the action.
@@ -3905,7 +3709,7 @@ function node_make_unsticky_action($node, $context = array()) {
  *
  * @ingroup actions
  */
-function node_promote_action($node, $context = array()) {
+function node_promote_action(Node $node, $context = array()) {
   $node->promote = NODE_PROMOTED;
   watchdog('action', 'Promoted @type %title to front page.', array('@type' => node_type_get_name($node), '%title' => $node->title));
 }
@@ -3913,7 +3717,7 @@ function node_promote_action($node, $context = array()) {
 /**
  * Sets the promote property of a node to 0.
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  * @param $context
  *   (optional) Array of additional information about what triggered the action.
@@ -3921,7 +3725,7 @@ function node_promote_action($node, $context = array()) {
  *
  * @ingroup actions
  */
-function node_unpromote_action($node, $context = array()) {
+function node_unpromote_action(Node $node, $context = array()) {
   $node->promote = NODE_NOT_PROMOTED;
   watchdog('action', 'Removed @type %title from front page.', array('@type' => node_type_get_name($node), '%title' => $node->title));
 }
@@ -3929,20 +3733,20 @@ function node_unpromote_action($node, $context = array()) {
 /**
  * Saves a node.
  *
- * @param $node
+ * @param Node $node
  *   The node to be saved.
  *
  * @ingroup actions
  */
-function node_save_action($node) {
-  node_save($node);
+function node_save_action(Node $node) {
+  $node->save();
   watchdog('action', 'Saved @type %title', array('@type' => node_type_get_name($node), '%title' => $node->title));
 }
 
 /**
  * Assigns ownership of a node to a user.
  *
- * @param $node
+ * @param Node $node
  *   A node object to modify.
  * @param $context
  *   Array of additional information about what triggered the action. Includes
@@ -3954,7 +3758,7 @@ function node_save_action($node) {
  * @see node_assign_owner_action_submit()
  * @ingroup actions
  */
-function node_assign_owner_action($node, $context) {
+function node_assign_owner_action(Node $node, $context) {
   $node->uid = $context['owner_uid'];
   $owner_name = db_query("SELECT name FROM {users} WHERE uid = :uid", array(':uid' => $context['owner_uid']))->fetchField();
   watchdog('action', 'Changed owner of @type %title to uid %name.', array('@type' =>  node_type_get_name($node), '%title' => $node->title, '%name' => $owner_name));
@@ -4055,7 +3859,7 @@ function node_unpublish_by_keyword_action_submit($form, $form_state) {
 /**
  * Unpublishes a node containing certain keywords.
  *
- * @param $node
+ * @param Node $node
  *   A node object to modify.
  * @param $context
  *   Array of additional information about what triggered the action. Includes
@@ -4068,9 +3872,9 @@ function node_unpublish_by_keyword_action_submit($form, $form_state) {
  *
  * @ingroup actions
  */
-function node_unpublish_by_keyword_action($node, $context) {
+function node_unpublish_by_keyword_action(Node $node, $context) {
   foreach ($context['keywords'] as $keyword) {
-    $elements = node_view(clone $node);
+    $elements = node_view($node->createDuplicate());
     if (strpos(drupal_render($elements), $keyword) !== FALSE || strpos($node->title, $keyword) !== FALSE) {
       $node->status = NODE_NOT_PUBLISHED;
       watchdog('action', 'Set @type %title to unpublished.', array('@type' => node_type_get_name($node), '%title' => $node->title));
@@ -4139,55 +3943,6 @@ function node_modules_disabled($modules) {
 }
 
 /**
- * Controller class for nodes.
- *
- * This extends the DrupalDefaultEntityController class, adding required
- * special handling for node objects.
- */
-class NodeController extends DrupalDefaultEntityController {
-
-  /**
-   * Overrides DrupalDefaultEntityController::attachLoad().
-   */
-  protected function attachLoad(&$nodes, $revision_id = FALSE) {
-    // Create an array of nodes for each content type and pass this to the
-    // object type specific callback.
-    $typed_nodes = array();
-    foreach ($nodes as $id => $entity) {
-      $typed_nodes[$entity->type][$id] = $entity;
-    }
-
-    // Call object type specific callbacks on each typed array of nodes.
-    foreach ($typed_nodes as $node_type => $nodes_of_type) {
-      if (node_hook($node_type, 'load')) {
-        $function = node_type_get_base($node_type) . '_load';
-        $function($nodes_of_type);
-      }
-    }
-    // Besides the list of nodes, pass one additional argument to
-    // hook_node_load(), containing a list of node types that were loaded.
-    $argument = array_keys($typed_nodes);
-    $this->hookLoadArguments = array($argument);
-    parent::attachLoad($nodes, $revision_id);
-  }
-
-  /**
-   * Overrides DrupalDefaultEntityController::buildQuery().
-   */
-  protected function buildQuery($ids, $conditions = array(), $revision_id = FALSE) {
-    // Ensure that uid is taken from the {node} table,
-    // alias timestamp to revision_timestamp and add revision_uid.
-    $query = parent::buildQuery($ids, $conditions, $revision_id);
-    $fields =& $query->getFields();
-    unset($fields['timestamp']);
-    $query->addField('revision', 'timestamp', 'revision_timestamp');
-    $fields['uid']['table'] = 'base';
-    $query->addField('revision', 'uid', 'revision_uid');
-    return $query;
-  }
-}
-
-/**
  * Implements hook_file_download_access().
  */
 function node_file_download_access($field, $entity_type, $entity) {
diff --git a/core/modules/node/node.pages.inc b/core/modules/node/node.pages.inc
index 4e94b26..efc827a 100644
--- a/core/modules/node/node.pages.inc
+++ b/core/modules/node/node.pages.inc
@@ -81,7 +81,12 @@ function node_add($type) {
   global $user;
 
   $types = node_type_get_types();
-  $node = (object) array('uid' => $user->uid, 'name' => (isset($user->name) ? $user->name : ''), 'type' => $type, 'langcode' => LANGUAGE_NOT_SPECIFIED);
+  $node = entity_create('node', array(
+    'uid' => $user->uid,
+    'name' => (isset($user->name) ? $user->name : ''),
+    'type' => $type,
+    'langcode' => LANGUAGE_NOT_SPECIFIED,
+  ));
   drupal_set_title(t('Create @name', array('@name' => $types[$type]->name)), PASS_THROUGH);
   $output = drupal_get_form($type . '_node_form', $node);
 
@@ -100,7 +105,10 @@ function node_form_validate($form, &$form_state) {
   // $form_state['node'] contains the actual entity being edited, but we must
   // not update it with form values that have not yet been validated, so we
   // create a pseudo-entity to use during validation.
-  $node = (object) $form_state['values'];
+  $node = $form_state['node'];
+  foreach ($form_state['values'] as $key => $value) {
+    $node->{$key} = $value;
+  }
   node_validate($node, $form, $form_state);
   entity_form_field_validate('node', $form, $form_state);
 }
@@ -115,16 +123,13 @@ function node_form_validate($form, &$form_state) {
  * @see node_form_submit_build_node()
  * @ingroup forms
  */
-function node_form($form, &$form_state, $node) {
+function node_form($form, &$form_state, Node $node) {
   global $user;
 
   // During initial form build, add the node entity to the form state for use
   // during form building and processing. During a rebuild, use what is in the
   // form state.
   if (!isset($form_state['node'])) {
-    if (!isset($node->title)) {
-      $node->title = NULL;
-    }
     node_object_prepare($node);
     $form_state['node'] = $node;
   }
@@ -391,7 +396,7 @@ function node_form_build_preview($form, &$form_state) {
 /**
  * Generates a node preview.
  *
- * @param $node
+ * @param Node $node
  *   The node to preview.
  *
  * @return
@@ -399,7 +404,7 @@ function node_form_build_preview($form, &$form_state) {
  *
  * @see node_form_build_preview()
  */
-function node_preview($node) {
+function node_preview(Node $node) {
   if (node_access('create', $node) || node_access('update', $node)) {
     _field_invoke_multiple('load', 'node', array($node->nid => $node));
     // Load the user's name when needed.
@@ -453,7 +458,7 @@ function theme_node_preview($variables) {
 
   $preview_trimmed_version = FALSE;
 
-  $elements = node_view(clone $node, 'teaser');
+  $elements = node_view($node->createDuplicate(), 'teaser');
   $trimmed = drupal_render($elements);
   $elements = node_view($node, 'full');
   $full = drupal_render($elements);
@@ -484,7 +489,7 @@ function theme_node_preview($variables) {
 function node_form_submit($form, &$form_state) {
   $node = node_form_submit_build_node($form, $form_state);
   $insert = empty($node->nid);
-  node_save($node);
+  $node->save();
   $node_link = l(t('view'), 'node/' . $node->nid);
   $watchdog_args = array('@type' => $node->type, '%title' => $node->title);
   $t_args = array('@type' => node_type_get_name($node), '%title' => $node->title);
@@ -658,7 +663,7 @@ function node_revision_revert_confirm_submit($form, &$form_state) {
   $node_revision->revision = 1;
   $node_revision->log = t('Copy of the revision from %date.', array('%date' => format_date($node_revision->revision_timestamp)));
 
-  node_save($node_revision);
+  $node_revision->save();
 
   watchdog('content', '@type: reverted %title revision %revision.', array('@type' => $node_revision->type, '%title' => $node_revision->title, '%revision' => $node_revision->vid));
   drupal_set_message(t('@type %title has been reverted back to the revision from %revision-date.', array('@type' => node_type_get_name($node_revision), '%title' => $node_revision->title, '%revision-date' => format_date($node_revision->revision_timestamp))));
diff --git a/core/modules/node/node.test b/core/modules/node/node.test
index 1dbcaf3..7233dab 100644
--- a/core/modules/node/node.test
+++ b/core/modules/node/node.test
@@ -245,15 +245,13 @@ class NodeRevisionsTestCase extends NodeWebTestCase {
     // the "log message" field), and check that the original log message is
     // preserved.
     $new_title = $this->randomName(10) . 'testNodeRevisionWithoutLogMessage1';
-    $updated_node = (object) array(
-      'nid' => $node->nid,
-      'vid' => $node->vid,
-      'uid' => $node->uid,
-      'type' => $node->type,
-      'title' => $new_title,
-      'log' => '',
-    );
-    node_save($updated_node);
+
+    $updated_node = $node->createDuplicate();
+    $updated_node->title = $new_title;
+    $updated_node->log = '';
+    $updated_node->revision = FALSE;
+
+    $updated_node->save();
     $this->drupalGet('node/' . $node->nid);
     $this->assertText($new_title, t('New node title appears on the page.'));
     $node_revision = node_load($node->nid, NULL, TRUE);
@@ -265,15 +263,13 @@ class NodeRevisionsTestCase extends NodeWebTestCase {
     // Save a new node revision without providing a log message, and check that
     // this revision has an empty log message.
     $new_title = $this->randomName(10) . 'testNodeRevisionWithoutLogMessage2';
-    $updated_node = (object) array(
-      'nid' => $node->nid,
-      'vid' => $node->vid,
-      'uid' => $node->uid,
-      'type' => $node->type,
-      'title' => $new_title,
-      'revision' => 1,
-    );
-    node_save($updated_node);
+
+    $updated_node = $node->createDuplicate();
+    $updated_node->title = $new_title;
+    $updated_node->revision = TRUE;
+    $updated_node->log = NULL;
+
+    $updated_node->save();
     $this->drupalGet('node/' . $node->nid);
     $this->assertText($new_title, 'New node title appears on the page.');
     $node_revision = node_load($node->nid, NULL, TRUE);
@@ -534,7 +530,7 @@ class NodeCreationTestCase extends NodeWebTestCase {
     );
 
     try {
-      node_save((object) $edit);
+      entity_create('node', $edit)->save();
       $this->fail(t('Expected exception has not been thrown.'));
     }
     catch (Exception $e) {
@@ -1156,7 +1152,7 @@ class NodeSaveTestCase extends NodeWebTestCase {
   public static function getInfo() {
     return array(
       'name' => 'Node save',
-      'description' => 'Test node_save() for saving content.',
+      'description' => 'Test $node->save() for saving content.',
       'group' => 'Node',
     );
   }
@@ -1189,12 +1185,12 @@ class NodeSaveTestCase extends NodeWebTestCase {
       'nid' => $test_nid,
       'is_new' => TRUE,
     );
-    $node = node_submit((object) $node);
+    $node = node_submit(entity_create('node', $node));
 
     // Verify that node_submit did not overwrite the user ID.
     $this->assertEqual($node->uid, $this->web_user->uid, t('Function node_submit() preserves user ID'));
 
-    node_save($node);
+    $node->save();
     // Test the import.
     $node_by_nid = node_load($test_nid);
     $this->assertTrue($node_by_nid, t('Node load by node ID.'));
@@ -1215,7 +1211,7 @@ class NodeSaveTestCase extends NodeWebTestCase {
       'title' => $this->randomName(8),
     );
 
-    node_save((object) $edit);
+    entity_create('node', $edit)->save();
     $node = $this->drupalGetNodeByTitle($edit['title']);
     $this->assertEqual($node->created, REQUEST_TIME, t('Creating a node sets default "created" timestamp.'));
     $this->assertEqual($node->changed, REQUEST_TIME, t('Creating a node sets default "changed" timestamp.'));
@@ -1224,14 +1220,14 @@ class NodeSaveTestCase extends NodeWebTestCase {
     $created = $node->created;
     $changed = $node->changed;
 
-    node_save($node);
+    $node->save();
     $node = $this->drupalGetNodeByTitle($edit['title'], TRUE);
     $this->assertEqual($node->created, $created, t('Updating a node preserves "created" timestamp.'));
 
     // Programmatically set the timestamps using hook_node_presave.
     $node->title = 'testing_node_presave';
 
-    node_save($node);
+    $node->save();
     $node = $this->drupalGetNodeByTitle('testing_node_presave', TRUE);
     $this->assertEqual($node->created, 280299600, t('Saving a node uses "created" timestamp set in presave hook.'));
     $this->assertEqual($node->changed, 979534800, t('Saving a node uses "changed" timestamp set in presave hook.'));
@@ -1245,7 +1241,7 @@ class NodeSaveTestCase extends NodeWebTestCase {
       'changed' => 979534800, // Drupal 1.0 release.
     );
 
-    node_save((object) $edit);
+    entity_create('node', $edit)->save();
     $node = $this->drupalGetNodeByTitle($edit['title']);
     $this->assertEqual($node->created, 280299600, t('Creating a node uses user-set "created" timestamp.'));
     $this->assertNotEqual($node->changed, 979534800, t('Creating a node doesn\'t use user-set "changed" timestamp.'));
@@ -1254,7 +1250,7 @@ class NodeSaveTestCase extends NodeWebTestCase {
     $node->created = 979534800;
     $node->changed = 280299600;
 
-    node_save($node);
+    $node->save();
     $node = $this->drupalGetNodeByTitle($edit['title'], TRUE);
     $this->assertEqual($node->created, 979534800, t('Updating a node uses user-set "created" timestamp.'));
     $this->assertNotEqual($node->changed, 280299600, t('Updating a node doesn\'t use user-set "changed" timestamp.'));
@@ -1266,20 +1262,20 @@ class NodeSaveTestCase extends NodeWebTestCase {
    */
   function testDeterminingChanges() {
     // Initial creation.
-    $node = (object) array(
+    $node = entity_create('node', array(
       'uid' => $this->web_user->uid,
       'type' => 'article',
       'title' => 'test_changes',
-    );
-    node_save($node);
+    ));
+    $node->save();
 
     // Update the node without applying changes.
-    node_save($node);
+    $node->save();
     $this->assertEqual($node->title, 'test_changes', 'No changes have been determined.');
 
     // Apply changes.
     $node->title = 'updated';
-    node_save($node);
+    $node->save();
 
     // The hook implementations node_test_node_presave() and
     // node_test_node_update() determine changes and change the title.
diff --git a/core/modules/node/tests/node_access_test.module b/core/modules/node/tests/node_access_test.module
index d9e6e2f..76cd807 100644
--- a/core/modules/node/tests/node_access_test.module
+++ b/core/modules/node/tests/node_access_test.module
@@ -26,7 +26,7 @@ function node_access_test_node_grants($account, $op) {
 /**
  * Implements hook_node_access_records().
  */
-function node_access_test_node_access_records($node) {
+function node_access_test_node_access_records(Node $node) {
   $grants = array();
   // For NodeAccessBaseTableTestCase, only set records for private nodes.
   if (!variable_get('node_access_test_private') || $node->private) {
@@ -199,28 +199,28 @@ function node_access_test_node_load($nodes, $types) {
  * Implements hook_node_predelete().
  */
 
-function node_access_test_node_predelete($node) {
+function node_access_test_node_predelete(Node $node) {
   db_delete('node_access_test')->condition('nid', $node->nid)->execute();
 }
 
 /**
  * Implements hook_node_insert().
  */
-function node_access_test_node_insert($node) {
+function node_access_test_node_insert(Node $node) {
   _node_access_test_node_write($node);
 }
 
 /**
  * Implements hook_nodeapi_update().
  */
-function node_access_test_node_update($node) {
+function node_access_test_node_update(Node $node) {
   _node_access_test_node_write($node);
 }
 
 /**
  * Helper for node insert/update.
  */
-function _node_access_test_node_write($node) {
+function _node_access_test_node_write(Node $node) {
   if (isset($node->private)) {
     db_merge('node_access_test')
       ->key(array('nid' => $node->nid))
diff --git a/core/modules/node/tests/node_test.module b/core/modules/node/tests/node_test.module
index b0ebc14..924ba8b 100644
--- a/core/modules/node/tests/node_test.module
+++ b/core/modules/node/tests/node_test.module
@@ -25,7 +25,7 @@ function node_test_node_load($nodes, $types) {
 /**
  * Implements hook_node_view().
  */
-function node_test_node_view($node, $view_mode) {
+function node_test_node_view(Node $node, $view_mode) {
   if ($view_mode == 'rss') {
     // Add RSS elements and namespaces when building the RSS feed.
     $node->rss_elements[] = array(
@@ -66,7 +66,7 @@ function node_test_node_grants($account, $op) {
 /**
  * Implements hook_node_access_records().
  */
-function node_test_node_access_records($node) {
+function node_test_node_access_records(Node $node) {
   // Return nothing when testing for empty responses.
   if (!empty($node->disable_node_access)) {
     return;
@@ -100,7 +100,7 @@ function node_test_node_access_records($node) {
 /**
  * Implements hook_node_access_records_alter().
  */
-function node_test_node_access_records_alter(&$grants, $node) {
+function node_test_node_access_records_alter(&$grants, Node $node) {
   if (!empty($grants)) {
     foreach ($grants as $key => $grant) {
       // Alter grant from test_page_realm to test_alter_realm and modify the gid.
@@ -123,7 +123,7 @@ function node_test_node_grants_alter(&$grants, $account, $op) {
 /**
  * Implements hook_node_presave().
  */
-function node_test_node_presave($node) {
+function node_test_node_presave(Node $node) {
   if ($node->title == 'testing_node_presave') {
     // Sun, 19 Nov 1978 05:00:00 GMT
     $node->created = 280299600;
@@ -141,7 +141,7 @@ function node_test_node_presave($node) {
 /**
  * Implements hook_node_update().
  */
-function node_test_node_update($node) {
+function node_test_node_update(Node $node) {
   // Determine changes on update.
   if (!empty($node->original) && $node->original->title == 'test_changes') {
     if ($node->original->title != $node->title) {
diff --git a/core/modules/node/tests/node_test_exception.module b/core/modules/node/tests/node_test_exception.module
index 0fe9f35..13ba06e 100644
--- a/core/modules/node/tests/node_test_exception.module
+++ b/core/modules/node/tests/node_test_exception.module
@@ -9,7 +9,7 @@
 /**
  * Implements hook_node_insert().
  */
-function node_test_exception_node_insert($node) {
+function node_test_exception_node_insert(Node $node) {
   if ($node->title == 'testing_transaction_exception') {
     throw new Exception('Test exception for rollback.');
   }
diff --git a/core/modules/path/path.module b/core/modules/path/path.module
index f89927a..bf7b836 100644
--- a/core/modules/path/path.module
+++ b/core/modules/path/path.module
@@ -181,7 +181,7 @@ function path_form_element_validate($element, &$form_state, $complete_form) {
 /**
  * Implements hook_node_insert().
  */
-function path_node_insert($node) {
+function path_node_insert(Node $node) {
   if (isset($node->path)) {
     $path = $node->path;
     $path['alias'] = trim($path['alias']);
@@ -198,7 +198,7 @@ function path_node_insert($node) {
 /**
  * Implements hook_node_update().
  */
-function path_node_update($node) {
+function path_node_update(Node $node) {
   if (isset($node->path)) {
     $path = $node->path;
     $path['alias'] = trim($path['alias']);
@@ -219,7 +219,7 @@ function path_node_update($node) {
 /**
  * Implements hook_node_predelete().
  */
-function path_node_predelete($node) {
+function path_node_predelete(Node $node) {
   // Delete all aliases associated with this node.
   path_delete(array('source' => 'node/' . $node->nid));
 }
diff --git a/core/modules/poll/poll.module b/core/modules/poll/poll.module
index 154db55..2d02984 100644
--- a/core/modules/poll/poll.module
+++ b/core/modules/poll/poll.module
@@ -609,10 +609,10 @@ function poll_delete($node) {
 /**
  * Return content for 'latest poll' block.
  *
- * @param $node
+ * @param Node $node
  *   The node object to load.
  */
-function poll_block_latest_poll_view($node) {
+function poll_block_latest_poll_view(Node $node) {
   global $user;
   $output = '';
 
@@ -621,7 +621,7 @@ function poll_block_latest_poll_view($node) {
   // the wrong output being displayed on subsequent calls.  The cloning and
   // unsetting of $node->content prevents the block output from being the same
   // as the node output.
-  $node = clone $node;
+  $node = $node->createDuplicate();
   unset($node->content);
 
   // No 'read more' link.
diff --git a/core/modules/poll/poll.test b/core/modules/poll/poll.test
index ace8d56..04ac1a3 100644
--- a/core/modules/poll/poll.test
+++ b/core/modules/poll/poll.test
@@ -227,7 +227,7 @@ class PollCreateTestCase extends PollWebTestCase {
       'weight' => 1000,
     );
 
-    node_save($node);
+    $node->save();
 
     $this->drupalGet('poll');
     $this->clickLink($title);
diff --git a/core/modules/rdf/rdf.test b/core/modules/rdf/rdf.test
index 6c7635f..6a9755a 100644
--- a/core/modules/rdf/rdf.test
+++ b/core/modules/rdf/rdf.test
@@ -620,10 +620,10 @@ class RdfTrackerAttributesTestCase extends DrupalWebTestCase {
    *
    * Tests the tracker page for RDFa markup.
    *
-   * @param $node
+   * @param Node $node
    * The node just created.
    */
-  function _testBasicTrackerRdfaMarkup($node) {
+  function _testBasicTrackerRdfaMarkup(Node $node) {
     $url = url('node/' . $node->nid);
 
     $user = ($node->uid == 0) ? 'Anonymous user' : 'Registered user';
diff --git a/core/modules/search/search.module b/core/modules/search/search.module
index 0bbdd0d..4623f78 100644
--- a/core/modules/search/search.module
+++ b/core/modules/search/search.module
@@ -798,7 +798,7 @@ function search_touch_node($nid) {
 /**
  * Implements hook_node_update_index().
  */
-function search_node_update_index($node) {
+function search_node_update_index(Node $node) {
   // Transplant links to a node into the target node.
   $result = db_query("SELECT caption FROM {search_node_links} WHERE nid = :nid", array(':nid' => $node->nid), array('target' => 'slave'));
   $output = array();
@@ -813,7 +813,7 @@ function search_node_update_index($node) {
 /**
  * Implements hook_node_update().
  */
-function search_node_update($node) {
+function search_node_update(Node $node) {
   // Reindex the node when it is updated. The node is automatically indexed
   // when it is added, simply by being added to the node table.
   search_touch_node($node->nid);
diff --git a/core/modules/search/search.test b/core/modules/search/search.test
index 376c8e1..d22d3bc 100644
--- a/core/modules/search/search.test
+++ b/core/modules/search/search.test
@@ -822,7 +822,7 @@ class SearchCommentTestCase extends SearchWebTestCase {
     // Hide comments.
     $this->drupalLogin($this->admin_user);
     $node->comment = 0;
-    node_save($node);
+    $node->save();
 
     // Invoke search index update.
     $this->drupalLogout();
diff --git a/core/modules/simpletest/drupal_web_test_case.php b/core/modules/simpletest/drupal_web_test_case.php
index ded4ad0..892ed76 100644
--- a/core/modules/simpletest/drupal_web_test_case.php
+++ b/core/modules/simpletest/drupal_web_test_case.php
@@ -962,8 +962,8 @@ class DrupalWebTestCase extends DrupalTestCase {
     );
     $settings['body'][$settings['langcode']][0] += $body;
 
-    $node = (object) $settings;
-    node_save($node);
+    $node = entity_create('node', $settings);
+    $node->save();
 
     // Small hack to link revisions to our test user.
     db_update('node_revision')
diff --git a/core/modules/simpletest/tests/form_test.module b/core/modules/simpletest/tests/form_test.module
index 4b0f295..cf0ca93 100644
--- a/core/modules/simpletest/tests/form_test.module
+++ b/core/modules/simpletest/tests/form_test.module
@@ -1742,7 +1742,9 @@ function form_test_form_user_register_form_alter(&$form, &$form_state) {
   );
   // If requested, add the test field by attaching the node page form.
   if (!empty($_REQUEST['field'])) {
-    $node = (object)array('type' => 'page');
+    $node = entity_create('node', array(
+      'type' => 'page',
+    ));
     field_attach_form('node', $node, $form, $form_state);
   }
 }
@@ -1760,12 +1762,12 @@ function form_test_user_register_form_rebuild($form, &$form_state) {
  */
 function form_test_two_instances() {
   global $user;
-  $node1 = (object) array(
+  $node1 = entity_create('node', array(
     'uid' => $user->uid,
     'name' => (isset($user->name) ? $user->name : ''),
     'type' => 'page',
     'langcode' => LANGUAGE_NOT_SPECIFIED,
-  );
+  ));
   $node2 = clone($node1);
   $return['node_form_1'] = drupal_get_form('page_node_form', $node1);
   $return['node_form_2'] = drupal_get_form('page_node_form', $node2);
diff --git a/core/modules/statistics/statistics.module b/core/modules/statistics/statistics.module
index af1f833..701cf53 100644
--- a/core/modules/statistics/statistics.module
+++ b/core/modules/statistics/statistics.module
@@ -114,7 +114,7 @@ function statistics_permission() {
 /**
  * Implements hook_node_view().
  */
-function statistics_node_view($node, $view_mode) {
+function statistics_node_view(Node $node, $view_mode) {
   if ($view_mode != 'rss') {
     if (user_access('view post access counter')) {
       $statistics = statistics_get($node->nid);
@@ -393,7 +393,7 @@ function _statistics_format_item($title, $path) {
 /**
  * Implements hook_node_predelete().
  */
-function statistics_node_predelete($node) {
+function statistics_node_predelete(Node $node) {
   // clean up statistics table when node is deleted
   db_delete('node_counter')
     ->condition('nid', $node->nid)
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 47cde42..ad2edb0 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -1841,7 +1841,7 @@ function taxonomy_field_presave($entity_type, $entity, $field, $instance, $langc
 /**
  * Implements hook_node_insert().
  */
-function taxonomy_node_insert($node) {
+function taxonomy_node_insert(Node $node) {
   // Add taxonomy index entries for the node.
   taxonomy_build_node_index($node);
 }
@@ -1852,7 +1852,7 @@ function taxonomy_node_insert($node) {
  * The index lists all terms that are related to a given node entity, and is
  * therefore maintained at the entity level.
  *
- * @param $node
+ * @param Node $node
  *   The node object.
  */
 function taxonomy_build_node_index($node) {
@@ -1918,7 +1918,7 @@ function taxonomy_build_node_index($node) {
 /**
  * Implements hook_node_update().
  */
-function taxonomy_node_update($node) {
+function taxonomy_node_update(Node $node) {
   // Always rebuild the node's taxonomy index entries on node save.
   taxonomy_delete_node_index($node);
   taxonomy_build_node_index($node);
@@ -1927,7 +1927,7 @@ function taxonomy_node_update($node) {
 /**
  * Implements hook_node_predelete().
  */
-function taxonomy_node_predelete($node) {
+function taxonomy_node_predelete(Node $node) {
   // Clean up the {taxonomy_index} table when nodes are deleted.
   taxonomy_delete_node_index($node);
 }
@@ -1935,10 +1935,10 @@ function taxonomy_node_predelete($node) {
 /**
  * Deletes taxonomy index entries for a given node.
  *
- * @param $node
+ * @param Node $node
  *   The node object.
  */
-function taxonomy_delete_node_index($node) {
+function taxonomy_delete_node_index(Node $node) {
   if (variable_get('taxonomy_maintain_index_table', TRUE)) {
     db_delete('taxonomy_index')->condition('nid', $node->nid)->execute();
   }
diff --git a/core/modules/taxonomy/taxonomy.test b/core/modules/taxonomy/taxonomy.test
index de50f2a..301b4bb 100644
--- a/core/modules/taxonomy/taxonomy.test
+++ b/core/modules/taxonomy/taxonomy.test
@@ -1238,16 +1238,12 @@ class TaxonomyTermIndexTestCase extends TaxonomyWebTestCase {
     $this->assertEqual(1, $index_count, t('Term 2 is indexed once.'));
 
     // Redo the above tests without interface.
-    $update_node = array(
-      'nid' => $node->nid,
-      'vid' => $node->vid,
-      'uid' => $node->uid,
-      'type' => $node->type,
-      'title' => $this->randomName(),
-    );
+    $updated_node = $node->createDuplicate();
+    $updated_node->title = $this->randomName();
+    unset($updated_node->{$this->field_name_1});
+    unset($updated_node->{$this->field_name_2});
 
     // Update the article with no term changed.
-    $updated_node = (object) $update_node;
     node_save($updated_node);
 
     // Check that the index was not changed.
@@ -1263,8 +1259,7 @@ class TaxonomyTermIndexTestCase extends TaxonomyWebTestCase {
     $this->assertEqual(1, $index_count, t('Term 2 is indexed once.'));
 
     // Update the article to change one term.
-    $update_node[$this->field_name_1][$langcode] = array(array('tid' => $term_1->tid));
-    $updated_node = (object) $update_node;
+    $updated_node->{$this->field_name_1}[$langcode] = array(array('tid' => $term_1->tid));
     node_save($updated_node);
 
     // Check that both terms are indexed.
@@ -1280,8 +1275,7 @@ class TaxonomyTermIndexTestCase extends TaxonomyWebTestCase {
     $this->assertEqual(1, $index_count, t('Term 2 is indexed.'));
 
     // Update the article to change another term.
-    $update_node[$this->field_name_2][$langcode] = array(array('tid' => $term_1->tid));
-    $updated_node = (object) $update_node;
+    $updated_node->{$this->field_name_2}[$langcode] = array(array('tid' => $term_1->tid));
     node_save($updated_node);
 
     // Check that only one term is indexed.
diff --git a/core/modules/tracker/tracker.module b/core/modules/tracker/tracker.module
index a5887f2..062a7f3 100644
--- a/core/modules/tracker/tracker.module
+++ b/core/modules/tracker/tracker.module
@@ -190,7 +190,7 @@ function _tracker_user_access($account) {
  *
  * Adds new tracking information for this node since it's new.
  */
-function tracker_node_insert($node, $arg = 0) {
+function tracker_node_insert(Node $node, $arg = 0) {
   _tracker_add($node->nid, $node->uid, $node->changed);
 }
 
@@ -199,7 +199,7 @@ function tracker_node_insert($node, $arg = 0) {
  *
  * Adds tracking information for this node since it's been updated.
  */
-function tracker_node_update($node, $arg = 0) {
+function tracker_node_update(Node $node, $arg = 0) {
   _tracker_add($node->nid, $node->uid, $node->changed);
 }
 
@@ -208,7 +208,7 @@ function tracker_node_update($node, $arg = 0) {
  *
  * Deletes tracking information for a node.
  */
-function tracker_node_predelete($node, $arg = 0) {
+function tracker_node_predelete(Node $node, $arg = 0) {
   db_delete('tracker_node')
     ->condition('nid', $node->nid)
     ->execute();
diff --git a/core/modules/translation/tests/translation_test.module b/core/modules/translation/tests/translation_test.module
index e3bb4b5..20e5dee 100644
--- a/core/modules/translation/tests/translation_test.module
+++ b/core/modules/translation/tests/translation_test.module
@@ -8,6 +8,6 @@
 /**
  * Implements hook_node_insert().
  */
-function translation_test_node_insert($node) {
+function translation_test_node_insert(Node $node) {
   drupal_write_record('node', $node, 'nid');
 }
diff --git a/core/modules/translation/translation.module b/core/modules/translation/translation.module
index ef34ea6..2517614 100644
--- a/core/modules/translation/translation.module
+++ b/core/modules/translation/translation.module
@@ -219,7 +219,7 @@ function translation_form_node_form_alter(&$form, &$form_state) {
  * translation set. If no language provider is enabled, "fall back" to simple
  * links built through the result of translation_node_get_translations().
  */
-function translation_node_view($node, $view_mode) {
+function translation_node_view(Node $node, $view_mode) {
   // If the site has no translations or is not multilingual we have no content
   // translation links to display.
   if (isset($node->tnid) && language_multilingual() && $translations = translation_node_get_translations($node->tnid)) {
@@ -275,7 +275,7 @@ function translation_node_view($node, $view_mode) {
 /**
  * Implements hook_node_prepare().
  */
-function translation_node_prepare($node) {
+function translation_node_prepare(Node $node) {
   // Only act if we are dealing with a content type supporting translations.
   if (translation_supported_type($node->type) &&
     // And it's a new node.
@@ -325,7 +325,7 @@ function translation_node_prepare($node) {
 /**
  * Implements hook_node_insert().
  */
-function translation_node_insert($node) {
+function translation_node_insert(Node $node) {
   // Only act if we are dealing with a content type supporting translations.
   if (translation_supported_type($node->type)) {
     if (!empty($node->translation_source)) {
@@ -360,7 +360,7 @@ function translation_node_insert($node) {
 /**
  * Implements hook_node_update().
  */
-function translation_node_update($node) {
+function translation_node_update(Node $node) {
   // Only act if we are dealing with a content type supporting translations.
   if (translation_supported_type($node->type)) {
     if (isset($node->translation) && $node->translation && !empty($node->langcode) && $node->tnid) {
@@ -389,7 +389,7 @@ function translation_node_update($node) {
  *
  * Ensures that duplicate translations can't be created for the same source.
  */
-function translation_node_validate($node, $form) {
+function translation_node_validate(Node $node, $form) {
   // Only act on translatable nodes with a tnid or translation_source.
   if (translation_supported_type($node->type) && (!empty($node->tnid) || !empty($form['#node']->translation_source->nid))) {
     $tnid = !empty($node->tnid) ? $node->tnid : $form['#node']->translation_source->nid;
@@ -403,7 +403,7 @@ function translation_node_validate($node, $form) {
 /**
  * Implements hook_node_predelete().
  */
-function translation_node_predelete($node) {
+function translation_node_predelete(Node $node) {
   // Only act if we are dealing with a content type supporting translations.
   if (translation_supported_type($node->type)) {
     translation_remove_from_set($node);
diff --git a/core/modules/translation/translation.pages.inc b/core/modules/translation/translation.pages.inc
index 66dfc42..80be119 100644
--- a/core/modules/translation/translation.pages.inc
+++ b/core/modules/translation/translation.pages.inc
@@ -8,7 +8,7 @@
 /**
  * Page callback: Displays a list of a node's translations.
  *
- * @param $node
+ * @param Node $node
  *   A node object.
  *
  * @return
@@ -16,7 +16,7 @@
  *
  * @see translation_menu()
  */
-function translation_node_overview($node) {
+function translation_node_overview(Node $node) {
   include_once DRUPAL_ROOT . '/core/includes/language.inc';
 
   if ($node->tnid) {
diff --git a/core/modules/translation/translation.test b/core/modules/translation/translation.test
index e2b8358..7aa88e3 100644
--- a/core/modules/translation/translation.test
+++ b/core/modules/translation/translation.test
@@ -347,7 +347,7 @@ class TranslationTestCase extends DrupalWebTestCase {
   /**
    * Creates a translation for a basic page in the specified language.
    *
-   * @param $node
+   * @param Node $node
    *   The basic page to create the translation for.
    * @param $title
    *   The title of a basic page in the specified language.
@@ -359,7 +359,7 @@ class TranslationTestCase extends DrupalWebTestCase {
    * @return
    *   Translation object.
    */
-  function createTranslation($node, $title, $body, $langcode) {
+  function createTranslation(Node $node, $title, $body, $langcode) {
     $this->drupalGet('node/add/page', array('query' => array('translation' => $node->nid, 'target' => $langcode)));
 
     $field_langcode = LANGUAGE_NOT_SPECIFIED;
@@ -408,7 +408,7 @@ class TranslationTestCase extends DrupalWebTestCase {
   /**
    * Tests whether the specified language switch links are found.
    *
-   * @param $node
+   * @param Node $node
    *   The node to display.
    * @param $translation
    *   The translation whose link has to be checked.
@@ -420,7 +420,7 @@ class TranslationTestCase extends DrupalWebTestCase {
    * @return
    *   TRUE if the language switch links are found, FALSE if not.
    */
-  function assertLanguageSwitchLinks($node, $translation, $find = TRUE, $types = NULL) {
+  function assertLanguageSwitchLinks(Node $node, $translation, $find = TRUE, $types = NULL) {
     if (empty($types)) {
       $types = array('node', 'block-locale');
     }
