1

Drupal 8.x

I am trying to compare the current users last access timestamp to other timestamp values and am wondering how to get the current users "last_access" in HOOK_preprocess_node().

MYMODULE.module:

function MYMODULE_preprocess_node(&$variables) {
  kpr(\Drupal::currentUser()->getLastAccessedTime());
}

This returns a timestamp, however, it updates quite infrequently and not on node load, or even after cache clear, and does not seem node specific.

I've accessed this value in HOOK_preprocess_user() with $variables['elements']['#user']->access->value, however, I would like the same value available in node.

How do you get the timestamp of the user's last view of a node per node?

2

This doesn't work because the node is cached. Disabling caching of the node wouldn't be a good idea, performance-wise. Try instead a lazy builder to retrieve the access time and create a placeholder, so that the node can be cached, no matter how often the access time is updated:

/**
 * Implements hook_preprocess_HOOK() for node templates.
 */
function mymodule_preprocess_node(&$variables) {
  $variables['content']['timestamp'] = [
    '#lazy_builder' => ['mymodule_timestamp_current_user', []],
    '#create_placeholder' => TRUE,
  ];
}

/**
 * #lazy_builder callback; builds timestamp markup with current user access time.
 */
function mymodule_timestamp_current_user() {
  $timestamp = \Drupal::currentUser()->getLastAccessedTime();
  return [
    '#markup' => $timestamp,
    '#cache' => ['max-age' => 0],
  ];
}

Statistics

This code extends the code you had so far. If you are referring to statistical node view data have a look at the core Statistics module, which provides a timestamp when the node was last viewed. However, not on a per user basis, but this gives you an idea how you can record such statistical data.

  • I am testing this, but it seems to return the same results. The timestamp returned is the same for every node. Note: I am viewing standard nodes, and node teasers view rendered via content in a views block. Any thoughts? – Prestosaurus Oct 18 '18 at 15:45
  • 2
    The first part of the answer is about the caching aspect how to include a timestamp in a node in general. For the second part, since core doesn't record view access timestamps for nodes by default you need to do this before you can display such information. – 4k4 Oct 18 '18 at 16:04
  • I think I am missing something... It seems like \Drupal::currentUser()->getLastAccessedTime() in mymodule_timestamp_current_user() returns the same value that \Drupal::currentUser()->getLastAccessedTime() in mymodule_preprocess_node(&$variables) does. The same timestamp value for every node. I think I understand what is written above, create a variable per user with no cache, and then call it in preprocess_node(), but it looks like when \Drupal::currentUser()->getLastAccessedTime() is called in preprocess_node() it gets rendered as if built in that function. – Prestosaurus Oct 18 '18 at 17:49
  • @Prestosaurus — To me it seems that getLastAccessedTime returns just the time when the current user logged in to your site for the last time. Under no circumstances this method seems to get connected to a/the node, no? – leymannx Dec 9 '18 at 21:28
  • 1
    @leymannx that appears correct. There is no association to 'node'. Ready for alternative ideas... hidden form, custom SQL...? – Prestosaurus Dec 9 '18 at 23:42
1

User::getLastAccessedTime will only give you

The timestamp when the account last accessed the site.

Under no circumstances this value is going to be associated to a node.


So what I'd do now is the following:

  1. Add a new field to the user that's going to be used to store the data we need. Maybe a JSON field (module A or module B, I don't know which one is better) or any other custom field that is capable of holding an array of nodes and timestamps. Hide that field from display.

  2. Add a new pseudo/extra field to your node type that will get and display the last accessed time for the current node, and then will set a new timestamp. Which maybe could look like this (untested!):


use Drupal\node\Entity\NodeType;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;

function MYMODULE_entity_extra_field_info() {

  $extra = [];

  foreach (NodeType::loadMultiple() as $bundle) {

    $extra['node'][$bundle->id()]['display']['last_accessed'] = [
      'label'       => t('Node Last Accessed'),
      'description' => t('Displays the timestamp the current node got last accessed by the current user'),
      'visible'     => FALSE, // Initially set to hidden on Display Settings.
    ];
  }

  return $extra;
}

function MYMODULE_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {

  $current_uid = \Drupal::currentUser()->id();
  $current_user = \Drupal\user\Entity\User::load($current_uid);

  $current_nid = $entity->id();

  // Now you have the current user entity and the current node ID.

  // Now you need to get the value of the custom field which probably should
  // be an associated array of node IDs and timestamps.
  $access_data = $current_user->get('field_node_access_data')->getValue();

  // Now you need to look up if the current node ID already is in it and if so, 
  // retrieve its timestamp value.
  $timestamp = ... // your job.

  if ($display->getComponent('last_accessed')) {

    $build['last_accessed'] = [
      '#markup' => $timestamp,
      // I hope this works that way. That the cache gets disabled properly for
      // just this pseudo field.
      '#cache' => ['max-age' => 0],
    ];
  }

  // And now finally after the timestamp has been rendered it needs to get updated.
  $current_user->set('field_node_access_data')->... // Your job. Probably something with time().
  $current_user->save();
}

Again, this is absolutely untested. And if the cache busting for the markup in the pseudo field display doesn't work I'd probably try the same approach with a custom block. Or the lazy builder approach as highlighted in the other answer. But a block can easily be excluded from cache and you can use a theme, contexts and what else a little bit easier maybe than what you have in a preprocess hook.

  • I have been testing with a HOOK_install(), HOOK_shema() type approach. Writing directly to the DB. I really like the approach above. A field that could be placed on a node would be very flexible... I will take some time to test this out – Prestosaurus Dec 10 '18 at 16:18
  • @Prestosaurus – You may have seen I did a pretty similar thing now in drupal.stackexchange.com/a/273986/15055. Just that there the pseudo field is placed on the user. – leymannx Dec 13 '18 at 23:25
  • @Prestosaurus – For your problem I would recommend to add a real field to the user or user table via hook_install. This field will never be exposed anywhere. It's just there to store the data (custom table actually would be fine as well). And then you place a pseudo field on the node and ajaxify its content similar to what I've done in the other answer. – leymannx Dec 13 '18 at 23:28
0

Working with database tables I have come up with this option so far:

MYMODULE.module:

/**
 * This creates a database to store the last access timestamp per node per user.
 */

/**
 * implements HOOK_preprocess_node().
 */
function node_access_timestamp_by_user_preprocess_node(&$variables) {

  // Set current user ID.
  $uid = \Drupal::currentUser()->id();

  // Check if user is authenticated.
  if ($uid != 0) {

    // Get current NID, Timestamp, and DB.
    $currentNID = \Drupal::routeMatch()->getRawParameter('node');
    $currentTimestamp = intval(time());
    $database = \Drupal::database();

    // Query our DB.
    $queryDBEntries = $database
      ->select('node_access_timestamp_by_user', 'timestamp')
      ->fields('timestamp')
      ->condition('uid', $uid, "=")
      ->condition('nid', $currentNID, "=");

    // Fetch DB data.
    $result = $queryDBEntries->execute()->fetchAll();

    // Check if table row exists and overwrite.
    if ($result[0]->uid == $uid && $result[0]->nid == $currentNID) {
      $database
        ->update('node_access_timestamp_by_user')
        ->fields([
          'timestamp' => $currentTimestamp,
        ])
        ->condition('uid', $uid, '=')
        ->condition('nid', $currentNID, '=')
        ->execute();
    }

    // Check if table row does not exist and create.
    if ($result[0]->uid != $uid && $result[0]->nid != $currentNID) {
      $database
        ->insert('node_access_timestamp_by_user')
        ->fields([
          'uid' => $uid,
          'nid' => $currentNID,
          'timestamp' => $currentTimestamp,
        ])
        ->execute();
    }

    // Set our $variables to print in twig.
    if ($result[0]->nid == $currentNID) {
      $variables['nodeAccessTimestampByUser_uid'] = $result[0]->uid;
      $variables['nodeAccessTimestampByUser_nid'] = $result[0]->nid;
      $variables['nodeAccessTimestampByUser_timestamp'] = $result[0]->timestamp;
    }

  }

}

/**
* Implements HOOK_install().
*
* Creates some default entries on this module custom table.
*
* @see hook_install()
*
* @ingroup node_access_timestamp_by_user
*/
function node_access_timestamp_by_user_install() {

  $currentTimestamp = intval(time());
  $database = \Drupal::database();

  // Add a default entry.
  $fields = [
    'uid' => '1',
    'nid' => '1',
    'timestamp' => $currentTimestamp,
  ];
  $database
    ->insert('node_access_timestamp_by_user')
    ->fields($fields)
    ->execute();
}

/**
* Implements HOOK_schema().
*
* Defines the database tables used by this module.
*
* @see hook_schema()
*
* @ingroup node_access_timestamp_by_user
*/
function node_access_timestamp_by_user_schema() {

  $currentTimestamp = intval(time());

  $schema['node_access_timestamp_by_user'] = [
    'description' => 'Stores node access timestamp by user per node.',
    'fields' => [
      'uid' => [
        'type' => 'int',
        'not null' => TRUE,
        'default' => 1,
        'description' => "User's {users}.uid",
    ],
      'nid' => [
        'type' => 'int',
        'not null' => TRUE,
        'default' => 1,
        'description' => "Node ID",
    ],
      'timestamp' => [
        'type' => 'int',
        'not null' => TRUE,
        'default' => $currentTimestamp,
        'description' => "Timestamp",
    ],
    ],
    'primary key' => ['nid'],
    'indexes' => [
      'uid' => ['uid'],
      'nid' => ['nid'],
      'timestamp' => ['timestamp'],
    ],
  ];

  return $schema;
}

node.html.twig:

<table>
  <tbody>
      <tr>
          <td>UID</td>
          <td>{{ nodeAccessTimestampByUser_uid }}</td>
      </tr>
      <tr>
        <td>NID</td>
        <td>{{ nodeAccessTimestampByUser_nid }}</td>
    </tr>
    <tr>
        <td>Timestamp</td>
        <td>{{ nodeAccessTimestampByUser_timestamp }}</td>
    </tr>
  </tbody>
</table>

This is only moderately tested, and I would not be someone who could talk about performance impacts.

I am overwriting tables fields instead of adding to... I feel that if you stored every instance like a history, the database would get gigantic.

  • Hmmm, I think this is going to be cached too hard, as @4k4 explicitly highlighted in their answer. Did you test this already with all caches enabled? – leymannx Dec 12 '18 at 17:50
  • I have been testing that today, cache does seem an obstacle. Unfortunate, I liked the data in the DB, but even a custom view is getting cached data. You're welcome to take a look: drupal.org/project/node_access_timestamp_by_user. I am going to try your answer next but will need some time, I have little experience with a couple of the aspects involved. – Prestosaurus Dec 12 '18 at 18:13
  • I'll have a look. It's very important information that this is for a contrib module. Certain things may need to be considered more thoroughly then. – leymannx Dec 12 '18 at 18:44
  • I am happy to see this through, would like to see a solution. – Prestosaurus Dec 12 '18 at 19:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.