Updating the Search API to D8 – Part 2: Configuration and schema

Tue, 2013-07-30 20:48 -- drunken monkey

In Part 1 of this series, I explained how to create a basic configuration entity in Drupal 8. But the task wasn't completely finished: you should also always specify the schema for your configuration entities (as well as for other configuration). So in this (slightly shorter) tutorial part, I will cover the general new Configuration API as well as configuration schemas.

The new Configuration API

The Configuration API is one of the better-documented innovations in Drupal 8, so there are thankfully only few question marks here. Also, if you've been reading other posts about Drupal 8, you've probably already seen some examples.
But basically, the variables from D7 are now better structured and are grouped in named configuration objects that are specific to a module. So inside that object, you can go crazy with setting names without having to fear any clashes. Usually there is one configuration object per module, with the name "[module].settings", but a module with a larger number of settings can also split them into multiple objects to categorize them (all named "[module].[something]").
By default, configuration is stored in YAML files (though this is pluggable, unless I'm mistaken). Modules provide their default configuration settings (yes, no more passing the default in every single variable_get()!) in "[module].[something].yml" files in the module's config/ directory, and on module install these settings are copied to the site's active configuration. (If you later introduce new variables, you'll have to add them to existing sites with hook_update_N() – default configuration changes aren't picked up after the initial install.)

So, to return to the Search API example, we have the following in the config/search_api.settings.yml file:

cron_batch_count: '10'
cron_worker_runtime: '15'

We use them in code in the following form:

<?php
// In Drupal 7:
$batch_count = variable_get('search_api_batch_per_cron', 10);

// In Drupal 8:
$batch_count = \Drupal::config('search_api.settings')->get('cron_batch_count');
?>

The configuration schema

Drupal 8 contains (among many other things) a massive effort to provide more metadata to our content and configuration, so they can easier be viewed and worked with in a generic way. (See the documentation stub about the Typed Data API.) To that end, modules are also encouraged to provide a clear schema for the configuration they're using. This is documented pretty well already in the handbook.
Don't confuse this configuration schemas with database schemas! There is still hook_schema() for defining your database tables, this is something completely different. (Just mentioning it since I was confused for a short time, too.)

Basically, you just create a config/schema/[module].schema.yml file in your module directory into which you write some metadata about your other configuration files. As you can see by the file extension, the schema is again written in YAML, so no new syntax there. So for the above configuration we'd put the following into config/schema/search_api.schema.yml:

# Schema for the configuration files of the Search API module.

search_api.settings:
  type: mapping
  label: 'Search API settings'
  mapping:
    cron_batch_count:
      type: integer
      label: 'Number of batches to create per cron run'
    cron_worker_runtime:
      type: integer
      label: 'Maximum working time per cron batch'

The top-level key search_api.settings is the configuration object name (and therefore everything before .yml in the config file name) and the keys under mapping reflect all settings in this object.
(See the documentation for details on the syntax and available types.)

The main reason for doing this seems currently to make configuration translation easier and more powerful. You can also, e.g., automatically create forms for your module's configuration. And it's also very easy to imagine other contrib modules to come up with ways of using this metadata to work with configuration they don't know in a generic way.
All in all, and especially since the Search API is all about dealing with unknown data in a generic way, I really like this addition and think it makes a great feature in terms of developer experience.

Configuration entities

So, since configuration entities are stored with the configuration API, do they need a schema, too?
Glad you asked: yes, they do! In fact, I'd say it's even more important for them, since there is even more use for metadata for entities. (It might be I'm a bit biased there, though.)
The schema for entities is almost as straight-forward as for normal configuration, but before we look at that, we have to quickly go back and look at how configuration entities are stored.

If you remember, in the first part of this series we set config_prefix = "search_api.index" in the entity type settings for the index entity. What this means is that all search indexes we create will be stored in configuration objects with names in the form of search_api.index.[index id]. In theory, we could therefore even load the data for an index manually with \Drupal::config("search_api.index.$id") – but of course that would make the whole Entity API and our own entity class useless.

So now, if we want to define a schema for our entities, how do we do that? Easy, we can just use an asterisk as a wildcard for everything after the prefix:

search_api.index.*:
  type: mapping
  label: 'Search index'
  mapping:
    machine_name:
      type: string
      label: 'Machine name'
    # All other properties of search indexes.

And that easily you can provide metadata for your entities! (Though I'm still not sure about some details, e.g., regarding arrays with options for plugins, where I don't know the keys beforehand but they are important. Are these sequences or mappings? Just guessing for now and waiting for things to break or people to complain.)

Providing default entities

One other thing that becomes very easy with configuration entities is providing default entities. E.g., Views is now using this to provide its pre-defined views (like archive and glossary). Since the Search API in D7 also creates a pre-configured node index when it is installed, I used the same mechanism to provide that index in D8.

The process is very simple. You just go to the UI (if that's already implemented, that is), create a new entity and configure it the way you'd like to have it as a default. Then, just copy its YAML file from the active config directory to your module's config/ directory and you're almost done. It's just very important to remember the last step: delete the uuid: … line from the config file in your module. Otherwise, the whole purpose of having a UUID would be defeated.

You can see the complete config directory, with schema and default index, in the repository.

Upgrading existing D7 entities to configuration

The last thing we need for upgrading indexes is a hook_update_N() implementation that moves all defined indexes from the D7 database to the D8 configuration storage. Since this code isn't written yet for blocks (and a lot of other configuration entities) I had to search a bit there, but then I found the Contact module which already has this implemented. The function turned out to be pretty straight-forward after all, and it seems you can use the Configuration API just like normal. Here is a shortened version of the function:

<?php
/**
 * Update search indexes to be stored in the configuration.
 */
function search_api_update_8001() {
 
$uuid = new Uuid();
 
$result = db_query('SELECT * FROM {search_api_index}');
  foreach (
$result as $index) {
   
$config = \Drupal::config('search_api.index.' . $index->machine_name);
   
$config->set('uuid', $uuid->generate());
    foreach (
$index as $key => $value) {
     
$config->set($key, $value);
    }
   
$config->set('langcode', Language::LANGCODE_NOT_SPECIFIED);
   
$config->save();
  }
}

/**
 * Drop the {search_api_index} table.
 */
function search_api_update_8002() {
 
db_drop_table('search_api_index');
}
?>

As said, pretty straight-forward: we just load all indexes and for each of them create a config object with the same values. Only the added langcode and uuid fields require some extra lines.
And what I left out here: there is also some extra code required since I dropped the numerical id field as well as the module and status fields previously used for Entity API exportables. You will also need extra code for serialized fields (because you need to unserialize them before saving them in the config).

I don't really know why the table is dropped in a separate update hook, but the Contact module does it that way, and I'd rather be safe than sorry.

See the module's hook_update_80xx() implementations for the complete code used. The handbook also recommends creating tests for the upgrade path, which is actually pretty easy now, so I also went ahead and did that. (The test doesn't pass for me locally, yet, but I can't tell whether that's my fault or Drupal's, so I'm leaving it for now. No real point in testing as long as 90% of the module doesn't work anyways.)

Summary

That's it for today. It turned out to be quite more than I'd thought, but I think most of it isn't particularly complicated. I hope it helps a few others.
Next time we'll probably get into creating new plugin types (which at the moment seems rather complicated, but at least well-documented).

Other posts in this series

Image credit: Humanity Gnome Theme.

Add new comment

To prevent spam, submitting full URLs in comments is not allowed. Please omit the "http[s]://" portion of the URL and I will restore the complete URL on review.

Filtered HTML

  • Lines and paragraphs break automatically.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <q> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <sup> <sub> <p> <br>
  • To post pieces of code, surround them with <code>...</code> tags. For PHP code, you can use <?php ... ?>, which will also colour it based on syntax.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.