Drupal 8

Adding Properties to Drupal 8 Configuration Entities

Submitted by djevans on Thu, 08/10/2017 - 07:02
Paragraphs

I'm currently working on porting the Views Save module to Drupal 8. Users can save searches executed as Views, and can choose to publish them (making them accessible to other users), or keep them private.

The Saved Views can be of different types, and an administrator should be able to specify whether new Saved Views of a particular type should be published by default. I've created a SavedViewType class, which is a configuration entity that acts as a bundle for SavedView.

First, I declared some getters and setters on the interface.

/**
 * Provides an interface for defining Saved View type entities.
 */
interface SavedViewTypeInterface extends ConfigEntityInterface {

  /**
   * Get the options for this entity.
   *
   * @return array
   *   An associative array of options.
   */
  public function getOptions();

  /**
   * Set the options for this config entity.
   *
   * @param array $options
   *   An array of options.
   *
   * @return \Drupal\views_save\Entity\SavedViewTypeInterface
   *   The called SavedViewType entity.
   */
  public function setOptions(array $options);

}

Then I added a property to the SavedViewType class, along with some getters and setters:

class SavedViewType extends ConfigEntityBundleBase implements SavedViewTypeInterface {

  /**
   * The options for the Saved View.
   *
   * @var array
   */
  protected $options;

  /**
   * {@inheritdoc}
   */
  public function getOptions() {
    return $this->options;
  }

  /**
   * {@inheritdoc}
   */
  public function setOptions(array $options) {
    $this->options = $options;
    return $this;
  }

}

Finally, I added some elements to the entity form.

class SavedViewTypeForm extends EntityForm {

  /**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {

    // <snip>

    /** @var \Drupal\views_save\Entity\SavedViewType $saved_view_type */
    $saved_view_type = $this->entity;
    $options = $saved_view_type->getOptions();

    $form['options'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Options'),
    ];

    $form['options']['published'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Published by default'),
      '#description' => $this->t('If enabled, saved views of this type will be published by default.'),
      '#default_value' => !empty($options['published']),
    ];

    return $form;
  }
}

When I tried to create some Saved View Types through the UI, though, the value for the 'published' property wasn't being saved.

It turns it I should have defined the property in the configuration schema!

In /config/schema/entity_type.schema.inc:

views_save.saved_view_type.*:
  type: config_entity
  label: 'Saved View type config'
  mapping:
    id:
      type: string
      label: 'ID'
    label:
      type: label
      label: 'Label'
    options:
      type: mapping
        label: 'Options'
        mapping:
          published:
            type: boolean
            label: 'Published'
    uuid:
      type: string

The Saved View Type then saved correctly.

The moral of the story is: don't forget the schema!

Creating Views Pager Plugins in Drupal 8

Submitted by djevans on Tue, 08/01/2017 - 18:55
Paragraphs

If you have a View with a pager in Drupal 8, you might need to display different numbers of results depending on the current page number. Here’s how to achieve that using Views plugins.

The Plugin Hierarchy

Just like in Drupal 7, Views plugins are a hierarchy of classes, with some differences in structure so that they conform with the other plugin types introduced in Drupal 8.

We can see that there are two pagers that display results on multiple pages: ‘Default pager’ (Drupal\views\Plugin\views\pager\Full) and ‘Mini pager’ (Drupal\views\Plugin\views\pager\Mini). We’ll extend the Default pager first.

Making the plugin visible

To make our plugin visible to the UI, we set properties on the class annotations. Set a unique ID for the plugin as the id key and descriptive information in title, short_title and help

/**
 * @ViewsPager(
 *  id = "first_page_full_pager",
 *  title = @Translation("Paged output, separate count on first page"),
 *  short_title = @Translation("Full pager with separate first page"),
 *  help = @Translation("Paged output with separate count on the first page"),
 *  theme = "pager",
 *  register_theme = FALSE
 * )
 */

 

Creating Custom Behaviour

To allow administrators to specify the number of items to show on the first page, we need to define an option for the pager:

public function defineOptions() {
  $options = parent::defineOptions();
  $options['items_first_page'] = ['default' => 10];
  return $options;
}

Once we have done that, we create a field on the settings form.

public function buildOptionsForm(&$form, FormStateInterface $form_state) {
  parent::buildOptionsForm($form, $form_state);
  $form['items_per_page']['#weight'] = -50;
  $form['items_first_page'] = [
    '#title' => t('Items on first page'),
    '#type' => 'number',
    '#description' => t('The number of items to show on the first page'),
    '#default_value' => $this->options['items_first_page'],
    '#weight' => -49,
  ];
}

getItemsPerPage() returns the number of results to display on a given page, and getPagerTotal() returns the total number of page links that should be displayed for a given number of results.

In both cases, we need to take the items_per_page and items_first_page options into account.

public function getItemsPerPage() {
  $key = $this->getCurrentPage() === 0
    ? 'items_first_page'
    : 'items_per_page';
  return isset($this->options[$key]) ? $this->options['key'] : 0;
}

public function getPagerTotal() {
  $first_page_items = $this->options['items_first_page'];
  $items_per_page = $this->options['items_per_page'];
  return $this->total_items > $first_page_items
    ? ceil(1 + (($this->total_items - $first_page_items) / $items_per_page))
    : 1;
}

We override SqlBase::query() in order to set the correct limit and offset:

public function query() {
  parent::query();
  $this->view->query->setLimit($this->getItemsPerPage());
  if ($this->current_page > 0) {
    $offset = $this->options['items_first_page'];
    $offset += ($this->current_page - 1) * $this->options['items_per_page'];
    $offset += $this->options['offset'];
    $this->view->query->setOffset($offset);
  }
}

Finally, we need to set a summary for the pager, to appear in the Views UI.

public function summaryTitle() {
  if (empty($this->options['items_first_page'])) {
    return parent::summaryTitle();
  }
  if (!empty($this->options['offset'])) {
    return $this->formatPlural($this->options['items_per_page'],
      '@count item, skip @skip, @first on first page',
      'Paged, @count items, skip @skip, @first on first page',
      [
        '@count' => $this->options['items_per_page'],
        '@skip' => $this->options['offset'],
        '@first' => $this->options['items_first_page'],
      ]
    );
  }
  return $this->formatPlural($this->options['items_per_page'],
    '@count item, @first on first page',
    'Paged, @count items, @first on first page',
    [
      '@count' => $this->options['items_per_page'],
      '@first' => $this->options['items_first_page'],
    ]
  );
}

 

Refactoring

To extend the mini pager, it looks like we need to extend the Mini class in pretty much the same way. In order to avoid duplicating the code we have just written, we can factor out the common methods into a trait. This way, our new plugins can share functionality while extending different superclasses.

(There's a good explanation of traits in PHP on Brendan Bates' site: Traits, The Right Way.)

Our individual plugin classes, FullFirstPage and MiniFirstPage will then both use the new FirstPageTrait, but have different implementations of summaryTitle(), as well as different metadata in their annotations.

Here's the implementation of summaryTitle() for the mini pager:

public function summaryTitle() {
  if (empty($this->options['items_first_page'])) {
    return parent::summaryTitle();
  }
  if (!empty($this->options['offset'])) {
    return $this->formatPlural($this->options['items_per_page'],
      'Mini pager, @count item, skip @skip, @first on first page',
      'Mini pager, @count items, skip @skip, @first on first page',
      [
        '@count' => $this->options['items_per_page'],
        '@skip' => $this->options['offset'],
        '@first' => $this->options['items_first_page'],
      ]
    );
  }
  return $this->formatPlural($this->options['items_per_page'],
    'Mini pager, @count item, @first on first page',
    'Mini pager, @count items, @first on first page',
    [
      '@count' => $this->options['items_per_page'],
      '@first' => $this->options['items_first_page'],
    ]
  );
}

 

The Views First Page module is now available on drupal.org.