Updating the Search API to D8 – Part 1: Creating an entity type

Sat, 2013-07-27 23:56 -- drunken monkey

TL; DR:

The 8.x version of Drupal has entered Alpha stage and people everywhere are telling you to port your modules now. However, proper documentation is scarce and existing tutorials or examples only explain the simplest steps. Bad for modules like the Search API, which define new entity types and plugins and aren't as easy to port.
Still, I decided to venture into the unknown and start porting now. It was about as bad as I'd feared and I'm still far, far from finished, but I nevertheless wanted to share the first advanced pieces of updating wisdom I've found. Hopefully it will help others get started more smoothly than I did.

Where to start

So, you've decided to port your complex module (which, e.g., lets you take your pant entities off) to D8. Great! But where to start?
Of course, there's the much-praise list of change records. A quick glance will tell you that there's currently 464 entries in there – so better close that tab again and move on. (The search is also very bad, as it finds everything and sorts it by date instead of relevance. So even if you know something has changed (e.g., the entity system) and just need information on that, it's rather hard to find it. It will also be mostly spread across multiple records.)
The documentation is surprisingly detailed and up-to-date in some areas (e.g., Plugins) and as out-of-date and monosyllabic as you'd fear in others (e.g., entities), so it's really a hit-or-miss there. If you're looking for something, try to find it in change records or the documentation first, but prepare for the possibility that you'll have to resort to the surest, but also most exhausting option: digging through core code and seeing how they do it.

Caveat

Before I start, a word of caution which probably isn't necessary: the whole D8 code is still very much in flux, so everything is volatile and very much subject to change. I myself expect to have to revisit much of what I talk about here to adapt it to upcoming core changes (not that I'm even trying to keep track of them beforehand). Especially since the entity system seems to be among the particularly volatile, as some things simply don't work right now, others aren't very elegant and there seem to be three versions of entities (a backwards-compatibility (BC) layer, "normal" entities and something by the name of EntityNG). I'm curious what they'll come up with there, in the end.

Creating an entity type in D8

So, let's get started! How to create a new entity type …

Different kinds of entities

First off, you'll have to decide what kind of entity you want: content entities, like nodes or users, are typically stored in the database, same as D7 entities. They should be used for content which usually isn't deployed from a staging server but can be created in any amount on the production site. Configuration entities, on the other hand, integrate with the new configuration system in D8 and can thus be easily deployed from a staging environment to a production site. Use these, therefore, for entities which are basically site configuration – blocks or content types are good examples here.

Since both entity types I use in the Search API (search indexes and search servers) are doubtlessly configuration (and where already exportable in D7), my example will use configuration entities. Most steps will be the same for both kinds, I think (haven't tried creating a content entity type yet, though), and I'll point out any places where they differ.

Reconnaissance

As mentioned before, the documentation for the D8 Entity API isn't very helpful yet, and the only documentation page talking about creating an entity type is just a stub/placeholder right now. So, to get information I just looked at examples in core modules, especially at the Block entity. (Quick side note: you should already be familiar with PSR-0 in D8. Otherwise, better read up on that now – although there's already talk about changing it.)
So, looking at the Block (or, more precisely, \Drupal\block\Plugin\Core\Entity\Block) class you'll see right away that entity types, like plugins, now use annotations for their definitions – no more hook_entity_info() anymore*, you just specify all information about the entity type right along the class definition.

Which, of course, is another innovation: instead of using stdClass for entities by default (Can I get some barfing sounds, please? … Thank you!), all entities will now be objects implementing EntityInterface, usually with a type-specific subclass of Entity.

* That is, in fact, not true. The truth is that the discovery system for plugins, and therefore for entity types, has been made much (much) more flexible. Annotations are just the main (and recommended) way of plugin specification, but you can use hooks just as well. hook_entity_info_alter() is still there, as well.

Creating the entity class with annotation

So, to finally get started with some code, let's just copy the class and annotation structure from Block and adapt it to our use case. As said, the Search API uses two different entity types. I wanted to start porting with the search indexes, so I copied the Block class definition, pasted it into the newly-created lib/Drupal/search_api/Plugin/Core/Entity/Index.php file in the search_api module directory and adapted it to my needs. Before I discuss the code itself, I just want to point out two things:

  • The annotation-based plugin discovery works by checking a certain sub-namespace of all modules. Therefore, your entity type will only be found if the entity class is placed into the \Drupal\[your module]\Plugin\Core\Entity namespace.
  • Due to the use of namespaces, no additional module-specific prefix is necessary to avoid clashes: therefore, I called the class Index, not SearchApiIndex (like in D7).

Now, without further ado, the class skeleton, which would already be functional (except that we're referencing five classes and an interface we have yet to create):

<?php
/**
 * @file
 * Contains Drupal\search_api\Plugin\Core\Entity\Index.
 */

namespace Drupal\search_api\Plugin\Core\Entity;

use

Drupal\Core\Entity\Annotation\EntityType;
use
Drupal\Core\Annotation\Translation;
use
Drupal\Core\Config\Entity\ConfigEntityBase;
use
Drupal\search_api\IndexInterface;

/**
 * Defines a search index configuration entity class.
 *
 * @EntityType(
 *   id = "search_api_index",
 *   label = @Translation("Search index"),
 *   module = "search_api",
 *   controllers = {
 *     "storage" = "Drupal\search_api\IndexStorageController",
 *     "access" = "Drupal\search_api\IndexAccessController",
 *     "render" = "Drupal\search_api\IndexRenderController",
 *     "form" = {
 *       "default" = "Drupal\search_api\IndexFormController",
 *       "delete" = "Drupal\search_api\Form\IndexDeleteForm"
 *     }
 *   },
 *   config_prefix = "search_api.index",
 *   entity_keys = {
 *     "id" = "machine_name",
 *     "label" = "name",
 *     "uuid" = "uuid",
 *     "status" = "enabled"
 *   },
 *   links = {
 *     "canonical" = "/admin/config/search/search_api/index/{search_api_index}",
 *     "edit-form" = "/admin/config/search/search_api/index/{search_api_index}/edit",
 *   }
 * )
 */
class Index extends ConfigEntityBase implements IndexInterface {
}
?>

Up to the @EntityType, everything should be clear, I hope. Just don't forget the use statements for the two annotations, or you'll get errors your IDE won't understand. See the Plugin API documentation for a general description of how annotations work and should be used. The EntityType API documentation seems to be the best place to look up the meaning of the individual keys. To summarize the keys used here:

id
The unique ID for the entity type. Since there is no namespace involved here, it should be properly prefixed with the module name.
label
As you might have guessed, this is the human-readable name of the entity type. The @Translation(…) syntax ensures that the name is properly translatable for other languages. (If you have been reading Gábor Hojtsy's blog post series it's probably no surprise to you that language support has improved much in D8. There even seems to be a mandatory langcode property on all entities.)
module
The module which defines this entity type, probably to react in the right way when the module is disabled/uninstalled.
controllers
This is a map of several classes which serve as controllers for certain aspects of the entity type. While in D7 there was (at least in core) only a single controller, which was only used for loading entities, this has now been vastly expanded to use controllers for every aspect of entity management. The storage controller, for example, takes care of all CRUD operations for the entity type. For now, I just copied the files used by the block module, renamed them and emptied their class bodies. I'll just see what use I can make of them later on. Using the respective base classes here, instead of your custom subclasses, should mostly also work, though.
config_prefix
This is specific for configuration entities and defines the prefix under which configuration entries for entities of this type will be created. For example, the setting here means that a search index with the machine name "test" will be saved to the file search_api.index.test.yml (with the default configuration manager). This prefix is also important for defining the configuration schema (discussed later).
entity_keys
This is another known key from D7, and just lists fields on this entity type in which certain special values are stored.
id is, of course, the entity's ID. Just note that, for configuration entities, it now should usually be a string, not an integer (unless you want to make them fielable, in which case an integer-valued ID seems to be still required).
uuid is new, and recommended for all entities, as it will simplify the deployment process. The storage controller (at least all of the default implementations in core – you can of course write your own implementation not doing this) will automatically take care of filling this field for new entities, unless a value is given.
status is new, too, and specific to configuration entities. It's apparently designed to provide a standard way for telling active from inactive configuration, and enabling/disabling it.
Not shown here, there are also the old revision and bundle keys.
Also note that, as explained in this bug report, the entity class will currently not automatically use these values for implementing the id(), uuid() and status() methods, even though you'd probably expect that. Renaming the fields from their standard names, like I did, will therefore not work without overriding these methods.
links
This key isn't contained in the block entity specification, but apparently highly recommended, according to the documentation. I don't know exactly what it's there for, but included it nevertheless based on the old Austrian principle of “Nutzt's nix, schod't's nix.” (Roughly: Even if it's useless, it's harmless.)

The class itself inherits from ConfigEntityBase, since we want a configuration entity. For content entities, just inherit from Entity directly. It is also strongly recommended to create your own sub-interface of EntityInterface, like I did here, which should contain all public methods of your entity class. And even if your entity class doesn't add any custom methods, having this interface is still very helpful for proper type-hinting (and documentation) in function signatures. (As you hopefully know, you should never use a class for type-hinting, always interfaces.)

By the way, the convention seems to be to put all controllers and the interface directly into the top namespace of the module (\Drupal\[your module]), so that's what I did. Only the deletion form seems to go into the Form sub-namespace, for whatever reason.

Methods in your entity class

For making use of your own entity class, e.g., to customize how certain operations are handled, there are numerous methods provided which can be overridden. For a list, just look at the documentation for ConfigEntityInterface. Especially useful, at least in my case, is that there are pre*() and post*() hook methods for reacting to all CRUD operations. Since I had previously some custom code for handling exactly that, I was glad to update it to this new, much cleaner style.

Other than those, you can of course also add additional methods to the class, specific to your use case. I don't know whether there is already some standard on how much logic should be contained in entity classes, but for now I'm just bundling all methods which deal with search indexes in their entity class (like I already used to in D7). You can of course handle that differently if you are, e.g., more comfortable with procedural style and want to keep as much code as possible in functions. (As said, I'm not aware of any standards or recommendations, but there might of course be some somewhere which say different.)

Coda

As said, I'm just learning about D8 myself, rummaging through the code and trying to make (more or less educated) guesses about what it all means. If you know more than me about entities in D8 and I've made some errors, please correct me in the comments! Also, if you are interested in the Search API, please come help in any way (even some short answers to pressing questions would be great) in the D8 port meta issue. Thanks!

The Search API D8 update, and therefore this tutorial, is far from finished. Not even the index entity is really complete after this tutorial, and there's another entity type (servers) which uses pluggable backends, as well as two other types of plugins to port. So expect more posts as I learn more and work toward a full D8 version of the Search API.

Image credit: Doxygen.

Comments

Submitted by Bast (not verified) on

Great article. And very interesting feedback on porting a difficult module. Happy to see look at the following articles.

Add new comment

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>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.