Creating Custom Config Entities in Drupal 8

Drupal 8 is skipping through the betas and it won’t be long until we’re staring at a release candidate. With that in mind, i’m now taking the time to learn some of the key concepts that you’ll need to know as a day to day site builder using Drupal 8.

Custom Config Entity Types

A custom configuration entity type referred to as a config entity for most of this article is a custom definition of an entity that allows you to provide a config class, validation schema and custom storage.

They'll have hundreds of practical usages during development custom development. To give some examples, core uses them for user roles, blocks, image styles and plenty more. Use your IDE to see what’s extending ConfigEntityBase if you’re interested.

Creating Our Config Entity Type

Schema

First up, lets define our schema. The schema allows us to say what fields our config entity should have and what type those fields should be.

Our module is called sample_config_entity and the config entity is going to be called "Ball" to represent a snooker ball. With that in mind, the first line of the schema file format should be, module.entity_type_id, eg, sample_config_entity.ball.*

sample_config_entity config schema sample_config_entity.schema.yml // Insert YAML file here

The rest of the file is pretty self explanatory, we declare that "ball" is of type config_entity and then we define a mapping of the fields on our config_entity. Note, i'm using string, label and integer in the example but you can find all the core types in: core/config/schema/core.data_types.schema.yml

Config Entity Class

The schema is written but we still need a class to represent the config entity in PHP. It starts of pretty simple and we’ll add to it as we move through this tutorial.

config_entity_sample
  src
    Entity
      Ball.php

<?php
/**
 * @file
 * Contains \Drupal\sample_config_entity\Entity\Ball.
 */
 
namespace Drupal\sample_config_entity\Entity;
 
use Drupal\Core\Config\Entity\ConfigEntityBase;
 
 
/**
 * Defines the Ball entity.
 *
 * The ball entity stores information about a snooker ball.
 *
 * @ConfigEntityType(
 *   id = "ball",
 *   label = @Translation("Ball"),
 *   module = "sample_config_entity",
 *   config_prefix = "ball",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "label",
 *     "weight" = "weight"
 *   }
 * )
 */
class Ball extends ConfigEntityBase {
 
  /**
   * The ball machine name.
   *
   * @var string
   */
  public $id;
 
  /**
   * The human readable name of this ball.
   *
   * @var string
   */
  public $label;
 
  /**
   * The position weight (not physical) of this ball.
   *
   * @var int
   */
  public $weight;
 
  /**
   * The color of this ball.
   *
   * @var string
   */
  public $color;
 
  /**
   * The value of this ball measured in points.
   *
   * @var integer
   */
  public $point_value;
}
 
?>

We have some boilerplate that you’ll soon get used to working with Drupal 8. Specifically, a namespace, some use statements for our parent class and of course the class itself. On top of the class is a rather large comment, that maybe familiar if you’re used to working with Doctrine or Symfony.

The annotation we’re using is @ConfigEntityType, that tells Drupal that our class represents a config entity. There are a number of keys that we can specify, our Ball config entity currently has id, label, module, config_prefix, handlers and entity_keys. We’ll be adding more later but for an extensive list, you can check out Drupal\Core\Config\Entity\ConfigEntityType and it’s parent, Drupal\Core\Entity\EntityType whose properties map 1 to 1 with the available annotation keys.

Finally, our class has some public properties that represent the class members on this object and the keys we outlined in our schema file previously. At this point, we have a config entity type and we can provide a few with our own module.

sample_config_entity
  config
    install
      sample_config_entity.ball.green.yml 

id: green_ball
label: Green Ball
color: green
point_value: 3

Config entity type interface

I can hear the OO enthusiasts screaming so we better have a little tidy up. I’m not going to dive into the science of using interfaces in this article, just think of them as documentation and a contract for your class. Lets see:

<?php
/**
 * @file
 * Contains \Drupal\sample_config_entity\Entity\BallInterface.
 */
 
namespace Drupal\sample_config_entity\Entity;
 
use Drupal\Core\Config\Entity\ConfigEntityInterface;
 
/**
 * Interface for balls.
 */
interface BallInterface extends ConfigEntityInterface {
  public function getColor();
  public function getPointValue();
}
?>

Now we update our Ball class to implement the interface, we implement the newly added getter methods and we change our public properties to protected for the sake of encapsulation.

<?php
/**
 * @file
 * Contains \Drupal\sample_config_entity\Entity\Ball.
 */
 
namespace Drupal\sample_config_entity\Entity;
 
use Drupal\Core\Config\Entity\ConfigEntityBase;
 
 
/**
 * Defines the Ball entity.
 *
 * The ball entity stores information about a snooker ball.
 *
 * @ConfigEntityType(
 *   id = "ball",
 *   label = @Translation("Ball"),
 *   module = "sample_config_entity",
 *   config_prefix = "ball",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "label",
 *     "weight" = "weight"
 *   }
 * )
 */
class Ball extends ConfigEntityBase implements BallInterface {
 
  /**
   * The ball machine name.
   *
   * @var string
   */
  protected $id;
 
  /**
   * The human readable name of this ball.
   *
   * @var string
   */
  protected $label;
 
  /**
   * The position weight (not physical) of this ball.
   *
   * @var int
   */
  protected $weight;
 
  /**
   * The color of this ball.
   *
   * @var string
   */
  protected $color;
 
  /**
   * The value of this ball measured in points.
   *
   * @var integer
   */
  protected $point_value;
 
  /**
   * {@inheritdoc}
   */
  public function getColor() {
    return $this->color;
  }
 
  /**
   * {@inheritdoc}
   */
  public function getPointValue() {
    return $this->point_value;
  }
}
?>

Providing Config Entities to Third Party Modules

If another module wanted to provide a config object of type "Ball" then they could easily do so as follows:

sample_black_ball
  config
    install
      sample_config_entity.ball.black_ball.yml

The provided config entity must match the schema declared by the module defining the entity. An example probably explains this best:

id: black_ball
label: Black Ball
color: black
# This will be cast and the imported point_value will be 0.
point_value: "Not an integer"

One thing worth noting is that when a third party module that provides a config object is uninstalled, the installed config objects are not removed however when the module that provided the config entity definition is deleted, all config objects are deleted.

Adding a Admin View with a DraggableListBuilder

So far, we’ve been working on code only API’s for the config entity but it would be nice to have some admin tools as well. Lets go ahead and create a list view of all our config objects.

Update our Ball config entity to reference our BallListBuilder, quite simple.

<?php
/**
 * Defines the Ball entity.
 *
 * The ball entity stores information about a snooker ball.
 *
 * @ConfigEntityType(
 *   id = "ball",
 *   label = @Translation("Ball"),
 *   module = "sample_config_entity",
 *   config_prefix = "ball",
 *   handlers = {
 *     "list_builder" = "Drupal\sample_config_entity\BallListBuilder"
 *   },
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "label",
 *     "weight" = "weight"
 *   }
 * )
 */
?>

Add a BallListBuilderClass

<?php
/**
 * @file
 * Contains \Drupal\sample_config_entity\BallListBuilder.
 */
 
namespace Drupal\sample_config_entity;
 
use Drupal\Core\Config\Entity\DraggableListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\String;
 
/**
 * Defines a class to build a listing of user ball entities.
 *
 * @see \Drupal\user\Entity\Ball
 */
class BallListBuilder extends DraggableListBuilder {
 
  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'sample_config_entity_ball_form';
  }
 
  /**
   * {@inheritdoc}
   */
  public function buildHeader() {
    $header['label'] = t('Name');
    return $header + parent::buildHeader();
  }
 
  /**
   * {@inheritdoc}
   */
  public function buildRow(EntityInterface $entity) {
    $row['label'] = $this->getLabel($entity);
    return $row + parent::buildRow($entity);
  }
 
 
  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    parent::submitForm($form, $form_state);
 
    drupal_set_message(t('The ball settings have been updated.'));
  }
 
}
 
?>

There shouldn’t be anything particularly strange here, we have a fairly straight forward class that adds the entity label to each row and adds a header called “Name”.

Finally, we need to register a path at which we can view our list of snooker balls. Create a sample_config_entity.routing.yml file with the following entry.

# This is the name of the route for the BallListBuilder.
sample_config_entity.ball_list:
  # The path to access the list.
  path: '/admin/config/balls'
  defaults:
    # _entity_list has a specifical meaning and is used for all ListBuilders. We
    # must ensure that the value is the same as our config entity type id.
    _entity_list: 'ball'
    # The page title.
    _title: 'Snooker Balls'
  # The requirements upon which we can show the page. The only restriction here
  # is that the user must have 'administer site configuration'
  requirements:
    _permission: 'administer site configuration'

OK, this is probably the most complicated bit, i’ve explained each line in comments in the code above so please read that carefully.

The overall flow goes like this, the route provides _entity_list: ball which is used to lookup the config entity type definition, the definition is used to find the list_builder under the handlers and finally the BallListBuilder class is used to build the page.

Yay, a simple list view:

Adding Fields to the List Builder

Wouldn’t it be nice if we could not only see the name of the ball but also the point_value and the color? No problem, let's update our list builder as so:

<?php
  /**
   * {@inheritdoc}
   */
  public function buildHeader() {
    $header['label'] = t('Name');
    $header['color'] = t('Color');
    $header['point_value'] = t('Points');
    return $header + parent::buildHeader();
  }
 
  /**
   * {@inheritdoc}
   */
  public function buildRow(EntityInterface $entity) {
    $row['label'] = $this->getLabel($entity);
    $row['color']['#markup'] = String::checkPlain($entity->getColor());
    $row['point_value']['#markup'] = String::checkPlain($entity->getPointValue());
    return $row + parent::buildRow($entity);
  }
?>

Adding Support for Editing Config Entities

To edit our config objects there are a few things we need to provide. The four steps are:

  • Add a BallForm under src\Form\BallForm.php
  • Add the form class to our Ball config entity definition under handlers > form
  • Add a new route to sample_config_entity.routing.yml
  • Add our new edit link template to the Ball config entity definition.


The Ball form.

<?php
/**
 * @file
 * Contains \Drupal\sample_config_entity\Form\BallForm.
 */
 
namespace Drupal\sample_config_entity\Form;
 
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
 
/**
 * Form controller for the ball entity edit forms.
 */
class BallForm extends EntityForm {
 
  /**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {
    /** @var \Drupal\sample_config_entity\Entity\Ball $entity */
    $entity = $this->entity;
    $form['label'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Ball name'),
      '#default_value' => $entity->label(),
      '#size' => 30,
      '#required' => TRUE,
      '#maxlength' => 64,
      '#description' => $this->t('The name for this snooker ball.'),
    );
    $form['id'] = array(
      '#type' => 'machine_name',
      '#default_value' => $entity->id(),
      '#required' => TRUE,
      '#disabled' => !$entity->isNew(),
      '#size' => 30,
      '#maxlength' => 64,
      '#machine_name' => array(
        'exists' => ['\Drupal\sample_config_entity\Entity\Ball', 'load'],
      ),
    );
    $form['color'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Ball Color'),
      '#default_value' => $entity->getColor(),
      '#size' => 30,
      '#required' => TRUE,
      '#maxlength' => 64,
      '#description' => $this->t('The color of this ball.'),
    );
    $form['point_value'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Point Value'),
      '#default_value' => $entity->getPointValue(),
      '#size' => 30,
      '#required' => TRUE,
      '#maxlength' => 64,
      '#description' => $this->t('The number of points this ball is worth.'),
    );
 
    return parent::form($form, $form_state, $entity);
  }
 
  /**
   * {@inheritdoc}
   */
  public function save(array $form, FormStateInterface $form_state) {
    /** @var \Drupal\sample_config_entity\Entity\Ball $entity */
    $entity = $this->entity;
 
    // Prevent leading and trailing spaces.
    $entity->set('label', trim($entity->label()));
    $entity->set('color', $form_state->getValue('color'));
    $entity->set('point_value', $form_state->getValue('point_value'));
    $status = $entity->save();
 
    $edit_link = $this->entity->link($this->t('Edit'));
    $action = $status == SAVED_UPDATED ? 'updated' : 'added';
 
    // Tell the user we've updated their ball.
    drupal_set_message($this->t('Ball %label has been %action.', ['%label' => $entity->label(), '%action' => $action]));
    $this->logger('sample_config_entity')->notice('Ball %label has been %action.', array('%label' => $entity->label(), 'link' => $edit_link));
 
    // Redirect back to the list view.
    $form_state->setRedirect('sample_config_entity.ball_list');
  }
}
 
?>

Updated Ball config entity definition.

<?php
/**
 * Defines the Ball entity.
 *
 * The ball entity stores information about a snooker ball.
 *
 * @ConfigEntityType(
 *   id = "ball",
 *   label = @Translation("Ball"),
 *   module = "sample_config_entity",
 *   config_prefix = "ball",
 *   handlers = {
 *     "list_builder" = "Drupal\sample_config_entity\BallListBuilder",
 *     "form" = {
 *       "default" = "Drupal\sample_config_entity\Form\BallForm"
 *     },
 *   },
 *   links = {
 *     "edit-form" = "sample_config_entity.ball.edit_form"
 *   },
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "label",
 *     "weight" = "weight"
 *   }
 * )
 */
?>

The route for editing our config objects.

sample_config_entity.ball.edit_form:
  path: '/admin/config/ball/manage/{ball}'
  defaults:
    # Special _entity_form key to provide the edit form.
    _entity_form: ball.default
    _title: 'Edit ball'
  requirements:
    _permission: 'administer site configuration'

Adding Operations to the list builder

Now how about a way to access the edit forms? Sure, lets go ahead and add some operations to the list view.

<?php
  /**
   * {@inheritdoc}
   */
  public function getDefaultOperations(EntityInterface $entity) {
    $operations = parent::getDefaultOperations($entity);
 
    if ($entity->hasLinkTemplate('edit-form')) {
      $operations['edit'] = array(
        'title' => t('Edit ball'),
        'weight' => 20,
        'url' => $entity->urlInfo('edit-form'),
      );
    }
    return $operations;
  }
?>

Adding Support for Adding New Config Entities

We already have a BallForm which we’ve used for editing and the same form can quite simply be re-used for adding new Ball’s. All we need to do is create the route like so:

sample_config_entity.ball_add:
  path: '/admin/config/ball/add'
  defaults:
    _entity_form: ball.default
    _title: 'Add ball'
  requirements:
    _permission: 'administer site configuration'

You’ll notice that the button always says “Save”. Not all bad, but we could probably make this a little nicer. Lets go ahead and customise the button label based on whether we’re editing or adding a new ball:

<?php
  /**
   * {@inheritdoc}
   */
  protected function actions(array $form, FormStateInterface $form_state) {
    $actions = parent::actions($form, $form_state);
    $actions['submit']['#value'] = ($this->entity->isNew()) ? $this->t('Add ball') : $this->t('Update ball');
    return $actions;
  }
?>

The only way to add a new ball is by visiting “/admin/config/ball/add“ which is less than ideal. Lets add a local action to our list view that allows us to add new balls.

sample_config_entity.links.action.yml

sample_config_entity.ball_add:
  route_name: sample_config_entity.ball_add
  title: 'Add new ball'
  appears_on:
    - sample_config_entity.ball_list

So simple!

Adding Support for Deleting Config Entities

Deleting config objects requires a little bit more work again. The steps required are:

  • Add the delete form under src\Form\BallDeleteForm.php
  • Add our form handler to the ball definition pointing at our new form.
  • Add the route to sample_config_entity.routing.yml
  • Add a new link template to our Ball definition pointing at our new route.

Delete form

<?php
/**
 * @file
 * Contains \Drupal\sample_config_entity\Form\BallDeleteForm.
 */
 
namespace Drupal\sample_config_entity\Form;
 
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
 
/**
 * Provides a deletion confirmation form for Ball entity.
 */
class BallDeleteForm extends EntityConfirmFormBase {
 
  /**
   * {@inheritdoc}
   */
  public function getQuestion() {
    return $this->t('Are you sure you want to delete the ball %name?', array('%name' => $this->entity->label()));
  }
 
  /**
   * {@inheritdoc}
   */
  public function getCancelUrl() {
    return new Url('sample_config_entity.ball_list');
  }
 
  /**
   * {@inheritdoc}
   */
  public function getConfirmText() {
    return $this->t('Delete');
  }
 
  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->entity->delete();
    $this->logger('sample_config_entity')->notice('Ball %name has been deleted.', array('%name' => $this->entity->label()));
    drupal_set_message($this->t('Ball %name has been deleted.', array('%name' => $this->entity->label())));
    $form_state->setRedirectUrl($this->getCancelUrl());
  }
 
}
 
?>

Now we update our Ball definition with the delete entity form.

<?php
/**
 * @ConfigEntityType(
 *   id = "ball",
 *   label = @Translation("Ball"),
 *   module = "sample_config_entity",
 *   config_prefix = "ball",
 *   admin_permission = "administer site configuration",
 *   handlers = {
 *     "list_builder" = "Drupal\sample_config_entity\BallListBuilder",
 *     "form" = {
 *       "default" = "Drupal\sample_config_entity\Form\BallForm",
 *       "delete" = "Drupal\sample_config_entity\Form\BallDeleteForm"
 *     },
 *   },
 *   links = {
 *     "edit-form" = "sample_config_entity.ball.edit_form",
 *     "delete-form" = "sample_config_entity.ball.delete_form"
 *   },
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "label",
 *     "weight" = "weight"
 *   }
 * )
 */
?>

And finally, the add a route for deleting our config entities.

sample_config_entity.ball.delete_form:
  path: '/admin/config/ball/manage/{ball}/delete'
  defaults:
    _entity_form: ball.delete
    _title: 'Delete ball'
  requirements:
    _permission: 'administer site configuration'

Note, I needed to set either an access controller under handlers or a admin_permission on our Ball config entity definition. Without this we inherit EntityAccessControlHandler and I was unable to delete my config objects.

Final list view:

Additional Info

Recently core added a config_export key which allows you to specify the keys that are exported for your config entity. This has performance benefits in scenarios where your entity is loaded with a cold cache. Read more here: https://www.drupal.org/node/2481909

TLDR

Feel free to just go grab the sample code from Github: https://github.com/benjy/Sample-Config-Entity

For the Lazy

You can generate a config entity using the awesome Drupal Console.

php console.phar generate:module --module generated_entity
php console.phar generate:entity:config --module generated_entity

Author Info

Ben Dougherty
Developer

In his free time Ben can be found lurking in the Drupal issue queues, chatting in #drupal-contribute and listening to Oasis. He is known for his superhuman debugging abilities and occasionally enjoys a beer or two.

Comments

Hi, nice writeup. How do you handle migrations?

What do you do when the schema changes and the module is in production?

Add new comment

Plain text

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