Feeds

Author

May 07 2019
May 07

For many Drupal 8 projects that have minimal interaction with their users, the need to set up a notification system quickly comes to the forefront. Being notified of a new comment, a response to a comment, a new publication on a particular subject, or a user, are recurring needs.

To satisfy this type of need, we can rely on the Message module (and the Message stack) which can allow us to achieve this type of result with the help of the Flag module. But we are not going to talk here about these two generic modules, which can do much more, but about a new module Entity Activity whose sole purpose is to log all types of actions performed, by users, according to their subscriptions, on a project.

The Entity Activity module will allow us to generate any type of message, on any type of content entity on the tree main operations of the content life cycle: its creation, its update and its deletion.

Global configuration

The initial configuration of the module is quite quick to set up. The majority of the configuration to be done is done from the menu Configuration > Content authoring > Entity Activity

First, you need to activate on which types of entities you want to be able to generate notifications. And so of course allow users to subscribe to these entities.

Entity Activity Settings

At the same time, you can configure a purge of the different notifications (based either on overall time or a maximum number of notifications per user) if necessary.

Multilingual support

For multilingual projects, subscriptions are also multilingual in the sense that a user can subscribe to content for each of his or her independently available translations. Notification messages are then generated in the current language when the operation is performed. You can force the generation of a log message in the user's preferred language (if defined), by checking the corresponding option in the general settings.

User preferred language option

Warning. This option is costly in performance because each log message must be regenerated for each owner of a subscription. Use it only if you really need log messages to be generated in the user's preferred language.

Configuration of activated content entities

In a second step, we will configure the display modes of the entities we have activated, to display a Subscribe on widget that will allow users to subscribe (or unsubscribe) on each of the entities.

Article subscribe on

You can then display on the different view modes required, on the different content types, this Subscribe on button, as above with the example of the Article content type.

From now on, users, with the appropriate permission, will be able to subscribe and unsubscribe on each activated and configured entity. And they will be able to find from their account a summary of all their subscriptions.

User's subscriptions

Note that the Remove button on each subscription is present because it has been activated in the Subscription entity view mode from the Configuration > Content Authoring > Entity Activity > Subscriptions settings > Manage display configuration page.

Configuration of notification generators.

Thirdly, we must configure which messages will be generated from which operation and for which subscriptions. To this end, we can create as many Generators as necessary according to the needs of a project.

Entity Activity Generators

Each notification generator has a base of four plugin types corresponding to the four main content entity types of a Drupal 8 project, namely content, taxonomy term, user and comment. It is possible to add as many plugins as necessary for specific project needs, but we will have the opportunity to look at this later.

The configuration of these Plugins is identical and follows the same logic.

Plugin log generator

The possible configuration options are as follows.

  • Enable: whether the plugin is active or not
  • Published: option to generate a notification only if the target entity is published
  • Operation: the type of operation from which the plugin must generate a notification (insert / update / delete)
  • Bundles: it is possible to limit the action of a plugin to only one or more bundles of a content entity type. You can leave blank to apply the plugin to all bundles regardless.
  • Subscribed on: this is the configuration option in a way master. It allows you to define from which subscriptions the plugin will generate the notification. The two possible options are Source Entity and Entity Referenced. The first (Source Entity) is the most basic and allows you to select the subscriptions that have been made on the entity itself (content, user, etc.). The second option (Entity Referenced) will allow you to select the subscriptions made on a referenced entity (from an Entity Reference field so) by the current entity. The typical use case is to be able to generate a notification for all users subscribing to an Alpha theme (Taxonomy Term) when new content is published on that theme, or another use case is the generation of a notification when publishing a comment about a content. But the possibilities here are extremely varied and countless use cases can be covered with this second option. In the example above we have chosen here to generate a notification when publishing content to all users who have subscribed to the author of the content.
  • Include parent term: this option allows to include all parent terms in case the entity on which subscriptions are searched is a taxonomy term. Useful if you want a user who has subscribed to the taxonomy term Fruit, for example, to also be notified if content is published with the taxonomy term Orange, which would be a child of the term Fruit.
  • Log message: This is of course the message that will be generated and notified to users. You can configure the text format to use, and of course you can use all the tokens related to the target entity type of the plugin.
  • Use cron: this option allows you to disable the generation of the actual notifications of the operation performed on the entity. Indeed, depending on the number of subscriptions, generating all notifications can be a long and costly process in terms of performance. This option then allows to delegate to the scheduled tasks the generation of these notifications, so as not to penalize the user who performs the operation in question. This option is highly recommended.

You can then create and activate as many notification generators to cover all the business needs of your project, each notification generator can itself have several plugins in charge of generating the notification itself.

Configuring notifications

To finalize the implementation of your notification system, you can then configure two elements on the Log entities of the module that carry these notifications.

From the Configuration > Content authoring > Entity Activity > Log settings > Manage Display

Log manage display

You can activate the extra field Remove log, which will expose a button allowing the owner of the notification (the user who subscribed) to delete this notification (if he has the appropriate permission).

And you can also configure the base field Read, with the Log read / unread field formatter, which will expose a button allowing the notification's owner to change the status from Not Read to Read or vice versa.

Inform the user of new notifications

All this is only really useful if users can be easily informed of the presence of new notifications. To this end, the module offers a block called User log block, which must be placed and configured in the appropriate region of the project theme.

User log block settings

The purpose of this block is to display the total number of unread notifications. For this purpose you can add a (short) text that will be displayed next to this number, you can use an icon font to display an icon next to this number (using the classes of this font), but also configure the maximum number of notifications that will be embedded as well as the view mode used to display them, as well as add a link to the user's page listing all his notifications, whatever their status. Of course you can overload the Twig template to customize the rendering of this block.

Extension of the Entity Activity module

Since notification generators rely on a  plugin type, it is possible to create easily your own plugin to add a very specific logic to a project, overloading the basic methods used, or simply to support a new content entity type from a contributed module.

Plugins must be placed on the Plugin/LogGenerator namespace and may look like this for example if you want to overload and add a very particular logic on one or more of the methods of this Plugin type. 

/**
 * @LogGenerator(
 *   id = "my_custom_node",
 *   label = @Translation("My custom Node Log Generator"),
 *   description = @Translation("Generate custom log for the entity type Node or for related entities referenced."),
 *   source_entity_type = "node",
 *   bundles = {}
 * )
 */
class MyCustomNodeLogGenerator extends LogGeneratorBase {

  public function getEntitiesSubscribedOn(ContentEntityInterface $entity) {
    // Stuff.
  }
  
  public function preGenerateLog(ContentEntityInterface $entity, AccountProxyInterface $current_user = NULL) {
    // Stuff.
  }

  public function generateLog(array $settings) {
    // Stuff.
  }

}

The module also exposes two parameters that can be overridden from the settings.php file of a project.

$settings['entity_activity_max_log'] = 100;

This setting overrides the maximum number of notifications (default 50) that can be bulk marked as read by a user. Indeed, it has a button on its page listing its notifications which allows you to mark all its notifications as read. This parameter therefore defines the number of notifications that will be processed per pass in the update process that will then be launched.

$settings['entity_activity_purge_user_always'] = TRUE;

This parameter allows you to force the purge per user (maximum number of notifications per user) each time a cron task is executed. Indeed, this method of purging can be very costly in terms of time and performance, depending on the number of users to be treated. Also by default, this method is only executed once a day, at night. This parameter therefore overrides this default behavior.

In addition, the notifications and subscriptions provided by this module are themselves content entities. As such, they can be customized by adding additional fields. In this case, it is up to the project to define the value of these fields in a programmatic way according to business needs. To this end, events are dispatched for each hook implemented by Drupal on the CRUD cycle (presave, postsave, update, insert, delete, etc.).

Finally, the entities provided by this module (Log and Subscription) have a basic rendering that can be used in many cases. The rendering of these entities is heavily based on a Twig template that you can then override to meet your needs.

Possible developments

To date, Entity Activity allows you to quickly configure a project to generate notifications for users based on their subscriptions. It can also be easily extended using Plugins to add specific business logic if necessary. A natural evolution would be to be able to generate and send by email (according to a frequency chosen by the user) a list of the last unread notifications, even if for the moment this feature is not part of the functional scope of the module. But a Drupal 8 developer can easily add this functionality (either in the module itself or in another contributed module), the foundations laid opening up many possibilities in relation to this recurring theme on community projects.

Conclusion

To conclude, Entity Activity is a simple configuration module, as it focuses exclusively on generating notifications, or logging events occurring on a Drupal 8 project. But this simplicity is not at the expense of the necessary modularity as to the generation (and their underlying conditions) of the different notifications to cover most, if not all, of the recurring use cases and needs.

Apr 11 2019
Apr 11

The generation of an automatic summary for relatively long articles is a recurring need for content publishing. A summary provides better visibility for the reader, and an effective way to navigate within an article as soon as it is a little dense. And as for the editor, this saves him from having to manually manage a summary that will have to be updated as an article is modified. In other words, it eliminates a source of potential and multiple input errors. Let's discover the Toc.js module which allows us to easily generate a summary in a modular way whatever the page of a Drupal 8 site.

The Toc.js module offers full integration with the content types of a Drupal project. To use it, simply activate it in the configuration page of a content type.

Toc.js settings

The proposed options allow to precisely adapt the table of contents to be generated according to the type of content, these fields etc. To understand the configuration options, let's go back to the module's operating principle.

This module uses the jQuery TOC.js library, and as such the generation of the summary is done once the page is rendered, which allows a great flexibility on the coverage of the desired content, unlike for example a solution based on a text format. This approach allows you to target any part of a page from a CSS selector (the Container in the configuration options), whether this page is generated from simple fields, or less simple ones like paragraphs for example.

The other configuration options allow you to specify the behavior of the generated summary, including

  • the CSS selectors to target to generate the summary titles
  • The minimum number of titles to generate an automatic summary. It is not necessarily appropriate to generate a summary when an article has only two unfortunate sections.
  • The type of HTML list to generate
  • The possibility of inserting a Return to Summary link on each content title that has been the target of the automatic summary
  • Enabling soft scrolling to the content part by clicking on a title in the summary
  • The ability to highlight the summary titles if their corresponding section in the article arrives in the visible area when scrolling the page
  • The ability to make the summary stick to the page scrolling (useful if you want the summary, placed in a sidebar, to always be visible when scrolling a page)

And finally, if you activate the included Toc.js per node submodule, you also have the possibility to enable / disable the automatic summary in a granular way, content by content.

Once configured, the positioning of the automatic summary can be easily configured in the display modes of the content type.
 

Toc display settings

You can then use the Core Layout Builder module, or via a Twig template, to customize the positioning of the summary that will be generated.

However, if we can generate an automatic summary here in a few clicks for Drupal 8 content types, this is not (yet) true for any page type (other content entities, custom pages, views, etc.).

To meet this need, rather than a specific integration (for each possible content entity) that could be laborious, the Toc.js module provides a new type of Block that allows us to place and generate a summary this time on any type of page (including also content types), whether it is a taxonomy term page, another content entity, a view, etc. 

Toc.js block

Its configuration options are similar to those available on content types.

Toc.js block settings

And we can obtain, after a few magic CSS rules, a summary of our customized content, depending on the nature of the pages of a project.

Toc.js summary example

Note that the uses of the Toc.js module can be extended to other needs than the generation of an automatic summary or a table of contents. For example, you can easily generate a contextual sub-menu based on the page allowing smooth scrolling navigation. Or generate an automatic menu of a One page site managed on a Drupal website factory for example.

Toc.js menu example

Another way of seeing and finally formatting a summary, simply by extending its scope to all the elements that make up a page and not just those of a content.

Mar 27 2019
Mar 27

Drupal 8 has a multitude of field types to cover a large number of use cases and situations when it comes to structuring and modeling content. Among these, we have a List field type which, as its name suggests, allows us to configure an input field based on a list of predefined options. This list of options must be set manually in the field's storage options at the time of creation.

But we can also use this field based on a list of options that can be provided dynamically. Let's look at how we need to proceed to have a field that allows us to choose from a list of dynamic options.

For the example, we will create a list field that will provide us with a list of all content types available on a Drupal 8 project. The creation of such a field will take place in 4 steps.

  1. Creating the field with an empty list of options
  2. Export of the field configuration
  3. Modification of the field configuration by associating a function to it to provide the list of options
  4. Importing the new field configuration

Creating the List field

Creating such a field is quite simple. Let's add a field called Content type (machine name field_content_type) on a paragraph type for example.

add field list type

Et laissons les options de champs vide.

Field list settings empty

Exporting the field configuration

We can export the configuration of the new field, with the drush cex command, or with the Features module, or using the configuration export interface available natively with Drupal Core.

We get this configuration for our new field

langcode: fr
status: true
dependencies:
  module:
    - options
    - paragraphs
id: paragraph.field_content_type
field_name: field_content_type
entity_type: paragraph
type: list_string
settings:
  allowed_values: {  }
  allowed_values_function: ''
module: options
locked: false
cardinality: 1
translatable: true
indexes: {  }
persist_with_no_fields: false
custom_storage: false

Changing the field configuration

We then edit this field configuration to associate a function on the allowed_values_function parameter. This function will be in charge of providing a dynamic list of possible options, namely the different types of content present on the project.

Our configuration then becomes

langcode: fr
status: true
dependencies:
  module:
    - options
    - paragraphs
id: paragraph.field_content_type
field_name: field_content_type
entity_type: paragraph
type: list_string
settings:
  allowed_values: {  }
  allowed_values_function: my_module_allowed_values_bundle
module: options
locked: false
cardinality: 1
translatable: true
indexes: {  }
persist_with_no_fields: false
custom_storage: false

Of course, we must now create this function in the my_module module.

/**
 * Set dynamic allowed values for the bundle field.
 *
 * @param \Drupal\field\Entity\FieldStorageConfig $definition
 *   The field definition.
 * @param \Drupal\Core\Entity\ContentEntityInterface|null $entity
 *   The entity being created if applicable.
 * @param bool $cacheable
 *   Boolean indicating if the results are cacheable.
 *
 * @return array
 *   An array of possible key and value options.
 *
 * @see options_allowed_values()
 */
function my_module_allowed_values_bundle(FieldStorageConfig $definition, ContentEntityInterface $entity = NULL, $cacheable) {
  $entity_type_id = 'node';
  $options = [];
  /** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entityBundleInfo */
  $entityTypeBundleInfo = \Drupal::service('entity_type.bundle.info');
  $entity_type_bundles = $entityTypeBundleInfo->getBundleInfo($entity_type_id);

  foreach ($entity_type_bundles as $key => $entity_type_bundle) {
    $options[$key] = $entity_type_bundle['label'];
  }
  return $options;
}

Importing the new field configuration

We can then import this new configuration using the drush cim command, or using the Features module or from the configuration import interface, depending on your project, whether it is a simple site or a Drupal 8 web factory.

And the trick is done. Now the configuration of the field indicates that the list of options is provided dynamically.

Liste des types de contenu

And you can now offer a selection from the different content types on the site. This type of implementation is particularly effective when it comes to making components, whatever they are, which can then be used on any type of project, from a simple site, to a complex site or a Drupal 8 web factory.

Mar 13 2019
Mar 13

By default, Drupal 8 has two methods for building the breadcrumb trail. For content, this method is based on the URL of the page, and for taxonomy terms this method is based on the vocabulary hierarchy.

The default construction of a breadcrumb

Let's explore in more detail the construction of the breadcrumb for content.

Let's take an example of a content page with the following URL:

/services/freelance/drupal/webfactory-drupal

The last part of the URL (webfactory-drupal) corresponds to the title of the page. Drupal will then inspect the rest of the URL and for each part look for if a content matches that URL.

So, Drupal will inspect this URL, to see if it matches existing content.

/services/freelance/drupal

If so (let's imagine that a content whose title is Drupal Specialist has this URL), the title of the page is added to the breadcrumb trail.

Then, he inspects this URL, to see if it matches existing content.

/services/freelance

If so (the content title is Freelance Drupal for example), the page title is added to the breadcrumb trail.

And finally Drupal inspects the last part of the URL, to see if it still matches existing content.

/services

The page title (Services) is then added to the breadcrumb trail.

So for this example, if each part of the path corresponds to an existing content page, the breadcrumb trail generated for this URL will be the following.

Home > Services > Freelance Drupal > Drupal specialist > Web factory Drupal

It is thus possible to build a custom-made, relevant breadcrumb trail using this detection by parent path, either by using a manual alias for listing pages, pivot pages or landing pages, or by using the Pathauto module to automatically build a relevant alias for content to be automatically placed in a section of a site (typical example, news, events, services, etc.). 

Note that the generation of the last part of the breadcrumb trail, namely the title of the current page, Web factory Drupal in our example, is the responsibility of the theme. As a general rule, you will find an option in any correct theme that allows you to display or not the title of the current page in the breadcrumb trail. Or it can be done with a simple hook.

/**
 * Implements hook_preprocess_HOOK().
 */
function MY_THEME_preprocess_breadcrumb(&$variables) {
  $request = \Drupal::request();
  $route_match = \Drupal::routeMatch();
  $page_title = \Drupal::service('title_resolver')->getTitle($request, $route_match->getRouteObject());

  $variables['#cache']['contexts'][] = 'url';
  if (count($variables['breadcrumb']) <= 1) {
    $variables['breadcrumb'] = [];
  }
  else {
    $breadcrumb_title = theme_get_setting('breadcrumb_title');
    if ($breadcrumb_title) {
      $variables['breadcrumb'][] = array(
        'text' => $page_title
      );
    }
  }
}

The breadcrumb trail for the pages of taxonomy terms is built according to a different logic: according to the hierarchy of terms and this regardless of the alias used for the pages of terms.

These two methods of breadcrumb generation are the default methods included in Drupal Core. It is of course possible to modify this default behavior by means of contributed modules, such as Breadcrumb Menu for example which generates the breadcrumb depending on the position of the page in the main menu and which in the absence of the page in the menu switches to the default Drupal Core generation, or by means of a custom module.

Customize the breadcrumb trail with a module

Altering the construction of the breadcrumb is done by means of a service tagged with the breadcrumb_builder tag. For example, for example

my_module.term_breadcrumb:
  class: Drupal\my_module\MyModuleTermBreadcrumbBuilder
  arguments: ['@entity_type.manager', '@entity.repository', '@config.factory', '@path.validator', '@path.alias_manager']
  tags:
  - { name: breadcrumb_builder, priority: 1010 }

The priority given to a service of this type makes it possible to order which rules to apply first, the highest priorities being those applied first.

The MyModuleTermBreadcrumbBuilder Class must implement two methods

  • The applies() method that will allow us to indicate when to apply this rule of construction of the breadcrumb
  • The build() method that will build the breadcrumb itself.

So if we want to add a parent to the breadcrumb trail of taxonomy terms pages, for example, our class will look like this.

/**
 * Provides a custom taxonomy breadcrumb builder that uses the term hierarchy.
 */
class MyModuleTermBreadcrumbBuilder implements BreadcrumbBuilderInterface {
  use StringTranslationTrait;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManager
   */
  protected $entityTypeManager;

  /**
   * The entity repository.
   *
   * @var \Drupal\Core\Entity\EntityRepositoryInterface
   */
  protected $entityRepository;

  /**
   * Drupal\Core\Config\ConfigFactoryInterface definition.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected  $configFactory;

  /**
   * The taxonomy storage.
   *
   * @var \Drupal\Taxonomy\TermStorageInterface
   */
  protected $termStorage;

  /**
   * The settings of my module taxonomy configuration.
   *
   * @var \Drupal\Core\Config\Config
   */
  protected $taxonomySettings;

  /**
   * The path validator service.
   *
   * @var \\Drupal\Core\Path\PathValidatorInterface
   */
  protected $pathValidator;

  /**
   * The alias manager.
   *
   * @var \Drupal\Core\Path\AliasManagerInterface
   */
  protected $aliasManager;

  /**
   * MyModuleTermBreadcrumbBuilder constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   * @param \Drupal\Core\Path\PathValidatorInterface $path_validator
   * @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ConfigFactoryInterface $config_factory, PathValidatorInterface $path_validator, AliasManagerInterface $alias_manager) {
    $this->entityTypeManager = $entity_type_manager;
    $this->entityRepository = $entity_repository;
    $this->configFactory = $config_factory;
    $this->pathValidator = $path_validator;
    $this->aliasManager = $alias_manager;
    $this->termStorage = $this->entityTypeManager->getStorage('taxonomy_term');
    $this->taxonomySettings = $this->configFactory->get('my_module.taxonomy_settings');
  }

  /**
   * {@inheritdoc}
   */
  public function applies(RouteMatchInterface $route_match) {
    return $route_match->getRouteName() == 'entity.taxonomy_term.canonical'
      && $route_match->getParameter('taxonomy_term') instanceof TermInterface;
  }

  /**
   * {@inheritdoc}
   */
  public function build(RouteMatchInterface $route_match) {
    $breadcrumb = new Breadcrumb();
    $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
    /** @var \Drupal\taxonomy\TermInterface $term */
    $term = $route_match->getParameter('taxonomy_term');
    $breadcrumb_parent = $this->taxonomySettings->get('vocabularies.' . $term->bundle() . '.breadcrumb_parent');
    if ($breadcrumb_parent) {
      $url = $this->pathValidator->getUrlIfValid($breadcrumb_parent);
      if ($this->pathValidator->isValid($breadcrumb_parent)) {
        $path = $this->aliasManager->getPathByAlias($breadcrumb_parent);
        if(preg_match('/node\/(\d+)/', $path, $matches)) {
          $node = Node::load($matches[1]);
          if ($node instanceof NodeInterface) {
            $node = $this->entityRepository->getTranslationFromContext($node);
            $breadcrumb->addCacheableDependency($node);
            $breadcrumb->addLink(Link::createFromRoute($node->label(), 'entity.node.canonical', ['node' => $node->id()]));
          }
        }
      }
    }

    // Breadcrumb needs to have terms cacheable metadata as a cacheable
    // dependency even though it is not shown in the breadcrumb because e.g. its
    // parent might have changed.
    $breadcrumb->addCacheableDependency($term);
    // @todo This overrides any other possible breadcrumb and is a pure
    //   hard-coded presumption. Make this behavior configurable per
    //   vocabulary or term.
    $parents = $this->termStorage->loadAllParents($term->id());
    // Remove current term being accessed.
    array_shift($parents);
    foreach (array_reverse($parents) as $term) {
      $term = $this->entityRepository->getTranslationFromContext($term);
      $breadcrumb->addCacheableDependency($term);
      $breadcrumb->addLink(Link::createFromRoute($term->getName(), 'entity.taxonomy_term.canonical', ['taxonomy_term' => $term->id()]));
    }

    // This breadcrumb builder is based on a route parameter, and hence it
    // depends on the 'route' cache context.
    $breadcrumb->addCacheContexts(['route']);

    return $breadcrumb;
  }

}

This class largely follows the breadcrumb construction logic provided by Drupal Core, and only adds a parent to the breadcrumb built according to a configuration parameter. This same logic can also be applied to the breadcrumb trail of content pages, in case you want a view for example in the breadcrumb trail, or any other page that is not a content page.

In the end, customizing a breadcrumb trail can be done in many ways, as is often the case with Drupal, but I must admit that finally the default pattern responds to many use cases and is very often sufficient with a configuration zest at the level of generating aliases of the pages of a Drupal 8 project. Finally, we can also note the Custom Menu Breadcrumbs module which allows us to configure a main parent from a menu item for content of a certain type.

Feb 13 2019
Feb 13

The Micro Site module allows you to set up a Drupal web factory, from a single Drupal 8 instance, to power a multitude of sites, of a different nature if necessary. Micro Site provides a new content entity that allows you to customize these different site types at will, using the APIs available on Drupal 8, and modify their behavior according to business needs. Let's detail how we can proceed, through a basic example of providing a modular footer on different powered sites, and display it on all the pages that can make up the site.

This operation can be summed up as a little bit of site building, by adding and configuring a few fields, and a little bit of theming by adapting the general template of the pages of a site to be able to display this footer.

Site building

Add field on site type

For example, on a site type called Generic we can create a new Paragraphs field called Site Footer.

field site footer

Then we can fill in the footer of the page when editing the microsite, from a certain number of configured paragraphs (links type to add a list of links, or text type to put free text, or why not a paragraph to load a registration form, etc.).

Footer exemple

In this example we use paragraphs to feed the footer of the micro site, to allow a great modularity for each micro site to customize its footer according to its needs. But we could just as easily have used simpler, more constrained fields to limit the possibilities that could be filled in the footer. It is here to assess the business needs of each type of site that we want to set up.

Theming

We are not going to announce a great innovation, or a thundering trick, for the future. Once the field (or fields) created that will feed the footer of the micro site, all that remains is to display it at the template level page.html.twig, or on a surcharge specific to site: page--site.html.twig or a surcharge more specific to the site type, for example page--site--generic.html.twig.

First, we implement a prepocess function on the site pages to provide the template with the site_footer variable.

/**
 * Implements hook_preprocess_HOOK().
 *
 * Provide site variables to override footer, menu in the page template
 * dedicated to site entities.
 */
function micro_drupal_preprocess_page(&$variables) {
  $negotiator = \Drupal::service('micro_site.negotiator');
  /** @var \Drupal\micro_site\Entity\SiteInterface $site */
  $site = $negotiator->getActiveSite();
  if ($site instanceof SiteInterface) {
    if ($site->hasField(MicroDrupalConstant::SITE_FOOTER)) {
      if (!$site->get(MicroDrupalConstant::SITE_FOOTER)->isEmpty()) {
        $viewBuilder = \Drupal::entityTypeManager()->getViewBuilder('site');
        $field_footer = $site->get(MicroDrupalConstant::SITE_FOOTER);
        $site_footer = $viewBuilder->viewField($field_footer, 'footer');
        $variables['page']['site_footer'] = $site_footer;
      }
    }
  }
}

Then at the template level page--site.html.twig, we display this new variable.

{% if page.site_footer %}
  <!-- #site-footer -->
  <footer id="site-footer" class="clearfix site-footer-wrapper">
    <div class="site-footer-wrapper-inside clearfix">
      <div class="container">
        <div class="row">
          <div class="col-xs-12">
            <div class="site-footer">
              {{ page.site_footer }}
            </div>
          </div>
        </div>
      </div>
    </div>
  </footer>
  <!-- EOF: #site-footer -->
{% endif %}

And it's over. The rest is just a little bit of styling with some magical CSS rules.

What about a sidebar?

Using the Drupal 8 block positioning system, we can place as many blocks as necessary in a sidebar region, and control visibility by site, or by site type, as needed. But this configuration must then be done at the administrator level of the Drupal Master instance and not at the level of each micro site.

Following the same logic, we can very well set up dedicated fields (modular or not) to feed a sidebar present on each micro site, and then allow this time the administration and management, at each level of a micro site, of the content of these sidebars and control their visibility at this level. The implementation will be slightly more complex, to give full control to an administrator of a micro site on the visibility rules of the created content, but the basic principle remains the same: a touch of site building and a zest of theming to customize as much as necessary a micro site.

Jan 30 2019
Jan 30

Composer has become a must for relatively ambitious Drupal 8 projects. Even if it is still possible to initialize a Drupal 8 project with drush or simply by downloading a zip archive, these two methods can become limiting over time. Or at least not to facilitate the installation of new modules with dependencies on third-party libraries.

Another of the reasons I have encountered, why some Drupal 8 projects have not been initiated by Composer, is the lack of Composer support on some shared hosting, even so-called professional ones.

If starting a project without Composer can give the impression of saving time at the beginning (by bypassing a necessary phase of appropriation of Composer), it can very quickly become time-consuming, and the initial time saving fades away.

There comes the inevitable moment when you have to rely on Composer to simply manage even the installed modules.

Switching an existing Drupal 8 project to a code base managed by Composer can be done relatively easily.

Create a new project based on Composer

First, initialize a new project with the recommended Composer template (and if necessary install Compose first).

composer create-project drupal-composer/drupal-project:8.x-dev NEW_PROJECT --stability dev --no-interaction

This command will create a new NEW_PROJECT directory, and download the latest version of Drupal, and its dependencies

The initialized directory will look like this.

├── config
├── drush
├── scripts
├── vendor
└── web

Install the active contributed modules and theme on the new project

This is the most daunting part of the procedure, especially if the number of activated modules is large. It is necessary to install the different modules or themes contributed, activated on the project, with Composer.

You can list the different modules from the Extensions configuration page, or list the modules with a drush command.

drush8 pml | grep "Enabled"

This allows you to obtain the list of enabled modules, as well as their version.

 Administration       Admin Toolbar (admin_toolbar)                                                       Module  Enabled        8.x-1.24    
 Administration       Admin Toolbar Extra Tools (admin_toolbar_tools)                                     Module  Enabled        8.x-1.24    
 Chaos tool suite     Chaos tools (ctools)                                                                Module  Enabled        8.x-3.0     
 Custom               Linkit (linkit)                                                                     Module  Enabled        8.x-4.3     
 Field types          Datetime (datetime)                                                                 Module  Enabled        8.6.3       
 Field types          File (file)                                                                         Module  Enabled        8.6.3       
 Field types          Image (image)                                                                       Module  Enabled        8.6.3       
 Field types          Link (link)                                                                         Module  Enabled        8.6.3       
 Field types          Options (options)                                                                   Module  Enabled        8.6.3       
 Field types          Text (text)                                                                         Module  Enabled        8.6.3       
 Multilingual         Interface Translation (locale)                                                      Module  Enabled        8.6.3       
 Multilingual         Language (language)                                                                 Module  Enabled        8.6.3       
 Other                Contact storage (contact_storage)                                                   Module  Enabled        8.x-1.0-bet 
 Other                Pathauto (pathauto)                                                                 Module  Enabled        8.x-1.3     
 Other                Redirect (redirect)                                                                 Module  Enabled        8.x-1.3     
 Other                Redirect 404 (redirect_404)                                                         Module  Enabled        8.x-1.3     
 Other                Token (token)                                                                       Module  Enabled        8.x-1.5     
 SEO                  Metatag (metatag)                                                                   Module  Enabled        8.x-1.7     
 SEO                  Metatag: Open Graph (metatag_open_graph)                                            Module  Enabled        8.x-1.7     
 SEO                  Simple XML Sitemap (simple_sitemap)                                                 Module  Enabled        8.x-2.12    
 Spam control         Antibot (antibot)                                                                   Module  Enabled        8.x-1.2     
 Spam control         Honeypot (honeypot)                                                                 Module  Enabled        8.x-1.29    
 Statistics           Matomo Analytics (matomo)                                                           Module  Enabled        8.x-1.7     
 Bootstrap            Bootstrap (bootstrap)                                                               Theme   Enabled        8.x-3.13    

And so for each module present on the existing project, you can install them with the command, for example for the redirect module.

composer require drupal/redirect ~1.3

and this for each module.

Another alternative, instead of launching this command module by module, is to complete the composer.json file with this list of modules, the minimum version to recover, then launch the command

composer update --with-dependencies

In order to update and download all the modules contained in the composer.json file.

Retrieve user files

Once all the active modules have been uploaded to the new Composer-based project, all that remains is to copy the user files (generally the sites/default/files directory), the custom modules if any, as well as the libraries, configure the settings.php file for connection to the database.

And that's it.

Remember to empty the caches before accessing your new project based on Composer, this time in order to rebuild the Module Registry, in case the modules have not been installed in the modules/contrib directory on the existing project.

Versioning the source code of a Drupal project

Depending on your hosting, constraints and deployment process, and if you use git (highly recommended) it may be interesting to version all the Drupal source code. Indeed, by default, the Drupal Composer template excludes the following directories.

# Ignore directories generated by Composer
/drush/contrib/
/vendor/
/web/core/
/web/modules/contrib/
/web/themes/contrib/
/web/profiles/contrib/
/web/libraries/

You can comment on these lines in order to be able to version the entire code base. However, some third-party modules or libraries may not be versioned because the download process will have used a git clone command, and as such will have created a .git directory in the project directory, excluding this directory (by the presence of a git submodule) from your versioned main code database.

In order to be able to version the entire code base, we can slightly modify the behavior of the project's Composer template.

We will modify the file below to add a command allowing us to delete the .git directories present in the 2 main vendor and web directories after each update command made with composer.

scripts/
└── composer
    └── ScriptHandler.php

We add these new methods to the ScriptHandler class

protected static function getDrupalRoot($project_root) {
  return $project_root .  '/web';
}

protected static function getVendorRoot($project_root) {
  return $project_root .  '/vendor';
}

public static function removeGitDirectories(Event $event) {
  $root = static::getDrupalRoot(getcwd());
  exec('find ' . $root . ' -name \'.git\' | xargs rm -rf');
  exec('find ' . $root . ' -name \'.gitignore\' | xargs rm -rf');
  
  $vendor = static::getVendorRoot(getcwd());
  exec('find ' . $vendor . ' -name \'.git\' | xargs rm -rf');
  exec('find ' . $vendor . ' -name \'.gitignore\' | xargs rm -rf');
}

Then in the composer.json file, we modify the scripts section to get

"scripts": {
    "drupal-scaffold": "DrupalComposer\\DrupalScaffold\\Plugin::scaffold",
    "pre-install-cmd": [
        "DrupalProject\\composer\\ScriptHandler::checkComposerVersion"
    ],
    "pre-update-cmd": [
        "DrupalProject\\composer\\ScriptHandler::checkComposerVersion"
    ],
    "post-install-cmd": [
        "DrupalProject\\composer\\ScriptHandler::createRequiredFiles",
        "DrupalProject\\composer\\ScriptHandler::removeGitDirectories"
    ],
    "post-update-cmd": [
        "DrupalProject\\composer\\ScriptHandler::createRequiredFiles",
        "DrupalProject\\composer\\ScriptHandler::removeGitDirectories"
    ]
},

Thus, with each installation or update command launched with composer, any git sub-modules created will then be deleted, allowing the entire source code to be versioned under the same project.

This strategy of whether or not to version the entire code base is not to be applied systematically. It must be evaluated project by project, according to your organization, your deployment processes and the nature of your projects, from a simple Drupal 8 site to a Drupal web factory. Advice from a Drupal expert, or from your DevOps teams if you have one, will certainly not be useless.

Jan 16 2019
Jan 16

It is not uncommon for a Drupal 8 project, because it has structured content, to develop many content types, each with many fields, which are themselves rendered in a different way through no less than many display modes. One of the consequences is that the design phase known as site building can then become extremely time-consuming, or even a source of many small and even more time-consuming omissions to correct / adjust (use of the same fields, associated help text, configuration of the form mode, display modes, etc.). And this can make this design phase appear to be the least motivating, although essential, phase.

For those who have already produced an ambitious Drupal 8 project, this must certainly bring back some memories.

Fortunately, with Drupal 8 we have two modules that allow us to significantly simplify and accelerate this phase. These are the Entity Clone and Field Tools modules. Let us briefly present these two modules to be consumed without moderation.

Entity clone

Entity Clone is a module that allows you to clone any content entity (or almost), including node, taxonomy terms, etc., but also some entity types and therefore content types. Thus, duplicating a complex content type, with many fields, can be done with a simple click, in a few seconds, then allowing to customize on a common and identical basis the different fields of a new type of content.

Its use is very simple. One click on the Clone operation and that's it.

Entity clone operation

In addition to the usual case of accelerating the site building phase of an ambitious project, this module can also be particularly useful in the context of a native multi-site architecture to provide semantically different but extremely similar content types in terms of configuration. And also in the context of a Drupal web Factory based on the Micro Site module, where this module can help us this time to clone the Site content entity, allowing us to create a new site, pre-configured and with existing content, with a simple click.

Its dual use both on content entities (which can save considerable time in terms of initial content production when it is relatively structured and rich, especially with paragraphs) and on entity types makes it particularly useful from all points of view. And to complete the table it should be mentioned that the Entity Clone module (in its dev version to date) is able to clone (or not, depending on the configuration of the module) any entity referenced by the cloned source entity. This is particularly effective for any content using paragraphs. Note the existence of a module, Entity Type Clone, with a more limited functional scope, focused on entity types, which I have not had the opportunity to test yet.

Field tools

The Field Tools module plays in another register. It allows you to duplicate fields, their display configurations, as well as display modes from one content type (or entity type in general) to one or more other bundles. This is particularly useful if you already have the different content types (and therefore cannot clone them) and you want to duplicate a field, a display mode, very quickly to other bundles. 

Its use is also rather intuitive. You can access the configuration page for each entity type from the Clone fields link in the list of available operations (as for the Entity Clone module) or from the Tools tab of the entity type.

Field tools usage

We can select the field(s) to duplicate, then select the different bundles to duplicate these fields to. A dreadful efficiency for anyone who has forgotten to add a field on the fifteen content types of a project.

By way of conclusion

These two modules can be added to your base module list for any Drupal 8 project. They can save you a multitude of clicks, many hours and most certainly also some omissions and errors for those who want to optimize the use of fields by a maximum transversal use through the different entity types of a Drupal 8 project. Whether it is in terms of initial design, or in terms of project maintenance, or even the subsequent exploitation of these fields through a business module. In any case, they have already saved me a lot of time, and certainly also kept the motivation of a Drupal developer intact. 

Dec 18 2018
Dec 18

Drupal 8 is a tool designed to meet the needs of the most ambitious web projects. We hear a lot about the notions of headless, API first, decoupling, etc. that resolutely allow solid architectures for ambitious projects. But this does not mean that Drupal 8 no longer propels more traditional, and sometimes even much less ambitious sites: simple, small, and even large, websites, but for which we want to benefit from the modularity, flexibility and robustness of Drupal.

Drupal 8, an industrialization solution for small or large sites

Drupal is also a solution that offers different possible architectures to industrialize the production of websites. A recurring need, that of being able to generate and manage multiple sites simply and quickly, often comes up again for projects whose ambition is not the complexity of a single site but the simplicity of managing a multitude of sites, more or less complex.

Drupal 8 offers us several possible solutions to industrialize website production: the multi-site architecture native to Drupal 8, and the Domain Access module.

The Drupal native multi-site architecture allows to generate and maintain multiple independent Drupal 8 sites that are based on the same foundation, the same base of the Drupal core and its contributed modules, while Domain Access offers to power different sites from a single Drupal 8 instance by simply separating content by means of access rights. Nevertheless, each of these solutions has its advantages and disadvantages. The maintenance of a multitude of Drupal 8 instances on a multi-site architecture can quickly become complex and time-consuming and one of its advantages, time to market, can be mitigated depending on the more or less complex organization within a structure and its IT. While Domain Access may require many adaptations, or even meet certain limits, as soon as the number of powered sites, or according to the different nature of the desired sites, reaches a certain stage.

After this brief introduction of the available solutions for a Drupal web-factory, the purpose of this post is not to go into the details of each of them, but to present a possible third way, based on the Micro Site module, and its many contributed modules.

Powering a multitude of Micro Sites with Drupal 8

The main idea of the multi-site architecture proposed by the Micro Site module is based on the following main idea:

Be able to publish a new site as easily and quickly as publishing a blog post

In fact, its concept can be compared to the multi-site solution proposed by Typo 3, and perhaps even inspired much of the genesis of the Micro Site module.

Typo 3 multi-sites

Typo 3 allows an administrator, or even a simple webmaster, to simply create a new site, configure its URL and some basic properties (name, logo, etc.), assign users to this new site and delegate rights to them, then simply publish pages linked to this new site to quickly have an autonomous site both in content and management, while being managed and maintained from a single interface.

Like Typo 3's multi-site solution, the Micro Site module is designed to:

  • Enable you to drive and manage hundreds (and more if necessary) of sites within the same Drupal 8 instance
  • Delegate management rights for a Micro Site within a dedicated and simplified administration space
  • Allow content and user sharing between Micro Sites.
  • Minimize adherence to a complex IT infrastructure
  • Allow an online launch, a time to market, extremely reduced
  • And benefit from all the modularity of Drupal to enable you to power Micro Sites with different properties

The Micro Site module (and its peripheral modules) can be used in many ways: dedicated sites for each department or laboratory of a university, event sites, partner sites, brand sites, allowing a federation or association to offer its members a packaged, ready-to-use website while facilitating content sharing and a common user base, any enterprise which wants master its multiple internet presence, and in general, to easily and simply propel and manage a galaxy of websites, from a few dozen to several hundred.

Architecture of the Micro Site module

The architecture of Micro Sites is extremely similar to the Domain Access module in that the Drupal 8 low-level access rights system is used to separate content between Micro Sites, but differs in one fundamental aspect: while Domain Access relies on configuration entities to define and create the different sites powered by a single instance, the Micro Site module relies on a content entity that will be used to provide the URL of the Micro Sites and many other things, such as giving users of a Drupal 8 instance, who are not administrators, the ability to independently create and publish a Micro Site.

By relying on a content entity, Micro Site allows you to natively customize at will, with simple site building and theming, simply by using the modularity offered by Drupal 8, the different types of Micro Site you want to be able to power. So for example:

  • The home page of a micro site is constituted by the content entity Site itself and can therefore be customized at will without any adherence to a general configuration of the Master Drupal instance.
  • By a simple site building and theming game, all the fields added on a Micro Site entity can be used on all the contents constituting the micro site. Thus the footer of a micro Site can be configured in a few clicks, and a template modification, by simply adding a few dedicated fields on the content entity provided by Micro Site.
  • Any customization, complex business need, for a type of Micro Site can be added by simply using the Drupal 8 eco-system. 

Micro Site, the solution that even makes coffee?

Like any industrialization solution, except to be the ultimate universal solution, the grail finally found, which even makes coffee, Micro Site has its strengths and weaknesses compared to other industrialization solutions. If the Drupal 8 native multi-site architecture, because each instance of the website factory is a complete, independent Drupal 8 site, allows you to benefit out of the box from the entire ecosystem of Drupal 8 contributed modules, this is less true with a Micro Site based architecture.

Some modules will be able to run out of the box, without specific integration, due to their nature (a module that provides a specific field widget such as jQuery minicolors, for example, or the Paragraphs module), other modules will require a very light integration, by a simple modification of their configuration, such as the Matomo module, while modules providing content entities (such as the Bibliography & Citation module or Simplenews) will require a more complete integration.

Micro Sites natively have a simplified management of users and their related rights on a Micro Site and its associated content. The idea is to cover 80% of use cases with an immediate, simple and effective solution. Thus a Micro Site has as standard 4 types of users (in addition to the owner of the Micro Site) who are:

  • Administrators: who have all the rights
  • Content managers: who have modification rights on all content linked to a micro site
  • Contributors: who can publish content related to a Micro site, and modify their own content
  • Members: who can simply view unpublished content

Concerning more complex or more specific needs, Micro site is not intended to deal with them natively. But quite simply, these needs can be addressed with a few alterations, or the addition of specific fields, and the application of their business logic by means of a customized module.

Micro Site is therefore not the ultimate solution that will be able to cover all cases of use in a few clicks. On the other hand, it offers you a solid architecture, an administration space and the APIs necessary for any alteration to achieve the desired result. And for the most common uses and needs, it is possible to create and modify your Micro Sites in the same way that you can manage different types of content with Drupal 8.

The Micro Site ecosystem

Micro Site is the core module, providing the Site entities, as well as the main APIs. Nevertheless, with only the Micro Site module, we are only able to create a micro site of the One Page type, a unique page that will be created by the Site entity itself. But several modules extend the capabilities of Micro Sites, allowing to go beyond the framework of a simple One Page site, the main ones being the following.

Micro Node

Micro Node is the module that allows you to integrate node (and thus any content type) within a Micro Site. Thus it allows to configure within a Master instance the different content types that will be available for Micro sites. These parameters can then be modulated for each type of Micro Site. And we can publish content on a Micro Site, or several Micro Sites, or even on all Micro Sites. 

Micro Menu Menu

The Micro Menu module allows you to create a Menu and allocate it exclusively to a Micro Site. In addition, it allows an automatic selection of this menu when editing or creating content, allowing to directly associate a content to an entry in this menu when you are in the context of a Micro Site.

Micro Theme

The Micro Theme module allows you to declare for each activated theme whether it can be used by a Micro Site. By default Micro Sites will use the default theme, but it is then possible to assign each Micro Site a different theme according to the needs. In addition, the Micro Theme module offers an interface to dynamically modify certain colors of the theme, the fonts used, similar to what can be found with the Core Color module. By default, this interface proposes a certain number of colors that can be configured, variables that can be extended, and for which it will be necessary to provide a corresponding CSS file, thus allowing this interface to be adapted to any theme used.

Micro Taxonomy

The Micro Taxonomy module allows to integrate the taxonomy of a Drupal 8 instance with the Micro Sites. On the one hand, by allowing a dedicated vocabulary to be allocated to each Micro Site, but also by making common vocabularies accessible to different types of Micro Sites, allowing them to consume or create taxonomy terms within these common and shared vocabularies.

Micro Path

The Micro Path module allows you to automatically manage aliases of identical URLs on several Micro Sites. Thus two contents published on two different Micro Sites can have the same alias. In addition, this module also allows you to create automatic alias patterns specific to a Micro Site, for the content types available, allowing you to modify automatic alias patterns configured in a general way on the Master instance.

Micro SSO

The Micro SSO module allows to set up an SSO authentication from the master instance to each Micro Site.

Micro User

The Micro User module allows you to control at the level of each Micro Site and/or Master instance which user can connect to a Micro Site or to the Master instance. Its functional scope is awaiting use cases that may require an extension of its functionalities.

Micro Sitemap

The Micro Sitemap module allows you to integrate the Sitemap module into the context of each Micro site, allowing you to customize an automatic site map from each Micro site.

Micro Contact

The Micro Contact module allows to integrate the Drupal 8 Contact module within each Micro Site, with the possibility to configure which contact form, configured on the Master instance, to use for each Micro Site. Note that the use of Webform forms can be done with a simple construction of a Paragraph allowing to load and render a Webform form, and as such a specific integration is not necessary. 

Micro Simple Sitemap

The Micro Simple Sitemap module allows you to generate a sitemap.xml file for each Micro site. Note that this module integrates the Simple Sitemap module in its current version 2.x, and therefore all its parameters made on a master instance, but that it is not excluded to use a simpler method, in an autonomous way, to generate XML sitemap files for each Micro Site.

Micro Bibcite

The Micro Bibcite module allows you to integrate the Bibliography & Citation module with a Micro Site. To date, only the Reference content entities provided by this module are supported.

In the end, these different modules make it possible to publish micro sites that can meet many editorial needs.

Brief overview of the functionalities of a Micro Site

As a preamble, due to the Bypass content access control permission automatically assigned to the User-1, it is advisable not to use this specific user in managing a Drupal instance with the Micro Site module, and therefore the special Administrator role. Otherwise, this user will have the unpleasant surprise of seeing all the content published on all the Micro Sites and all this happily mixed up. You must therefore create a new Role, to which you can assign all rights, except the Bypass content access control right, and assign this role to an administrator. This until this issue Remove the special behavior of uid #1 has been resolved.

General configuration

The Micro Site module provides the basic architecture to create and publish a new site, with a new Site content entity. It also provides a general configuration of the Master instance. The first action to be performed will be to globally configure the base URL of the Master instance that powers the Micro Sites, as well as the public URL of this instance (which can be the same as the base URL).

Micro site Settings

We can globally configure what content types will be available for use by Micro Sites. These parameters can be modulated by type of Micro Site.

Micro node settings

We can also configure which vocabularies can be used by Micro Sites. These parameters can also be modulated by type of Micro site.

Micro taxonomy settings

Creating Micro Site Types

Then it is possible to create different types of sites (One Page, Generic, Event, Department, Department, Brand, etc.) to be able to configure them differently.

Micro Site Types

Each Site type can be configured differently, depending on the options offered by the contributed modules, such as Micro Node or Micro Taxonomy.

Micro Site Type Configuration

Thus it is of course possible to add as many fields as necessary on a site type, just like a content type, but also to configure some general options that will then be available for all Micro Sites of this type that will be created. We can configure for each type of site:

  • The automatic creation of a dedicated menu for each Micro site
  • The automatic creation of a dedicated vocabulary reserved exclusively for each Micro Site created
  • The possibility to manage users and their different profiles on each Micro site of this type
  • The different types of content present on the master instance that can be used by each Micro Site
  • The different vocabularies present on the master instance that can be used by each Micro site

Management and creation of Micro Sites

Micro Sites can then be created and managed as the content types of a Drupal 8 project.

Sites list

A Micro Site has two statuses: Registered and Published. 

The Registered status will validate the URL of the Micro Site, as well as the presence of a valid Virtual Host. As soon as a Micro Site is Registered, any access to the Micro Site automatically returns to the URL of the Micro Site. The Registered status conditions access to the menus for managing and creating content associated with a Micro Site. In other words, it is impossible to start creating content associated with a Micro Site until it has been registered.

The Published status is more classic. An unpublished site will only be visible by its author, or its members (and if they have the appropriate permission), as well as any content associated with an unpublished Micro Site.

Management of a Micro site

The main idea here is that a micro site can be administered completely independently of the Master instance by simple users. Ultimately, a manager of a micro site may not even know that his site is hosted on a master Drupal instance. A Micro Site has an administration area accessible from the Local Tasks on the Micro Site home page.

Site local tasks

Thus we can customize at will the different screens that can constitute the administration of a Micro Site with regard to the different fields that have been attached to it.

In the example below, the default form for a Micro Site allows you to modify and view the following elements:

  • The name and email address of the Micro Site
  • The status of the Micro Site
  • The different fields allowing to fill in the top of the Micro Site page, its Home Page and its Footer (the use of paragraphs allows to have a great flexibility as for the content of these different elements)
  • Metatag informations
  • The URL of the Micro site
  • The Micro Site Owner
  • The Micro site's Logo and Favicon

Site edition default form

And another form mode, called Configuration, has been created and configured (see Provide a custom form mode to Drupal 8 entities) to group other fields to configure Micro Site behavior. For example below:

  • Micro Site users
  • Matomo / Piwik configuration for this Micro Site
  • Asset management for this Micro Site (i.e. the ability to write some specific CSS rules for this Micro Site)

Site edit configuration

The Micro Menu module has added a tab to manage the Micro Site menu entries directly from the Micro Site itself, without having to go through the standard Drupal administration menu interface.

Menu Micro Site

The Micro Node module has added a Content tab to manage all the content associated with the Micro Site, in the same way as on a traditional Drupal 8 instance.

Site content

The Micro Taxonomy module has added a Taxonomy tab to manage the vocabulary dedicated to the Micro Site, but also all the taxonomy terms of the shared vocabularies associated with the Micro Site.

Site taxonomy

Finally, we have a last tab Parameters which can be used by any contributed module to add a specific configuration screen to a Micro site. For example below the possibility to create a new automatic alias pattern specific to a Micro Site.

Site settings pattern

This quick overview of the administration space of a Micro Site shows us that a user can thus administer and manage the contents of a Micro Site directly from this space, without needing an administration access to any parameter on the Drupal Master instance. At least on the essential and necessary functions of managing a Micro site.

The initial design of a Micro Site aims to allow the publication and management of countless relatively simple websites, with a dedicated management space allowing the most common website management operations to be carried out quickly. A Micro site only brings together in a single space a subset of Drupal's administration functions. And it allows us to use natively, in the context of a Micro Site, the entities provided by Drupal, with little or no alteration. We can thus benefit from all the power of Drupal to meet more advanced needs if necessary. Need a multilingual Micro Site for example? It is simply accessible without effort, simply by activating multilingualism on the Master instance.

Is Micro Site usable in production?

Most of the modules presented here are still in alpha version. There is certainly still a lot of work to be done, both in terms of consolidation of APIs, the different use cases to be tested, and correct coverage in terms of automated tests, which is essential before we can switch to beta version. And any help will certainly be welcome to get through this landing.

Personally I already use them on two different projects with very good feedback. The functional coverage of the modules of this eco-system already covers many common needs. But there are certainly use cases not yet covered that will require more or less complex integration.

Some interesting aspects of Drupal still require integration, such as block management and placement, within a Micro Site. This function can of course be managed at the Drupal Master instance level itself using the visibility parameters provided by Micro Site, but the question arises as to whether or not this administration function can be delegated within a micro site. However, the use of Paragraphs, or even the new Layout Builder, makes it possible to meet many layout needs as of now. With a relevant site building zest, some retouching on theming and templates adapted to the different site types and the choice of a content architecture based on Paragraphs, countless possibilities are already within reach without having to make major changes to other modules.

In any case, for any multi-site project considered on a Drupal 8 base, Micro Site is an additional option to be seriously considered. As an illustration, you can get a quick idea of the possibilities offered by Micro Site to build a Drupal 8 website factory which already propels some sites of different nature such as this one Freelance Drupal and others. 

Dec 11 2018
Dec 11

Like display modes that allow you to display an entity in multiple ways, Drupal 8 allows you to create multiple form modes that can be used on entities, whether they are users, taxonomy terms, contents or any custom entity. Let's discover here how to use these form modes, from their creation to their use to customize the input of a user's information, for example.

The creation of form modes is quite simple and can be done in a few clicks, from the administration interface (from the path /admin/structure/display-modes/form).

Drupal 8 form modes

Let's add a new form mode that we will call for example Profil.

Form mode Profil

And the User entity now has a new Profile form mode, in addition to the existing Register form mode (used for the registration form on a Drupal 8 site).

And we find our new form mode on the configuration page of the forms display (path /admin/config/people/accounts/form-display) of Drupal users.
 

Profil form

That we can activate so that we can then configure which fields will be rendered in this form mode. For example, we can configure this form mode to fill only the First name, Last name, Organization and Picture fields that have been created for the User entity.

Form profil configuration

So far so good. But how do we use our new form mode? From which path?

To finalize this we will use a small module that we will call my_module.

This module will allow us to declare our new form mode for the User entity, and to create a route, as well as a menu, which will allow us to access and complete our form.

First, let's declare this form mode and associate a Class with it, from the file my_module.module.

/**
 * Implements hook_entity_type_build().
 */
function my_module_entity_type_build(array &$entity_types) {
  $entity_types['user']->setFormClass('profil', 'Drupal\user\ProfileForm');
}

Here we associate the default form class User ProfileForm with our profil form mode. We could just as easily have used a Class MyProfilCustomForm by extending the Class AccountForm.

All we have to do now is create a route, from the file my_module.routing.yml, and we can then access our form.

my_module.user.profile:
  path: '/user/{user}/profil'
  defaults:
    _entity_form: 'user.profil'
    _title: 'Profil'
  requirements:
    _entity_access: 'user.update'
    _custom_access: '\Drupal\my_module\Access\MyModuleUserAccess::editProfil'
    user: \d+
  options:
    _admin_route: FALSE

From the route declaration, we specify the path (/user/{user}/profile), the form mode to be used for the User entity, specify route access rights (the right to modify a user, as well as customized permissions if necessary), and can also specify whether the route corresponds to an administration route, or not, to define the theme under which the form will be rendered (backoffice or frontoffice).

To finalize our new form mode, we will create a dynamic menu entry in the user account menu, in order to give an access link to users or administrators. In the file my_module.links.menu.yml, let's add an entry to create the corresponding menu link.

my_module.user.profil:
  title: 'Profil'
  weight: 10
  route_name: my_module.user.profil
  base_route: entity.user.canonical
  menu_name: user-account
  class: Drupal\my_module\Plugin\Menu\ProfilUserBase

What is notable here, in this menu entry, is the class property that will allow us to define the dynamic {user} parameter of the route corresponding to this menu entry. 

This ProfileUserBase Class will only return the ID of the accessed user, if the menu link is displayed on the user's page, or return the ID of the connected user if it is not, otherwise.

<?php

namespace Drupal\my_module\Plugin\Menu;

use Drupal\Core\Menu\MenuLinkDefault;
use Drupal\Core\Url;
use Drupal\user\UserInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Menu\StaticMenuLinkOverridesInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;

/**
 * Profile Menu Link
 */
class ProfilUserBase extends MenuLinkDefault implements ContainerFactoryPluginInterface {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManager
   */
  protected $entityTypeManager;

  /**
   * The current route match service.
   *
   * @var \Drupal\Core\Routing\CurrentRouteMatch
   */
  protected $currentRouteMatch;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * Constructs a new MenuLinkDefault.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $static_override
   *   The static override storage.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   * @param \Drupal\Core\Routing\RouteMatchInterface $current_route_match
   *   The current route match service.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, StaticMenuLinkOverridesInterface $static_override, EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $current_route_match, AccountInterface $current_user) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $static_override);
    $this->entityTypeManager = $entity_type_manager;
    $this->currentRouteMatch = $current_route_match;
    $this->currentUser = $current_user;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('menu_link.static.overrides'),
      $container->get('entity_type.manager'),
      $container->get('current_route_match'),
      $container->get('current_user')
    );
  }

  public function getRouteParameters() {
    return ['user' => $this->getUserIdFromRoute()];
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    return ['user', 'url'];
  }


  /**
   * Get the Account user id from the request or fallback to current user.
   *
   * @return int
   */
  public function getUserIdFromRoute() {
    $user = $this->currentRouteMatch->getParameter('user');
    if ($user instanceof AccountInterface) {
     return $user->id();
    }
    elseif (!empty($user)) {
      $user = $this->entityTypeManager->getStorage('user')->load($user);
      if($user instanceof AccountInterface) {
        return $user->id();
      }
    }

    return $this->currentUser->id();
  }

}

And now you can use your new form mode that you can customize at will, either from the graphical interface or from a customized Form Class allowing you to introduce any business and/or complex logic easily.

Finally, I can't conclude this post on the Drupal 8 form mode without mentioning the Form Mode Manager module which can allow you to do all this without the need for a Drupal developer. Depending on your needs and the level of expertise you want, you can choose one or the other of these solutions. But this may be the subject of another blog post.

Oct 11 2018
Oct 11

In some cases, it can be extremely interesting to be able to override a configuration dynamically. One of the first use cases immediately noticeable is in the case of a site factory with a set of shared and deployed features, and therefore identical configurations shared. And this on a website factory that is based on the native multi-site architecture of Drupal 8, or based on modules like Domain or Micro site that can host multiple sites from a single instance Drupal 8.

The immediate benefit is to be able to dynamically use and configure the contributed Drupal modules in the context of an instance deployed on a site factory, and thus to benefit from all the work already done, while modifying only certain configuration elements of the module to adapt it to the context where it is used.

Let's move on to practice, on a concrete example. Imagine in the context of a site factory, a galaxy of sites with a set of shared taxonomy vocabularies. From a maintenance point of view, the advantage is to be able to maintain and modify any configuration related to these vocabularies on all the sites that use it. From the point of view of each site, the major issue is that the vocabulary name (and therefore its possible uses in field labels, automatically generated aliases, etc.) can and must differ.

We have several methods to overload the vocabulary name for each site. The simplest method, in the case of a multi-site architecture is to override it from the settings.php file of each instance.

$config['taxonomy.vocabulary.VOCABULARY_ID']['name'] = 'NEW_VOCABULATY_LABEL';

And it's over. But this method requires access to the server, and is therefore not administrable from a site instance itself by an administrator or a webmaster. In addition, this method can only be used in the context of a website factory based on Drupal's multi-site architecture, and not in the context of a Domain or Micro site based website factory, since both architectures actually use one and the same Drupal 8 instance.

A second method is to provide a configuration form that will be responsible for changing the vocabulary labels for each site instance.

For example

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

    foreach ($this->vocabularies as $id => $label) {
      $original_label = $this->configFactory->get('taxonomy.vocabulary.' . $id)->getOriginal('name', FALSE);
      $form[$id] = [
        '#type' => 'fieldset',
        '#title' => $label . ' (original : ' . $original_label . ')',
      ];
     
      $form[$id][$id . '_label'] = [
        '#type' => 'textfield',
        '#title' => $this->t('Label'),
        '#maxlength' => 64,
        '#size' => 64,
        '#default_value' => ($this->settings->get('vocabularies.' . $id . '.label')) ?: $label,
      ];

      $options_widget = [
        '' => $this->t('Default'),
        'entity_reference_autocomplete' => $this->t('Autocompletion (with automatic creation)'),
        'options_buttons' => $this->t('Checkboxes (without automatic creation)'),
        'options_select' => $this->t('Select list (without automatic creation)'),
      ];
      $form[$id][$id . '_widget'] = [
        '#type' => 'select',
        '#title' => $this->t('Widget'),
        '#options' => $options_widget,
        '#default_value' => ($this->settings->get('vocabularies.' . $id . '.widget')) ?: '',
      ];
      
    }

    return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    parent::submitForm($form, $form_state);

    foreach ($this->vocabularies as $id => $label) {
      $this->settings->set('vocabularies.' . $id . '.label', $form_state->getValue($id . '_label'));
      $this->settings->set('vocabularies.' . $id . '.widget', $form_state->getValue($id . '_widget'));
    }

    // We need to store here the node types and vocabularies because we can not
    // call the entity type manager in the TaxonomyConfigOverrides service
    // because of a circular reference.
    $this->settings->set('node', $this->getNodeTypeIds(FALSE));
    $this->settings->set('taxonomy_vocabulary', $this->getVocabularyIds(FALSE));

    $this->settings->save();
    drupal_flush_all_caches();
  }

}

As you can see, we can override a lot more than the simple label of a vocabulary. For example the widget used in content editing forms. And many other things.

Then once this form in place and its configuration elements saved, we just need to create a service that will implement the service config.factory.override.

services:

  MY_MODULE.taxonomy_overrider:
    class: \Drupal\MY_MODULE\TaxonomyConfigOverrides
    tags:
      - {name: config.factory.override, priority: 300}
    arguments: ['@config.factory']

And we create the class TaxonomyConfigOverrides

<?php

namespace Drupal\MY_MODULE;

use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigFactoryOverrideInterface;
use Drupal\Core\Config\StorageInterface;

/**
 * Override Taxonomy configuration per micro site.
 *
 * @package Drupal\MY_MODULE
 */
class TaxonomyConfigOverrides implements ConfigFactoryOverrideInterface {

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;
  
  /**
   * An array of all the vocabulary ids.
   *
   * @var array
   */
  protected $vocabularies;

  /**
   * An array of all the node type ids.
   *
   * @var array
   */
  protected $nodeTypes;

  /**
   * The settings of the factory taxonomy configuration.
   *
   * @var \Drupal\Core\Config\Config
   */
  protected $taxonomySettings;

  /**
   * Constructs the object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   */
  public function __construct(ConfigFactoryInterface $config_factory) {
    $this->configFactory = $config_factory;
    $this->taxonomySettings = $this->configFactory->get('MY_MODULE.taxonomy_settings');
    $this->vocabularies = $this->getVocabularyIds();
  }

  /**
   * {@inheritdoc}
   */
  public function loadOverrides($names) {
    $overrides = [];

    foreach ($this->vocabularies as $vocabularyId) {
      $label = $this->taxonomySettings->get('vocabularies.' . $vocabularyId . '.label');
      if (empty($label)) {
        continue;
      }

      $vocabulary_config_id = $this->getVocabularyConfigName($vocabularyId);
      if (in_array($vocabulary_config_id, $names)) {
        $overrides[$vocabulary_config_id]['name'] = $label;
      }

    }

    return $overrides;
  }

  /**
   * @param $vocabularyId
   * @return string
   */
  protected function getVocabularyConfigName($vocabularyId) {
    return "taxonomy.vocabulary.{$vocabularyId}";
  }

  /**
   * Get an array of vocabulary id.
   *
   * @return array
   *   An array of vocabulary id.
   */
  protected function getVocabularyIds() {
    return $this->taxonomySettings->get('taxonomy_vocabulary') ?: [];
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheSuffix() {
    return 'config_taxonomy_label';
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheableMetadata($name) {
    $meta = new CacheableMetadata();
    $meta->addCacheableDependency($this->taxonomySettings);
    return $meta;
  }

  /**
   * {@inheritdoc}
   */
  public function createConfigObject($name, $collection = StorageInterface::DEFAULT_COLLECTION) {
    return NULL;
  }

}

And so you can now share a common shared configuration on a multitude of sites, while allowing everyone to customize at will different configuration elements according to their needs.

Override vocabulary settings

Of course, this method must be used according to the needs and architecture of the site factory. Using such a method in the context of a multi-site architecture for anything and everything is not necessarily the best way. Neither the most optimum. When it can be enough simply to provide initial configuration elements, for this or that module, by the code and then let him live his life within the instance of the website factory.

Do not hesitate to seek advice from a Drupal 8 developer who can define with you the best strategy to ensure that your site factory has all the assets to evolve with confidence, and to gain maturity in the long term. 

Sep 26 2018
Sep 26

As we say in terms of computer programming, only two things are extremely complex: naming variables and invalidating the cache. Drupal 8 has an automatic caching system activated by default that is truly revolutionary, which makes it possible to offer a cache for anonymous visitors and especially for authenticated users without any configuration. This cache system is based on three basic concepts:

  • The cache tags
  • The context cache
  • Cache duration (max-age)

Tag caches allow you to tag content, pages, page elements with very precise tags allowing to easily and accurately invalidate all pages or page elements that have these caches tags. Context caches allow you to specify the criteria by which the cache of a page can vary (by user, by path, by language, etc.) while the max-age property can be used to define a maximum duration of cache validity.

But the purpose of this post is not to go into the details of this cache system, but rather to illustrate the use of the Cache API to set up its own cache for a specific use case. Imagine the need to build a hierarchical tree for a user, a tree based on a very prolific taxonomy. Once the hierarchical tree built, with all the included business data, satisfied, you can finally consult the fruit of your work ... after a waiting time of 17s ... Ouch.

temps d'attente 16s

In order to remedy this small inconvenience, we will set up a specific cache for our business needs and use the Cache API.

Setting up a cache is relatively easy. We can also use the default cache provided by Drupal 8 (the cache.default service), or declare and use our own cache, which will then have a dedicated table but which can also be managed separately, especially if we want to put this cache on a third party service such as Redis or Memcache.

To declare our cache, let's create the my.module.services.yml file in the directory of our my_module module.

services:

  my_module.my_cache:
    class: Drupal\Core\Cache\CacheBackendInterface
    tags:
      - { name: cache.bin }
    factory: cache_factory:get
    arguments: [my_cache]

And we declare our cache whose identifier will be my_cache.

Then within our Controller, we have to condition the computation intensive construction of our hierarchical tree to the existence or not of our cache. Which can be translated by these additional lines:

$cid = 'my_hierarchical_tree:' . $user->id();
$data_cached = $this->cacheBackend->get($cid);
if (!$data_cached) {
  // Extensive calcul...

  // Store the tree into the cache.
  $this->cacheBackend->set($cid, $data, CacheBackendInterface::CACHE_PERMANENT, $tags);
}
else {
  $data = $data_cached->data;
  $tags = $data_cached->tags;
}

To have a complete Controller that could look like this example.

<?php

namespace Drupal\my_module\Controller;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Controller\ControllerBase;

/**
 * Class HomeController.
 */
class MyModuleController extends ControllerBase {

  /**
   * The cache backend service.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cacheBackend;


  /**
   * Constructs a new HomeController object.
   */
  public function __construct(CacheBackendInterface $cache_backend) {
    $this->cacheBackend = $cache_backend;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('my_module.my_cache')
    );
  }

 /**
   * Build the hierarchical tree.
   *
   * @return array
   *   Return the render array of the hierarchical tree.
   */
  public function buildTree() {
    $user = $this->userStorage->load($this->currentUser->id());
    $argument_id = $this->getDefaultArgument($user);
 
    $cid = 'my_hierarchical_tree:' . $user->id();
    $data_cached = $this->cacheBackend->get($cid);

    if (!$data_cached) {
      $data = $this->getTree('VOCABULARY', 0, 10, $argument_id);
      $this->addUserStatusToTree($data['response'], $user);
      $this->addUserLevels($data['response']);

      $tags = isset($data['#cache']['tags']) ? $data['#cache']['tags'] : [];
      $tags = Cache::mergeTags($tags, [$cid]);

      // Store the tree into the cache.
      $this->cacheBackend->set($cid, $data, CacheBackendInterface::CACHE_PERMANENT, $tags);
    }
    else {
      $data = $data_cached->data;
      $tags = $data_cached->tags;
    }

    $build = [
      '#theme' => 'user--tree',
      '#user' => $user,
      '#data' => $data,
      '#cache' => [
        'tags' => $tags,
        'context' => ['user'],
      ],
    ];

    return $build;
  }

}

And of course it remains to invalidate the cache only on some very specific actions, actions that by their nature will modify the tree cached.

Cache::invalidateTags(['my_hierarchical_tree:' . $user->id()]);

And then we can get a more than correct result compared to the initial waiting time divided by about 8 (the times are here extracted from a development instance)

Temps d'attente de 2s

The performance improvement is very clear here, but when invalidating the cache then the user will have to wait a while so the data is rebuilt and then cached.

We can still improve this behavior, according to the project's business constraints: is it acceptable for a user to consult data that we know to no longer be valid, and therefore to use an invalid cache, and according to some conditions and duration? . If the answer is positive, then we can still improve our cache system, using the cache that we know is invalid.

The idea here is pretty simple. If the business data cache is invalid, then we can decide to deliver despite this data while triggering, in the background and therefore not perceptible by the user, a query that will recalculate the data.

$cid = 'my_hierarchical_tree:' . $user->id();
$data_cached = $this->cacheBackend->get($cid, TRUE); // Get the cached data even if invalid.
if (!$data_cached->valid) {
  $this->queueWorker->addItem(['user' => $user->id()]);
}

if (!$data_cached) {
  // Extensive calcul...

  // Store the tree into the cache.
  $this->cacheBackend->set($cid, $data, CacheBackendInterface::CACHE_PERMANENT, $tags);
}
else {
  $data = $data_cached->data;
  $tags = $data_cached->tags;
}

To do this we use a TRUE parameter to allow us to recover an invalid cache, and if so, we delegate to a QueueWorker plugin the task of recalculating the data during the next cron.

We could also handle this very well just as we invalidate the cache and delegate to a Queue the reconstruction of the data in the background. This would even allow us to recalculate the data even before the user comes to consult his profile.

And in this way the problem of a calculation time too long can be solved in a sustainable way, with some compromises. It's just a matter of finding a fair compromise depending on the project and its constraints, and a Drupal 8 developer can help you to distinguish the ins and outs of each solution, or even suggest other possible optimization paths, perhaps by going back even at the root cause of the root cause of an issue encountered.

Jul 12 2018
Jul 12

Drupal 8 provides an API, with EntityQuery, which significantly simplifies writing SQL queries to retrieve and list a set of contents. Thus it is very easy to retrieve a list of contents according to complex criteria and conditions, without needing to know precisely the tables and their syntax for each field associated with an entity.

For example, in a few lines we can recover all content that meets several conditions (here for example the contents referencing a set of instruments and corresponding to certain skills).

$query = $this->nodeStorage->getQuery()
  ->condition('status', 1)
  ->condition('type','CONTENT_TYPE_NAME')
  ->range(0, 20)
  ->sort('created', 'DESC');
if ($instrument_ids) {
  $query->condition(Const::NODE_INSTRUMENT, $instrument_ids, 'IN');
}
if ($skill_ids) {
  $query->condition(Const::NODE_LEVEL_SKILLS, $skill_ids, 'IN');
}
$result = $query->execute();

EntityQuery reaches certain limits if for example you need to retrieve information stored on another entity, and therefore another table. For example, if for obvious performance reasons you have created a custom entity to store all notes received by a content, and you now want to retrieve the list of contents sorted by rating, EntityQuery will not allow us to get to our purposes.

In these cases, we use the good old db_select() or its equivalent on Drupal 8 \Drupal::database()->select(). We will then be able to build our SQL query, perform all necessary joins, and at the same time use expressions to delegate to the database calculations that might be more complex elsewhere.

So to pick up the example above, our request would become

$query = $this->database->select('node_field_data', 'n');
$query->leftjoin('node__' . Const::NODE_INSTRUMENT, 'i', 'i.entity_id = n.nid');  // Joining with the NODE_INSTRUMENT field table
$query->leftjoin('node__' . Const::NODE_LEVEL_SKILLS, 'l', 'l.entity_id = n.nid'); // Joining with the NODE_LEVEL_SKILLS field table
$query->fields('n', ['nid']);
$query->condition('n.status', 1);
$query->condition('n.type', 'CONTENT_TYPE_NAME');
if ($instrument_ids) {
 $query->condition('i.' . Const::NODE_INSTRUMENT . '_target_id', $instrument_ids, 'IN');
}
if ($skill_ids) {
  $query->condition('l.' . Const::NODE_LEVEL_SKILLS . '_target_id', $skill_ids, 'IN');
}
$query->orderBy('created', 'DESC'); // For the moment we only sort by date of publication
$query->range(0, Const::HOME_MAX_ITEM_PER_LIST);
$result = $query->execute()->fetchAllKeyed(0, 0);

Query less intuitive to write than the version with EntityQuery. But all the interest now lies in the ability to add a new join on a personal entity that holds all the ratings assigned to each content.

Addition that takes place in a few lines. We add a join to the table of our entity (e_score) storing the notes, then adding an SQL expression we can automatically calculate the average of the notes and use this result to sort the results of our query.

$query = $this->database->select('node_field_data', 'n');
$query->leftjoin('e_score', 's', 's.node_id = n.nid');  // We add a join on the e_score table of our entity.
$query->leftjoin('node__' . Const::NODE_INSTRUMENT, 'i', 'i.entity_id = n.nid');
$query->leftjoin('node__' . Const::NODE_LEVEL_SKILLS, 'l', 'l.entity_id = n.nid');
$query->fields('n', ['nid']);
$query->condition('n.status', 1);
$query->condition('n.type', 'CONTENT_TYPE_NAME');
if ($instrument_ids) {
  $query->condition('i.' . Const::NODE_INSTRUMENT . '_target_id', $instrument_ids, 'IN');
}
if ($skill_ids) {
  $query->condition('l.' . Const::NODE_LEVEL_SKILLS . '_target_id', $skill_ids, 'IN');
}
$query->addExpression('AVG(s.score)', 'score'); // We calculate the average of each note on the table of the entity.
$query->groupBy('n.nid'); // And this average is calculated by grouping all the results by noted content.
$query->orderBy('score', 'DESC');  // We can now sort the results by the average of their scores.
$query->range(0, Const::HOME_MAX_ITEM_PER_LIST);
$result = $query->execute()->fetchAllKeyed(0, 0);

Thus we can now sort the results of the query according to the average of the scores obtained by each content, using the MySQL avg() function. We can use in the addition of the expression, here, any type of calculation (count(), sum(), and many other calculation or selection, etc) that the database is able to realize natively. Be careful, however, because then our query becomes linked to the database depending on the functions it offers and is no longer agnostic. But this is a minimal risk or at least that can be easily mastered by any Drupal Developer 8 or whatever.

Note that we can use the OR / AND condition groups on this type of query, and can chain them together, as on EntityQuery-based queries. For example, we add a group of OR conditions that contain a number of AND condition groups. Specifically all content that has the same level of competence as the level of the user on this skill. 

$query = $this->database->select('node_field_data', 'n');
$query->leftjoin('wp_score', 's', 's.node_id = n.nid');
$query->join('node__' . Const::NODE_INSTRUMENT, 'i', 'i.entity_id = n.nid');
$query->join('node__' . Const::NODE_LEVEL_SKILLS, 'l', 'l.entity_id = n.nid');
$query->join('node__' . Const::NODE_SKILLS_MAIN, 'm', 'l.entity_id = n.nid'); // We add a new join.
$query->fields('n', ['nid']);
$query->condition('n.status', 1);
$query->condition('n.type', 'element');
$query->condition('i.' . Const::NODE_INSTRUMENT . '_target_id', [$instrument_id], 'IN');

$condition_or = $query->orConditionGroup(); // We create a group of OR conditions.
$user_global_skills = $this->getUserGlobalSkills($user, FALSE); // We recover some properties of the user.
foreach ($user_global_skills as $type_skill => $score_skill) {
  $skill_level_id = $this->getUserLevelSkillId((float) $score_skill);
  if ($skill_level_id) {
    $skill_type_id = isset(Const::GLOBAL_SKILLS[$type_skill]) ? Const::GLOBAL_SKILLS[$type_skill] : '';
    $condition_and = $query->andConditionGroup(); // Nous créons un groupe de conditions AND.
    $condition_and->condition('l.' . Const::NODE_LEVEL_SKILLS . '_target_id', [$skill_level_id], 'IN');
    $condition_and->condition('m.' . Const::NODE_SKILLS_MAIN . '_target_id', [$skill_type_id], 'IN');
    $condition_or->condition($condition_and); // We add this group of AND conditions to the main group of OR conditions.
  }
}
$query->condition($condition_or); // And we finally add the OR condition group to the query.
$query->addExpression('AVG(s.score)', 'score');
$query->groupBy('n.nid');
$query->orderBy('score', 'DESC');
$query->range(0, Const::HOME_MAX_ITEM_PER_LIST);
$result = $query->execute()->fetchAllKeyed(0, 0);

In the end, even if they can be a little less readable, the requests based on a select() are equally powerful. Especially since the groups of conditions can also be used on it, to be able easily and legibly to chain complex conditions. And be able to return to it months later without too much...bitterness.

May 24 2018
May 24

May 2, 2018 Google has announced a major policy change regarding the use of its online services, including its popular mapping service Google Maps and all its associated APIs, to embed or generate location-based information. This policy change now pays for a service that was previously available for free under some relatively generous quota limits starting June 11, 2018. Please read this post for full details on this policy change and its implications. : Do not be evil...until...

This change to the Google Maps usage rules is a cruel reminder that Google's business model can be very similar, to quote the article cited above, with the crack dealer business model... the first doses are free and it becomes ruinous when you're addicted.

Even though most of the websites that I can make, or maintain, often have moderate use of Google Maps, and would most likely remain below the limit of the new monthly quota, the fact remains that this policy change puts you in a situation that you can not control once your bank details filled in order to continue the services of Google.

It is time to stop using these solutions, out of habit or out of ease, and to see what is doing very well elsewhere.

Let's look at how on Drupal 8 we can switch to an open source solution based on Leaflet and OpenStreetMap. We have several solutions with Drupal to implement a geolocation or mapping service.

On the one hand we can use modules Geofield, Geocoder and Leaflet. But these modules are very agnostic and are generally used already with OpenStreetMap.

On the other hand we have the Geolocation module which is a real toolbox and an extremely complete solution, almost out of the box, and actively maintained. It is also one of the most used modules for generating maps with Drupal 8. And it also has some advantages that make it particularly flexible and versatile, such as the possibility of overloading from the content creation page the various settings of the map configured globally at the level of the field itself. In short, Geolocation can be considered as an all-in-one solution, compared to the Geofield and Leaflet modules, which takes care of storing geographic data (Geofield) and displaying it (Leaflet).

Only small problem with Geolocation, This is a module heavily oriented on Google Maps, or even exclusively oriented Google Maps ...

.. At least until version 2.x of the module under development (a beta version is available). Indeed, with version 2 of the Geolocation module, we now also have the support of OpenStreetMap and Leaflet to generate beautiful open source maps. And this without almost any effort.

And with this addition, which was accompanied by a deep redesign of the module, changing the mapping system to leave the shores of Google and boarding with OpenStreetMap is a breeze. A big thanks to the maintainer of this module @christianadamski.

Migrate from Google Maps to OSM with Geolocation

Une carte GoogleMaps

Let's take a closer look at how to switch our maps generated with Google Maps to use Leaflet and OpenStreetMap.

We will take as support, to illustrate the few manipulations to make, a classic configuration, namely a Geolocation field created on a content type, or a paragraph, to locate a point, an address on a map. Note, as I write these lines, I invite you to use the development version, until the next version 2.0-beta2 is released with all the corrections made since the beta 1.

For users of Geolocation 8.1.x you can upgrade to 2.x without worry. there is no change in storage or data model. We can launch the module upgrade serenely with composer.

composer require drupal / geolocation: 2.x-dev

Once the source code is updated, you can start an update of the database. This update included with version 2.x is limited to just activate its sub-module Geolocation Google Maps by default, because now all features related to Google Maps have been integrated into this new sub module.

And we do not fail to activate the Geolocation Leaflet sub-module to make our transition smoothly.

The switch to OpenStreetMap can be done in a few clicks. We just need to select the Geolocation Leaflet - Geocoding and Map widget in the form view configuration, and then the Geolocation Leaflet - Map formatter for the display configuration.

widget leaflet

Let's configure the widget and the formatter of our Geolocation field. We can note that, besides the standard configuration elements such as the marker information, the position, the zoom level, we find the possibility of overriding the configuration of the widget from the content creation form if necessary.

Formatter Leaflet

Et hop. That's it. We now have a new map based this time on Leaflet and OpenStreetMap, without the necessary data recovery, since in the end we only changed the way the geographic data stored by the module is rendered.

Carte OSM

Thanks to the Geolocation module, in version 2.x, we can switch to an open source cartographic solution very simply and almost effortlessly. Admittedly, there is not (yet) all the functional wealth of Google Maps in the widget configuration or the display settings, but the essential is present. And I do not doubt that the module will get rich very quickly.

And you, have you already made the transition to OpenStreetMap?

Apr 19 2018
Apr 19

If we do not have (yet) a contributed module (commerce_stock should be available soon), Drupal Commerce 2 already has an API to set up an inventory control, with the Availability Manager service.

This is a collector service that will collect any services declared on the commerce.availability_checker tag. Its operating principle is for the moment quite simple. If a service that implements the AvailabilityCheckerInterface Interface returns FALSE, the product will no longer be considered available.

Let's look at how to set up a very simple stock control, based on a field (for example field_stock) that has been added on a product.

From our MY_MODULE module, declare our service using the file MY_MODULE.services.yml.

services:

  my_module.availability_product_variation:
    class: Drupal\my_module\AvailabilityProductVariation
    arguments: ['@entity_type.manager', '@cerema_commerce.core']
    tags:
      - { name: commerce.availability_checker, priority: 100 }

Then declare our Class AvailabilityProductVariation.

<?php

namespace Drupal\my_module;

use Drupal\commerce\Context;
use Drupal\commerce\PurchasableEntityInterface;
use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\commerce\AvailabilityCheckerInterface;

/**
 * Provides an availability checker that removes variations that are no longer available.
 */
class AvailabilityProductVariation implements AvailabilityCheckerInterface {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;


  /**
   * Constructs a new AvailabilityOrderProcessor object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The availability manager.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public function applies(PurchasableEntityInterface $entity) {
    if ($entity->hasField('field_stock')) {
      $stock = $entity->field_stock->value;
      if (is_null($stock)) {
        return FALSE;
      }
      return TRUE;
    }
    return FALSE;
  }

  public function check(PurchasableEntityInterface $entity, $quantity, Context $context) {
    $stock = (integer) $entity->{CeremaCommerceInterface::STOCK_FIELD_NAME}->value;
    $quantity = (integer) $quantity;
    if ($quantity > $stock) {
      return FALSE;
    }
  }
  
}

This service must implement 2 methods:

  • applies() which will determine whether or not the entity must be verified
  • check() who will actually check the availability of the product.

With this simple service, we then have control over the products added to the Cart according to a dedicated Stock field.

All that remains is to adjust the stock for each product order by subscribing to events propagated by an order, including the event when an order is placed, commerce_order.place.post_transition and the event when an order is cancelled, commerce_order.cancel.post_transition, for example.

We can now also use the AvailabilityManager service directly on the add to cart form to react to the availability of a product on the add to cart button. For example, we can alter this form with these few lines that will disable the add to cart button and display a warning message below.

/** @var \Drupal\commerce\AvailabilityManagerInterface $availabiltyManager */
$availabiltyManager = \Drupal::service('commerce.availability_manager');

$entityTypeManager = \Drupal::entityTypeManager();
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$order = $entityTypeManager->getStorage('commerce_order')->create(['type' => 'default']);
$store = $entityTypeManager->getStorage('commerce_store')->loadDefault();
$context = new \Drupal\commerce\Context($order->getCustomer(), $store);

if (!$availabiltyManager->check($purchased_entity, '1', $context)) {
  $form['actions']['submit']['#attributes']['disabled'] = TRUE;
  $form['actions']['submit']['#suffix'] = '<div class="description upper small out-stock">' . t('Out of stock') . '</div>';
}

It's also difficult to introduce simple stock management with Drupal Commerce 2 without mentioning a stock management module available on GitHub that looks promising and operational right now. But the generic track to be favored over the long term remains undoubtedly the contributed module commerce_stock as soon as it will be finalized.

Inventory management can vary infinitely on an e-commerce project. Should we block orders once the stock is finished, continue to record pending orders, while warning the customer? What to do under a certain limit reached? Send an alert to the manager? Show a message to visitors? If your business needs for inventory management remain standard, they will most likely be covered by the commerce stock module. And even if it were not the case, the API available will address the most atypical cases with the help of a Drupal 8 developper.

Finally, to conclude, do not hesitate to follow this issue Finalize the availability manager service which should introduce substantial modifications to the possibilities offered natively by this API.

Apr 04 2018
Apr 04

Drupal Commerce 2 allows to define out of the box multiple checkout flows, allowing to customize according to the order, the product purchased, the customer profile this buying process and modify it accordingly. This is an extremely interesting feature, in that it can simplify as much as necessary this famous checkout flows. Do you sell physical (and therefore with associated delivery) and digital (without delivery) products? In a few clicks you can have two separate checkout flows that will take into account these specificities.

On the other hand, during my first contact with Drupal Commerce 2, I was somewhat taken aback by the fact that an order type could only be associated with one and only one checkout flow. And that a product type could only be associated with one and only one order type. As a result, one product type could only be associated with one and only one checkout flow. Diantre! But how to make products that can have physical and digital variations (like books for example, with a paper version and a digital version) ?

Sélection d'un tunnel d'achat sur le type de commande

Which checkout flow to associate with an order type that can correspond to physical or digital products? Should we multiply the order types accordingly? What impact on the catalog architecture?

As you have understood, this has raised many questions.

A default purchase tunnel

Fortunately, I misinterpreted this setting of the checkout flow on the order type. After analysis, it became clear to me that it was not a question here of setting up a single checkout flow for an order type, but to define the checkout flow that would be used by default for this order type.

Indeed, as for the prices, the taxes, the order elements, the shop, etc. Drupal Commerce 2 uses the Resolver concept to determine which checkout flow to use. And because of this, Drupal Commerce 2 makes it possible to address multiple needs very easily, while offering a standard operation as soon as it is installed.

Thus, the determination of a checkout flow to be used for an order is made during the first entry of a cart (or a draft order) in the checkout flow.

/**
 * {@inheritdoc}
 */
public function getCheckoutFlow(OrderInterface $order) {
  if ($order->get('checkout_flow')->isEmpty()) {
    $checkout_flow = $this->chainCheckoutFlowResolver->resolve($order);
    $order->set('checkout_flow', $checkout_flow);
    $order->save();
  }

  return $order->get('checkout_flow')->entity;
}

Indeed, as can be seen, when entering into the checkout flow, if the order does not yet have an associated checkout flow, then it is called the Resolver chainCheckoutFlowResolver->resolve() which then determines which checkout flow to use and then saves it on the order.

Changing a checkout flow for a given order then becomes child's play.

A dynamic checkout flow

To determine in a very granular way which checkout flow to use for each order, it is then enough to carry out two operations.

  • When adding or deleting a product to the cart, simply reset the checkout flow associated with the order (because as we have seen, the latter is determined only if it has not already been associated and registered with an order)
  • Create a service that will implement the Resolver commerce_checkout.checkout_flow_resolver, with a higher priority than the service responsible for determining the default checkout flow (set to -100)

Let's move on to practice.

In our module named MY_MODULE, let's create a EventSubscriber service that will subscribe to add and delete products to a command to reset the purchase tunnel on the order.

In the directory of our module, we create the file my_module.services.yml and declare our service my_module.cart_update_subsciber.

services:

  my_module.cart_update_subscriber:
    class: Drupal\my_module\EventSubscriber\CartEventSubscriber
    arguments: ['@entity_type.manager']
    tags:
      - { name: event_subscriber }

  my_module.checkout_flow_resolver:
    class: Drupal\my_module\Resolver\CheckoutFlowResolver
    tags:
      - { name: commerce_checkout.checkout_flow_resolver, priority: 100 }

We also take this opportunity to create our my_module.checkout_flow_resolver service which will dynamically determine which checkout flow to use.

The first step is to ensure that the checkout flow associated with an order is reset when adding or deleting a product from the order with our Class CartEventSubscriber.

<?php

namespace Drupal\my_module\EventSubscriber;

use Drupal\commerce_cart\Event\CartEntityAddEvent;
use Drupal\commerce_cart\Event\CartEvents;
use Drupal\commerce_cart\Event\CartOrderItemRemoveEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CartEventSubscriber implements EventSubscriberInterface {

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events = [
      CartEvents::CART_ENTITY_ADD => ['onCartEntityAdd', -50],
      CartEvents::CART_ORDER_ITEM_REMOVE => ['onCartOrderItemRemove', -50],
    ];
    return $events;
  }

  /**
   * Resets the checkout flow status when an item is added to the cart.
   *
   * @param \Drupal\commerce_cart\Event\CartEntityAddEvent $event
   *   The cart event.
   */
  public function onCartEntityAdd(CartEntityAddEvent $event) {
    $cart = $event->getCart();
    if ($cart->hasField('checkout_flow')) {
      $cart->set('checkout_flow', NULL);
    }
  }

  /**
   * Resets the checkout flow status when an item is removed from the cart.
   *
   * @param \Drupal\commerce_cart\Event\CartOrderItemRemoveEvent $event
   *   The cart event.
   */
  public function onCartOrderItemRemove(CartOrderItemRemoveEvent $event) {
    $cart = $event->getCart();
    if ($cart->hasField('checkout_flow')) {
      $cart->set('checkout_flow', NULL);
    }
  
    // If an order item is removed, this can be the only shippable item in the
    // cart. So we reset the shipmemnt order when on order item is removed. The
    // customer could already go to the checkout and get a shipment. And if so
    // then he can have a free order but with a shipment and so a shipping amount.
    /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface[] $shipments */
    $shipments = $cart->get('shipments')->referencedEntities();
    foreach ($shipments as $shipment) {
      $shipment->delete();
    }
    $cart->set('shipments', NULL);
  }

}

Note here that in addition to the need to reset the checkout flow during these two events, we also delete any shipment item that could have been associated with an order. Indeed, in this example, if we want to determine different checkout flows depending on whether or not the order requires a shipment (and associated fees), we must ensure that if an order had shipment items, those they are recalculated if necessary in the checkout flow (the typical case is an order which have a physical product to be delivered, which would have gone through the checkout flow for the first time without reaching its conclusion, and for which the customer changes his mind to return to the cart and delete this physical product, thereby making his order without further shipment necessary).

Once this step has been completed, all that remains is to create our Resolver, which can dynamically determine which checkout flow to use.

<?php

namespace Drupal\my_module\Resolver;

use Drupal\my_module\MyModuleInterface;
use Drupal\commerce_checkout\Entity\CheckoutFlow;
use Drupal\commerce_checkout\Resolver\CheckoutFlowResolverInterface;
use Drupal\commerce_order\Entity\OrderInterface;

class CheckoutFlowResolver implements CheckoutFlowResolverInterface {

  /**
   * {@inheritdoc}
   */
  public function resolve(OrderInterface $order) {

    // Free product flag.
    $free = TRUE;
    // Immaterial product.
    $numerical = TRUE;

    if (!$order->getTotalPrice()->isZero()) {
      $free= FALSE;
    }

    // Have we a shippable product in the order ?
    foreach ($order->getItems() as $item) {
      $purchased_entity = $item->getPurchasedEntity();
      if ($purchased_entity->hasField(MyModuleInterface::WEIGHT_FIELD_NAME)) {
        if (!$purchased_entity->get(MyModuleInterface::WEIGHT_FIELD_NAME)->isEmpty()) {
          /** @var \Drupal\physical\Measurement $weight */
          $weight = $purchased_entity->get(MyModuleInterface::WEIGHT_FIELD_NAME)->first()->toMeasurement();
          if (!$weight->isZero()) {
            $numerical = FALSE;
            break;
          }
        }
      }
    }

    // If we only have product free and without weight. So go to the
    // direct checkout flow.
    if ($free && $numerical) {
      return CheckoutFlow::load('direct');
    }
    // If numerical product but non free, go to to the download checkout
    elseif (!$free && $numerical) {
      return CheckoutFlow::load('download');
    }

  }

}

Here, in this example, we determine if the order contains free products and / or containing a (non-empty) weight which therefore requires shipment. You can of course customize as much as you need, depending on your project, the determination of the checkout flow. The possibilities are endless, at your fingertips.

Thus in a few lines, we have just defined three types of checkout flows that can be used in a granular way for each order according to the elements of this order. Note here that we do not determine the checkout flow to use unless the order contains only digital and/or free products. In the opposite case, then we leave the hand to the default Resolver which will determine the checkout flow compared to the setting made on the order type configuration.

This example shows us how it can be easy, with the help of a Drupal 8 developer, to set up specific process with Drupal Commerce 2.

A modular design of Drupal Commerce 2

This introduction to checkout flows, finally, could be duplicated to many configuration elements of a Drupal Commerce 2 website. The configuration of a standard catalog, from the products, orders to order's items, the determination of the product price, a shop (in the case of a marketplace with multiple shops) is actually not a fixed configuration in the marble, but a configuration of the default behavior for an online store. This behavior can then be altered according to business needs in a simple and robust way.

An undeniable asset when it comes to setting up an e-commerce site with the logic and needs of the most classic to the most unusual.

Mar 08 2018
Mar 08

We have several solutions to automatically send emails emitted by a Drupal 8 project in HTML format. Without being able to quote them all, we can use SwiftMailer, MimeMail to send mails from the server itself, or Mailjet API, MailGun, etc. to send emails from a third-party platform. In a few clicks, we can then emit different emails, whether they are transactional (account creation, order creation, subscription, etc.) or business (Newsletters, Activity Log, What you missed, etc. .), in HTML format. It will then remain to implement one (or more) responsive email template that will be correctly read and interpreted on most mail software. And it's probably the most important part.

But some of these solutions can postulate that the content of these emails must be considered healthy and secure. And to do this these solutions check that the email's content corresponds to a checked markup, filtered and expurgated from possible XSS injections or whatnot. And if not, then they apply a filter that transforms any HTML tag into text, rendering your HTML formatting inoperative. This is the case for example of the SwifMailer module.

Deuce.

For example, you can find this little treatment on the content of an email, before formatting or rendering.

/**
 * Massages the message body into the format expected for rendering.
 *
 * @param array $message
 *   The message.
 *
 * @return array
 *   The message array with the body formatted.
 */
public function massageMessageBody(array $message) {
  // Get default mail line endings and merge all lines in the e-mail body
  // separated by the mail line endings. Keep Markup objects and escape others
  // and then treat the result as safe markup.
  $line_endings = Settings::get('mail_line_endings', PHP_EOL);
  $message['body'] = Markup::create(implode($line_endings, array_map(function ($body) {
    // If $item is not marked safe then it will be escaped.
    return $body instanceof MarkupInterface ? $body : Html::escape($body);
  }, $message['body'])));
  return $message;
}

Quite simply, if the content of the email is not a Markup instance (and thus filtered and expurgated from potentially dangerous strings), then we escape all the HTML tags.

Fortunately, most content that is sent by email come usually from the content of the site itself, content that will be passed through the caudine forks, and intractable, of Drupal 8, and will be presented to the mail formatting system as being verified markup and therefore healthy.

There remains a special case: that of all the emails emitted according to the different events related to the life of a user account (Creation, Waiting for approval, Deletion, Welcome Mail, etc.). All these little emails that we can configure from the Drupal 8 account management interface.

Gestion des emails utilisateur

And because these different messages are related to the configuration of a Drupal 8 site, they are saved as a simple string of characters.

And when they come to the formatting system sending emails, as the content is a simple string of characters, so unverified, the formatting applies its security measure and escapes all the HTML tags that you could have used. So must we have to resort to a Drupal 8 expert here to solve this thorny problem?

Fortunately we have a simple solution. Before the string is sent to the plugin responsible for formatting the email, we just need to intervene upstream, to transform these simple character strings to an instance of MarkupInterface.

Using a small module, implement hook_mail_alter() :

/**
 * Implements hook_mail_alter().
 */
function MYMODULE_mail_alter(&$message) {
  switch ($message['key']) {
    case 'page_mail':
    case 'page_copy':
    case 'cancel_confirm':
    case 'password_reset':
    case 'register_admin_created':
    case 'register_no_approval_required':
    case 'register_pending_approval':
    case 'register_pending_approval_admin':
    case 'status_activated':
    case 'status_blocked':
    case 'status_canceled':
      $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed; delsp=yes';
      foreach ($message['body'] as $key => $body) {
        $message['body'][$key] = new FormattableMarkup($body, []);
      }
      break;

  }
}

And for each of the transactional emails related to the life of a user account, we transform these strings of their body into an instance of FormattableMarkup. Note in passing that we can also change the email header to directly specify that the mail will be in HTML format, making it unnecessary to use a module just for that. But it is quite rare on a Drupal 8 project to only have to deal with this mail types.

A simple but effective solution for sending basic emails from a Drupal 8 site in an acceptable format. A small hook that any Drupal 8 developer can put in his basic package. 

Feb 14 2018
Feb 14

By default, Drupal Commerce 2 provides an activity log on the life of each order: the add to cart event, the checkout entry, the order placed, its possible shipment and its conlusion.

Each status of the command corresponds to an entry in a log that is generated. This provides a complete history for each order.

Default order activity

Having needed to customize this activity log, to allow managers of the online store to enter in this log various comments related to the management of the order, I discovered on this occasion the small module, used to generate this log, developed by the maintainers of Drupal Commerce 2. A small but extremely powerful module, titled Commerce log. My favorite modules.

We will discover how to use this module to insert additional log entries. These entries can be generated automatically as well as correspond to an user input.

The Commerce log module

This module declares 2 Plugin types : LogCategory and LogTemplate. The first allows us to declare log categories, while the second allows us to create log templates, which we will assign to previously created categories. Its operation is extremely similar to State Machine: no GUI (hence also less code, and therefore maintenance), and thus creation / declaration of the plugins using YAML configuration files.

But because a good example is sometimes better than a long speech, let's get right to the point right away. We will allow order managers to leave comments in this activity log.

A customized activity journal

Let's create a small MYMODULE module and declare our Log template right away.

The structure of our module will look like this

├── MYMODULE.commerce_log_categories.yml
├── MYMODULE.commerce_log_templates.yml
├── MYMODULE.info.yml
├── MYMODULE.install
├── MYMODULE.module
 

The category of a Log allows us to specify for which entity type the Log will be generated. Here we declare the MYMODULE_order category which will concern the Drupal Commerce Orders entities:

MYMODULE_order:
  label: Order comment
  entity_type: commerce_order

Then we declare our template, here order_comment that we assign to our category. Here we could have used the category already created by the commerce log module.

order_comment:
  category: commerce_order
  label: 'Comment'
  template: "<p><b>{{ 'Comment'|t }} :</b> {{ comment }}</p>"

The key information of the LogTemplate Plugin is its template that is declared. It's actually a simple Twig inline template. Here we declare our inline template which will use a variable comment.

And it's done !

Well almost. It only remains to generate our log entries at the right time. Since we want to record comments from the online store managers input, we will intervene before the order is saved.

We take care to create beforehand a simple text field, for example field_order_note, on the order entity type, and polish a little the order edit form to obtain this result.

Order form comment

Then we implement hook_entity_presave(), to intervene before saving an order. We will simply check if this field is filled in, and if necessary we will generate the corresponding log entry.

/**
 * Implements hook_entity_presave().
 */
function MYMODULE_entity_presave(EntityInterface $entity) {
  if ($entity instanceof OrderInterface) {
    if ($entity->hasField('field_order_note')) {
      if (!$entity->get('field_order_note')->isEmpty()) {
        // Get the comment, clear it from the order, and log it.
        $comment = $entity->get('field_order_note')->value;
        $entity->set('field_order_note', '');
        /** @var \Drupal\commerce_log\LogStorageInterface $logStorage */
        $logStorage = \Drupal::entityTypeManager()->getStorage('commerce_log');
        $logStorage->generate($entity, 'order_comment', ['comment' => $comment])->save();
      }
    }
  }
}

The key method here for generating the log entry is the generate() method from the LogStorage class.

This method expects as arguments, the entity itself, the identifier of the template to use, and finally an array of parameters to pass to the Twig inline template. We can therefore pass as many parameters as necessary to the Twig template to generate the log entry.

And then we can from the edit form, enter a comment.

un commentaire sur une commande

And we now find this note in the order activity log.

Un commentaire dans le journal de la commande

To go further with Commerce Log

Commerce log is used to generate the Drupal Commerce Order Activity Log. But its design allows us to generate logs for any entity type of Drupal 8. For example generate activity logs for some users, depending on some actions performed on the contents of a project, according to conditions based for example on user roles and / or common taxonomy terms. As well, the Message stack covers this need, but if you already have Drupal Commerce 2 on the project, it may not be useful to install these other modules for this. Give a try with commerce log. You will be surprised. But this will be the subject of another ticket.

Feb 01 2018
Feb 01

It is not uncommon when a multilingual Drupal 8 project is implemented, that the pages translations are not ready at the time of production. If making a Drupal 8 multilingual site has become really easy to implement, there is now a fundamental difference with Drupal 7: it is impossible to disable a language. Either install a language, or delete it. This change is assumed, and voluntary, so as to not generate unnecessary complexity for questionable gain.

But what to do, after having configured, translated the interfaces and looked after a whole multilingual project, when comes the production and no content is translated?

Delete untranslated languages? And thus lose all the translation of the project configuration? It's an option.

Leave the pages untranslated accessible to users and search bots? This can give a real bad image of the site. What is this site that offers several languages ​​but is not translated. Not to mention the system url that will inevitably be present, nor SEO impacts (even if we can reduce them significantly by adding on these pages the metatag noindex). And more generally an unfinished aspect. And do not try to hide urls, they will inevitably be found.

There is a third alternative, softer. Leave in place the languages ​​installed on the site, and simply prevent visitors from accessing it, by redirecting to the default language page. While allowing publishers to access and translate these pages. It can also give content managers time to translate an entire site while it is online.

Let's look at how to implement this solution.

We will create an EventSubscriber in a custom module my_module. We declare in the file my_module.services.yml this service which we call my_module.language_access.

my_module.language_access:
  class: Drupal\my_module\EventSubscriber\LanguageAccessSubscriber
  arguments: ['@current_user', '@language_manager']
  tags:
    - { name: event_subscriber }

Note that we load as arguments for this service, the service allowing us to load the current user (@current_user), and the service managing the site languages (@language_manager).

We have declared our class LanguageAccessSubscriber. Let's look at the code right away.

<?php

namespace Drupal\my_module\EventSubscriber;

use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Drupal\Core\Routing\RouteMatch;
use Symfony\Component\HttpFoundation\RedirectResponse;

/**
 * Class Subscriber.
 *
 * @package Drupal\my_module
 */
class LanguageAccessSubscriber implements EventSubscriberInterface {

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * Constructor.
   *
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   the language manager.
   */
  public function __construct(AccountInterface $current_user, LanguageManagerInterface $language_manager) {
    $this->currentUser = $current_user;
    $this->languageManager = $language_manager;
  }

  /**
   * {@inheritdoc}
   */
  static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST] = ['onRequestRedirect'];
    return $events;
  }

  /**
   * This method is called whenever the kernel.request event is
   * dispatched.
   *
   * @param GetResponseEvent $event
   */
  public function onRequestRedirect(GetResponseEvent $event) {
    $request = $event->getRequest();

    if ($this->currentUser->hasPermission('access non default language pages')) {
      return;
    }

    // If we've got an exception, nothing to do here.
    if ($request->get('exception') != NULL) {
      return;
    }

    $default_language = $this->languageManager->getDefaultLanguage();
    $active_language = $this->languageManager->getCurrentLanguage();

    if ($active_language->getId() != $default_language->getId()) {
      $route_match = RouteMatch::createFromRequest($request);
      $route_name = $route_match->getRouteName();
      $parameters = $route_match->getRawParameters()->all();
      $url = Url::fromRoute($route_name, $parameters, ['language' => $default_language]);
      $new_response = new RedirectResponse($url->toString(), '302');
      $event->setResponse($new_response);
    }
  }

}

This service declared as an Eventsubscriber (see the tags in the service declaration) will subscribe to the event kernel.request, event triggered very early during the propagation of a request. And with each request, this service will execute the onRequestRedirect() method.

If the current user has permission (custom permission to create in your custom module) to access all languages, then we do nothing.

if ($this->currentUser->hasPermission('access non default language pages')) {
  return;
}

Then, after having recovered both the active language and the default language, if they differ, we proceed to a temporary redirection (302) to the requested page but in the version of the default language of the site. For this, we create a RouteMatch object from the Request Symfony available from with the service, and generate a URL from that route by passing the default site language as an additional parameter.

$url = Url::fromRoute($route_name, $parameters, ['language' => $default_language]);

And it remains only to redirect to this new url.

$new_response = new RedirectResponse($url->toString(), '302');
$event->setResponse($new_response);

Of course you can just as easily decide to redirect to another page, or generate a 403 page or 404. It is according to your needs or your criteria.

In a few lines, we can now keep all the languages ​​installed on the Drupal 8 project, give time to translate them correctly, and this without impacting the site brand or its SEO.

To conclude, it is useful to exclude these untranslated pages from your sitemap.xml files if you generate one. Don't forget them. There is no need to tell search engines the existence of pages to index and for which they will be returned to their equivalent in the default language of the site.

A page-by-page redirection?

And if we needed to give access to content pages, as soon as they are translated. This is the same principle. It would be enough then to recover in the route parameters the entity in question (for example nodes), to check that the entity does indeed have a translation in the requested language, with the method hasTranslation(), and if not, proceeding again to a redirect. All waiting strategies are possible here.

A module ?

Would not there be space here for a module contributed? A motivated Drupal 8 developer?

Dec 13 2017
Dec 13

Drupal Commerce 2 now allows you to manage the various taxes and VAT to apply to an online store, regardless of its country and their respective rules in this area. Most of the contributed modules managing these elements on Commerce 1.x are therefore no longer necessary. Let's find out how to use the Drupal Commerce 2.x Resolver concept to set the VAT rate to apply to different products.

Creating a Tax Type

Before determining which VAT rate we wish to apply to this product or that product type, we must first configure the tax type that will be available for our online store.

Drupal commerce now includes in its core all the management and tax detection stuff. And it also includes taxes types already set, such as the (complex) European Union VAT, including the different rates applicable per country.

Drupal commerce TVA en France

Thus the addition of a tax is done in a few clicks. Just create a tax type, and select the Plugin corresponding to the European Union VAT (in our case), and voila.

Création d'un type de taxe

And to determine the VAT rates applicable to the online store, simply set your online store by specifying for which country it will apply the tax rules.

Drupal commerce store tax settings

Here by specifying France, the VAT rates that will be automatically associated with the online store will be those of the same country. After this very brief introduction to the initial setting up of a Drupal Commerce 2.x online store, let us come to the heart of the subject namely determining the VAT rate to apply depending on the type of product.

The concept of Drupal Commerce 2 Resolver

Drupal Commerce 2.x uses the concept of Resolver, which is neither more nor less than service collectors. To dynamically determine (and also very easily alterable) the tax rate applicable to a product, the order type corresponding to a product, the checkout flow type corresponding to an order, the price corresponding to a product, etc.

In short for each of these actions / reactions we have a service collector that will collect all the services of a certain tag, ordered by priority and then test them one by one, until a service returns a value. And if simply no service is present (or does not return value), then the Resolver implemented by default by Drupal commerce core will play its role.

Thus each declared Resolver service must implement a mandatory resolve() method, which the service collector (or ChainTaxResolver) will evaluate.

/**
 * {@inheritdoc}
 */
public function resolve(TaxZone $zone, OrderItemInterface $order_item, ProfileInterface $customer_profile) {
  $result = NULL;
  foreach ($this->resolvers as $resolver) {
    $result = $resolver->resolve($zone, $order_item, $customer_profile);
    if ($result) {
      break;
    }
  }

  return $result;
}

And as far as the VAT rate is concerned, Drupal Commerce implements a default Tax Resolver by means of this below service whose priority is set to -100.

services:
  commerce_tax.default_tax_rate_resolver:
    class: Drupal\commerce_tax\Resolver\DefaultTaxRateResolver
    tags:
      - { name: commerce_tax.tax_rate_resolver, priority: -100 }

This service determines the tax rate fairly simply.

/**
 * Returns the tax zone's default tax rate.
 */
class DefaultTaxRateResolver implements TaxRateResolverInterface {

  /**
   * {@inheritdoc}
   */
  public function resolve(TaxZone $zone, OrderItemInterface $order_item, ProfileInterface $customer_profile) {
    $rates = $zone->getRates();
    // Take the default rate, or fallback to the first rate.
    $resolved_rate = reset($rates);
    foreach ($rates as $rate) {
      if ($rate->isDefault()) {
        $resolved_rate = $rate;
        break;
      }
    }
    return $resolved_rate;
  }

}

It simply returns a tax rate that is set by default by the corresponding Plugin, or if no rate is set by default, simply the first rate of those corresponding to the tax zone.

This is a basic rule, which can very easily be sufficient for an e-commerce solution that only sells products whose VAT is the default of the country concerned.

But if several VAT rates are eligible according to the product (reduced VAT rate for services, VAT rate corresponding to cultural products, etc.), then we can very easily determine which VAT rate to apply according to rules that can rely on any product's attributes, or even the customer profile.

Determine a VAT rate with Drupal Commerce 2

To vary an applicable VAT rate according to a product, or a product type, we just need to implement a service that will declare the trade_tax.tax_rate_resolver tag with a priority at least higher than the default TaxResolver provided by Drupal Commerce.

Declare this service.

services:
  my_module.product_tax_resolver:
    class: Drupal\my_module\Resolver\ProductTaxResolver
    tags:
      - { name: commerce_tax.tax_rate_resolver, priority: 100 }

We give it a priority of 100 so that it is evaluated before the default Resolver.

For then, we can evaluate the tax rate to be applied using the resolve() method.

<?php

namespace Drupal\my_module\Resolver;

use Drupal\commerce_product\Entity\ProductInterface;
use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\commerce_tax\Resolver\TaxRateResolverInterface;
use Drupal\commerce_tax\TaxZone;
use Drupal\profile\Entity\ProfileInterface;

/**
 * Returns the tax zone's default tax rate.
 */
class ProductTaxResolver implements TaxRateResolverInterface {

  /**
   * {@inheritdoc}
   */
  public function resolve(TaxZone $zone, OrderItemInterface $order_item, ProfileInterface $customer_profile) {
    $rates = $zone->getRates();

    // Get the purchased entity.
    $item = $order_item->getPurchasedEntity();
    
    // Get the corresponding product.
    $product = $item->getProduct();

    $product_type = $product->bundle();

    // Take a rate depending on the product type.
    switch ($product_type) {
      case 'book':
        $rate_id = 'reduced';
        break;
      default:
        // The rate for other product type can be resolved using the default tax
        // rate resolver.
        return NULL;
    }

    foreach ($rates as $rate) {
      if ($rate->getId() == $rate_id) {
        return $rate;
      }
    }

    // If no rate has been found, let's others resolvers try to get it.
    return NULL;
  }

}

In this example, we simply check the associated product type, and if it is a book product (a book eligible for the 5.5% VAT rate), then we simply return the corresponding VAT rate. And for all other products, eligible for the default VAT rate, we let the default Resolver do its work.

We can see here that we could have just as well evaluated a VAT rate based for example on an attribute of the product, and not globally for a product type. This service collector based approach allows us to implement business rules that can be complex in a few lines in an extremely simple and modular way.

To your Resolver !

As mentioned above, Resolver are used extensively on Drupal Commerce 2.x. You want to vary the checkout flow according to the products of an order. Take a Resolver. You want to calculate the price of a product dynamically, according to specific business rules, draw another Resolver. You want to dynamically vary the order type by product, yet another Resolver.

The Resolver will open perspectives to see contributed modules blooming whose primary purpose will be to offer a configuration interface on predefined business rules. But we can now implement a Resolver very simply for business rules that can be very complex, and without having to leave the heavy artillery.

Fire! 

Nov 30 2017
Nov 30

We saw in a previous post how to set up a publishing process on Drupal 8 with the modules Content moderation and Workflows. We will address here a similar problematic but relying this time on the module State machine, module that will allow us to set up one or more business workflow on any Drupal entity. Note that the state machine module is one of the essential components of Drupal Commerce 2.x.

The operation of the module is quite simple in its design:

  • Workflows must be defined in a custom module using a YAML file: these workflows contain both the different possible states and the different possible transitions between each state.
  • The module provides a new field type: a State field.
  • We can add as many state fields as needed to an entity, and we can configure which workflow each field will use.
  • And now every time a State field is changed, an event will be propagated (see Drupal 8 and events), and we can then act on these events and trigger as many actions as needed

It will thus remain to implement certain logics so that the fields thus created fulfill their role at best.

In particular, we will have to generate the different permissions allowing us to assign rights to each transition (or to adopt a different logic concerning rights management, which could for example be based on certain users's attributes) and still trigger actions appropriate for each active status for a content or any entity.

Note that you may need to apply this patch to have a generic event propagated by the State machine field. If not, you can still simply use propagated standard events, events based on the transition ID triggered.

Let's take a closer look at how state machines work.

Creating workflows

Unlike the Content Moderation and Workflow modules now integrated into the core of Drupal, where we can achieve everything in a few clicks, business workflow configuration with State machine requires the creation of a custom module to declare workflows and a small part of Site Building that we will discuss later briefly.

We will create a new module that we will call my_workflow. This module will have this basic structure.

├── composer.json
├── my_workflow.info.yml
├── my_workflow.module
├── my_workflow.permissions.yml
├── my_workflow.services.yml
├── my_workflow.workflow_groups.yml
├── my_workflow.workflows.yml
├── src
│   ├── EventSubscriber
│   │   └── WorkflowTransitionEventSubscriber.php
│   ├── Guard
│   │   └── PublicationGuard.php
│   ├── MyWorkflowPermissions.php
│   ├── Tests
│   │   └── LoadTest.php
│   ├── WorkflowHelper.php
│   ├── WorkflowHelperInterface.php
│   └── WorkflowUserProvider.php
└── templates
    └── my_workflow.html.twig

The most important elements that will interest us here will be the files

  • my_workflow.workflow_groups.yml which will allow us to create workflow groups
  • my_workflow.workflows.yml which will enable us to declare and describe precisely each workflow
  • PublicationGuard.php which will allow us to manage rights access to each transition
  • And lastly WorkflowtransitionEventSubscriber.php which will allow us to react according to the different statutes and transitions of each workflow

A state machine workflow must be part of a workflow group. We will therefore create two workflow groups (publication and technic) for our example by simply declaring them in the my_workflow.workflow_groups.yml file.

publication:
  label: Publication
  entity_type: node
technic:
  label: Technic
  entity_type: node

We can now declare our two business workflows that we will call Publication status and Technical status in my_workflow.workflows.yml file.

publication_default:
  id: publication_default
  label: Default publication
  group: publication
  states:
    new:
      label: New
    draft:
      label: Draft
    proposed:
      label: Proposed
    validated:
      label: Validated
    needs_update:
      label: Needs update
    archived:
      label: Archived
  transitions:
    save_as_draft:
      label: Save as draft
      from: [new, draft, needs_update]
      to: draft
    save_new_draft:
      label: Save new draft
      from: [validated]
      to: draft
    propose:
      label: Propose
      from: [new, draft, needs_update]
      to: proposed
    update_proposed:
      label: Update
      from: [proposed]
      to: proposed
    validate:
      label: Publish
      from: [new, draft, proposed]
      to: validated
    update_validated:
      label: Update
      from: [validated]
      to: validated
    needs_update:
      label: Request changes
      from: [proposed, validated]
      to: needs_update
    unarchive:
      label: Unarchive
      from: [archived]
      to: draft
    archive:
      label: Archive
      from: [validated]
      to: archived

technic_default:
  id: technic_default
  label: Default technic
  group: technic
  states:
    new:
      label: New
    draft:
      label: Draft
    need_review:
      label: Need review
    validated:
      label: Validated
    need_update:
      label: Need update
  transitions:
    save_as_draft:
      label: Save as draft
      from: [new, draft, need_review, validated, need_update]
      to: draft
    ask_review:
      label: Ask review
      from: [new, draft, need_update]
      to: need_review
    update_need_review:
      label: Update review
      from: [need_review]
      to: need_review
    update_need_update:
      label: Stay in need update
      from: [need_update]
      to: need_update
    validate:
      label: Validate
      from: [new, draft, need_review, need_update]
      to: validated
    update_validated:
      label: Update validated
      from: [validated]
      to: validated
    need_update:
      label: Request changes
      from: [need_review, validated]
      to: need_update

For each of the workflows we will declare the different states of the workflow (under the key states) as well as the different possible transitions between these states (under the key transitions). We will then be able to control the various rights to access these transitions very finely.

We have with the file above a standard process, classic, and expected by the State machine module. But as it is a manual configuration (using a YAML file) of a Workflow Plugin State Machine module, nothing forbids us here to add arbitrary properties that will make our business process even smarter. We will only have to detect the presence of these arbitrary properties and their value during the propagation of State machine events. For example, we will associate with certain statuses several properties that may be useful in our wprkflow:

  • published: true to automatically publish content
  • archived: true to unpublish content automatically
  • notify_owner: true to automatically notify the author of the content
  • notify_role: editor to automatically notify users with role editor for example
  • notify_users: field_ref_users to notify referenced users from a particular field (here the field_ref_users field)
  • etc.
  • etc.

We could just add the create_task: my_task property for example to create any task in a third party system, or create a Task entity on the Drupal 8 site. But I think you understand the principle: we can do everything according to the business needs.

For example, our processes will now look more like this, with these new properties that we added for the purposes of the business workflows we want to implement.

publication_default:
  id: publication_default
  label: Default publication
  group: publication
  states:
    new:
      label: New
    draft:
      label: Draft
    proposed:
      label: Proposed
    validated:
      label: Validated
      published: true
    needs_update:
      label: Needs update
    archived:
      label: Archived
      archived: true
  transitions:
    save_as_draft:
      label: Save as draft
      from: [new, draft, needs_update]
      to: draft
    save_new_draft:
      label: Save new draft
      from: [validated]
      to: draft
    propose:
      label: Propose
      from: [new, draft, needs_update]
      to: proposed
    update_proposed:
      label: Update
      from: [proposed]
      to: proposed
    validate:
      label: Publish
      from: [new, draft, proposed]
      to: validated
    update_validated:
      label: Update
      from: [validated]
      to: validated
    needs_update:
      label: Request changes
      from: [proposed, validated]
      to: needs_update
    unarchive:
      label: Unarchive
      from: [archived]
      to: draft
    archive:
      label: Archive
      from: [validated]
      to: archived

technic_default:
  id: technic_default
  label: Default technic
  group: technic
  states:
    new:
      label: New
    draft:
      label: Draft
    need_review:
      label: Need review
      notify_role: technic
      notify_users: field_notify_users
    validated:
      label: Validated
      notify_owner: true
    need_update:
      label: Need update
      notify_owner: true
  transitions:
    save_as_draft:
      label: Save as draft
      from: [new, draft, need_review, validated, need_update]
      to: draft
    ask_review:
      label: Ask review
      from: [new, draft, need_update]
      to: need_review
    update_need_review:
      label: Update review
      from: [need_review]
      to: need_review
    update_need_update:
      label: Stay in need update
      from: [need_update]
      to: need_update
    validate:
      label: Validate
      from: [new, draft, need_review, need_update]
      to: validated
    update_validated:
      label: Update validated
      from: [validated]
      to: validated
    need_update:
      label: Request changes
      from: [need_review, validated]
      to: need_update

We can now enable our module, and configure the State machine fields.

Initial configuration

Just add a State field on the type of entity for which we want to set up a workflow. Here we will create a State machine field (Publication status) on the content type Article.

add state machine field

And we configure this field to use one of the workflows that we declared in our module.

Configuration state machine

We can configure the form display mode to position the State machine field, and in the example below, we also take this opportunity to disable the native Published field, which will no longer serve us directly.

configuration form mode

We can also configure different display view modes for the State machine field by displaying the value of the current status (here for a Publication status field) or by choosing to display a form that gives access to the different possible transitions (here for another field that we have created also: Technical status) directly from the content's view page.

State machine view mode example

Management of access rights on transitions

By default, State machine does not offer any type of permissions for transitions. By default, all transitions are available to users who can edit a State machine field.

To control access to transitions, just create one (or more) services that will implement GuardInterface. And it is through this service that we can simply prohibit access to transitions, according to our logic. For the example, we will first take a simple case and generate as many permissions as there are possible transitions. The goal is to propose from the permissions management interface checkboxes to assign user roles access to certain transitions.

Let's create the file my_workfloww.permissions.yml, with the following content

permission_callbacks:
  - \Drupal\my_workflow\MyWorkflowPermissions::permissions

Let's create the MyWorkPermissions class

<?php

namespace Drupal\my_workflow;

use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\core\Entity\EntityTypeManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\state_machine\WorkflowManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;


/**
 * Defines a class for dynamic permissions based on workflows.
 */
class MyWorkflowPermissions implements ContainerInjectionInterface {

  use StringTranslationTrait;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The workflow manager.
   *
   * @var \Drupal\state_machine\WorkflowManagerInterface
   */
  protected $workflowManager;


  /**
   * Constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkflowManagerInterface $workflow_manager) {
    $this->entityTypeManager = $entity_type_manager;
    $this->workflowManager = $workflow_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('plugin.manager.workflow')
    );
  }

  /**
   * Returns an array of transition permissions.
   *
   * @return array
   *   The transition permissions.
   */
  public function permissions() {
    $permissions = [];
    $workflows = $this->workflowManager->getDefinitions();
    /* @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
    foreach ($workflows as $workflow_id => $workflow) {
      foreach ($workflow['transitions'] as $transition_id => $transition) {
        $permissions['use ' . $transition_id . ' transition in ' . $workflow_id] = [
          'title' => $this->t('Use the %label transition', [
            '%label' => $transition['label'],
          ]),
          'description' => $this->t('Workflow group %label', [
            '%label' => $workflow['label'],
          ]),
        ];
      }
    }
    return $permissions;
  }

}

Let's clear the caches, and we get our beautiful rights management interface.

Permissions state machine

It only remains to create our service that will control access to transitions on the basis of these permissions.

Let's create the file my_workflow.services.yml and declare our service my_workflow.publication_guard

services:
  my_workflow.publication_guard:
    class: Drupal\my_workflow\Guard\PublicationGuard
    arguments: ['@current_user', '@plugin.manager.workflow']
    tags:
      - { name: state_machine.guard, group: publication }
  my_workflow.workflow.helper:
    class: Drupal\my_workflow\WorkflowHelper
    arguments: ['@current_user']
  my_workflow.workflow_transition:
    class: Drupal\my_workflow\EventSubscriber\WorkflowTransitionEventSubscriber
    arguments: ['@my_workflow.workflow.helper']
    tags:
      - { name: event_subscriber }

Our file also contains some other services on which we will have the opportunity to return. Note that your service must be assigned to a workflow group using the group key. And we tag it with the state_machine.guard tag for this service to be collected and evaluated by the state machine module.

Create the PublicationGuard class.

<?php

/**
 * @file
 * Contains \Drupal\my_workflow\Guard\PublicationGuard.
 */

namespace Drupal\my_workflow\Guard;

use Drupal\Core\Session\AccountProxyInterface;
use Drupal\state_machine\Guard\GuardInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowTransition;
use Drupal\Core\Entity\EntityInterface;
use Drupal\state_machine\WorkflowManagerInterface;

class PublicationGuard implements GuardInterface {

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * The workflow manager.
   *
   * @var \Drupal\state_machine\WorkflowManagerInterface
   */
  protected $workflowManager;

  /**
   * Constructs a new PublicationGuard object.
   *
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   The current user..
   */
  public function __construct(AccountProxyInterface $current_user, WorkflowManagerInterface $workflow_manager) {
    $this->currentUser = $current_user;
    $this->workflowManager = $workflow_manager;
  }

  /**
   * {@inheritdoc}
   */
  public function allowed(WorkflowTransition $transition, WorkflowInterface $workflow, EntityInterface $entity) {
    // Don't allow transition for users without permissions.
    $transition_id = $transition->getId();
    $workflow_id = $workflow->getId();
    if (!$this->currentUser->hasPermission('use ' . $transition_id . ' transition in ' . $workflow_id)) {
      return FALSE;
    }
  }

}

The main (and only) method to use is the allowed() method that must return FALSE when you want to deny access to a transition under certain conditions. Otherwise the method should not return anything to allow other services collected by the State Machine GuardFactory to also evaluate their condition. And if none service returns FALSE then the State machine module will give access to the transition.

In the example above, we simply use the permissions previously created to evaluate if the user does NOT have permission and in this case return FALSE.

But we could as well have done without all this logic based on standard Drupal permissions, and evaluate the access to transitions according to much more complex criteria while avoiding a lot of clicks on the management interface of Drupal permissions

For example

/**
 * {@inheritdoc}
 */
public function allowed(WorkflowTransition $transition, WorkflowInterface $workflow, EntityInterface $entity) {
  // Don't allow transition for users without the right attribute.
  if (!$this->userHasComplexAttribute($workflow, $entity)) {
    return FALSE;
  }
}

where the userHasComplexAttribute() method can evaluate what business logic requires (an attribute of the user, the value of any field, or both at the same time, etc.).

In short, possibilities are multiples, within range of a few lines.

Once the implementation of access rights to transitions has been achieved, we will be able to finalize our business workflows by triggering the required actions.

Setting up business logic

Once the fields are created and linked to the workflows, permissions set up, all that remains is to use the Symfony2 event system, available on Drupal 8. Each State machine field will propagate several events during each transition: a pre-transition event and a post-transition event.

The identifiers of these events are of the form [group_id].[transition_id].[pre_transition | post_transition], where group_id is the identifier of the workflow group, transition_id is the identifier of the transition.

With the patch mentioned in the introduction (Fire generic events when transition are applied), a more generic event will be propagated, whose identifier will be state_machine.[pre_transition | post_transition]. It is this last event that we will use in our example.

To react to these events, we will implement an EventSubscriber service, which can be found in the service declaration file of our module below. We will also use another WorkflowHelper service, which will contain some utilitarian methods. 

services:
  my_workflow.publication_guard:
    class: Drupal\my_workflow\Guard\PublicationGuard
    arguments: ['@current_user', '@plugin.manager.workflow']
    tags:
      - { name: state_machine.guard, group: publication }
  my_workflow.workflow.helper:
    class: Drupal\my_workflow\WorkflowHelper
    arguments: ['@current_user']
  my_workflow.workflow_transition:
    class: Drupal\my_workflow\EventSubscriber\WorkflowTransitionEventSubscriber
    arguments: ['@my_workflow.workflow.helper']
    tags:
      - { name: event_subscriber }

Let's discover the WorkflowTransitionEventSubscriber class that will allow us to react according to the different transitions.

<?php

namespace Drupal\my_workflow\EventSubscriber;

use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\my_workflow\WorkflowHelperInterface;
use Drupal\state_machine\Event\WorkflowTransitionEvent;
use Drupal\state_machine\Plugin\Workflow\WorkflowInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowState;
use Drupal\state_machine_workflow\RevisionManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Event subscriber to handle actions on workflow-enabled entities.
 */
class WorkflowTransitionEventSubscriber implements EventSubscriberInterface {

  /**
   * The workflow helper.
   *
   * @var \Drupal\my_workflow\WorkflowHelperInterface
   */
  protected $workflowHelper;

  /**
   * Constructs a new WorkflowTransitionEventSubscriber object.
   *
   * @param \Drupal\my_workflow\WorkflowHelperInterface $workflowHelper
   *   The workflow helper.
   */
  public function __construct(WorkflowHelperInterface $workflowHelper) {
    $this->workflowHelper = $workflowHelper;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    return [
      'state_machine.pre_transition' => 'handleAction',
    ];
  }

  /**
   * handle action based on the workflow.
   *
   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
   *   The state change event.
   */
  public function handleAction(WorkflowTransitionEvent $event) {
    $entity = $event->getEntity();

    // Verify if the new state is marked as published state.
    $is_published_state = $this->isPublishedState($event->getToState(), $event->getWorkflow());

    if ($entity instanceof EntityPublishedInterface) {
      if ($is_published_state) {
        $entity->setPublished();
      }
      else {
        $entity->setUnpublished();
      }

    }
  }

  /**
   * Checks if a state is set as published in a certain workflow.
   *
   * @param \Drupal\state_machine\Plugin\Workflow\WorkflowState $state
   *   The state to check.
   * @param \Drupal\state_machine\Plugin\Workflow\WorkflowInterface $workflow
   *   The workflow the state belongs to.
   *
   * @return bool
   *   TRUE if the state is set as published in the workflow, FALSE otherwise.
   */
  protected function isPublishedState(WorkflowState $state, WorkflowInterface $workflow) {
    return $this->workflowHelper->isWorkflowStatePublished($state->getId(), $workflow);
  }

}

Here we subscribe to the state_machine.pre_transition event using the getSubscribedEvents() method, and we then call the handleAction() method which in this simple example will allow us to publish or unpublish content based on the status of the State machine field. To do this, we check our custom properties that we associate with the different states of the workflow using the isWorkflowStatePublished() method of the WorkflowHelper Utility class.

This last method will inspect the plugin settings for the current workflow, and check if the current status has the published:true property.

<?php

namespace Drupal\my_workflow;

use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowTransition;

/**
 * Contains helper methods to retrieve workflow related data from entities.
 */
class WorkflowHelper implements WorkflowHelperInterface {

  /**
   * The current user proxy.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * Constructs a WorkflowHelper.
   *
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
   *   The service that contains the current user.
   */
  public function __construct(AccountProxyInterface $currentUser) {
    $this->currentUser = $currentUser;
  }


  /**
   * {@inheritdoc}
   */
  public function isWorkflowStatePublished($state_id, WorkflowInterface $workflow) {
    // We rely on being able to inspect the plugin definition. Throw an error if
    // this is not the case.
    if (!$workflow instanceof PluginInspectionInterface) {
      $label = $workflow->getLabel();
      throw new \InvalidArgumentException("The '$label' workflow is not plugin based.");
    }

    // Retrieve the raw plugin definition, as all additional plugin settings
    // are stored there.
    $raw_workflow_definition = $workflow->getPluginDefinition();
    return !empty($raw_workflow_definition['states'][$state_id]['published']);
  }

  /**
   * {@inheritdoc}
   */
  public function isWorkflowStateArchived($state_id, WorkflowInterface $workflow) {
    // We rely on being able to inspect the plugin definition. Throw an error if
    // this is not the case.
    if (!$workflow instanceof PluginInspectionInterface) {
      $label = $workflow->getLabel();
      throw new \InvalidArgumentException("The '$label' workflow is not plugin based.");
    }

    // Retrieve the raw plugin definition, as all additional plugin settings
    // are stored there.
    $raw_workflow_definition = $workflow->getPluginDefinition();
    return !empty($raw_workflow_definition['states'][$state_id]['archived']);
  }

}

We could just as easily check for the presence of notify_owner, notify_role, notify_users, and so on.

For example, should we notify some referenced users from a certain field? We will then recover the identifier of the field referencing them:

/**
 * {@inheritdoc}
 */
public function isWorkflowStateNotifyUsers($state_id, WorkflowInterface $workflow) {
  // We rely on being able to inspect the plugin definition. Throw an error if
  // this is not the case.
  if (!$workflow instanceof PluginInspectionInterface) {
    $label = $workflow->getLabel();
    throw new \InvalidArgumentException("The '$label' workflow is not plugin based.");
  }

  // Retrieve the raw plugin definition, as all additional plugin settings
  // are stored there.
  $raw_workflow_definition = $workflow->getPluginDefinition();
  $field_name = !empty($raw_workflow_definition['states'][$state_id]['notify_users']) ? $raw_workflow_definition['states'][$state_id]['notify_users'] : FALSE;
  return $field_name;
}

Then we can notify them from the EventSubscriber service.

/**
 * handle action based on the workflow.
 *
 * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
 *   The state change event.
 */
public function handleAction(WorkflowTransitionEvent $event) {
  $entity = $event->getEntity();

  // Verify if we should notify some users.
  $field_name = $this->workflowHelper-> isWorkflowStateNotifyUsers($event->getToState(), $event->getWorkflow());

  if ($entity instanceof FieldableEntityInterface) {
    if ($field_name) {
      $this->notifyUsers($field_name, $event, $entity);
    }

  }
}

In a few lines and some methods, we can now implement any necessary reaction based on a transition and a particular status, let's remember, on any entity of Drupal 8.

Content Moderation or State Machine?

Which solution to choose between Content moderation and State machine? We were able to see it during this long presentation of the State machine module, this solution is primarily oriented Drupal 8 Developer, in contrast to the Content moderation solution, more oriented Drupal site builder, which allows to set up a workflow a few clicks (even though this solution can be extended by additional developments as well).

Each solution corresponds to a particular need: Content moderation (and the workflows module) responds very quickly to the need for one (and only one) standard publication workflow on contents (node ​​entities) of a drupal 8 site, while State machine can respond, just as quickly, to more complex needs (multiple workflows on the same content, workflows on any drupal 8 entity).

For their support and predictable maintenance, Content moderation as a Drupal 8 core module has an undeniable advantage. But the State machine module is not left out because it is one of the major building blocks of Drupal Commerce 2.x. These two solutions are therefore guaranteed to be in the landscape of the Drupal ecosystem for a certain time. Be that as it may, this aspect should not be a major element in a decision between a particular solution. Do not hesitate to contact a Drupal 8 expert who can advise you on the best strategy to adopt in relation to your project.

If you wish, you can recover the source code of the examples presented in this post on this GitHub repository.

Nov 16 2017
Nov 16

Managing native configuration with Drupal 8 makes it very easy to make changes or additions to the configuration from a site instance (such as a development environment) to another site instance (the production environment). These configuration exports and imports of a site are made in one piece: that is, the entire configuration of a site, which is updated. Thus if configuration additions have been made to the production site, they will be overwritten at the next import of the configuration if these configurations are not present also on the source environment.

But there are valid use cases where certain configurations can and must be modified directly in production. Examples of immediate use are, for example, the creation of new Webform forms, or the creation or update of new Newsletters managed with the SimpleNews module. It is quite legitimate for a webmaster to modify or create new NewsLetters on the production site. It's almost like content, except that ... it's a configuration entity.

Let's discover how to manage these particular cases with the module Configuration split, module that will allow us to maintain an organized process to manage the evolution and maintenance of a site in production while allowing the modification of some configurations live.

Configuration of the module Configuration split

We will illustrate the configuration to be put in place to allow users to create new Simplenews Newsletters on the production site.

After installing the Configuration split module, we create a new parameter for this module. We assume that the Drupal 8 project is based on composer and that its synchronization folder is located on the ../config/sync path relative to the project docroot.

First we create a new ../config/excluded directory. This directory will host the YML files of the configurations that have been created in production.

Then we define a new Configurations split parameter.

Configuration split settings

We give it a name, tell it the configuration files storage directory created previously (../config/excluded), then the most important thing: we do not enable this configuration by leaving the Active option unchecked.

We continue the configuration, using the Conditional Split section.

Configuration split conditionnal

Here we can select an existing configuration, or specify configurations using wildcard. Here we indicate to cut the configurations of all Simplenews newsletters, by means of simplenews.newsletter.* files. And we enable the Split only when different option, which allows us to keep identical configurations between environments in the main synchronization directory containing all site configuration files.

Then we can record, to get.

Configuration split status

Note again the Inactive status of our configuration. It must remain so on all source environments other than production.

New process for importing the configuration

Once this configuration is created, we export it, and then commit it on our depot and then deploy it on all our environments, including production. For example like this, but of course this is to adapt to your current process.

dev > drush cex
dev > git add .
dev > git commit -m "config split excluded"
dev > git push

prod > git pull
prod > drush cim

Once this excluded configuration imported on the production site, we will activate it from the settings.php file by adding a configuration override, which will obviously only be present on the production environment.

<?php

// This will allow module config per environment and exclude newsletter from being overridden
$config['config_split.config_split.excluded']['status'] = TRUE;

And we get on our production site our Excluded configuration which is now activated.

Config split active on production

And it's over, or almost. From now, it will be necessary before all import of configuration on the site in production, to launch beforehand an export of the configuration excluded by means of the command drush config-split export (or drush csex).

This command will detect if new configurations are present on the production site, or if existing ones have been modified, compared to the general configuration of the site, and if necessary will export these modified or added configurations in the directory ../config/excluded.

And the general configuration import command, drush cim, will gather both the configuration present in the main directory ../config/sync, and also the configuration present in the ../config/excluded directory.

The new process of updating and importing the configuration to the production environment can then look like this

prod> drush csex -y excluded
prod> drush cim -y

Or using a small shell script

#!/bin/bash
echo "-----------------------------------------------------------"
echo "Exporting excluded config"
drush csex -y excluded

echo "-----------------------------------------------------------"
echo "Importing configuration"
drush cim -y

From now on, you will be able to create and manage our Newsletters as simply as it was content, directly on the production site. Of course the same concept can apply to any configuration that you want to be able to manage in production: Webform forms for example, or the positioning of some blocks, etc. while maintaining a structured process for maintaining your site in production using Drupal 8 Configuration Management.

Thanks to Jon Minder for his article Advanced Drupal 8 Configuration Management (CMI) Workflows that helped me more, and of course the creator and maintainer of this module particularly useful.

Nov 08 2017
Nov 08

Content metadata (menu settings, comment settings, publishing options, URL path parameters, publication information, etc.) are displayed by default on the node form in a side panel. This has the advantage of giving immediate visibility on these options while writing its content.

For example, our form below provides an overview of content and metadata.

Création d'une page

But there are use cases where the lateral position of this information is detrimental to the general ergonomics, because reducing the space available for the node form. This can be the case, for example, if you use the Field Group module to structure and group the information to be entered (in the form of vertical tabs for example), and even more if you use the paragraphs also organized with the Field group module.

For example, below our form has a place so small that it becomes difficult to exploit unless you have a 32-inch screen.

Création d'une article

No need here for a Drupal expert. Let's find out how we can make the position of these metadata customizable according to the needs and general ergonomics of the Drupal 8 project.

We will act at the level of the theme of administration. To do this, you will need to create an administration sub-theme that will be based on an administration theme you have chosen (the default theme seven, or the excellent theme Adminimal).

After creating your sub-theme (we've titled it bo here), you'll have a file structure that looks like this

bo
├── bo.info.yml
├── bo.libraries.yml
├── bo.theme
├── config
│   ├── install
│   │   ├── block.block.bo_breadcrumbs.yml
│   │   ├── block.block.bo_content.yml
│   │   ├── block.block.bo_help.yml
│   │   ├── block.block.bo_local_actions.yml
│   │   ├── block.block.bo_messages.yml
│   │   ├── block.block.bo_page_title.yml
│   │   ├── block.block.bo_primary_local_tasks.yml
│   │   ├── block.block.bo_secondary_local_tasks.yml
│   │   └── bo.settings.yml
│   └── schema
│       └── bo.schema.yml
├── css
│   ├── node_meta.css
│   ├── node_meta.css.map
│   ├── style.css
│   └── style.css.map
├── js
│   └── bo.js
├── less
│   ├── node_meta.less
│   └── style.less
└── theme-settings.php

We will need these particular files

  • bo.libraries.yml to declare our library and load the file node_meta.css to provide some css rules.
  • bo.theme to alter form for editing and creating content
  • theme-settings.php to provide configuration options at our sub-theme of administration

Make the metadata position parametrizable

We start by providing configuration options from the administration sub-theme. The general idea is to provide a configuration form that will look like the below capture, form that will allow us to set the position of the metadata panel according to the content types. If you have different business logic then you can of course provide these options based on it and not the content types.

formulaire de configuration au niveau du thème

To provide this configuration form, we will alter, with the theme-settings.php file, the theme settings editing form to add these options.

<?php

use Drupal\node\Entity\NodeType;
use Drupal\Core\Form\FormStateInterface;

function bo_form_system_theme_settings_alter(&$form, FormStateInterface &$form_state) {

  $form['bo_settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('BO Theme Settings'),
    '#collapsible' => FALSE,
    '#collapsed' => FALSE,
  );

  $form['bo_settings']['tabs'] = array(
    '#type' => 'vertical_tabs',
    '#default_tab' => 'basic_tab',
  );
  
  $form['bo_settings']['basic_tab']['basic_settings'] = array(
    '#type' => 'details',
    '#title' => t('Basic Settings'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#group' => 'tabs',
  );

  $form['bo_settings']['basic_tab']['basic_settings']['node_form_meta'] = array(
    '#type' => 'item',
    '#markup' => '<div class="theme-settings-title">'.t("Node form meta block position").'</div>',
  );
  
  $form['bo_settings']['basic_tab']['basic_settings']['node_form_meta'] = array(
    '#type' => 'checkbox',
    '#title' => t('Display block meta as vertical tabs'),
    '#description'   => t('Use the checkbox to display the node meta block as vertical tabs and under the main node form.'),
    '#default_value' => theme_get_setting('node_form_meta', 'bo'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );

  $options = array_map(function (NodeType $nodeType) { return $nodeType->label(); }, NodeType::loadMultiple());
  $default_value = theme_get_setting('node_form_meta_types', 'bo');
  $form['bo_settings']['basic_tab']['basic_settings']['node_form_meta_types'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Select the content types'),
    '#description'   => t('Select the content type on which display the node meta block as vertical tabs and under the main node form.'),
    '#default_value' => $default_value ? array_filter($default_value) : [],
    '#options' => $options,
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#states' => [
      'visible' => [
        ':input[name="node_form_meta"]' => ['checked' => TRUE],
      ],
    ]
  );

}

Alter the node form

Once these options are configured, we can now alter the node form from the bo.theme file.

/**
 * Implements hook_form_alter().
 */
function bo_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // Set advanced settings in node form as verticals tabs.
  if (theme_get_setting('node_form_meta', 'bo')) {
    /** @var \Drupal\node\NodeInterface $node */
    $node = $form_state->getFormObject()->getEntity();
    $types_enabled = theme_get_setting('node_form_meta_types', 'bo') ? array_filter(theme_get_setting('node_form_meta_types', 'bo')): [];
    if (in_array($node->bundle(), $types_enabled)) {
      $form['advanced']['#type'] = 'vertical_tabs';
      $form['meta']['#type'] = 'details';
      $form['meta']['#title'] = t('Informations');
      $form['#attached']['library'][] = 'bo/node_meta';
    }
  }
}

There you go ! Note that we load the library bo/node_meta to provide some css override. For the snippet to be complete, you will find the node_meta.css file below.

.node-form .entity-meta details[open] {
  background-image: none;
}
.node-form .entity-meta__header,
.node-form .entity-meta details {
  border-top: 0 none transparent;
  border-bottom: 0 none transparent;
}
.node-form .entity-meta__header:first-child,
.node-form .entity-meta details:first-child {
  border-top-color: transparent;
}
@media screen and (min-width: 768px) {
  .node-form .layout-region-node-main,
  .node-form .layout-region-node-footer {
    float: inherit;
    /* LTR */
    width: 100%;
    padding-right: 0;
    /* LTR */
    box-sizing: border-box;
  }
  .node-form [dir="rtl"] .layout-region-node-main,
  .node-form [dir="rtl"] .layout-region-node-footer {
    float: inherit;
    padding-left: 0;
    padding-right: 0;
  }
  .node-form .layout-region-node-secondary {
    float: inherit;
    /* LTR */
    width: 100%;
  }
  .node-form [dir="rtl"] .layout-region-node-secondary {
    float: inherit;
  }
}

You may need to overload this library if your admin theme differs somewhat. In this case from your theme, you can add these few lines in the my_theme.info.yml file of your admin theme to overload this library.

libraries-override:
  # Replace an entire library.
  bo/node_meta: my_theme/node_meta

Where my_theme/node_meta will be your own library loading the relevant css file.

And all this allows us to position this content metadata panel according to our needs, allowing us to benefit from all the available width of the browser to improve user comfort.

Création d'un article

Note that this snippet is only useful if you are using the seven admin theme, or another admin theme that extends Seven (such as the adminimal theme for example). Indeed, by default this metadata panel is displayed under the main form in the form of vertical tabs. It's the Seven theme that override the display of this panel. If you do not use Seven, you should not need these snippets. Or maybe but then to do the opposite.

What if we made a module?

It is after several similar alterations on different content types that I wanted to make these options configurable. And it was while writing this blog post following a discussion during the DrupalCamp Lannion, that the idea of a small module Meta position, proposing this feature and making unnecessary this override at the level of the administration theme, came to me. This small module is very simple, but can be useful to everyone. Anyway it is a need that I meet very often, for which I will not need any more to alter the admin theme. The interest also lies for the site builders / webmasters, allowing a fine configuration in a few clicks from the configuration interface.

As we say, little brooks make great rivers.

Oct 19 2017
Oct 19

It is not uncommon to propose to filter contents according to dates, and in particular depending on the year. How to filter content from a view based on years from a date field? We have an immediate solution using the Search API module coupled with Facets. This last module allows us very easily to add a facet, to a view, based on a date field of our content type, and to choose the granularity (year, month, day) that we wish to expose to the visitors. But if you do not have these two modules for other reasons, it may be a shame to install them just for that. We can get to our ends pretty quickly with a native Views option, the contextual filter. Let's discover in a few images how to get there.

Creating the content view to be filtered by year

Let's say we have a content type named bulletin, which has a Date field, and we want to filter by year. To do this, we designed a view that lists all bulletin contents. Here is the general, very classic, configuration of the view.

View bulletin general configuration

We distinguish in the view's settings a section Contextual filters. These contextual filters can be added and configured to a view in order to be able to filter its results from these filters. These filters can be configured to be provided from the view URL, or from a specific context, from a particular field of a current content (if we display the view beside a content), from a parameter in the request, etc. In fact the possibilities are endless and we can also provide our own logic to contextual filters very easily by implementing a Views Plugin type @ViewsArgumentDefault. Finally, these contextual filters are created in relation to a particular field of the contents that you visualize with Views.

Adding and configuring the contextual argument

We will add a contextual filter to our view, using somewhat specific field types that allow us to get aggregate values ​​of Date fields. We click on the button to add a contextual argument.

Add contextual filter

And we will look for our Date field (field_date) of our bulletin content type in an aggregated form per year (Date in the form YYYY).

After validating our choice, we get the control panel of our contextual filter.

settings contextual filter

Since we want to filter our content from a query parameter, we will configure the panel When the filter value is not in the URL. Note that if we had wanted to filter these contents from the URL directly (for example with a URL type /bulletins/2017) the configuration is almost nonexistent.

We will choose the Provide default value option, and select a query parameter as the default value type. We give a name to our parameter (year), supply it a fallback value if the query parameter is not present (all). And we set an exception value (all), which, if it is received by our query parameter, will let us ignore our argument, and therefore show all results.

All we have to do is save and test our view.

view filtered by year

And we simply add to the URL the parameter ?year=2017 to filter the contents of the year 2017.

Adding a Custom Exposed Filter

All we have to do now is add a select list in the exposed filters of the view to offer the visitor an interface to select the desired year.

You noted in the general view configuration that the Tags field was added as a filter criteria and was also exposed. This is not innocent, because it allows us to get the exposed filter form already operational, form in which we will only have to add our custom option for the years.

To do this, we create a small module, which we call my_module, and we will alter this form.

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
 * Implements hook_form_FORM_ID_alter().
 */
function my_module_form_views_exposed_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  if (isset($form['#id']) && $form['#id'] == 'views-exposed-form-bulletins-page') {
    $options = [
      'all' => t('- All -'),
      '2015' => '2015',
      '2016' => '2016',
      '2017' => '2017',
    ];

    $form['year'] = [
      '#title' => new TranslatableMarkup('By year'),
      '#type' => 'select',
      '#options' => $options,
      '#size' => NULL,
      '#default_value' => 'all',
    ];
  }
}

We add to the exposed form corresponding to the view whose identifier is bulletins, and to the display whose identifier is page, a simple select element whose year name corresponds to the query parameter set in the contextual filter of the view. And we provide him with some options, hardcoded here, over available years and of course with our exception value all, to be able to return all the results without any filters.

Filter year added

There you go. We have a simple view, with a minimum of code, which allows us to filter content by date according to an annual granularity, granularity that we can modify at will by modifying the contextual filter of the view if needed.

Make filter options dynamic

The options we have provided are unlikely to be sustainable over time. What is being done in 2018? Are we changing the code? Let's improve a bit our view to make the years available in the select list dynamic.

We will simply, since the alteration of the exposed form, make a query on all contents bulletin to retrieve all the dates and propose the different available years. But because this type of query can be costly, for a simple select list field, we will use the Drupal Cache API to cache these results and not have to recalculate them each time the page loads.

Let's improve the snippet seen above.

/**
 * Implements hook_form_FORM_ID_alter().
 */
function my_module_form_views_exposed_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  if (isset($form['#id']) && $form['#id'] == 'views-exposed-form-bulletins-page') {

    $options = &drupal_static(__FUNCTION__);
    if (is_null($options)) {
      $cid = 'my_module:bulletin:year';
      $data = \Drupal::cache()->get($cid);
      if (!$data) {
        $options = [];
        $options['all'] = new TranslatableMarkup('- All -');
        $query = \Drupal::entityQuery('node');
        $query->condition('type', 'bulletin')
          ->condition('status', 1)
          ->sort('field_date', 'ASC');
        $result = $query->execute();
        if ($result) {
          $nodes = Node::loadMultiple($result);
          foreach ($nodes as $node) {
            $date = $node->field_date->value;
            if ($date) {
              $date = new DrupalDateTime($date, new DateTimeZone('UTC'));
              $year = $date->format('Y');
              if (!isset($options[$year])) {
                $options[$year] = $year;
              }
            }
          }
        }

        $cache_tags = ['node:bulletin:year'];
        \Drupal::cache()->set($cid, $options, CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
      }
      else {
        $options = $data->data;
      }

    }
    
    $form['year'] = [
      '#title' => new TranslatableMarkup('By year'),
      '#type' => 'select',
      '#options' => $options,
      '#size' => NULL,
      '#default_value' => 'All',
    ];
    
  }
}

Thus the options of the available years will be computed a first time, then recovered directly from the cache of Drupal. We have taken care to add a custom cache tag node:bulletin:year specific to the data stored in cache so as to be able to invalidate them just when it will be necessary. Indeed, we permanently cache the result of our options, and we just have to invalidate these data if, and only if, a new bulletin is created or modified, and if it contains a date whose year is not present in the cached options.

Cache Invalidation

With the magic cache tags, nothing is simpler for a Drupal 8 developer. Let's look at the snippet that will take care of invalidating the cached options.

use \Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Cache\Cache;

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function my_module_node_presave(EntityInterface $entity) {
  $bundle = $entity->bundle();
  if ($bundle == 'bulletin') {
     // Check if a bulletin updated has a new year, and invalidate the
    // options cached used in the custom views filter for filtering by year.
    $cid = 'my_module:bulletin:year';
    $data = \Drupal::cache()->get($cid);
    if ($data) {
      $options = $data->data;
      $date = $entity->field_date->value;
      if ($date) {
        $date = new DrupalDateTime($date, new DateTimeZone('UTC'));
        $year = $date->format('Y');
        if (!isset($options[$year])) {
          Cache::invalidateTags(['node:bulletin:year']);
        }
      }
    }
  }
}

For each update of a bulletin content, we retrieve the cached options ($data), and then compare the year of the date of the content being saved with the cached values. In case of absence of the year, thanks to the magic cache tags, then we simply need to invalidate the custom cache tag (node:bulletin:year) that we have associated with the cached options. And the next time you load the bulletins view, the options will be recalculated, updated with the new year, and re-cached for an indefinite period, and no doubt annually.

As a conclusion

The contextual filters of the Views module, available with Drupal 8 Core, offer a considerable range of possibilities to cover a wide range of needs. And when these are not enough, a simple implementation of a Plugin, in a few lines, allows us to insert our own business logic into the Views mechanism in a robust and maintainable way, thus conciliating the robustness of Views and the specificities of a project. And finally, for an apparently simple need (filter by year), but far from being as obvious (working with dates is always a source of surprise), these allow to treat the subject in a simple way, without resorting to heavier means. And, often, simples solutions are the more effective solutions. Why make it complicated when it can be simple ?

Jul 20 2017
Jul 20

You want to allow your content editors to easily retrieve and integrate media, images, videos, sounds, presentations from third-party platforms? The Media entity suite has an extra string to its bow with the URL embed module. This module uses the PHP Embed library, and allows you to simply retrieve from a url a media from a third-party platform like Twitter, YouTube, Instagram, SoundCloud, Spotify, Vimeo, Slideshare, etc. To mention only the best known.

This module allows you to integrate a remote media within a text field or from a dedicated link field type. Let us discover how this module works, very simply besides.

Installing the module

You can install this module with Composer (composer require drupal/url_embed ~1.0). This method will download the module, as well as its dependencies, namely the Embed module and the PHP Embed library. If you want to install it from drush, or by downloading its archive, you will have to download these dependencies, and in particular place the PHP library in the /vendor directory of your site.

Configuration and use with CKEditor

Once installed, this module provides an embed button by default. This button will be available later in the CKeditor options for the text formats using it. You can customize this button, change its icon, and add as many as you need.

Bouton URL Embed pour insérer des médias dans un body

This button will then be available in the available CKeditor buttons.

Configuration CKEditor

To enable media integration, drag the previously created button into the CKeditor  active bar. In the example below we configure the full HTML text format.

We activate too the two filters provided by the URL embed module.

URL embed filters

The first filter Convert URLs to URL embeds automatically converts a link pasted directly into the text body, and transform it into an url that can be processed by the second filter.

The second filter Display embedded URLs allows to transform the link added with the dedicated media dialog box.

In order to automatically transform the pasted urls directly into the body, the first filter must be placed before the second in the filter processing order settings applied to the text format. 

Ordre des filtres

If you have enabled the Limit allowed HTML and correct faulty HTML filters, then the following tag should be added to the Allowed Tags input field:

<drupal-url data-*>

As shown in the capture below.

Balises autorisées

You also have the option of filtering the type of url that you want to convert automatically. For example, if you want only Twitter urls to be converted automatically, you can enter https://twitter.com in this field. Leave it empty to allow any type of urls.

limiter par type d'url les urls converties

And your text format is configured. All you have to do now is to insert media using links, either with the button and its dialog box.

Insérer un média distant

Or simply by copying / pasting the url into the body of the text.

Insertion d'un media dans un body

And the corresponding rendering

Le media twitter inséré dans le corps du texte

Using Embed URLs with fields

The use of this module with Link fields is even easier.

After you add a Link field to a content type, you simply configure its display mode and select the Embedded URL format.

Format d'affichage Embedded URL

And you just need to enter any supported url in the field.Insertion d'un lien vidéo avec url embed

For the following rendering

Une vidéo rendue depuis un champ formaté par URL Embed

Embed URL and Media Entity Suite

The Media Entity suite already includes many extensions to add many media types (image, video, document, etc.) to a structured catalog, as well as remote media such as Twitter, Slideshare, Soundcloud, Spotify, Youtube, Dailymotion, Etc.

URL Embed is a lighter alternative, but also with fewer options, for those who want to easily insert different types of remote media, without wanting to set up an internal media library system. Although in alpha version, the module is quite operational, despite some small bugs on certain types of media or urls (Some providers (Facebook, Twitter) badly rendered in editor and Some URLs are rendered correctly only the first time), and still requires some minor improvements before it can be available in stable version. A module to keep in its favorite module.

Jul 11 2017
Jul 11

Drupal 8 natively allows to insert images within a body field, of course if the format text used allows this functionality. But can we do the same and easily insert documents, attachments, within a body text ? We have many solutions with Drupal 8 to associate documents with content. From a dedicated field, allowing us to master its location on the rendered page, from paragraphs allowing the author to position his document wherever he likes, while maintaining mastery of the final rendering, or with the Media entity suite that also allows us to integrate media, including documents, into a body with the module entity embed. In short, there are many solutions.

We also have a very simple solution, configurable in one minute top chrono, to be able to upload a document into a body with the module D8 editor file upload. Let's quickly discover this module.

After installing the module, we will configure the text format, with CKEditor, we will use with the body field.

Configure the full HTML text format and click the corresponding button.

Les formats de texte de Drupal 8

You notice a new button (a paper clip) in the list of available buttons to activate the upload file widget. We drag and drop this button into the active toolbar.

La barre d'outils de CKEditor

We dropped this new button in the active toolbar (next to the Image button). This action displays the plugin settings that allow us to specify the file transfer directory, the allowed file extensions, and the maximum size of a file.

Activation du wigdet de chargement d'un fichier pour CkEditor

If you have enabled the Limit allowed HTML tags and correct faulty HTML in this text format, make sure that the <a> tag has the possible attributes of data-entity-type and data-entity-uuid, as shown below.

les balises autorisées

And it's done. In less than a minute we have just configured the module and we can now upload as many documents as needed into our body text.

We can now upload a document into any body field that will use the full HTML format.

Widget de transfert de fichier

It is possible to rename the file to give it a more attractive name than the default file name.

Fichier transféré

And your content rendered with its attachment.

Rendu du contenu avec le fichier joint

The formatting capabilities of files associated with a content are limited. But it is not the vocation of this module that allows to simply upload a file, in the same way as the images, into a text long field type. For an equally simple rendering. For more advanced requirements for formatting the associated documents, or the needs related to control access to these documents, you will have to recourse to other solutions.

An effective, simple module that responds to an immediate and often vital need. Sometimes it is this kind of small details that can make a big difference in the choice in a tool, and meets the basic functions that any publisher expects to have out of the box. No ?

Jul 06 2017
Jul 06

Having had the opportunity to read a recent comparative study between Drupal 8 and SPIP, I discovered a Drupal perception that is not mine. Far be it from me to want to compare Drupal and SPIP. Indeed, I know very badly SPIP, at least only as a basic user, and much less than Drupal, and so I will not venture on such a comparison. Also, I wanted to share these mythical, real or perceived defects of Drupal 8, which I recently discovered and which I think deserve a counterpoint.

Drupal 8 Upward Compliance

Drupal 7 modules are incompatible with Drupal 8. It must also be said that the technological break introduced with the new version of Drupal 8 left no room for a such option. On the other hand, Drupal 8's backward compatibility to its future major versions is guaranteed. Let's say that the technological breakthrough of Drupal 8, with the adoption of current best practices in web development, the reduction of its past technical debt, was sufficient to be able to embrace now a smooth upgrade path, for the next major Drupal version.

The low number of modules and their quality

12,000 modules for Drupal 7. Only 3,000 modules for Drupal 8 including 1,000 in stable version. The module offer on Drupal 8 is actually less plethora, but at the same time more coherent, with modules with similar functional perimeters that come together. But it is certain that we do not have the same functional coverage as Drupal 7. Do you need them with Drupal 8 is an another question. And the answer can only be done on a case-by-case basis.

But another aspect also emerges on Drupal 8: the implementation of a business logic can be done much more easily and quickly, thanks to its new architecture. When you could achieve the same result on Drupal 7 with 1 or 5 modules. The advantages and disadvantages are discussed. And also depend on the skills available to design your Drupal 8 project.

The question of the number of stable versions of the modules is much more pernicious. I know modules in beta 10 times more reliable than a so-called stable version of another Drupal 8 module. It all depends on the maintainer's personality, his spirit of perfection, or not. The use of such number is therefore questionable. But addressing this issue of the stability of available modules can lead to a doubt about the quality of these. Drupal has an organized process (The Project Application Review) to allow new developers to publish their module on drupal.org. And overall, the quality of the available modules is felt, especially since the object-oriented architecture of Drupal 8 makes it more difficult to write spaghetti code. It should be noted that for some months now this process is no longer compulsory for new entrants, but in parallel a new policy in terms of security cover has been put in place.

Usage statistics show low adoption of Drupal 8

Usage statistics should be analyzed with great caution. Indeed these statistics come from the Drupal sites that have the Update module enabled, module which allows to warn a webmaster of updates available. Apart from this it is typically the type of modules that is never activated on many Drupal projects. Why ? On the one hand this module will perform queries on drupal.org to detect the available updates, and therefore will impact the website's performance. On the other hand because many Drupal 8 sites are managed from Composer or Drush, tools that allow you to collect this information from the command line and launch the appropriate updates in seconds.

But do not hide that Drupal 8 is progressing slowly. Is it due to a less crowded presence of modules contributed on Drupal 8? Is it a preference to migrate directly to Drupal 9, and not have to do two migrations? Is it due to this technological breakthrough and a rise in skills needed? Is it due to a positioning issue of Drupal 8 that clearly addresses large-scale projects to the detriment of less ambitious projects? There is a clear debate within the Drupal community.

The Next End of Drupal 8 Maintenance

"Drupal 8 could be maintained only until 2019, then in 2021 for security maintenance" I could read. In short, a short life span for the necessary investment with an uncertainty on the effort to produce to migrate to the next major Drupal release, Drupal 9.

The date of the end of Drupal 8 maintenance is not known to date, and such an announcement today is more some  astrology than proven facts. In addition, the new policy for backwards compatibility allows for a smooth and continuous migration towards future versions of Drupal, minor or major. Read Do I have to wait for Drupal 9 for my web project? on this subject for more details.

Drupal 8 has few themes

Drupal has few ready-made themes on drupal.org, compared to Wordpress for example. But we can find many, in the same way as other CMS, on the online platforms offering these products. And for $ 40 you can acquire a theme Waouh!. Depending on your need, your project, this can be quite enough. You get what you pay for, but no more. These themes are often distributions that integrate business or functional logic at the level of the theme layer. In short, a real hodgepodge, like all themes ready to use on these platforms as a rule for all CMS. But if you do not foresee heavy adjustments, these themes can be a good compromise.

With large projects, or with many customizations, it is always better to design your own theme, if you do not want to fight against it and its implementations at some point. And in this case Drupal 8 has many built-in frameworks (Bootstrap, Foundation, etc.) that provide a solid basis for realizing your responsive theme.

Content composition with Drupal 8

Comparing the power of composition of a content with Drupal 8 and the typographical shortcuts of SPIP seems to reveal a profound ignorance of Drupal. Drupal makes it possible to compose complex content in a structured way, allowing the webmaster, the editor, in brief the content producer, to make complex layouts by focusing on its content only and not its formatting. The Paragraphs module is one of the methods of structuring this data of a content while giving the users a great freedom in their composition. While the typographical shortcuts of SPIP make it possible to make elaborate layouts, but by fighting more or less, within a single text field.

Clearly we are not talking about the same thing at all here. Structured data enabling contents cross-referencing, transverse or vertical navigations, allowing layouts completely controlled by the theme layer, and not by the content's author, which is neither work nor vocation, have nothing to do with the use of a single text field where one inserts pelle-mell, as best he can, its contents and their shaping.

Powerful but complicated rights management

"Rights management under Drupal 8 is powerful but can quickly become complicated with over 100 check boxes". Certainly this may be true. But this also reveals a lack of knowledge of Drupal or a poor approach / project design. Many solutions allow you to manage access rights other than by assigning rights roles on content. The use of taxonomy can be another approach for making rights management dynamic: rights are not assigned to roles on content in a static way, but can be given dynamically depending on some content's attributes. And this without heavy parameterization. Rights management, as appropriate, can be simplified. But it is not the management of Drupal's rights that is complicated, it is rights management needs that can be complex. And Drupal can address any kind of need. If the needs are simple, rights management will be just as simple.

A reduced functional coverage of Drupal 8

Yes, I admit Drupal does not natively have the system of headings of SPIP, nor its harvesting, nor typographic shortcuts, nor the possibility of inserting an attachment in a text body, the possibility of programming dates of Publication, and formatting capabilities of SPIP articles. For some functional failures, I think Drupal 8 can even assume it proudly. But to say that the functional coverage of Drupal 8 is smaller than that of SPIP, the arms fall to me.

Migrating time-consuming data

Migrating from a SPIP site to Drupal 8 is time consuming with a lot of manual data recovery. In fact it depends on the target site. If the Drupal project is produced with the same structure of SPIP, a title, a body of text, a field keywords, a heading (more probably still some others that I must forget) then this migration can be completely automated . Drupal 8 natively has a framework (Migrate) to implement this migration scheme, from database to database (a house mill can also do the trick, everything depends on the constraints of the migration). But if you want to take advantage of a migration to Drupal 8 to structure your data (for example, put a date in a Date field, put a price in a Price field, put an image in an Image field, etc.), make your content enrich to allow cross-referencing, cross-browsing, cross-publishing, so yes a SPIP migration to Drupal will be time-consuming because you will have to intervene on your migrated contents to enrich them with the new data that do not yet exist.

Administer Drupal 8 is complicated

Yes Drupal 8 is not simple to administer. But we can not compare SPIP and Drupal 8 on this pure aspect of administration. Both CMS have a completely different approach. SPIP is a turnkey site, with an identical backoffice (with plugins installed) on all SPIP sites. You will not find a Drupal site identical to another Drupal site. The backoffice of Drupal 8 is at the disposal of an administrator who will build the site, create its types of contents, its listings, its taxonomies. An administrator who will also build a backoffice intended for site users, webmasters, for publishing content. The backoffice of SPIP is at the disposal of a webmaster who will be able to begin to publish its content, to create its headings.

Yes Drupal 8 is more complicated because it is not the same target of users, if one compares a site SPIP and a site Drupal freshly installed. But the power of Drupal 8 is precisely to be able to easily design a customized backoffice for webmasters without all the configuration options that belong to a technical administration and not an editorial administration. Drupal is too a CMF (Content Management Framework) before being a CMS.

To illustrate this, you can do a SPIP like with Drupal 8. The reverse is not possible.

Drupal 8 is less efficient

Clearly Drupal 8 is less powerful than Drupal 7, if we make a raw benchmark comparison. And certainly even less powerful than SPIP. Just as SPIP must be less efficient than a static HTML page. Simply Drupal 8 embeds much more code, and does not offer the same level of architecture and functionality.

Nevertheless, Drupal 8 has a high-performance cache system for both anonymous and authenticated users, with a revolutionary dynamic cache system. It integrates natively into its core BigPipe, a technique invented by Facebook, to further improve the felt performance, and the so-called TurboLinks technique, with the Refreshless module, which allows to load only parts of pages that change, not the entire page.

Drupal 8 has a wide range of tools to enable it to propel very high traffic sites. For example, if the cache management from the database is not sufficient to meet the traffic of your site, you can again change easily this system to store the native cache of Drupal on Redis or Memcache. And if this is not enough to meet the tens or hundreds of thousands of visitors a day, it is always possible to couple a Varnish or Nginx in front of your site.

Drupal 8 is buggy

Oh good? You know a computer system, relatively complex, that has no bugs. Big deal. Yes Drupal has a bugtracker, public, which allows the Drupal community to work together, fix bugs, exchange, propose new features. Hiding bugs from a system does not mean they do not exist. Drupal is also a platform, a community, mature, organized.

It should also be recognized that Drupal 8 is a young system, which has been rewritten almost completely, and like any new system it must wipe off some youthful defects. The systematic writing of automated tests is also a novelty introduced with Drupal 8, automated tests that can only enhance its maturity in the short and medium term. The more complex a system, the more diverse and varied it is, the more numerous and demanding its users, and the more potential bugs. A simple information system with a limited base of users will have mathematically fewer bugs, either because of less functional coverage or because they have not yet been discovered.

Finally, Drupal 8 with its new versioning policy and a roadmap for functional evolutions in every minor release is a constantly evolving system. You found too bugs in Drupal Core that are linked to its experimental module policy (see https://www.drupal.org/core/experimental), a policy that allows to integrate as soon as possible new functionalities in Core, and thus allow developers to iterate on this new module. Most experimental modules integrated in the core are not intended to be used in production, unless they have expert skills on Drupal, and therefore integrate the Drupal 8 core in alpha version. Based on this finding, it makes perfect sense to find many major or critical bugs.

It is certain that a Formula 1 car demands more maintenance, is more demanding, is more prone to breakdowns than a car produced on the same model for many years. It is his nature.

Drupal is not secure

Five security bulletins, including a critical flaw, were released in 2016. Does this mean Drupal 8 is not secure?

Zero risk does not exist, the infallible security no more. Drupal 8 is recognized as one of the most secure CMS in the world. The important thing is not to know if Drupal has had or will have a security flaw, but how Drupal handles this one. The management of the issue SA-CORE-2014-005 is a perfect illustration.

In addition, it is important to note that these security advisories, critical or not, make it possible to evaluate very precisely the threat, and if the conditions of a potential attack are indeed gathered for your site Drupal. This is not always the case, given the modular aspect of Drupal core, and often conditions are not met.

Conclusion

Some defects of Drupal 8 are not mythical. They are real. Drupal 8 is bigger and slower, if you measure its raw performance without the tools available. But some defects are also compensated by a whole palette of tools made available. Other flaws in my opinion seem more to be a real misunderstanding of Drupal, when it is not simply the brief counter. Yes it is more complicated to build a five floors building than a wooden hut. They are not the same materials, the same techniques, nor the same foundations. But in the end, for the daily use of a building or a hut, to use a lift, nothing complicated, it will be necessary to press a button (except that the hut will have no elevator), to light a heater you will have to turn a button, etc. The only difference will be the respective capacities of these constructions.

Jun 20 2017
Jun 20

In a previous post, we saw the Drupal 8's new policies for versioning, support and maintenance for its minor and major versions. This policy has evolved somewhat since the last DrupalCon Baltimore conference in April 2017. And this evolution of Drupal's strategy deserves a little attention because it can bring new light to those who hesitate to migrate their site on Drupal 8. Or those who are wondering about the relevance of launching their web project on Drupal 8.

Since drupal 7 is maintained until the release of Drupal 9, the issue of waiting for Drupal 9 release can be legitimate, especially since the technological break between Drupal 7 and Drupal 8 has been consistent, we can say, with a re-writing from scratch of a very large part of its source code (~ 80%), but also at the same time a reduction of its technical debt.

And uncertainty hangs up to now on the height of the march that would be between Drupal 8 and Drupal 9. Let's face this uncertainty.

A maintenance of Drupal 8 until 2019?

The publication of the Drupal 8 road map has apparently misled some readers somewhat stunned. Indeed, the schedule published on the cycle of releases on drupal.org can sometimes be misinterpreted. So much so that now the online version of this schedule has a watermark Example only.

drupal release cycle

This diagram explains the principle of the publication cycle of the minor versions of Drupal 8, and the maintenance principles of Drupal 8 from the moment Drupal 9 will be released. This diagram does not indicate that Drupal 9 will be published 4 and a half years after the release of Drupal 8, in 2019. Note the 8.n.x of the last minor version.

And yet I could see these categorical assertions circulating:

  • Maintenance of Drupal 8 is scheduled until 2019
  • Drupal 8 will receive security maintenance only from 2020 to 2021

And this with the support of a somewhat adapted scheme (no doubt that it was misunderstood), with the addition of the years on the abscissa axis.

False release cycle drupal 8Schema modified (and skewed) of the release cycle of Drupal's major and minor versions.

To date nobody knows if the Drupal 8.4.x version will be the last minor version of Drupal 8, with the launch of Drupal 9 in 2018. In fact we are already talking about versions 8.5.x and 8.6.x currently and new additions in Drupal 8 core, whether it's the publishing process, content versioning, redesigned media management, JsonAPI integration, and so on.

No. The end of the Drupal 8 maintenance is not scheduled for 2019. In fact, to be clear, nobody knows. You can also have a Drupal 8.6.x version as the latest LTS minor version as a Drupal 8.14.x LTS version in 2022.

And if anyone dares to say otherwise to this day, then he has a vision of the future out of the ordinary, and would do well to play the Lotto right away.

Will Drupal 9 be a technological break with Drupal 8?

Drupal 8 was a real technological breakthrough. And it is understandable that some reluctance is expressed if the effort required to migrate a Drupal 8 project on the future Drupal 9 version is as important as that between Drupal 7 and Drupal 8. Given this uncertainty, we can understand that many sites on Drupal 7 can wait for Drupal 9 version, in case...

Instead of two migrations, which are not trivial for complex sites (and still it depends on the project, some Drupal 7 sites can be migrated in a few days at most), I prefer to make only one.

This is logical and understandable.

But since DrupalCon Baltimore last April, Dries Buytaert has clarified this policy on the next major versions of Drupal (see Making Drupal upgrades easy forever) and lifted this uncertainty and fear.

Migrating from Drupal 8 to Drupal 9 will be as easy as migrating from a minor version of Drupal 8 to another. In fact Drupal 9 will be neither more nor less than a new Drupal 8 lightened of all its API which will have been deprecated during its different minor versions. An image is better than all speeches.

Migration Drupal 9

The consequences of this policy with regard to the future Drupal 9 are multiple

  • For those who use Drupal 8 core, the migration to Drupal 9 will be instantaneous. In fact it will be just as simple as a minor version change.
  • The Drupal 8 contrib modules will be ~90-95% compatible with Drupal 9, or even 100% if the maintainers maintains their modules regularly and replace the deprecated functions by the new introduced APIs
  • The custom modules, developed to measure, will be compatible with Drupal 9 as well as the contrib modules

One could almost say that Drupal 9 will be the continuity of Drupal 8 with a consolidated API, refined, and purified but also with all the functional richness, at the click, introduced with all the additions that are released with the Drupal 8.x minor versions.

In fact, if we were to try to guess the date of the maintenance end of Drupal 8 today, we would just as well try to find a needle in a haystack. And even if it was found, the good deal! Drupal 9 would be an additional minor version, just a little more special. With all the ecosystem contributed modules preserved and compatible.

In other words, the compatibility of the modules is now guaranteed on the major ascending versions.

And this is very good news. No ?

Jun 07 2017
Jun 07

Creating a responsive mega menu is often a regular prerequisite on any project, Drupal 8 or other. And if we can find some solutions offering to create mega menus easily, very often these solutions remain quite rigid and can hardly be adapted to the prerequisites of a project. But what is a mega menu? It is nothing more than a menu that contains a little more than a list of links (proposed by the menu system of Drupal 8), with specific links, text, images, call to actions, etc.

Rather than a rigid solution, which may be appropriate if we are willing to meet its requirements, we can also use a more flexible, open solution that also requires a little more work at the theming level. Let's find out how to build a mega menu with the Simple Mega Menu module.

General overview

The main idea of ​​this module is based on two assumptions:

  • A mega menu can be designed using all the power of the Field API of Drupal 8, in fact in the same way that we construct different content type by adding different field types to them.
  • The final rendering is delegated to the Twig template engine, leaving a large margin of maneuver for positioning the different elements of a mega menu.

Simple mega menu is therefore not a solution out of the box that will provide you with a mega menu immediately usable (whatever it offers an example of mega menu pre-configured if one activates the example module). It simply allows to use a content entity as support for its mega menu, and thus to be able to use any type of field; Including links, references to content, images, videos, text, embedded widgets, views, and more. In short, everything you can do with the Drupal 8 content entity types.

For rendering the mega menu, the module provides two Twig functions:

  • has_megamenu (item.url): lets you know if a menu link has a mega menu attached to it
  • view_megamenu (item.url, view_mode): allows to display the mega menu in the specified display mode

With these two Twig functions, you can then display a mega menu in any ways, formatted from the twig template of the mega menu entity, to the exact place where you want to position your mega menu in relation to the menu link (and its possible children).

Let's find out more about how Simple Mega Menu works.

Creating a mega menu type

Once the module is installed, we need to create a mega menu type available in the Administration > Structure menu, at the URL /admin/structure/simple_mega_menu_type.

Add mega menu type

We create a mega menu entity type and we select for which menus the contents of this type of mega menu will be available.

Create mega menu type

Once the mega menu type saved we can then manage, like any content entity type, the different fields that will compose it, its view modes, form modes, etc. 

Add fields to mega menu

Here we have added some fields

  • Links: a multiple Link field
  • Image: an image field
  • Text: a long text field
  • Bottom links: another link multiple field

Add fields to mega menu

We then configure the different view modes of our entity. By default, the module installs two view modes, Before and After. You can of course delete them, or add as many as you want.

Here we configure the Image and Text fields to be displayed in the Before view mode.

Mega menu view mode before configuration

And the After view mode is configured to display only links from the Link field.

Mega menu view mode after configuration

To add and configure other display modes, simply go to the configuration url (/admin/structure/display-modes/view) and add a new one.

Mega menu add new view mode

To stay simple in this tutorial, we will not add the Bottom view mode, which we should configure to allow us to display only the Bottom Links field. But I think you have understood the principle.

After these few configuration operations, strictly similar to the management of content types, we have a mega menu type that will allow us to create our contents and associate these contents with elements of the targeted menu(s) selected previously.

You should also remember to set permissions to allow visitors or users of your site to view mega entities published or not.

Mega menu configuration permissions

 

Creating the mega menu contents

We can access from the menu Contents > Simple mega menu list to the list of mega menus entities created.

Liste des mega menus

And of course we can create as many as necessary using the appropriate button.

Création d'un élément de mega menu

And our first content of our mega menu is created.

Premier élément du mega menu

Linking mega menu items to menu links

To display our first mega menu content, we must select it from the menu link for which we want to display this mega menu. To do this, a new field is available in the menu link form. Select (autocompletion field) the previously created mega menu from the Simple Mega menu field.

Association mega menu to menu link

And here it is finished, or almost. You can repeat as many as necessary on the different links of your menu this operation. Also make sure that your menu block is configured correctly to display a sufficient menu depth.

Menu bloc configuration

By default, with the Twig template provided by the module, the mega menu attached to links will only be displayed if this link has child links. So make sure you have some child links on your main menu link.

Menu hierarchy

If you do not have or do not want child links, just do it, just modify the default Twig template by copying it to your theme, and edit a few lines according to yours needs.

Customizing the mega menu with Twig

The Simple Mega Menu module provides a default template for menus, for menus that are targeted by a mega menu type. This template is just a simple override of the default Twig template included with Drupal 8, on which a few lines have been added. These few lines will allow you to inject each part of your mega menu to a very precise place of your menu HTML structure.

Let's discover this template

{% import _self as menus %}

{#
  We call a macro which calls itself to render the full tree.
  @see http://twig.sensiolabs.org/doc/tags/macro.html
#}
{{ attach_library('simple_megamenu/base') }}
{{ menus.menu_links(items, attributes, 0) }}

{% macro menu_links(items, attributes, menu_level) %}
  {% import _self as menus %}
  {% if items %}
    {% if menu_level == 0 %}
      <ul{{ attributes.addClass('menu', 'menu--simple-mega-menu') }}>
    {% else %}
      <ul {{ attributes.removeClass('menu--simple-mega-menu') }}>
    {% endif %}
    {% for item in items %}
      {%
        set classes = [
          'menu-item',
          item.is_expanded ? 'menu-item--expanded',
          item.is_collapsed ? 'menu-item--collapsed',
          item.in_active_trail ? 'menu-item--active-trail',
        ]
      %}
      <li{{ item.attributes.addClass(classes) }}>
        {{ link(item.title, item.url) }}
        {% if item.below %}
          {% if has_megamenu(item.url) %}
            <div class="mega-menu-wrapper">
              <div class="mega-menu-background"></div>
              {{ view_megamenu(item.url, 'before') }}
              {{ menus.menu_links(item.below, attributes.addClass('mega-menu-item'), menu_level + 1) }}
              {{ view_megamenu(item.url, 'after') }}
            </div>
          {% else %}
            {{ menus.menu_links(item.below, attributes.removeClass('mega-menu-item'), menu_level + 1) }}
          {% endif %}
        {% endif %}
      </li>
    {% endfor %}
    </ul>
  {% endif %}
{% endmacro %}

The most significant part of this template is of course where the mega menu is rendered.

<li{{ item.attributes.addClass(classes) }}>
  {{ link(item.title, item.url) }}
  {% if item.below %}
    {% if has_megamenu(item.url) %}
      <div class="mega-menu-wrapper">
        <div class="mega-menu-background"></div>
        {{ view_megamenu(item.url, 'before') }}
        {{ menus.menu_links(item.below, attributes.addClass('mega-menu-item'), menu_level + 1) }}
        {{ view_megamenu(item.url, 'after') }}
      </div>
    {% else %}
      {{ menus.menu_links(item.below, attributes.removeClass('mega-menu-item'), menu_level + 1) }}
    {% endif %}
  {% endif %}
</li>

Using the has_megamenu() function, we can test whether the link has a mega menu attached to it, using the view_mega_menu() function we can display our mega menu in any view mode. And we have complete freedom to position our mega menu according to our needs.

For example your main menu links do not have children links but just a mega menu. Modify this portion of code as well and the case is set.

<li{{ item.attributes.addClass(classes) }}>
  {{ link(item.title, item.url) }}
  {% if has_megamenu(item.url) %}
    <div class="mega-menu-wrapper">
      <div class="mega-menu-background"></div>
      {{ view_megamenu(item.url, 'before') }}
      {{ view_megamenu(item.url, 'after') }}
    </div>
  {%endif %}
  {% if item.below %}
    {{ menus.menu_links(item.below, attributes, menu_level + 1) }}
  {% endif %}
</li>

You need a specific class for your main links containing a mega menu? Just add an extra class.

{%
  set classes = [
    'menu-item',
    item.is_expanded ? 'menu-item--expanded',
    item.is_collapsed ? 'menu-item--collapsed',
    item.in_active_trail ? 'menu-item--active-trail',
    has_megamenu(item.url) ? menu-item--megamenu,
  ]
%}

etc. etc.

Mega menus within range of Twig

This module does not offer a packaged solution within click range, just within range of Twig. But personally I think I have never been able to use such a solution, as the constraints are often multiple. And since the solution provided by this module is limited to just doing the necessary, namely to insert particular HTML markup within a list of classic menu links, this one remains compatible with any other module intervening on the menu system, modules that can offer you, for example, an amazing responsive menu such as those provided by the module Responsive and off-canvas menu.

This module allows us to offer a graphical interface to editors to update easily elements of a mega menu, relying mainly on the Drupal 8 core and its entities, while allowing a Drupal developer (or a developper front end knowing Twig) to precisely position these different elements, and to be able to get in a few hours a mega menu completely mastered. The rest, formatting of the mega menu in particular, is no longer a matter of Magic CSS. And it's not necessarily the easiest part.

May 24 2017
May 24

It may sometimes be necessary to render a single field of a content or entity. For example, for a simplified display of contents relating to the content consulted, the use of specific fields in other contexts, etc. Obtaining programmatically the rendering of a field may be problematic for the Drupal 8 cache invalidation system, since the resulting render array would not contain the cache tags of the source entity. So this field displayed In another context can not be invalidated if the source node were to be updated.

Let's take a look at some solutions available to us.

Using View Mode

To avoid this problem, and to avoid having to start managing the cache tags manually (Drupal 8 is already doing very well, and certainly better), we can opt for a specific view mode for the entity in question, and this view mode will only contain the field that we want to display. Thus, we will render this field individually, by simply displaying this content in this specific view mode, and thus all the cache tags or context linked to our source content will be added automatically to the page in which this field will be used. And we do not have to manage the tags caches of the source node, instead of Drupal 8 core.

Rendering an individual field programmatically

This last solution applies if this type of need remains marginal, and the number of fields to be displayed individually is limited, because if we were to create as many view modes as individual fields, for a very large number of fields, this option could quickly become indigestible, time consuming and very painful to maintain.

We can then use a few lines of code to render this individual field, and be able to inject it into any page.

This snippet will allow us to retrieve the render array for a field individually.

/**
 * Implements hook_preprocess_HOOK().
 */
function my_module_preprocess_node(&$variables) {
  /** @var \Drupal\node\NodeInterface $node */
  $node = $variables['elements']['#node'];

  $entity_type = 'node';
  $entity_id = 2;
  $field_name = 'body';

  /** @var \Drupal\node\NodeInterface $source */
  $source = \Drupal::entityTypeManager()->getStorage($entity_type)->load($entity_id);
  $viewBuilder = \Drupal::entityTypeManager()->getViewBuilder($entity_type);
  $output = '';

  if ($source->hasField($field_name) && $source->access('view')) {
    $value = $source->get($field_name);
    $output = $viewBuilder->viewField($value, 'full');
    $output['#cache']['tags'] = $source->getCacheTags();
  }

  if ($node->id() == '1') {
    $variables['content']['other_body'] = $output;
  }

}

In this example, we retrieve the body field from a node (with the id 2), reconstruct its render array in the "full" view mode, and then add to the array the cache tag of the node Source (node:2). Finally, we add this field in the variables supplied to the Twig template for the node whose id is 1.

Without adding the source node's cache tag with the line below, then if the source node is updated, its body field rendered in another context will remain the same. Or worse, if we unpublish this source content, some of its content will always be visible in another context.

$output ['# cache'] ['tags'] = $source->getCacheTags();

It is enough to check in the headers of the page on the content (node ​​1), without this addition, only the cache of the current node is present (node:1).
 

Entêtes de la page et debug des cache tags

By adding the source node cache tag to the render array of the individual field, we ensure that this field is always up-to-date, regardless of the context in which it is displayed. Thus we can check the cache tags of the content (node ​​1) on which we injected this field coming from the node 2.

Entêtes de la page et debug des cache tags

We now have the cache tag node:2 present in the headers, making sure that this page will be invalidated as soon as the source content is modified. We can now, in all tranquility, resort to this method to inject individual fields of any entity into another context.

It is enough to not forget the cache tags (among others), and during the development phase, it is necessary to think to activate the cache on a regular basis (which does not deactivate the caches definitively during the development phase?). We were able to see, the cache system, its management, its invalidation, must be an integral part of the development process, otherwise there will certainly be some issues when deploying to production the Drupal 8 project.

Apr 04 2017
Apr 04

The token module is one of these essential modules on any Drupal 8 project. It allows you to use tokens in certain input fields, whether configuration or content, to target the value of one entity field. Many modules use it to allow users or site builder to provide dynamic value without the need for coding.

Let's see how to access the content's values from these tokens, but also to the values ​​indirectly associated with these contents, from Entity reference fields.

Access fields directly attached to an entity

Recovering the value of a content's field is done simply. For example, to retrieve the title of a content, we will use the token [node:title]. Similarly to retrieve the value of a particular field of content (so a node in Drupal terminology), eg field_name, we can use [node:field_name]. Generally, when you have the ability to use tokens, you have a list of tokens available in the context you are in (a node, a user, etc.).

La liste des jetons disponibles

And you have all the time to browse the list of available tokens and select those that correspond to your need.

So far, so good !

Access to fields of entities referenced by an entity

But we may also need to access, always with tokens, to field values ​​that are no longer attached directly to the entity that is the reference context, but which are attached to another entity referenced by our content, or even why not, to an entity that is referenced by another entity itself referenced by our content.

Are you still following?

For example, we may have content (a node), which reference an another content, the latter content referencing a taxonomy term, and we wish to retrieve the name of that taxonomy term.

Or another (deeper) example: a content that references another content, the latter content references some paragraphs (paragraph entity), paragraphs to which an image is attached (the paragraph therefore references a File entity that stores the image) . And we want to retrieve the image's url, from the initial content, and this always with a simple token.

The Drupal 8 token module allows us to achieve our ends by using tokens chained to each other (previously, on Drupal 7, we needed the entity_token module to achieve the same results, module included with the entity module). But sometimes these chained tokens are not all present in the list of tokens available, especially when one reaches 3 levels of depth or more. At the same time, the possibilities of presenting chained tokens up to three (or more) levels of depth become so numerous, that this list would become too long, disrupting the selection of the main tokens.

I confess to having looked for a certain time the syntax to use. But after a few readings, and deductions, bingo, its obviousness is obvious (oups!). I therefore share the result here.

The key here is to use a special token, entity, which will allow us to chain the entities referenced one to the other from our token and to traverse them as for a small Sunday ballad. And because a drawing is better than a long speech, let us take a few examples.

  • We have a content (A) that references another content (B) with an Entity Reference field field_ref_content, and the content B contains an image, image referenced from field field_image. If we want to have the url of this image from the A content, we just need to use the following token: [node:field_ref_content:entity:field_image:entity:url]
  • We have a content (A) that references (field_ref_content) another content (B), the latter content references a paragraph (field_paragraph), paragraph to which an image is attached (the paragraph therefore references the image from field field_image). And we want to retrieve the image's url, from the content A. Our token to be used is then: [node:field_ref_content:entity:field_paragraph:entity:field_image:entity:url].
  • In the case of multiple fields it is of course necessary to target which entity to be accessed using its delta (or its index). For example, a content (A) that references paragraphs (multiple field field_paragraphs), paragraphs that have an image. To access the url of this image for the first paragraph (delta 0) we will then use the token: [node:field_paragraphs:0:entity:field_image:entity:url]

Finally, it is rather simple, no need here of a Drupal 8 expert. To access the values ​​of a referenced entity, it is therefore enough to use the token entity and to associate it with the Entity Reference field used, and then we access the entirety of this entity and therefore its fields, and this in a recursive way, to infinity if needed.

Apr 04 2017
Apr 04

The token module is one of these essential modules on any Drupal 8 project. It allows you to use tokens in certain input fields, whether configuration or content, to target the value of one entity field. Many modules use it to allow users or site builder to provide dynamic value without the need for coding.

Let's see how to access the content's values from these tokens, but also to the values ​​indirectly associated with these contents, from Entity reference fields.

Access fields directly attached to an entity

Recovering the value of a content's field is done simply. For example, to retrieve the title of a content, we will use the token [node:title]. Similarly to retrieve the value of a particular field of content (so a node in Drupal terminology), eg field_name, we can use [node:field_name]. Generally, when you have the ability to use tokens, you have a list of tokens available in the context you are in (a node, a user, etc.).

La liste des jetons disponibles

And you have all the time to browse the list of available tokens and select those that correspond to your need.

So far, so good !

Access to fields of entities referenced by an entity

But we may also need to access, always with tokens, to field values ​​that are no longer attached directly to the entity that is the reference context, but which are attached to another entity referenced by our content, or even why not, to an entity that is referenced by another entity itself referenced by our content.

Are you still following?

For example, we may have content (a node), which reference an another content, the latter content referencing a taxonomy term, and we wish to retrieve the name of that taxonomy term.

Or another (deeper) example: a content that references another content, the latter content references some paragraphs (paragraph entity), paragraphs to which an image is attached (the paragraph therefore references a File entity that stores the image) . And we want to retrieve the image's url, from the initial content, and this always with a simple token.

The Drupal 8 token module allows us to achieve our ends by using tokens chained to each other (previously, on Drupal 7, we needed the entity_token module to achieve the same results, module included with the entity module). But sometimes these chained tokens are not all present in the list of tokens available, especially when one reaches 3 levels of depth or more. At the same time, the possibilities of presenting chained tokens up to three (or more) levels of depth become so numerous, that this list would become too long, disrupting the selection of the main tokens.

I confess to having looked for a certain time the syntax to use. But after a few readings, and deductions, bingo, its obviousness is obvious (oups!). I therefore share the result here.

The key here is to use a special token, entity, which will allow us to chain the entities referenced one to the other from our token and to traverse them as for a small Sunday ballad. And because a drawing is better than a long speech, let us take a few examples.

  • We have a content (A) that references another content (B) with an Entity Reference field field_ref_content, and the content B contains an image, image referenced from field field_image. If we want to have the url of this image from the A content, we just need to use the following token: [node:field_ref_content:entity:field_image:entity:url]
  • We have a content (A) that references (field_ref_content) another content (B), the latter content references a paragraph (field_paragraph), paragraph to which an image is attached (the paragraph therefore references the image from field field_image). And we want to retrieve the image's url, from the content A. Our token to be used is then: [node:field_ref_content:entity:field_paragraph:entity:field_image:entity:url].
  • In the case of multiple fields it is of course necessary to target which entity to be accessed using its delta (or its index). For example, a content (A) that references paragraphs (multiple field field_paragraphs), paragraphs that have an image. To access the url of this image for the first paragraph (delta 0) we will then use the token: [node:field_paragraphs:0:entity:field_image:entity:url]

Finally, it is rather simple, no need here of a Drupal 8 expert. To access the values ​​of a referenced entity, it is therefore enough to use the token entity and to associate it with the Entity Reference field used, and then we access the entirety of this entity and therefore its fields, and this in a recursive way, to infinity if needed.

Feb 21 2017
Feb 21

Drupal 8 has several solutions and methods to manage access rights on each elements included in a content, and this in a very granular way. Enabling view or edit access on some field included in a content type can be achieved very simply, with a few lines of code, or with the Field Permissions module. We can use this module to allow certain roles to view or update a particular field.

The problem with the case of documents associated with content is slightly different. You may want to let view rights to a document or file attached (via a File field) to a content while controlling the rights to be able to download this document. In other words, you can want to manage the rights to download a file while allowing its visualization (and so its existence).

This is where the Protected file module answers. This module allows to define, for each attached file, if downloading it is publicly accessible or if it requires a particular role. In the case of a protected file, the module then presents an alternate link (configurable, for example the link to the authentication page) instead of the download link.

Let's discover this module.

Prerequisites for the installation of the module

In order to control access to files, this module can only work if the site has a private file system configured. In fact, files stored on the Drupal public file system are accessible directly from the Web server, and consequently Drupal can not control access rights to these files.

Using the module

The Protected file module provides a new field type called ... Protected file. This new field type extends the File field type provided by Drupal core and is almost similar in terms of configuration. To enable file access control, we need to add a new field to our content.

Configuring the Protected file

Let's add a Protected file field to our article content type.

Ajour du champ

And we can configure its storage settings.

Paramètres de stockage du champ

We note that the private file system is automatically selected and locked. We configure the field for an unlimited number of files.

Then we configure the parameters of the instance of this field on the content type Article on which we created it.

Paramètres du champ

We configure the various parameters, which are identical to those of a standard File field type (allowed extensions, upload directory, maximum file size, etc.).

Configuring Display Settings for the Protected File Field

We configure the display settings for our new field.

Paramètres d'affichage du champ

We have several options. We can :

  • Choose to open the file in a new tab or not
  • Configure the url that will replace the file's download url, if the user does not have sufficient access rights.
  • Choose to open the previously defined url in a modal window, or not
  • Define the message that will feed the title tag of the link set above. This message is provided as a variable to the template for rendering the links and can therefore be displayed directly with a simple override of the template in your theme

Configuring permissions

All you have to do is to set the permissions according your needs.

Permissions protected file

And the configuration is complete. We can now publish content and associated documents, protected or not.

Enabling Download Protection

Using the module is really simple. In the content creation / editing form, we can, for each file uploaded, activate or not this protection, by checking the corresponding checkbox.

Formulaire d'ajout des fichiers

And with the result : an authenticated user can access to the files download links.

Fichiers protégés téléchargeables

And for anonymous visitors

Fichiers protégés

In this example above, the download link of the PDF file example 1 has been replaced by the url that we defined in the display settings (/user/login). And a click on the protected file opens a modal window on this page.

Fenêtre modal de login

The Protected file module allows users to simply control access to the documents provided in a content. It should be noted that direct download links, sent by e-mail, for example, by an authenticated user, are also managed, and require the same access rights.

Feb 07 2017
Feb 07

Drupal 8 makes it possible to carry out certain mass actions on the site's contents, such as publishing or unpublishing massively contents, positioning them at the top of lists, etc. It may be useful to provide to certain user profiles some customized actions related to the specificities of their site, such as highlighting certain taxonomy terms, changing the value of a specific field, and thus avoiding heavy and tedious update operations to users on each of the content to be modified.

To simplify the management of these contents, we can create customized actions, which will rely on the plugin Action provided by Drupal core, actions that can be launched massively from a view, like what proposed the Views bulk operation module for Drupal 7, whose the migration to Drupal 8 of some of its features is under discussion.

How to highlight certain taxonomy terms

We will go on an use case that is not uncommon to meet. Imagine a site that has a Keywords vocabulary, and we want to be able to easily highlight certain keywords that would be placed in a block of a dedicated view.

We can simply create a boolean field (we will call it field_push) on the Keywords vocabulary and we will then be able to distinguish which keywords we want to put forward on this block, simply by checking this checkbox on each taxonomy term, and of course to filter the keywords on that field into a specific view to retrieve them.

Un champ booléen attaché aux termes de taxonomy Keywords

We can then edit each keyword to check or not this option. But for a publisher, or a webmaster, this task can quickly prove tedious. It is necessary to identify the keywords already put forward or not, go on each of them, then modify them according to the moment. On a site with many keywords, this can become very time-consuming or even source of error.

Modifier les termes de taxonomy un par un

Creating an administrative View

We can create a view that will allow publishers to manage these keywords and their highlights. This will allow them to quickly identify which keywords are put forward, and to be able to modify them in mass in a few clicks.

Création d'une vue basée sur les termes de taxonomy

We therefore create a view based on taxonomy terms, and we restrict this view to the Keywords vocabulary.

We can quickly obtain a view listing the keywords, their status with the boolean field highlighting them, and an action list on each taxonomy term to be able to modify them one by one.

Une vue d'administration basique des termes

But we do not have on our view this magic field called Bulk update which allows us to launch mass updates on the selected items of our view, like what we can have on a view listing contents or users.

If we do not have this Bulk update field in views for taxonomy terms, it is simply because by default no action is yet defined on this entity type. But creating a customized action, thanks to the Drupal 8 Plugin system, can be done very simply.

Create a custom bulk update action

We will create a module that we will call My BO (machine name: my_bo) that will provide us these custom actions. The structure of this module is illustrated below. You can find the entire code of this sample module on this GitHub repository.

Structure du module My BO

In the module's config folder, we provide the schema (my_bo.schema.yml) of the configuration implemented, namely the configurations of our two custom actions: system.action.term_push_front.yml and system.action.term_unpush_front.yml.

The src/ Plugin/Action folder will contain the classes of the two Action Plugins created.

Let's look at the contents of the file system.action.term_push_front.yml

# File system.action.term_push_front.yml
langcode: en
status: true
dependencies:
  module:
    - taxonomy
id: term_push_front
label: 'Push term in front'
type: taxonomy_term
plugin: term_push_front
configuration: {  }

The key elements of the plugin configuration are:

  • Its dependencies: we declare the taxonomy module in order to be able to use the taxonomy terms
  • The identifier of the configuration (id) that must correspond to the identifier included in the file name
  • The entity type on which the plugin will act (here taxonomy_term entities)
  • And finally the identifier of the Plugin Class (key plugin). It is this identifier that we declare in our class


Let's browse the file of this Plugin, the file src/Plugin/Action/TermPushFront.php

<?php

namespace Drupal\my_bo\Plugin\Action;

use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;

/**
 * Push term in front.
 *
 * @Action(
 *   id = "term_push_front",
 *   label = @Translation("Push term in front"),
 *   type = "taxonomy_term"
 * )
 */
class TermPushFront extends ActionBase {

  /**
   * {@inheritdoc}
   */
  public function execute($entity = NULL) {
    /** @var \Drupal\taxonomy\TermInterface $entity */
    if ($entity->hasField('field_push')) {
      $entity->field_push->value = 1;
      $entity->save();
    }

  }

  /**
   * {@inheritdoc}
   */
  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
    /** @var \Drupal\taxonomy\TermInterface $object */
    $result = $object->access('update', $account, TRUE)
      ->andIf($object->field_push->access('edit', $account, TRUE));

    return $return_as_object ? $result : $result->isAllowed();
  }

}

This Action plugin simply extends the ActionBase class and overrides its two main methods.

  • The execute() method, which will execute the desired operation, here change the value of our boolean field field_push
  • The access() method, which will verify that the user initiating the update operation has the right to modify the entity in question and this particular field

You will notice in the Annotations of the Plugin, its identifier, the same one declared in the plugin configuration file, as well as the entity type on which our Action Plugin applies.

After enabling this module, we can see now this bulk update field in our administration view.

Champ views de mise à jour massive sur les termes de taxonomy

And we can select the actions that will be available via this bulk update field.

Paramètres du champ de mise à jour massive

An administration view for custom bulk upgrades

We then have a view allowing some users to mass update the site's keywords, on a specific field. We can also note that creating a specific action can allow us to allow some users to update a property of an entity, such as taxonomy terms, without having rights to that entity itself. All you have to do is to customize the access() method of the plugin to modulate these rights.

Une vue d'administration de termes de taxonomy avec des mises à jour en masse possible

Going further with configurable actions

In this post, we created simple actions to modify the value of a field in a predetermined way. Boolean fields lend themselves quite well. But we can also easily create configurable actions that allow you to modify the value of a text field in a massive way, for example by entering or selecting a new value when you start the update. To do this, it is enough that the Plugin extends not the ActionBase class, but the ConfigurableActionBase class that will allow us to implement a form allowing this interaction. But this may be the subject of another post.

Jan 25 2017
Jan 25

Drupal 8 has a new core module, the Tour module, which allows us to set up a contextual help to guide users of a Drupal 8 site for their first steps on it, whether for the discovery of their user profile, the possibilities offered to them, how to create content or the overall management of the site's contents.

Little known, probably because it does not yet have a user interface, this module can be very useful to create in a few minutes contextual help that will make the status of your site at a higher speed, from the users's point of view. Myself, although I knew the existence of the tour module, I had never really bothered to look at it. And yet what a gain for the user experience!

A contributed module should provide a user interface to create and manage the different steps of a guided tour, but until the module is fully operational on Drupal 8 we need to create the different content elements of our guided tour  from a YAML configuration file.

The setting up of guided tours is relatively simple and is carried out in 2 steps

  • We create our guided tour within a YAML configuration file, give it an identifier, a label and assign this guided tour to one or more routes
  • Then we add as many steps as necessary in this configuration file, assigning them to a class or an html identifier present in the page markup.

Let's create a guided tour of our new site

We will create a new module, which we will call my_tour, and add the Tour module as a dependency.

#File my_tour.info.yml

name: 'My Tour'
type: module
description: 'Provide Tours.'
core: 8.x
dependencies:
  - tour
version: 8.x-1.0

We will add our first guided tour by creating the file tour.tour.front.yml in the config / install directory of our module. The name of this file is important and should follow this pattern: tour.tour. [Identifier] .yml. The identifier of our guided tour must then imperatively be front.

The structure of our module looks like this.

Structure de notre module Drupal 8

You will notice the presence of the file my_tour.features.yml. Given the lack of user interface, this file will allow us to easily manage and update content tips of the guided tour. This file my_tour.features.yml containing just the value true, allows us to declare our module as a feature and will allow us to be able to import its content in the active configuration in our Drupal 8 project.

Indeed our guided tour, created with the file tour.tour.front.yml, will be installed and loaded when our module will be installed. To avoid having to uninstall and reinstall the module to update the tips's content of the tour, we can therefore use Features.

Creation of the guided tour and its steps

The configuration file for a guided tour consists of a main part stating the visit, its identifier, its label, and the route name on which the tour will be active, followed by a tips entry containing all the steps of this tour. Below is an example of a guided tour (tips's content are in french)

langcode: fr
status: true
id: front
label: 'Bienvenue sur le site'
module: my_tour
routes:
  - route_name: view.front.page_front
tips:
  front-main:
    id: front-main
    plugin: text
    label: 'Bienvenue'
    body: 'Cette visite guidée se lance automatiquement au chargement des pages si elle est disponible. Vous pouvez désactiver ce lancement automatique depuis votre <a href="https://www.flocondetoile.fr/user">compte utilisateur</a>, et relancer à tout moment une visite en cliquant sur <stong>Tour</strong> situé en haut à droite de la barre d''outils'
    weight: 1
  front-menu-edition:
    id: front-menu-edition
    plugin: text
    label: 'Menu Edition'
    body: 'Ce menu vous donne accès aux tableaux de bord et lien de création des différents contenus'
    weight: 2
    attributes:
      data-id: toolbar-item-uas-core-toolbar-content
  front-menu-users:
    id: front-menu-users
    plugin: text
    label: 'Menu Utilisateurs'
    body: 'Ce menu vous permet de gérer les utilisateurs du site, d''en créer de nouveaux, de leur attribuer des roles, etc.'
    weight: 3
    attributes:
      data-id: toolbar-item-uas-core-toolbar-users
  front-menu-structure:
    id: front-menu-structure
    plugin: text
    label: 'Menu Structure'
    body: 'Ce menu vous permet de gérer les éléments structurants du site, tels que les menus, le placement des blocs, la gestion des taxonomies (mots clés, catégories, rubriques)'
    weight: 4
    attributes:
      data-id: toolbar-item-uas-core-toolbar-structure
  front-general-help:
    id: front-general-help
    plugin: text
    label: Aide contextuelle
    body: 'Chaque page dispose de sa propre aide contextuelle telle que vous êtes en train de la lire. Pour la consulter il vous suffir de cliquer sur le <strong>? Tour</strong>  en haut à droite'
    weight: 5
    location: left
    attributes:
      data-id: toolbar-tab-tour
  front-general-config:
    id: front-general-config
    plugin: text
    label: 'Configuration par défaut'
    body: 'Lors de sa génération le site est livré avec une configuration par défaut, et des contenus pré-renseignés.'
    weight: 6
  front-general-menu:
    id: front-general-menu
    plugin: text
    label: 'Menu de Navigation principal'
    body: 'Le menu de navigation principal peut contenir des liens vers des contenus, des termes de taxonomy, ou encore certaines vues pre-paramétrés pour certaines focntionnalités types telles que les actualités, les événements, les productions. Vous pouvez gérer ce menu depuis le Menu Structutre, ou encore y accéder depuis le lien contextuel qui apparaît au survol du menu.'
    weight: 7
    attributes:
      data-id: navbar
  front-general-slider:
      id: front-general-slider
      plugin: text
      label: 'Diaporama'
      body: 'Vous disposez d''un type de bloc vous permettant de créer des diaporamas et de les placer sur n''importe quelle page. Vous pouvez retouver les blocs diaporama créés, ainsi que tout autre type de bloc, dans la <a href="https://www.flocondetoile.fr/admin/structure/block/block-content">librairie des blocs</a>.'
      weight: 8
      attributes:
        data-id: block-slider-hp
  front-general-block:
    id: front-general-block
    plugin: text
    label: 'Bloc de contenu'
    body: 'Outre les contenus que vous pouvez publier sur le site, vous pouvez aussi créer des blocs de contenu simple et les placer sur les pages qui vous conviennent. La différence majeure entre un contenu et un bloc est que les contenus dispose d''une page dédiée, avec une adresse url, tandis que les blocs doivent nécessairement être placés sur des pages existantes. '
    weight: 8
    attributes:
      data-id: block-presentation-site
  front-general-accueil:
    id: front-general-accueil
    plugin: text
    label: 'Les dernières actualités'
    body: 'Ce bloc qui remonte les derniers contenus publiés sur le site ne peut pas être enlever. Si vous ne souhaitez pas ce bloc il vous suffit de ne mettre aucun contenu en avant sur la page d''accueil. Pour ce faire, aller sur votre tableau de bord, et lancer l''aide contextuelle'
    weight: 9
    location: top
    attributes:
      data-class: 'view-accueil'
  front-footer:
    id: front-footer
    plugin: text
    label: Pied de page
    body: 'Le pied de page contient quelques blocs de menu, dont notamment le menu liens utiles et le menu Pied de page. Vous pouvez personnaliser les liens contenus dans ces menus depuis la gestion des menus.'
    weight: 10
    location: top
    attributes:
      data-class: footer

The main part of our visit consists of the following elements. We have the identifier id of the guided tour, which must be identical to the identifier set in the name of the YAML file, its label, as it will appear on the Help page if the Help module is installed, and routes on which the tour will be active. Here our visit will be active on the homepage of the site, which corresponds to a view whose route name is view.front.page_front.

langcode: fr
status: true
id: front
label: 'Bienvenue sur le site'
module: my_tour
routes:
  - route_name: view.front.page_front

The different steps (tips) of our guided tour consist of the following elements:

  • Id: the identifier of the step
  • Plugin: the plugin type (only the text plugin is available)
  • Label: the label will appear as the title of the displayed tooltip
  • Body: it contains the html content of the tooltip
  • Weight: the weight assigned to step. The steps are displayed in ascending order of weight
  • Location: optional parameter, you can specify where the tooltip will be positioned relative to the targeted html element. Possible values ​​are top, left, right, bottom
  • Attributes: optional parameter. We can attach our tooltip to a html class present in our page (specifying data-class: NAME-OF-CLASS), or to an html id present on the page (specifying data-id). If we declare a data-class or data-id attribute, and this attribute is not present on the page then the step is not displayed. If, on the other hand, we do not specify at all this parameter for our step then this one will be displayed centered, inside a modal on the page, without being attached to any element html.
front-footer:
  id: front-footer
  plugin: text
  label: Pied de page
  body: 'Le pied de page contient quelques blocs de menu, dont notamment le menu liens utiles et le menu Pied de page. Vous pouvez personnaliser les liens contenus dans ces menus depuis la gestion des menus.'
  weight: 10
  location: top
  attributes:
    data-class: footer

And that's all.

You now have a guided tour on your homepage. On each page that has a visit, the launch button Tour of the visit will appear on the Drupal toolbar.

Barre d'outil Drupal 8 avec une visite disponible

Automatic launch of a guided tour

But for new users, unfamiliar with the site, it may turn out that they take a while to realize the existence of such a feature. If they noticed one day and click on the Tour button.

Depending on the situation, it may be interesting to automatically start a visit, if it exists, when loading a page. So the user immediately detects this contextual help, he can not miss it.

This can be achieved very simply. The simplest method can be to trigger a visit if it exists according to a setting present in the user's profile. It is enough, for example, to add a Boolean field to the users entities, for example the field field_tour_disabled, which by default is not checked. To deactivate the automatic launch, the user would only have to check this option in his profile.

Compte utilisateur

And it is enough to load a library on the pages if this option is not checked.

/**
 * Implements hook_page_attachments().
 */
function my_tour_page_attachments(array &$attachments) {
  // Automatically launch tour if not disabled by user.
  if (\Drupal::currentUser()->hasPermission('access tour')) {
    /** @var \Drupal\user\Entity\User $user */
    $user = User::load(\Drupal::currentUser()->id());
    $tour_disabled = $user->field_tour_disabled->value;
    if (empty($tour_disabled)) {
      $attachments['#attached']['library'][] = 'my_tour/tour';
    }
  }
}

And the js file in our my_tour/tour library might look like this:

(function ($, Drupal) {
  'use strict';
  $(document).ready(function() {
    var help = $('.toolbar-icon-help');
    if (help.length) {
      help.click();
    }
  });
}(jQuery, Drupal));

Note that we could also use the user's cookies to register this deactivation, and that there are certainly many other methods to achieve the same purposes.

And now, as soon as a page has a contextual help (or a guided tour), it is launched automatically.

Une visite guidée lancée automatiquement

While this may seem slightly invasive at first, it is nevertheless an efficient method to inform users about the existence of these guided tour which can help him to apprehend more easily and quickly a new interface. Automatic start that users will can activate / deactivate from their profil.

The documentation is dead! Vive Tour!

If the Tour module can seem, at first, be unfinished, because it does not have a user interface, It is none the less that it is really very effective. It allows you to write contextual documentation, built directly into user interfaces, giving users immediate access to relevant information, rather than directing them to a 300-pages document full of screenshots, document that needs to be drafted and updated .

Writing the steps for these visits can be done simply by using any text editor.

We can also note the existence of the project Tour Builder (needs work) whose the main idea is to exchange Tours with ease between Tour Writers and module Coders.

Jan 25 2017
Jan 25

Drupal 8 has a new core module, the Tour module, which allows us to set up a contextual help to guide users of a Drupal 8 site for their first steps on it, whether for the discovery of their user profile, the possibilities offered to them, how to create content or the overall management of the site's contents.

Little known, probably because it does not yet have a user interface, this module can be very useful to create in a few minutes contextual help that will make the status of your site at a higher speed, from the users's point of view. Myself, although I knew the existence of the tour module, I had never really bothered to look at it. And yet what a gain for the user experience!

A contributed module should provide a user interface to create and manage the different steps of a guided tour, but until the module is fully operational on Drupal 8 we need to create the different content elements of our guided tour  from a YAML configuration file.

The setting up of guided tours is relatively simple and is carried out in 2 steps

  • We create our guided tour within a YAML configuration file, give it an identifier, a label and assign this guided tour to one or more routes
  • Then we add as many steps as necessary in this configuration file, assigning them to a class or an html identifier present in the page markup.

Let's create a guided tour of our new site

We will create a new module, which we will call my_tour, and add the Tour module as a dependency.

#File my_tour.info.yml

name: 'My Tour'
type: module
description: 'Provide Tours.'
core: 8.x
dependencies:
  - tour
version: 8.x-1.0

We will add our first guided tour by creating the file tour.tour.front.yml in the config / install directory of our module. The name of this file is important and should follow this pattern: tour.tour. [Identifier] .yml. The identifier of our guided tour must then imperatively be front.

The structure of our module looks like this.

Structure de notre module Drupal 8

You will notice the presence of the file my_tour.features.yml. Given the lack of user interface, this file will allow us to easily manage and update content tips of the guided tour. This file my_tour.features.yml containing just the value true, allows us to declare our module as a feature and will allow us to be able to import its content in the active configuration in our Drupal 8 project.

Indeed our guided tour, created with the file tour.tour.front.yml, will be installed and loaded when our module will be installed. To avoid having to uninstall and reinstall the module to update the tips's content of the tour, we can therefore use Features.

Creation of the guided tour and its steps

The configuration file for a guided tour consists of a main part stating the visit, its identifier, its label, and the route name on which the tour will be active, followed by a tips entry containing all the steps of this tour. Below is an example of a guided tour (tips's content are in french)

langcode: fr
status: true
id: front
label: 'Bienvenue sur le site'
module: my_tour
routes:
  - route_name: view.front.page_front
tips:
  front-main:
    id: front-main
    plugin: text
    label: 'Bienvenue'
    body: 'Cette visite guidée se lance automatiquement au chargement des pages si elle est disponible. Vous pouvez désactiver ce lancement automatique depuis votre <a href="http://flocondetoile.fr/user">compte utilisateur</a>, et relancer à tout moment une visite en cliquant sur <stong>Tour</strong> situé en haut à droite de la barre d''outils'
    weight: 1
  front-menu-edition:
    id: front-menu-edition
    plugin: text
    label: 'Menu Edition'
    body: 'Ce menu vous donne accès aux tableaux de bord et lien de création des différents contenus'
    weight: 2
    attributes:
      data-id: toolbar-item-uas-core-toolbar-content
  front-menu-users:
    id: front-menu-users
    plugin: text
    label: 'Menu Utilisateurs'
    body: 'Ce menu vous permet de gérer les utilisateurs du site, d''en créer de nouveaux, de leur attribuer des roles, etc.'
    weight: 3
    attributes:
      data-id: toolbar-item-uas-core-toolbar-users
  front-menu-structure:
    id: front-menu-structure
    plugin: text
    label: 'Menu Structure'
    body: 'Ce menu vous permet de gérer les éléments structurants du site, tels que les menus, le placement des blocs, la gestion des taxonomies (mots clés, catégories, rubriques)'
    weight: 4
    attributes:
      data-id: toolbar-item-uas-core-toolbar-structure
  front-general-help:
    id: front-general-help
    plugin: text
    label: Aide contextuelle
    body: 'Chaque page dispose de sa propre aide contextuelle telle que vous êtes en train de la lire. Pour la consulter il vous suffir de cliquer sur le <strong>? Tour</strong>  en haut à droite'
    weight: 5
    location: left
    attributes:
      data-id: toolbar-tab-tour
  front-general-config:
    id: front-general-config
    plugin: text
    label: 'Configuration par défaut'
    body: 'Lors de sa génération le site est livré avec une configuration par défaut, et des contenus pré-renseignés.'
    weight: 6
  front-general-menu:
    id: front-general-menu
    plugin: text
    label: 'Menu de Navigation principal'
    body: 'Le menu de navigation principal peut contenir des liens vers des contenus, des termes de taxonomy, ou encore certaines vues pre-paramétrés pour certaines focntionnalités types telles que les actualités, les événements, les productions. Vous pouvez gérer ce menu depuis le Menu Structutre, ou encore y accéder depuis le lien contextuel qui apparaît au survol du menu.'
    weight: 7
    attributes:
      data-id: navbar
  front-general-slider:
      id: front-general-slider
      plugin: text
      label: 'Diaporama'
      body: 'Vous disposez d''un type de bloc vous permettant de créer des diaporamas et de les placer sur n''importe quelle page. Vous pouvez retouver les blocs diaporama créés, ainsi que tout autre type de bloc, dans la <a href="http://flocondetoile.fr/admin/structure/block/block-content">librairie des blocs</a>.'
      weight: 8
      attributes:
        data-id: block-slider-hp
  front-general-block:
    id: front-general-block
    plugin: text
    label: 'Bloc de contenu'
    body: 'Outre les contenus que vous pouvez publier sur le site, vous pouvez aussi créer des blocs de contenu simple et les placer sur les pages qui vous conviennent. La différence majeure entre un contenu et un bloc est que les contenus dispose d''une page dédiée, avec une adresse url, tandis que les blocs doivent nécessairement être placés sur des pages existantes. '
    weight: 8
    attributes:
      data-id: block-presentation-site
  front-general-accueil:
    id: front-general-accueil
    plugin: text
    label: 'Les dernières actualités'
    body: 'Ce bloc qui remonte les derniers contenus publiés sur le site ne peut pas être enlever. Si vous ne souhaitez pas ce bloc il vous suffit de ne mettre aucun contenu en avant sur la page d''accueil. Pour ce faire, aller sur votre tableau de bord, et lancer l''aide contextuelle'
    weight: 9
    location: top
    attributes:
      data-class: 'view-accueil'
  front-footer:
    id: front-footer
    plugin: text
    label: Pied de page
    body: 'Le pied de page contient quelques blocs de menu, dont notamment le menu liens utiles et le menu Pied de page. Vous pouvez personnaliser les liens contenus dans ces menus depuis la gestion des menus.'
    weight: 10
    location: top
    attributes:
      data-class: footer

The main part of our visit consists of the following elements. We have the identifier id of the guided tour, which must be identical to the identifier set in the name of the YAML file, its label, as it will appear on the Help page if the Help module is installed, and routes on which the tour will be active. Here our visit will be active on the homepage of the site, which corresponds to a view whose route name is view.front.page_front.

langcode: fr
status: true
id: front
label: 'Bienvenue sur le site'
module: my_tour
routes:
  - route_name: view.front.page_front

The different steps (tips) of our guided tour consist of the following elements:

  • Id: the identifier of the step
  • Plugin: the plugin type (only the text plugin is available)
  • Label: the label will appear as the title of the displayed tooltip
  • Body: it contains the html content of the tooltip
  • Weight: the weight assigned to step. The steps are displayed in ascending order of weight
  • Location: optional parameter, you can specify where the tooltip will be positioned relative to the targeted html element. Possible values ​​are top, left, right, bottom
  • Attributes: optional parameter. We can attach our tooltip to a html class present in our page (specifying data-class: NAME-OF-CLASS), or to an html id present on the page (specifying data-id). If we declare a data-class or data-id attribute, and this attribute is not present on the page then the step is not displayed. If, on the other hand, we do not specify at all this parameter for our step then this one will be displayed centered, inside a modal on the page, without being attached to any element html.
front-footer:
  id: front-footer
  plugin: text
  label: Pied de page
  body: 'Le pied de page contient quelques blocs de menu, dont notamment le menu liens utiles et le menu Pied de page. Vous pouvez personnaliser les liens contenus dans ces menus depuis la gestion des menus.'
  weight: 10
  location: top
  attributes:
    data-class: footer

And that's all.

You now have a guided tour on your homepage. On each page that has a visit, the launch button Tour of the visit will appear on the Drupal toolbar.

Barre d'outil Drupal 8 avec une visite disponible

Automatic launch of a guided tour

But for new users, unfamiliar with the site, it may turn out that they take a while to realize the existence of such a feature. If they noticed one day and click on the Tour button.

Depending on the situation, it may be interesting to automatically start a visit, if it exists, when loading a page. So the user immediately detects this contextual help, he can not miss it.

This can be achieved very simply. The simplest method can be to trigger a visit if it exists according to a setting present in the user's profile. It is enough, for example, to add a Boolean field to the users entities, for example the field field_tour_disabled, which by default is not checked. To deactivate the automatic launch, the user would only have to check this option in his profile.

Compte utilisateur

And it is enough to load a library on the pages if this option is not checked.

/**
 * Implements hook_page_attachments().
 */
function my_tour_page_attachments(array &$attachments) {
  // Automatically launch tour if not disabled by user.
  if (\Drupal::currentUser()->hasPermission('access tour')) {
    /** @var \Drupal\user\Entity\User $user */
    $user = User::load(\Drupal::currentUser()->id());
    $tour_disabled = $user->field_tour_disabled->value;
    if (empty($tour_disabled)) {
      $attachments['#attached']['library'][] = 'my_tour/tour';
    }
  }
}

And the js file in our my_tour/tour library might look like this:

(function ($, Drupal) {
  'use strict';
  $(document).ready(function() {
    var help = $('.toolbar-icon-help');
    if (help.length) {
      help.click();
    }
  });
}(jQuery, Drupal));

Note that we could also use the user's cookies to register this deactivation, and that there are certainly many other methods to achieve the same purposes.

And now, as soon as a page has a contextual help (or a guided tour), it is launched automatically.

Une visite guidée lancée automatiquement

While this may seem slightly invasive at first, it is nevertheless an efficient method to inform users about the existence of these guided tour which can help him to apprehend more easily and quickly a new interface. Automatic start that users will can activate / deactivate from their profil.

The documentation is dead! Vive Tour!

If the Tour module can seem, at first, be unfinished, because it does not have a user interface, It is none the less that it is really very effective. It allows you to write contextual documentation, built directly into user interfaces, giving users immediate access to relevant information, rather than directing them to a 300-pages document full of screenshots, document that needs to be drafted and updated .

Writing the steps for these visits can be done simply by using any text editor.

We can also note the existence of the project Tour Builder (needs work) whose the main idea is to exchange Tours with ease between Tour Writers and module Coders.

Jan 05 2017
Jan 05

We saw in a previous post how we could automatically generate the image styles defined on a site for each uploaded source image. We will continue this post for this time to carry out the same operation using the Cron API of Drupal 8, which allows us to desynchronize these mass operations from actions carried out by users, and which can therefore penalize performances.

For the record, the objective is to be able to generate all the image styles of a source image at the time of its import, thus improving the overall performance of the site, especially during the first consultations of its pages.

To achieve our goals, we will create a new Plugin that will extend the QueueWorker plugin. This plugin will allow us to create a specific cron task that we can call and instantiate when importing a source image and thus plan the automatic creation of all image styles.

To create our Plugin, we simply create the file MyModuleImageStyle.php in the src/Plugin/QueueWorker folder of our module.

<?php

namespace Drupal\my_module\Plugin\QueueWorker;

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Executes interface translation queue tasks.
 *
 * @QueueWorker(
 *   id = "my_module_image_style",
 *   title = @Translation("Generate image styles"),
 *   cron = {"time" = 60}
 * )
 */
class MyModuleImageStyle extends QueueWorkerBase implements ContainerFactoryPluginInterface {

  /**
   * The image style entity storage.
   *
   * @var \Drupal\image\ImageStyleStorageInterface
   */
  protected $imageStyleStorage;

  /**
   * Constructs a new LocaleTranslation object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param array $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Entity\EntityStorageInterface $image_style_storage
   *   The image style storage.
   */
  public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityStorageInterface $image_style_storage) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->imageStyleStorage = $image_style_storage;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('entity_type.manager')->getStorage('image_style')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    /** @var \Drupal\file\Entity\File $entity */
    $entity = $data['entity'];
    $styles = $this->imageStyleStorage->loadMultiple();
    $image_uri = $entity->getFileUri();
    /** @var \Drupal\image\Entity\ImageStyle $style */
    foreach ($styles as $style) {
      $destination = $style->buildUri($image_uri);
      $style->createDerivative($image_uri, $destination);
    }
  }

}

The most significant method of our plugin is the processItem method, which will generate the different image styles when the scheduled task is called via the cron.

So, to take our previous example on generating image styles when importing the source image, we now only need to invoke our new scheduled task (QueueWorker).

use Drupal\Core\Entity\EntityInterface;

/**
 * Implements hook_entity_insert().
 * Generate all image styles once an Image is uploaded.
 */
function my_module_entity_insert(EntityInterface $entity) {
  /** @var \Drupal\file\Entity\File $entity */
  if ($entity instanceof FileInterface) {
    $image = \Drupal::service('image.factory')->get($entity->getFileUri());
    /** @var \Drupal\Core\Image\Image $image */
    if ($image->isValid()) {
      $queue = \Drupal::queue('my_module_image_style');
      $data = ['entity' => $entity];
      $queue->createItem($data);
    }
  }
}

It will then be necessary to configure the cron to be executed on a regular basis, preferably in a decorrelated manner from the user actions, for example by using cron tasks scheduled on the server hosting the website.

Jan 05 2017
Jan 05

We saw in a previous post how we could automatically generate the image styles defined on a site for each uploaded source image. We will continue this post for this time to carry out the same operation using the Cron API of Drupal 8, which allows us to desynchronize these mass operations from actions carried out by users, and which can therefore penalize performances.

For the record, the objective is to be able to generate all the image styles of a source image at the time of its import, thus improving the overall performance of the site, especially during the first consultations of its pages.

To achieve our goals, we will create a new Plugin that will extend the QueueWorker plugin. This plugin will allow us to create a specific cron task that we can call and instantiate when importing a source image and thus plan the automatic creation of all image styles.

To create our Plugin, we simply create the file MyModuleImageStyle.php in the src/Plugin/QueueWorker folder of our module.

<?php

namespace Drupal\my_module\Plugin\QueueWorker;

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Executes interface translation queue tasks.
 *
 * @QueueWorker(
 *   id = "my_module_image_style",
 *   title = @Translation("Generate image styles"),
 *   cron = {"time" = 60}
 * )
 */
class MyModuleImageStyle extends QueueWorkerBase implements ContainerFactoryPluginInterface {

  /**
   * The image style entity storage.
   *
   * @var \Drupal\image\ImageStyleStorageInterface
   */
  protected $imageStyleStorage;

  /**
   * Constructs a new LocaleTranslation object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param array $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\Core\Entity\EntityStorageInterface $image_style_storage
   *   The image style storage.
   */
  public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityStorageInterface $image_style_storage) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->imageStyleStorage = $image_style_storage;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('entity_type.manager')->getStorage('image_style')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function processItem($data) {
    /** @var \Drupal\file\Entity\File $entity */
    $entity = $data['entity'];
    $styles = $this->imageStyleStorage->loadMultiple();
    $image_uri = $entity->getFileUri();
    /** @var \Drupal\image\Entity\ImageStyle $style */
    foreach ($styles as $style) {
      $destination = $style->buildUri($image_uri);
      $style->createDerivative($image_uri, $destination);
    }
  }

}

The most significant method of our plugin is the processItem method, which will generate the different image styles when the scheduled task is called via the cron.

So, to take our previous example on generating image styles when importing the source image, we now only need to invoke our new scheduled task (QueueWorker).

use Drupal\Core\Entity\EntityInterface;

/**
 * Implements hook_entity_insert().
 * Generate all image styles once an Image is uploaded.
 */
function my_module_entity_insert(EntityInterface $entity) {
  /** @var \Drupal\file\Entity\File $entity */
  if ($entity instanceof FileInterface) {
    $image = \Drupal::service('image.factory')->get($entity->getFileUri());
    /** @var \Drupal\Core\Image\Image $image */
    if ($image->isValid()) {
      $queue = \Drupal::queue('my_module_image_style');
      $data = ['entity' => $entity];
      $queue->createItem($data);
    }
  }
}

It will then be necessary to configure the cron to be executed on a regular basis, preferably in a decorrelated manner from the user actions, for example by using cron tasks scheduled on the server hosting the website.

Dec 15 2016
Dec 15

Drupal 8 allows you to generate image styles for various effects (reduction, cut-out, black-and-white, saturation, etc.) for each uploaded image. You can very quickly have many images styles, and even more if you use responsive images, allowing to propose different dimensions according to the terminal used to consult your website.

These image styles are generated during a first lookup of a page using these, which can be penalizing in terms of performance or even rendering, when you have many media, with so many image styles as break points, such as on a landing page or a listing page.

The snippet below will allow you to automatically generate all possible image styles when uploading the source image.

use Drupal\image\Entity\ImageStyle;
use Drupal\Core\Entity\EntityInterface;
use Drupal\file\FileInterface;

/**
 * Implements hook_entity_insert().
 * Generate all image styles once an Image is uploaded.
 */
function MYMODULE_entity_insert(EntityInterface $entity) {
  /** @var \Drupal\file\Entity\File $entity */
  if ($entity instanceof FileInterface) {
    $image = \Drupal::service('image.factory')->get($entity->getFileUri());
    /** @var \Drupal\Core\Image\Image $image */
    if ($image->isValid()) {
      $styles = ImageStyle::loadMultiple();
      $image_uri = $entity->getFileUri();
      /** @var \Drupal\image\Entity\ImageStyle $style */
      foreach ($styles as $style) {
        $destination = $style->buildUri($image_uri);
        $style->createDerivative($image_uri, $destination);
      }
    }
  }
}

The result will be a slightly longer time for the editor, when he will save the content, but a consequent gain for the visitors, in terms of performance and speed of loading pages, because then all the possible versions of a source image will have already been generated, avoiding many PHP calls during the first lookup of a page, especially if it contains many images.

To improve the user experience, it is also possible to use the Cron API of Drupal 8 to queue the generation of these image styles when saving a source image, reducing the saving time for editors. But this may be the subject of another post.

Oct 20 2016
Oct 20

The Paragraphs module is a very good alternative to a WYSIWYG editor for those who wants to allow Drupal users ans editors to make arrangements of complex pages, combining text, images, video, audio, quote, statement blocks, or any other advanced component.

Rather than let the user to struggle somehow with the text editor to make advanced pages, but never able to reach the level made possible with Paragraphs, we can offer him to compose his page with structured content’s components, each of these components being responsible for rendering the content, according to the settings selected, in a layout keeper under control.

Cite one example among dozens of others (possibility are endless). Rather than offering a simple bulleted list from the text editor, we can create a component that can generate a more refined bulleted list : each item in the bulleted list could for example have a pictogram, a title, a brief description and a possible link, and the content publisher could simply select the number of elements he wishes per row. For example a report of this kind.

Une liste à puces mise en forme avec paragraphs

Offer these components enables an inexperienced user to create complex page layouts, with the only constraint to focus on its content, and only its content.

The different possibles form mode availables for paragraph components

We have several options to display, inside the content edit form, the components created with Paragraphs. we can show them in :

  • Open mode: the edit form of the Paragraph component is open by default
  • Closed mode: the edit form of the Paragraph component is closed by default
  • Preview mode: the paragraph component is displayed as rendered on the front office

Paramètres d'affichage du formulaire d'un composant paragraph

I tend to prefer to retain the default closed mode, on content edit form, to improve editor’s experience. Because if the page consists of many components (and it’s the purpose of Paragraphs module), in open mode the content’s edit form tends to scare the user as the number of form may be important, and also makes it very difficult reorganization of the different components (order change), while the pre-visualization method involves either to integrate theses components inside the administration theme or to opt for using default theme when editing content.

The disadvantage of using the closed mode for editing components

The use of closed mode provides an overview of the different components used on the page (the content), and to rearrange them easily with simple drag / drop. Modification of the various components available is done by uncollapse / collapse them on demand.

Formulaire d'édition d'un contenu composé de paragraphs

With this editing mode, the content’s components are listed and have for title the paragraph’s type used. This can be a major drawback if the content uses many components of the same type, the publisher does not have immediate cues to distinguish which content relates to each component.

Modify the label of paragraph components

We can overcome this issue by creating a small module, which will be responsible for changing the label of each component by retrieving the contents of certain fields of our paragraphs.

The general idea is to alter the content edit form, detect if content contains entity reference revision fields (used by paragraphs), and if so, to recover for each paragraph the value of a field (eg a field whose machine name contains the word title), then change the label used in the edit form for each paragraph with this value.

Let's go to practice and PHP snippet. We will implement hook_form_alter().


/**
 * Implements hook_form_alter().
 */
function MYMODULE_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $form_object = $form_state->getFormObject();

  // Paragraphs are only set on ContentEntityForm object.
  if (!$form_object instanceof ContentEntityForm) {
    return;
  }

  /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
  $entity = $form_object->getEntity();
  // We check that the entity fetched is fieldable.
  if (!$entity instanceof FieldableEntityInterface) {
    return;
  }

  // Check if an entity reference revision field is attached to the entity.
  $field_definitions = $entity->getFieldDefinitions();
  /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
  foreach ($field_definitions as $field_name => $field_definition) {
    if ($field_definition instanceof FieldConfigInterface && $field_definition->getType() == 'entity_reference_revisions') {
      // Fetch the paragrahs entities referenced.
      $entities_referenced = $entity->{$field_name}->referencedEntities();
      /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity_referenced */
      foreach ($entities_referenced as $key => $entity_referenced) {
        
        $fields = $entity_referenced->getFieldDefinitions();
        $title = '';
        $text = '';
        
        foreach ($fields as $name => $field) {
          if ($field instanceof FieldConfigInterface && $field->getType() == 'string') {
            if (strpos($name, 'title') !== FALSE) {
              $title = $entity_referenced->{$name}->value;
            }
            // Fallback to text string if no title field found.
            elseif (strpos($name, 'text') !== FALSE) {
              $text = $entity_referenced->{$name}->value;
            }
          }
        }
        // Fallback to $text if $title is empty.
        $title = $title ? $title : $text;
        // Override paragraph label only if a title has been found.
        if ($title) {
          $title = (strlen($title) > 50) ? substr($title, 0, 50) . ' (...)' : $title;
          $form[$field_name]['widget'][$key]['top']['paragraph_type_title']['info']['#markup'] = '<strong>' . $title . '</strong>';
        }
      }

    }
  }

}

Let us review in more detail what we do in this alteration.

First we check that we are on a content entity form, and the entity that we are currently editing is fieldable.

$form_object = $form_state->getFormObject();

// Paragraphs are only set on ContentEntityForm object.
if (!$form_object instanceof ContentEntityForm) {
  return;
}

/** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
$entity = $form_object->getEntity();
// We check that the entity fetched is fieldable.
if (!$entity instanceof FieldableEntityInterface) {
  return;
}

We check then all the entity’s fields (a node, a content block, or any other content entity) and only treat the entity_reference_revisions  field type that correspond to the field implemented and used by Paragraphs module.

// Check if an entity reference revision field is attached to the entity.
$field_definitions = $entity->getFieldDefinitions();
/** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
foreach ($field_definitions as $field_name => $field_definition) {
  if ($field_definition instanceof FieldConfigInterface && $field_definition->getType() == 'entity_reference_revisions') {
    // Fetch the paragrahs entities referenced.
    $entities_referenced = $entity->{$field_name}->referencedEntities();
    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity_referenced */
    foreach ($entities_referenced as $key => $entity_referenced) {

      // Stuff.

    }

  }
}

For each detected Paragraph entities we will then retrieve the value of a field. In our example, we test first if this is a text field type (string), then test whether its machine name contains the word title, or the word text which will serve as fallback if no field containing title in its machine name is found.

$fields = $entity_referenced->getFieldDefinitions();
$title = '';
$text = '';
foreach ($fields as $name => $field) {
  if ($field instanceof FieldConfigInterface && $field->getType() == 'string') {
    if (strpos($name, 'title') !== FALSE) {
      $title = $entity_referenced->{$name}->value;
    }
    // Fallback to text string if no title field found.
    elseif (strpos($name, 'text') !== FALSE) {
      $text = $entity_referenced->{$name}->value;
    }
  }
}

This example is of course to adapt according to your own context. We could, for example, precisely target a specific field based on the type of paragraph detected. For example :

$bundle = $entity_referenced->bundle();
$title = '';
$text = '';
switch ($bundle) {
  case 'paragraph_imagetext':
    $title = $entity_referenced->field_paragraph_imagetext_title->value;
    break;
  case 'other_paragraph_type':
    $title = $entity_referenced->another_field->value;
    break;
  default:
    break;
}

Finally, we replace the label used by the paragraph type, if we have got a value for our new label.

// Fallback to $text if $title is empty.
$title = $title ? $title : $text;
// Override paragraph label only if a title has been found.
if ($title) {
  $title = (strlen($title) > 50) ? substr($title, 0, 50) . ' (...)' : $title;
  $form[$field_name]['widget'][$key]['top']['paragraph_type_title']['info']['#markup'] = '<strong>' . $title . '</strong>';
}

A happy user

The result then allows us to offer content publishers and editors a compact and readable edition form where he can immediately identify what content refers to a paragraph type.

Formulaire amélioré d'édition d'un contenu composé de paragraphes

This tiny alteration applied on the content edit form, and specifically on the default paragraph labels, makes it immediately more readable and understandable. It translates technical information, more oriented site builder, in a user-content information, giving him a better understanding and better comfort.

I wonder how this feature could be implemented using a contributed module (or inside the Paragraphs module itself ?), the biggest difficulty living here in the capacity of a Entity Reference Revisions field to target an infinite Paragraphs type, themselves containing a possible infinity fields. If you have an idea I'm interested?

You need a freelance Drupal ? Feel free to contact me.

Oct 20 2016
Oct 20

The Paragraphs module is a very good alternative to a WYSIWYG editor for those who wants to allow Drupal users ans editors to make arrangements of complex pages, combining text, images, video, audio, quote, statement blocks, or any other advanced component.

Rather than let the user to struggle somehow with the text editor to make advanced pages, but never able to reach the level made possible with Paragraphs, we can offer him to compose his page with structured content’s components, each of these components being responsible for rendering the content, according to the settings selected, in a layout keeper under control.

Cite one example among dozens of others (possibility are endless). Rather than offering a simple bulleted list from the text editor, we can create a component that can generate a more refined bulleted list : each item in the bulleted list could for example have a pictogram, a title, a brief description and a possible link, and the content publisher could simply select the number of elements he wishes per row. For example a report of this kind.

Une liste à puces mise en forme avec paragraphs

Offer these components enables an inexperienced user to create complex page layouts, with the only constraint to focus on its content, and only its content.

The different possibles form mode availables for paragraph components

We have several options to display, inside the content edit form, the components created with Paragraphs. we can show them in :

  • Open mode: the edit form of the Paragraph component is open by default
  • Closed mode: the edit form of the Paragraph component is closed by default
  • Preview mode: the paragraph component is displayed as rendered on the front office

Paramètres d'affichage du formulaire d'un composant paragraph

I tend to prefer to retain the default closed mode, on content edit form, to improve editor’s experience. Because if the page consists of many components (and it’s the purpose of Paragraphs module), in open mode the content’s edit form tends to scare the user as the number of form may be important, and also makes it very difficult reorganization of the different components (order change), while the pre-visualization method involves either to integrate theses components inside the administration theme or to opt for using default theme when editing content.

The disadvantage of using the closed mode for editing components

The use of closed mode provides an overview of the different components used on the page (the content), and to rearrange them easily with simple drag / drop. Modification of the various components available is done by uncollapse / collapse them on demand.

Formulaire d'édition d'un contenu composé de paragraphs

With this editing mode, the content’s components are listed and have for title the paragraph’s type used. This can be a major drawback if the content uses many components of the same type, the publisher does not have immediate cues to distinguish which content relates to each component.

Modify the label of paragraph components

We can overcome this issue by creating a small module, which will be responsible for changing the label of each component by retrieving the contents of certain fields of our paragraphs.

The general idea is to alter the content edit form, detect if content contains entity reference revision fields (used by paragraphs), and if so, to recover for each paragraph the value of a field (eg a field whose machine name contains the word title), then change the label used in the edit form for each paragraph with this value.

Let's go to practice and PHP snippet. We will implement hook_form_alter().


/**
 * Implements hook_form_alter().
 */
function MYMODULE_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $form_object = $form_state->getFormObject();

  // Paragraphs are only set on ContentEntityForm object.
  if (!$form_object instanceof ContentEntityForm) {
    return;
  }

  /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
  $entity = $form_object->getEntity();
  // We check that the entity fetched is fieldable.
  if (!$entity instanceof FieldableEntityInterface) {
    return;
  }

  // Check if an entity reference revision field is attached to the entity.
  $field_definitions = $entity->getFieldDefinitions();
  /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
  foreach ($field_definitions as $field_name => $field_definition) {
    if ($field_definition instanceof FieldConfigInterface && $field_definition->getType() == 'entity_reference_revisions') {
      // Fetch the paragrahs entities referenced.
      $entities_referenced = $entity->{$field_name}->referencedEntities();
      /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity_referenced */
      foreach ($entities_referenced as $key => $entity_referenced) {
        
        $fields = $entity_referenced->getFieldDefinitions();
        $title = '';
        $text = '';
        
        foreach ($fields as $name => $field) {
          if ($field instanceof FieldConfigInterface && $field->getType() == 'string') {
            if (strpos($name, 'title') !== FALSE) {
              $title = $entity_referenced->{$name}->value;
            }
            // Fallback to text string if no title field found.
            elseif (strpos($name, 'text') !== FALSE) {
              $text = $entity_referenced->{$name}->value;
            }
          }
        }
        // Fallback to $text if $title is empty.
        $title = $title ? $title : $text;
        // Override paragraph label only if a title has been found.
        if ($title) {
          $title = (strlen($title) > 50) ? substr($title, 0, 50) . ' (...)' : $title;
          $form[$field_name]['widget'][$key]['top']['paragraph_type_title']['info']['#markup'] = '<strong>' . $title . '</strong>';
        }
      }

    }
  }

}

Let us review in more detail what we do in this alteration.

First we check that we are on a content entity form, and the entity that we are currently editing is fieldable.

$form_object = $form_state->getFormObject();

// Paragraphs are only set on ContentEntityForm object.
if (!$form_object instanceof ContentEntityForm) {
  return;
}

/** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
$entity = $form_object->getEntity();
// We check that the entity fetched is fieldable.
if (!$entity instanceof FieldableEntityInterface) {
  return;
}

We check then all the entity’s fields (a node, a content block, or any other content entity) and only treat the entity_reference_revisions  field type that correspond to the field implemented and used by Paragraphs module.

// Check if an entity reference revision field is attached to the entity.
$field_definitions = $entity->getFieldDefinitions();
/** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
foreach ($field_definitions as $field_name => $field_definition) {
  if ($field_definition instanceof FieldConfigInterface && $field_definition->getType() == 'entity_reference_revisions') {
    // Fetch the paragrahs entities referenced.
    $entities_referenced = $entity->{$field_name}->referencedEntities();
    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity_referenced */
    foreach ($entities_referenced as $key => $entity_referenced) {

      // Stuff.

    }

  }
}

For each detected Paragraph entities we will then retrieve the value of a field. In our example, we test first if this is a text field type (string), then test whether its machine name contains the word title, or the word text which will serve as fallback if no field containing title in its machine name is found.

$fields = $entity_referenced->getFieldDefinitions();
$title = '';
$text = '';
foreach ($fields as $name => $field) {
  if ($field instanceof FieldConfigInterface && $field->getType() == 'string') {
    if (strpos($name, 'title') !== FALSE) {
      $title = $entity_referenced->{$name}->value;
    }
    // Fallback to text string if no title field found.
    elseif (strpos($name, 'text') !== FALSE) {
      $text = $entity_referenced->{$name}->value;
    }
  }
}

This example is of course to adapt according to your own context. We could, for example, precisely target a specific field based on the type of paragraph detected. For example :

$bundle = $entity_referenced->bundle();
$title = '';
$text = '';
switch ($bundle) {
  case 'paragraph_imagetext':
    $title = $entity_referenced->field_paragraph_imagetext_title->value;
    break;
  case 'other_paragraph_type':
    $title = $entity_referenced->another_field->value;
    break;
  default:
    break;
}

Finally, we replace the label used by the paragraph type, if we have got a value for our new label.

// Fallback to $text if $title is empty.
$title = $title ? $title : $text;
// Override paragraph label only if a title has been found.
if ($title) {
  $title = (strlen($title) > 50) ? substr($title, 0, 50) . ' (...)' : $title;
  $form[$field_name]['widget'][$key]['top']['paragraph_type_title']['info']['#markup'] = '<strong>' . $title . '</strong>';
}

A happy user

The result then allows us to offer content publishers and editors a compact and readable edition form where he can immediately identify what content refers to a paragraph type.

Formulaire amélioré d'édition d'un contenu composé de paragraphes

This tiny alteration applied on the content edit form, and specifically on the default paragraph labels, makes it immediately more readable and understandable. It translates technical information, more oriented site builder, in a user-content information, giving him a better understanding and better comfort.

I wonder how this feature could be implemented using a contributed module (or inside the Paragraphs module itself ?), the biggest difficulty living here in the capacity of a Entity Reference Revisions field to target an infinite Paragraphs type, themselves containing a possible infinity fields. If you have an idea I'm interested?

You need a freelance Drupal ? Feel free to contact me.

Pages

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web