Saving temporarily values of a form with Private Tempstore in Drupal 8

In this post we’ll see how to save temporarily values from a form and how to retrieve or process them later in a controller. To do that, we’ll use the Form API and the private tempstore (the temporary store storage system of Drupal 8).

The use case is the following: we need to build a simple RSS reader (a form) where the user could introduce the URL of an RSS file and the number of items to retrieve from that file.  Next, in a new page (a controller), the application should display the list of items with a link to each syndicated page .

The easiest way to achieve it would be to retrieve the values in our buildForm() method, process them and display the result thought a specific field of the form. But that’s not our use case.

To process the form’s values and to display the results in another page, we’ll first need to store the form's values and retrieve them later in a controller. But how and where to store and retrieve those values?

Long story short: Store and retrieve data with the Private Tempstore System of Drupal 8

Drupal 8 has a powerful key/value system to temporarily store user-specific data and to keep them available between multiple requests even if the user is not logged in. This is the Private Tempstore system.

// 1. Get the private tempstore factory, inject this in your form, controller or service.
$tempstore = \Drupal::service('tempstore.private');
// Get the store collection. 
$store = $tempstore->get('my_module_collection');
// Set the key/value pair.
$store->set('key_name', $value);

// 2. Get the value somewhere else in the app.
$tempstore = \Drupal::service('tempstore.private');
// Get the store collection. 
$store = $tempstore->get('my_module_collection');
// Get the key/value pair.
$value = $store->get('key_name');

// Delete the entry. Not mandatory since the data will be removed after a week.
$store->delete('key_name');

Fairly simple, isn't it? As you can see, the Private Tempstore is a simple key/value pair storage organized with collections (we usually give the name of our module to the collection) to maintain data available temporarily across multiple page requests for a specific user.

Now, that you have the recipe, we can go back to our use case where we are now going to figure out, how and where we could store and retrieve the values of our form.

Long story: Illustrate the Private Tempstore system in Drupal 8

For the long story part, we are going to create a module with a form and a controller to first get the data form the user, store the form’s data in a private tempstore and redirect the form to a controller where we’ll retrieve the data from the tempstore and process them.

This is the form.

drupal8-privatetempstore-form

And this is the controller.

drupal8-privatetempstore-controller

You can find and download the code of this companion module here.

Types of data storage in Drupal 8

In Drupal 8 we have various APIs for data storage:

Database API  - To interact directly with the database.
State API - A key/value storage to store data related to the state or individual environment (dev., staging, production) of a Drupal installation, like external API keys or the last time cron was run.
UserData API - A storage to store data related to an individual environment but specific to a given user, like a flag or some user preferences.
TempStore API - A key/value storage to keep temporary data (private o shared) across several page requests.
Entity API - Used to store content (node, user, comment …) or configuration (views, roles, …) data.
TypedData API - A low level API to create and describe data in a consistent way.    

Our use case refers to user-specific data we need for a short period of time, data that is also not specific to one environment, so the best fit is certainly the TempStore API but in its private flavour because the data and the results are different for each user.

The only difference between private and shared tempstore is that the private tempstore entries strictly belong to a single user whereas with shared tempstore, the entries can be shared between several users.

Storing the form's values with the Private TempStore storage

Our goal is to build a form where user can introduce an RSS file URL and a number of items to retrieve from that file, next we need to store temporarily those values to retrieve them later in a controller.

Let’s now take a closer look at how we could store (or save) our form’s values. As we get the data (url and items to retrieve) from a form, it’s clear that we are going to store the data in the submitForm() method since this method is called when the form is validated and submitted.

Here is the code of our form.

<?php
namespace Drupal\ex_form_values\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
// DI.
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
/**
 * Class WithControllerForm.
 *
 * Get the url of a RSS file and the number of items to retrieve
 * from this file.
 * Store those two fields (url and items) to a PrivateTempStore object
 * to use them in a controller for processing and displaying
 * the information of the RSS file.
 */
class WithStoreForm extends FormBase {
  /**
   * Drupal\Core\Messenger\MessengerInterface definition.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;
  /**
   * Drupal\Core\Logger\LoggerChannelFactoryInterface definition.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerFactory;
  /**
   * Drupal\Core\TempStore\PrivateTempStoreFactory definition.
   *
   * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
   */
  private $tempStoreFactory;
  /**
   * Constructs a new WithControllerForm object.
   */
  public function __construct(
    MessengerInterface $messenger,
    LoggerChannelFactoryInterface $logger_factory,
    PrivateTempStoreFactory $tempStoreFactory
  ) {
    $this->messenger = $messenger;
    $this->loggerFactory = $logger_factory;
    $this->tempStoreFactory = $tempStoreFactory;
  }
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('messenger'),
      $container->get('logger.factory'),
      $container->get('tempstore.private')
    );
  }
  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'with_state_form';
  }
  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['url'] = [
      '#type' => 'url',
      '#title' => $this->t('Url'),
      '#description' => $this->t('Enter the url of the RSS file'),
      '#default_value' => 'https://www.drupal.org/planet/rss.xml',
      '#weight' => '0',
    ];
    $form['items'] = [
      '#type' => 'select',
      '#title' => $this->t('# of items'),
      '#description' => $this->t('Enter the number of items to retrieve'),
      '#options' => [
        '5' => $this->t('5'),
        '10' => $this->t('10'),
        '15' => $this->t('15'),
      ],
      '#default_value' => 5,
      '#weight' => '0',
    ];
    $form['actions'] = [
      '#type' => 'actions',
      '#weight' => '0',
    ];
    // Add a submit button that handles the submission of the form.
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];
    return $form;
  }
  /**
   * Submit the form and redirect to a controller.
   *
   * 1. Save the values of the form into the $params array
   * 2. Create a PrivateTempStore object
   * 3. Store the $params array in the PrivateTempStore object
   * 4. Redirect to the controller for processing.
   *
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // 1. Set the $params array with the values of the form
    // to save those values in the store.
    $params['url'] = $form_state->getValue('url');
    $params['items'] = $form_state->getValue('items');
    // 2. Create a PrivateTempStore object with the collection 'ex_form_values'.
    $tempstore = $this->tempStoreFactory->get('ex_form_values');
    // 3. Store the $params array with the key 'params'.
    try {
      $tempstore->set('params', $params);
      // 4. Redirect to the simple controller.
      $form_state->setRedirect('ex_form_values.simple_controller_show_item');
    }
    catch (\Exception $error) {
      // Store this error in the log.
      $this->loggerFactory->get('ex_form_values')->alert(t('@err', ['@err' => $error]));
      // Show the user a message.
      $this->messenger->addWarning(t('Unable to proceed, please try again.'));
    }
  }
}

In the submitForm() method we can see two lines where we deal with the private tempstore in step 2 and 3.

$tempstore = $this->tempStoreFactory->get('ex_form_values');
//...
$tempstore->set('params', $params);

In the first line we call the PrivateTempStoreFactory to instantiate a new PrivateTempStore object trough its get() method. As you can see, we get the factory by DI. We also define the name of our collection (ex_form_values) with the same name as our module (by convention) and pass it to the get() method. So, at this time we created a new PrivateTempStore object for a collection named "ex_form_values".

In the second line we use the set() method of the PrivateTempStore object which will store the key/value pair we need to save, in our case, the key is 'params' and the value is the $params array that contains our form’s values.

The PrivateTempStoreFactory uses a storage based on the KeyValueStoreExpirableInterface, this storage is a key/value storage with an expiration date that allows automatic removal of old entities and this storage uses the default DatabaseStorageExpirable storage to save the entries in the key_value_expire table.

Here is the structure of this table:

key_value_expire-strucuture

The PrivateTempStore object has the following protected properties, all passed to the form by DI:
$storage - The key/value storage object used for this data.
$lockBackend - The lock object used for this data.
$currentUser - The current user who owned the data. If he's anonymous, the session ID will be used.
$requestStack - Service to start a session for anonymous user
$expire - The time to live for the entry in seconds. By default, entries are stored for one week (604800 seconds) before expiring as defined in the KeyValueStoreExpirableInterface.

We can’t set any of those values, they are all set by the KeyValueStoreExpirableInterface when we instantiate the new PrivateTempStore object.

The PrivateTempStore object method we are interested at this time is the set() method which will store the key/value we need to save, in our case, the key is 'params' and the value is the $params array that contains our form’s values. You can find the code of this method here and below, the beginning of this method:

public function set($key, $value) {

  // Ensure that an anonymous user has a session created for them, as
  // otherwise subsequent page loads will not be able to retrieve their
  // tempstore data.
  if ($this->currentUser
    ->isAnonymous()) {

    // @todo when https://www.drupal.org/node/2865991 is resolved, use force
    //   start session API rather than setting an arbitrary value directly.
    $this
      ->startSession();
    $this->requestStack
      ->getCurrentRequest()
      ->getSession()
      ->set('core.tempstore.private', TRUE);
  }
  // ....
}

As you can see at the beginning of this method, we ensure that an anonymous user has a session created for him, thanks to this, we can also retrieve a session ID that we will use to identify him. If it’s an authenticated user, we will use his UID. You can confirm that in the getOwner() method of the PrivateTempStore object.

When we submit the form, thanks to these two lines, we’ll save the form's values in the database. If you look at the key_value_expire table, you can see that there are other records, most of them with update information of our app., but also new records with the collection value of tempstore.private.ex_form_values. Here is a query on this collection when an anonymous and the admin user use the form.

key_value_expire-drupal8

We didn’t output the column 'value' because it’s too large. But you can see now how our form's values are stored in the database and particularly for anonymous users with their session ID.

At this time, we know how to store temporarily the values of a form in the privatetempstore of Drupal. This was not so hard, wasn’t it? Yes, Drupal is a great framework and it saves us a lot of time when developing a web application.

Now let’s see how to retrieve and process the values in a controller.

Retrieve our form's values from the privatetempstore

To retrieve the data, process them and display the results, we’ll need to redirect our form to a controller. Here is the code of this controller.

<?php
namespace Drupal\ex_form_values\Controller;
use Drupal\Core\Controller\ControllerBase;
// DI.
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\ex_form_values\MyServices;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use GuzzleHttp\ClientInterface;
// Other.
use Drupal\Core\Url;
/**
 * Target controller of the WithStoreForm.php .
 */
class SimpleController extends ControllerBase {
  /**
   * Tempstore service.
   *
   * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
   */
  protected $tempStoreFactory;
  /**
   * GuzzleHttp\ClientInterface definition.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected $clientRequest;
  /**
   * Messenger service.
   *
   * @var \\Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;
  /**
   * Custom service.
   *
   * @var \Drupal\ex_form_values\MyServices
   */
  private $myServices;
  /**
   * Inject services.
   */
  public function __construct(PrivateTempStoreFactory $tempStoreFactory,
                              ClientInterface $clientRequest,
                              MessengerInterface $messenger,
                              MyServices $myServices) {
    $this->tempStoreFactory = $tempStoreFactory;
    $this->clientRequest = $clientRequest;
    $this->messenger = $messenger;
    $this->myServices = $myServices;
  }
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('tempstore.private'),
      $container->get('http_client'),
      $container->get('messenger'),
      $container->get('ex_form_values.myservices')
    );
  }
  /**
   * Target method of the the WithStoreForm.php.
   *
   * 1. Get the parameters from the tempstore for this user
   * 2. Delete the PrivateTempStore data from the database (not mandatory)
   * 3. Display a simple message with the data retrieved from the tempstore
   * 4. Get the items from the rss file in a renderable array
   * 5. Create a link back to the form
   * 6. Render the array.
   *
   * @return array
   *   An render array.
   */
  public function showRssItems() {
    // 1. Get the parameters from the tempstore.
    $tempstore = $this->tempStoreFactory->get('ex_form_values');
    $params = $tempstore->get('params');
    $url = $params['url'];
    $items = $params['items'];
    // 2. We can now delete the data in the temp storage
    // Its not mandatory since the record in the key_value_expire table
    // will expire normally in a week.
    // We comment this task for the moment, so we can see the values
    // stored in the key_value_expire table.
    /*
    try {
      $tempstore->delete('params');
    }
    catch (\Exception $error) {
      $this->loggerFactory->get('ex_form_values')->alert(t('@err',['@err' => $error]));
    }
    */
    // 3. Display a simple message with the data retrieved from the tempstore
    // Set a cache tag, so when an anonymous user enter in the form
    // we can invalidate this cache.
    $build[]['message'] = [
      '#type' => 'markup',
      '#markup' => t("Url: @url - Items: @items", ['@url' => $url, '@items' => $items]),
    ];
    // 4. Get the items from the rss file in a renderable array.
    if ($articles = $this->myServices->getItemFromRss($url, $items)) {
      // Create a render array with the results.
      $build[]['data_table'] = $this->myServices->buildTheRender($articles);
    }
    // 5. Create a link back to the form.
    $build[]['back'] = [
      '#type' => 'link',
      '#title' => 'Back to the form',
      '#url' => URL::fromRoute('ex_form_values.with_store_form'),
    ];
    // Prevent the rendered array from being cached.
    $build['#cache']['max-age'] = 0;
    // 6. Render the array.
    return $build;
  }
}

First we inject all the services we need and also a custom service MyServices.php used to retrieve the RSS file and the required number of items.

This controller has a simple method called showRssItems(). This method performs six tasks:

1. Get the data we need from the PrivateTempstore
2. Delete the PrivateTempStore data from the database (not mandatory)
3. Display a simple message with the data retrieved from the tempstore
4. Get the items from the rss file in a render array
5. Create a link back to the form
6. Render the array.

The tasks that interest us are the first and the second one, where we deal with the tempstore.

$tempstore = $this->tempStoreFactory->get('ex_form_values');

This line is now straightforward for us, since we know that the get() method of the PrivateTempStoreFactory instantiate a new PrivateTempStore object for the collection 'ex_form_values'. Nothing new here.

$params = $tempstore->get('params');

In the line above, we just retrieve the value of the key 'params' from the storage collection into the variable $params. Remember that we are using the privatetempstorage, as we retrieve the value, we’ll also check if it's the same user with the getOwner() method of the PrivateTempStore object.

The second task is deleting the data from the tempstore, this is not mandatory since we are working with a temporary store where the data will be deleted automatically after a week by default. But if we expect a heavy use of our form, it could be a good idea since we won’t normally need the data anymore.

try {
  $tempstore->delete('params');
}
catch (\Exception $error) {
  $this->loggerFactory->get('ex_form_values')->alert(t('@err',['@err' => $error]));
}

As the delete() method of our privatetempstore object can trow an error, we’ll catch it in a 'try catch' block. For learning purpose I’ll suggest to comment those lines to see how the data are stored in the key_value_expire table.

The rest of the controller is pretty straightforward, we are going to pass the data to small methods of our custom service to retrieve the RSS file and the number of items we need, build a render array with the results as you can see in task number four. Next, we’ll create a simple link back to our form and render the entire array. Nothing strange here.

Recap

We wanted, trough a form, to get a RSS file URL, the number of items to retrieve from, and display this information in a controller. To do so, we needed to store the information of the form, retrieve it and process it later in a controller.

We decided to use the Private TempSore system of Drupal because:
- The information doesn’t belong to a specific environment or a particular state of our application (see State API for that case)
- The information is not specific to a user and doesn’t need to be stored with its profile (see UserData API for that case). On the other hand, anonymous users should also have access and use the form
- We need the information temporarily, for a short period of time
- It has to be private because the information belongs to each user (anonymous or authenticated)

To store the data, we used the submit() method of our form and the following lines:

$tempstore = $this->tempStoreFactory->get('ex_form_values') - to instantiate a new PrivateTempStore object with a collection name 'ex_form_values' that refers to our module in this case.

$tempstore->set('params', $params) - to save the value of the variable $params in the database with the key 'params'. Remember that as we use a private storage, Drupal will retrieve the user ID or its session ID if anonymous, and concatenate it with the key value in the 'name' row of the 'key_value_expire' table.

To retrieve the information in our controller’s showRssItems() method, we used the following code:

$tempstore = $this->tempStoreFactory->get('ex_form_values') - to instantiate a new PrivateTempStore object with a collection name 'ex_form_values' that refers to our module in this case.

$params = $tempstore->get('params') - to retrieve the value stored in the keyed 'params' record of this collection and for this particular user.

Yes! Once again, through this simple example, we can see how powerful Drupal is, and how its framework is easy to use.

And you? In which situation do you think we could use the privatetempstore? Please, share your ideas with the community.

More info:

class PrivateTempStoreFactory (api.drupal.org)

class PrivateTempStore (api.drupal.org)

PrivateTempStore::get (api.drupal.org)

PrivateTempStore::set (api.drupal.org)

Configuration Storage in Drupal 8 (drupal.org)

State API overview (drupal.org)

D8FTW: Storing Data in Drupal 8 (LARRY GARFIELD palantir.net)