26 Jul.2014

Drupal 8 & 9: The example how create a configurable block programmatically

Sticky Sharrre Bar

In this example, we will look at just a few examples.

  • how to create the programmatically configurable block with fields;
  • how to create a controller;
  • how to integrate your module with 3d party libraries

The code below will be considered as an example of the "Sticky Sharrre Bar" module that I have created a couple of years ago.

Let's go:

File
sticky_sharrre_bar.info.yml
name: Sticky Sharrre Bar
description: Provides a sticky <a href="http://sharrre.com/">Sharrre</a> block on your site.
version: VERSION
core: 8.x
package: Sharing
dependencies:
  - block
type: module
configure: block.admin_display
File
sticky_sharrre_bar.permissions.yml
'access sticky_sharrre_bar':
  title: 'View Sticky Sharrre Bar Block'
File
sticky_sharrre_bar.routing.yml
sticky_sharrre_bar.sticky_sharrre_bar_controller_sharrre:
  path: 'sharrre'
  defaults:
    _controller: '\Drupal\sticky_sharrre_bar\Controller\StickySharrreBarController::sharrre'
    _title: 'Emulation functional of php file from http://sharrre.com.'
  requirements:
    _permission: 'access sticky_sharrre_bar'
File
sticky_sharrre_bar.install
<?php
 
/**
 * @file
 * Install, update and uninstall functions for the sticky_sharrre_bar module.
 */
 
use Drupal\Component\Utility\Unicode;
 
define('WAYPOINTS_MIN_PLUGIN_VERSION', '4.0.0');
define('SHARRRE_PLUGIN_VERSION', '1.3.5');
 
/**
 * Implements hook_requirements().
 */
function sticky_sharrre_bar_requirements($phase) {
  $requirements = array();
 
  if (\Drupal::moduleHandler()->moduleExists('libraries')) {
    if ($phase == 'runtime') {
      $library = libraries_detect('jquery-waypoints');
      $error_type = isset($library['error']) ? Unicode::ucfirst($library['error']) : '';
      $error_message = isset($library['error message']) ? $library['error message'] : '';
 
      if (empty($library['installed'])) {
        $requirements['jquery_waypoints_plugin'] = array(
          'title' => t('jQuery Waypoints plugin'),
          'value' => t('@e: At least @a', array(
            '@e' => $error_type,
            '@a' => WAYPOINTS_MIN_PLUGIN_VERSION,
          )),
          'severity' => REQUIREMENT_ERROR,
          'description' => t('@error You need to download the <a href=":jquery_waypoints">jQuery Waypoints plugin</a>, extract the archive and place the jquery-waypoints directory in the %path directory on your server.', array(
            '@error' => $error_message,
            ':jquery_waypoints' => $library['download url'],
            '%path' => 'libraries',
          )),
        );
      }
      elseif (version_compare(trim($library['version']), WAYPOINTS_MIN_PLUGIN_VERSION, '>=')) {
        $requirements['jquery_waypoints_plugin'] = array(
          'title' => t('jQuery Waypoints plugin'),
          'severity' => REQUIREMENT_OK,
          'value' => $library['version'],
        );
      }
      else {
        $requirements['jquery_waypoints_plugin'] = array(
          'title' => t('jQuery Waypoints plugin'),
          'value' => t('At least @a', array('@a' => WAYPOINTS_MIN_PLUGIN_VERSION)),
          'severity' => REQUIREMENT_ERROR,
          'description' => t('You need to download a later version of the <a href=":jquery_waypoints">jQuery Waypoints plugin</a> and replace the old version located in the %path directory on your server.', array(
            ':jquery_waypoints' => $library['download url'],
            '%path' => $library['library path'],
          )),
        );
      }
 
      $library = libraries_detect('sharrre');
      $error_type = isset($library['error']) ? Unicode::ucfirst($library['error']) : '';
      $error_message = isset($library['error message']) ? $library['error message'] : '';
 
      if (empty($library['installed'])) {
        $requirements['sharrre_plugin'] = array(
          'title' => t('jQuery Sharrre plugin'),
          'value' => t('@e: At least @a', array(
            '@e' => $error_type,
            '@a' => SHARRRE_PLUGIN_VERSION,
          )),
          'severity' => REQUIREMENT_ERROR,
          'description' => t('@error You need to download the <a href=":sharrre">jQuery Sharrre plugin</a>, extract the archive and place the sharrre directory in the %path directory on your server.', array(
            '@error' => $error_message,
            ':sharrre' => $library['download url'],
            '%path' => 'libraries',
          )),
        );
      }
      elseif (version_compare(trim($library['version']), SHARRRE_PLUGIN_VERSION, '==')) {
        $requirements['sharrre_plugin'] = array(
          'title' => t('jQuery Sharrre plugin'),
          'severity' => REQUIREMENT_OK,
          'value' => $library['version'],
        );
      }
      else {
        $requirements['sharrre_plugin'] = array(
          'title' => t('jQuery Sharrre plugin'),
          'value' => t('Requires @a', array('@a' => SHARRRE_PLUGIN_VERSION)),
          'severity' => REQUIREMENT_ERROR,
          'description' => t('You need to download the 1.3.5 version of the <a href=":sharrre">jQuery Sharrre plugin</a> and replace the old version located in the %path directory on your server.', array(
            ':sharrre' => $library['download url'],
            '%path' => $library['library path'],
          )),
        );
      }
    }
  }
 
  return $requirements;
}

In this file, we define dependencies and libraries that are necessary for our module.

File
src\Controller\StickySharrreBarController.php
<?php
 
namespace Drupal\sticky_sharrre_bar\Controller;
 
use Drupal\Core\Controller\ControllerBase;
use Drupal\Component\Utility\Html;
use Drupal\Component\Serialization\Json;
use Symfony\Component\HttpFoundation\JsonResponse;
 
 
/**
 * Controller routines for Sticky Sharrre Bar route.
 *
 * @package Drupal\sticky_sharrre_bar\Controller
 */
class StickySharrreBarController extends ControllerBase {
  /**
   * Emulation functional of php file from http://sharrre.com.
   *
   * @return array
   *   A render array representing the administrative page content.
   */
  public function sharrre() {
    $url = $_GET['url'];
    $type = $_GET['type'];
    $output = array('url' => $url, 'count' => 0);
 
    // We shouldn't cache the router, to avoid the same result for all pages,
    // like {"url": null, "count": 0}
    \Drupal::service('page_cache_kill_switch')->trigger();
 
    if (filter_var($url, FILTER_VALIDATE_URL)) {
      if ($type == 'googlePlus') {
        $contents = $this->stickySharrreBarParse('https://plusone.google.com/u/0/_/+1/fastbutton?url=' . $url . '&count=true');
        preg_match('/window\.__SSR = {c: ([\d]+)/', $contents, $matches);
        if (isset($matches[0])) {
          $output['count'] = (int) str_replace('window.__SSR = {c: ', '', $matches[0]);
        }
      }
      else {
        if ($type == 'stumbleupon') {
          $content = $this->stickySharrreBarParse('http://www.stumbleupon.com/services/1.01/badge.getinfo?url=' . $url);
          $result = Json::decode($content);
          if (isset($result->result->views)) {
            $output['count'] = Html::escape($result->result->views);
          }
        }
      }
    }
 
    return new JsonResponse($output);
  }
 
  /**
   * Get necessary content use cURL.
   *
   * @param string $encoded_url
   *   Url of page.
   *
   * @return \Psr\Http\Message\StreamInterface
   *   Ready data
   */
  private function stickySharrreBarParse($encoded_url) {
    $client = \Drupal::httpClient();
    $request = $client->get($encoded_url);
 
    return $request->getBody();
  }
 
}
File
src\Plugin\Block\StickySharrreBarBlock.php
<?php
 
namespace Drupal\sticky_sharrre_bar\Plugin\Block;
 
use Drupal\Core\Block\BlockBase;
use Drupal\block\Entity\Block;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\Core\Link;
 
/**
 * Provides a 'StickySharrreBarBlock' block.
 *
 * @Block(
 *  id = "sticky_sharrre_bar_block",
 *  admin_label = @Translation("Sticky sharrre bar"),
 * )
 */
class StickySharrreBarBlock extends BlockBase {
  /**
   * {@inheritdoc}
   */
  protected function blockAccess(AccountInterface $account) {
    return AccessResult::allowedIfHasPermission($account, 'access sticky_sharrre_bar');
  }
 
  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return array(
      'label_display' => FALSE,
      'providers' => array(
        'googlePlus' => 'googlePlus',
        'facebook' => 'facebook',
        'twitter' => 'twitter',
        'linkedin' => 'linkedin',
      ),
      'use_module_css' => 1,
      'use_custom_css_selector' => '',
      'use_google_analytics_tracking' => 1,
    );
  }
 
  /**
   * {@inheritdoc}
   */
  public function blockForm($form, FormStateInterface $form_state) {
    $config = \Drupal::config('sticky_sharrre_bar.settings');
    $form['block_sticky_sharrre_bar'] = array(
      '#type' => 'fieldset',
      '#title' => $this->t('Sticky Sharrre Bar settings'),
    );
    $form['block_sticky_sharrre_bar']['providers'] = array(
      '#type' => 'checkboxes',
      '#options' => $config->get('providers_list'),
      '#title' => $this->t('Main share providers'),
      '#description' => $this->t('Choose which providers you want to show in this block instance.'),
      '#default_value' => $this->configuration['providers'],
    );
    // To add the field only if "google_analytics' is enabled.
    if (\Drupal::moduleHandler()->moduleExists('google_analytics')) {
      $form['block_sticky_sharrre_bar']['use_google_analytics_tracking'] = array(
        '#type' => 'checkbox',
        '#title' => $this->t('Allows tracking social interaction with "Google Analytics".'),
        '#description' => $this->t('For more details see the :url.',
          array(
            ':url' => Link::fromTextAndUrl($this->t('"Sharrre" documentation'),
              Url::fromUri('http://sharrre.com/track-social.html',
                array('attributes' => array('target' => '_blank'))))
              ->toString(),
          )
        ),
        '#default_value' => $this->configuration['use_google_analytics_tracking'],
      );
    }
    $form['block_sticky_sharrre_bar']['use_module_css'] = array(
      '#type' => 'checkbox',
      '#title' => $this->t('Use the css of the module.'),
      '#description' => $this->t('Disable if you want override the styles in your theme.'),
      '#default_value' => $this->configuration['use_module_css'],
    );
    $form['block_sticky_sharrre_bar']['use_custom_css_selector'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Custom CSS selector'),
      '#size' => 60,
      '#description' => $this->t('In some cases, module can not find the right region selector in your theme. You can manually set it. Examples: "#navbar", ".header". Is empty by default.'),
      '#default_value' => $this->configuration['use_custom_css_selector'],
 
    );
 
    return $form;
  }
 
  /**
   * Overrides \Drupal\block\BlockBase::blockSubmit().
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    $values = $form_state->getValue('block_sticky_sharrre_bar');
 
    $this->configuration['providers'] = $values['providers'];
    $this->configuration['use_module_css'] = $values['use_module_css'];
    $this->configuration['use_custom_css_selector'] = $values['use_custom_css_selector'];
    $this->configuration['providers'] = $values['providers'];
 
    if (isset($values['use_google_analytics_tracking'])) {
      $this->configuration['use_google_analytics_tracking'] = $values['use_google_analytics_tracking'];
    }
  }
 
  /**
   * Implements \Drupal\block\BlockBase::blockBuild().
   *
   * {@inheritdoc}
   */
  public function build() {
    $build = array();
    $enabled_providers = array();
 
    foreach ($this->configuration['providers'] as $key => $provider) {
      if ($provider != '0') {
        $enabled_providers[$key] = $provider;
      }
    }
 
    if (!empty($enabled_providers)) {
      $request = \Drupal::request();
      $route_match = \Drupal::routeMatch();
      $title = \Drupal::service('title_resolver')
        ->getTitle($request, $route_match->getRouteObject());
 
      if ($title == '') {
        $title = \Drupal::config('system.site')->get('name');
      }
 
      // FIXME: need load block info and get id of region.
      $instance = Block::load('stickysharrrebar');
      $region = $instance->get('region');
      $custom_css_selector = $this->configuration['use_custom_css_selector'];
 
      $js_variables = array(
        'providers' => $enabled_providers,
        'useGoogleAnalyticsTracking' => $this->configuration['use_google_analytics_tracking'],
        'blockRegion' => ($custom_css_selector != '') ? $custom_css_selector : $region,
        // TODO: fix region.
        'isCustomSelector' => ($custom_css_selector != '') ? TRUE : FALSE,
      );
 
      $build['content'] = array(
        '#theme' => 'sticky_sharrre_bar_block',
        '#providers' => $enabled_providers,
        '#url' => \Drupal::request()->getUri(),
        '#title' => Html::escape($title),
        '#attached' => array(
          'drupalSettings' => ['stickySharrreBar' => $js_variables],
          'library' => array(),
        ),
      );
      // Add and initialise plugins.
      $build['content']['#attached']['library'][] = 'sticky_sharrre_bar/jquery-waypoints';
      $build['content']['#attached']['library'][] = 'sticky_sharrre_bar/sharrre';
      $build['content']['#attached']['library'][] = 'sticky_sharrre_bar/sticky_sharrre_bar_js';
      // Very important place. We should cache the result by URL and Language,
      // because a node title can be the same for all pages.
      $build['content']['#cache'] = array(
        'contexts' => array('url', 'languages'),
      );
      if ($this->configuration['use_module_css'] == 1) {
        $build['content']['#attached']['library'][] = 'sticky_sharrre_bar/sticky_sharrre_bar_css';
      }
 
    }
 
    return $build;
  }
 
}
File
js\sticky_sharrre_bar.js
/**
 * @file
 * Sticky Sharrre Bar UI.
 */
 
(function ($, Drupal, drupalSettings) {
 
  'use strict';
 
  $.exists = function (selector) {
    return ($(selector).length > 0);
  };
 
  /**
   * Attaches the Sticky Sharrre Bar behavior to each block element.
   */
  Drupal.behaviors.stickySharrreBarRender = {
    attach: function (context) {
 
      var enableTracking = (drupalSettings.googleanalytics && drupalSettings.stickySharrreBar.useGoogleAnalyticsTracking === 1) ? true : false,
        blockRegion = drupalSettings.stickySharrreBar.blockRegion,
        isCustomSelector = drupalSettings.stickySharrreBar.isCustomSelector,
        selector = '',
        $block = $('.block-sticky-sharrre-bar', context);
 
      if (isCustomSelector) {
        selector = blockRegion;
      }
      else {
        // Try to find class, id or html tag of region.
        if ($.exists('.' + blockRegion)) {
          selector = '.' + blockRegion + ':first';
        }
        else if ($.exists('#' + blockRegion)) {
          selector = '#' + blockRegion;
        }
        else if ($.exists(blockRegion)) {
          selector = blockRegion + ':first';
        }
        else {
          return;
        }
      }
 
      // Attach the Waypoint and Sticky libraries to the selector.
      // Move the output code after selector.
      new Waypoint.Sticky({
        element: $block.insertAfter(selector).find('.sticky_sharrre_bar')
      });
 
      // The "sharrre" plugin requires this object.
      var buttons = {
        googlePlus: {},
        facebook: {},
        twitter: {},
        linkedin: {},
        digg: {},
        delicious: {},
        stumbleupon: {},
        pinterest: {},
        tumblr: {} // TODO: Available from v2.0.0 in "Sharrre" plugin.
      };
 
      $.each(drupalSettings.stickySharrreBar.providers, function (provider) {
        if (provider) {
          var currentProvider = {};
          currentProvider[provider] = true;
 
          $('#' + provider, context).sharrre({
            share: currentProvider,
            template: '<a class="share ' + provider + '" href="#">' + Drupal.t('Share on <span class="provider_name">!provider</span>', {'!provider': provider}, {}) + '</a></div><span class="count"><a href="#">{total}</a></span>',
            enableHover: false,
            enableTracking: enableTracking,
            enableCounter: true,
            buttons: buttons,
            urlCurl: (provider === 'stumbleupon' || provider === 'googlePlus') ? '/sharrre' : '',
            click: function (api, options) {
              api.simulateClick();
              api.openPopup(provider);
            }
          });
        }
      });
    }
  };
})(jQuery, Drupal, drupalSettings);
Conclusion

You can see the full version of the module on https://www.drupal.org/project/sticky_sharrre_bar, in this article just example without SASS, CSS, etc.

Ruslan Piskarov

Ukraine
PHP/WEB Developer / Drupal Expert. More than 11 years of experience delivering Drupal based General Purpose solutions for different sectors such as Job Boards, Product Portfolios, Geo Coding, Real Estate solutions, E-Commerce, Classifieds, Corporate and online Magazines/Newspapers.

Versions