Drupal 8 API: state

Pre-requisites

State is a bit like simplified configuration, only different

Since we discussed types of information in Drupal, it's been easy to see state as being a kind of more volatile yet simplified configuration:

More volatile
State is how Drupal keeps track of its internal, well, state: Drupal configuring itself with information about how recently it last run its own scheduled tasks, or where to find its own zipped-up Javascript files. As such it can change arbitrarily (without any particular user input) and frequently (any request might change at least some piece of state.)
Simplified
State is just a simple storage and retrieval mechanism of chunks of configuration based on keys. As such, although the chunks might look like configuration: they have no particular life cycle or behavioural complexity; they aren't meant to be exported through an equivalent of configuration management; and they are stored in a simple way.

As a consequence of these differences, state is also fundamentally differently from configuration in its PHP. Whereas configuration uses config entities, which provides configuration with a lifecycle, including long-term persistence and caching, state uses a key/value store, and must therefore implement its own caching strategy.

How state, caching and the key/value store interact

State implements its own simplistic caching using protected object variables, which means it only lasts for the duration of a PHP request. This is understandable if we consider: on the one hand, a request to a database-level cache will be no quicker than a request to a database-level key/value store; on the other hand, a more complex caching strategy will still be about as volatile as uncached state is anyway.

Drupal has a more generalized key/value store, which is partitioned into collections, and code should ideally only ever modify one collection for its "personal" use. Of these collections, state API's is called 'state'. This means state only sees its own key/value pairs, not those of potentially other parts of Drupal core or contributed code.

Because state has to negotiate between its own cache and the contents of the key/value store, then instantiating state followed by retrieving a value looks something like the following flowchart:

Flowchart of how state uses its internal cache and the key/value store to retrieve values

Both state and the key/value store are provided by Drupal as services, capable of dependency injection as discussed previously. Although we use the raw \Drupal::service() call in the examples at the very end, this is because we're running them from the command line: all the classes we save will use dependency injection.

Similarly, setting and deleting state proceeds via setting and deleting items in the key/value store. In addition, because state doesn't use entities, then state values have no "identity", which means that updating and creating both use the same "setting" operation.

Investigating state using a custom class

The following PHP class provides examples of how state works. You should save it to src/StateExample.php in your d8api module:

<?php
 
namespace Drupal\d8api;
 
use Drupal\Core\State\StateInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
 
/**
 * Run some state API tests.
 */
class StateExample {
 
  /**
   * @var StateInterface
   */
  protected $state;
 
  /**
   * @var KeyValueStoreInterface
   */
  protected $keyValueStore;
 
  /**
   * Implements __construct().
   *
   * @param StateInterface $state
   *   State service, injected into the new object.
   * @param KeyValueFactoryInterface $key_value_factory
   *   Key/value factory service, injected into the new object.
   */
  public function __construct(
    StateInterface $state,
    KeyValueFactoryInterface $key_value_factory
  ) {
    $this->state = $state;
    // Replicate the key/value store that state itself uses.
    $this->keyValueStore = $key_value_factory->get('state');
  }
 
  /**
   * Write a new state value; read it back.
   *
   * State names are stored in the name column of table key_value,
   * where the collection is "state".
   */
  public function writeNewStateAndRead() {
    print "Previous value for custom state: "
      . var_export($this->state->get('d8api.example'), TRUE) . ".\n";
    print "Setting custom state to: ['time' => " . REQUEST_TIME . "].\n";
    $this->state->set('d8api.example', ['time' => REQUEST_TIME]);
  }
 
  /**
   * Write and read state; meanwhile, change via key/value.
   *
   * Changing state directly through the key/value does not update the
   * cached data in state.
   */
  public function stateVersusDirectKeyValue() {
    // Set a value through state, which is cached.
    $this->state->set('d8api.outdated', 'What state thinks it is');
    // Set a value directly into the key/value store.
    $this->keyValueStore->set('d8api.outdated', 'What the store has changed it to');
 
    // State still has the old, stale value.
    print "State still has the value: '" . $this->state->get('d8api.outdated') . "'\n";
    // Clearing caches fixes this.
    $this->state->resetCache();
    print "State now retrieves the value: '" . $this->state->get('d8api.outdated') . "'\n";
  }
 
}

This class consists of three methods:

__construct()
Provides compatibility with dependency injection, to obtain both the state service and the key/value factory service.
writeNewStateAndRead()
Trivially demonstrates state get($key) and set($key, $value) behaviour, including a nonscalar $value
stateVersusDirectKeyValue()
Shows how state uses its internal cache, by also creating a custom key/value store to interrogate the 'state' collection directly, bypassing the cache.

Note that, whereas services inject the state directly into the constructor, a factory is provided for key/value objects. This permits state to use the factory to obtain a 'state' key/value collection, and other classes to do the same with their own custom collections, while minimizing the number of services subsequently required to just one.

What you should see

As previously discussed, you should have Drush installed, and we don't cover that here.

To run the two test methods in turn, you need to a) initialize an object of your new custom class b) inject the relevant services while doing so and then c) call the method.

Merely initializing the class is straightforward:

drush php-eval 'new \Drupal\d8api\StateExample(
  \Drupal::service("state"), \Drupal::service("keyvalue")
);'

This provides the class with the two relevant services. However, you'll need to call a method to obtain any output, as we'll show below.

Trivial get() and set() behaviour

Chain the object creation with a method call, by wrapping the new ... in brackets and following it with the method:

drush php-eval '(new \Drupal\d8api\StateExample(
  \Drupal::service("state"), \Drupal::service("keyvalue")
))->writeNewStateAndRead();'

If you run this a few times in turn you should see:

...
Previous value for custom state: array (
  'time' => 1480094066,
).
Setting custom state to: ['time' => 1480328225].
...
Previous value for custom state: array (
  'time' => 1480328225,
).
Setting custom state to: ['time' => 1480328237].
...

The state is being set with a nonscalar value (an array with a single element "time"), and each call first retrieves the state from the previous call, before updating it.

Demonstrating the cache inside state, compared to the raw key/value store

drush php-eval '(new \Drupal\d8api\StateExample(
  \Drupal::service("state"), \Drupal::service("keyvalue")
))->stateVersusDirectKeyValue();'

You should always see the output:

State still has the value: 'What state thinks it is'
State now retrieves the value: 'What the store has changed it to'

The first line is the result of modifying the data in the key/value store, but state has still cached the stale data; the second line shows state having cleared its internal cache and picking up on the changes.

If you see all of the above then congratulations! you have successfully manipulated state, and its underlying key/value store and cache.

Further reading

Comments

Thanks for the writeup on KeyValue in D8! There's nothing mentioned currently in the docs, and I was curious what the difference was vs the State API. Very helpful!

A pleasure! Glad it was of some use.

Add new comment