HOWTO.drupal8_plus_module_development - raynaldmo/HOWTO GitHub Wiki

Table of Contents

Drupal 8+ Module Development

Books and other resources

Object-Oriented PHP

Development Environment

  • Use ddev
  • More to come

Installing Drupal

Configuring local site for Development

Standard Files and Directories In a Module

  • MODULE_NAME.info.yml (required): Contains metadata about the module. It's the only required file. It's located in the root of the module directory.
  • MODULE_NAME.module: Holds hooks and procedural PHP code. It's optional and follows a strict naming convention.
  • composer.json: Manages the module's dependencies.
  • MODULE_NAME.api.php: Defines hooks for other modules. *.yml files: Files with a .yml extension other than .info.yml are all optional, and are used to provide module-specific configuration for things like routes, asset libraries, and some YAML-based plugin systems like menu links and migrations.
  • src/ directory: Contains PHP classes, following PSR-4 standards.
  • tests/ directory: Stores automated tests, typically PHPUnit tests.
  • config/ directory: Provides default configuration and schema definitions.
  • templates/ directory: Holds Twig templates for rendering output.

Drupal APIs

Creating Modules

drush --help gen
drush gen

# Generate a module
# Run from <docroot>, one level up from web/ (<web-root>)
drush gen module --destination=modules/custom/<optional_subdir_under_custom>

Routing (and Controllers)

Annotations

  • Annotations
  • A list of all annotations in Drupal 8 core
  • Annotations-based plugins
  • Annotations are specially-formatted PHP docblock comments used for class discovery and metadata description
    • Parsed to retrieve metadata and/or provide configuration for PHP classes
    • Basically is a key-value data structure with nesting support
  • Annotations syntax comes from the Doctrine project
  • Annotations are read by Doctrine annotation parser and turned into a PHP object
  • Mechanism for "Lazy" loading classes and efficient way to collect class metadata
  • D8 uses Annotations for -
    • Plugins
    • Content and Configuration Entities

PHP Attributes

  • Attribute-based plugins
  • PHP Attributes for Drupal Plugins
  • PHP attributes (introduced in PHP 8.0) are a native PHP feature that allows metadata to be attached
    to classes, methods, and properties in a structured manner
  • Prior to Drupal 10.2, the Plugin system used (Symfony Doctrine) Annotations to accomplish the same
    thing that PHP attributes are used for now

Hooks, Events and Plugins

Overview

  • Hooks, Events, Plugins (as well as Services) are all ways to extend Drupal functionality
  • Hooks and plugins are Drupal specific ways of extending functionality
  • Events and services are the generic Symfony way of extending functionality

Hooks

  • Drupal.org | Hooks
  • Hooks allow custom code to execute for every page request
  • Used to react to actions or conditions, alter forms and existing configuration, and extend Drupal in various ways
  • Each hook has a unique name (example: hook_entity_load()), a defined set of parameters, and a defined return value
  • Every hook has three parts: name, implementation, definition.
  • Three kinds of Hooks (definitions)
    • Gather a list of data
    • Alter existing data
    • React to an action
  • Core and contrib module define hooks in *.api.php files
  • Code in these files are hook definitions and are never executed - they're for documentation only
  • ModuleHandler class is responsible for invoking module hooks
  • Modules that define hooks can use three methods to trigger hook execution
    • invoke()
    • invokeAll()
    • alter()

Events

  • What Are Events?
  • Drupal Event API documentation
  • Hook Event Dispatcher
    • This module dispatches events for several Drupal core and module hooks
      This allows you to use the Drupal Event Subscriber system, instead of the outdated hook system,
      to react on certain events
  • Events are basically the OO version of hooks
  • Events allow various components to interact and communicate with one another
    • Example: hook_init() is now KernelEvents::REQUEST
  • Drupal Event system is built on Symfony event dispatcher
    • Implements Mediator design pattern
      • Receive a message from one object, and pass it on to another
      • Implemented by \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
  • Two things can be done with Events
    • Subscribe to an event
      • Subscribe to an event in order to notified when the event occurs - then execute custom code
    • Dispatch a new event when critical actions happen in your code
  • Subscribe to an event
    • In {module_name}/src/{optional_path}, create a class that implements EventSubscriberInterface
    • Implement getSubscribedEvents() method
    • In {module_name}.services.yml file, add service specifying above class and add 'event subscriber' tag
    • Pass in services (like logger.factory) and/or parameter/arguments to event listener via arguments array in {module_name}.services.yml file
# display current events
drush event

# generate an event subscriber
drush gen event-subscriber

Plugins

  • Plugin API Overview
  • Plugin API
  • Drupalize.me | Hooks, Plugins and Events
  • Drupal 8: Hooks, Events, and Event Subscribers
  • Custom Plugin Types
  • Using Drupal 8 Plugin Derivatives
  • Dynamic menu links in Drupal 8 with plugin derivatives
  • Adding CKEditor plugins to Drupal 8
  • Plugins provide a modular and standardized way of extending Drupal core or Drupal module functionality
    • Example: Plugins are used to create custom block, create a new views filter, create a new field type and
      much more
  • Plugins also provide a modular way for implementing new, custom functionality
  • Before Plugins, there was no standardized way of implementing new or extending functionality - This led to module developers implementing their own solutions
  • In D6/D7 hook_info() was sometimes used for this purpose - this solution wasn't extensible though
  • If a certain functionality needs user configuration, a Plugin should be used. otherwise a Service may be more appropriate
  • Drupal components that use Plugins
    • Blocks
    • Render API elements
    • Actions which can be triggered on configurable events
    • Image manipulation effects
    • Entity types
    • Field types, field widgets, and field formatters
    • Menu Links
    • CKEditor Text filters
    • Views fields, filters, sorting
  • Plugin System Components
    • Plugin type: Categories for plugins; for example, blocks or field formatters
    • Plugin manager: Defines how the plugins of a particular type will be discovered and instantiated
      • Creating a new plugin manager defines a new plugin type
      • Plugin managers are services generally prefixed with plugin.manager.*
    • Plugin (or Plugin instance): An individual unit of functionality (One Block, One Field Formatter, One Image effect)
    • Plugin consumer: Custom code that leverages the functionality provided by plugins
  • Example use case: Image Styles
    • Plugin type -> Image Effect
    • Plugin Instance -> Effect, Plugin manager -> Selection a new effect, Consumer -> Actual Image style)
  • Common Plugin types
    • Block: Add a new block that an administrator can place via the block layout UI
    • Field formatter: Add a new option for display data contained in fields of a specific type(s)
    • Field widget: Add a new form widget for collecting data for fields of a specific type(s)
    • Menu link: Define a menu link
    • Menu local task: Define a local task. For example, edit tabs on content pages
    • Views field: Add a new field option to Views

Get list of all Plugins

drush ev 'foreach (\Drupal::getContainer()->getServiceIds() as $id) { $a[$id] = is_object(\Drupal::service($id)) ? get_class(\Drupal::service($id)) : ""; } dump($a);' | grep plugin

  "plugin.manager.config_action" => "Drupal\Core\Config\Action\ConfigActionManager"
  "plugin.manager.link_relation_type" => "Drupal\Core\Http\LinkRelationTypeManager"
  "plugin_form.factory" => "Drupal\Core\Plugin\PluginFormFactory"
  "plugin.manager.entity_reference_selection" => "Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager"
  "plugin.manager.block" => "Drupal\Core\Block\BlockManager"
  "plugin.manager.field.field_type" => "Drupal\Core\Field\FieldTypePluginManager"
  "plugin.manager.field.field_type_category" => "Drupal\Core\Field\FieldTypeCategoryManager"
  "plugin.manager.field.widget" => "Drupal\Core\Field\WidgetPluginManager"
  "plugin.manager.field.formatter" => "Drupal\Core\Field\FormatterPluginManager"
  ...
  "plugin.manager.image.effect" => "Drupal\image\ImageEffectManager"
  ...

Get all image effect plugins

drush ev "dump(\Drupal::service('plugin.manager.image.effect')->getDefinitions())"
^ array:7 [
  "image_crop" => array:5 [
    "class" => "Drupal\image\Plugin\ImageEffect\CropImageEffect"
    "provider" => "image"
    "id" => "image_crop"
    "label" => Drupal\Core\StringTranslation\TranslatableMarkup^ {#2694
      #string: "Crop"
      #arguments: []
      #translatedMarkup: null
      #options: []
      #stringTranslation: null
    }
    "description" => Drupal\Core\StringTranslation\TranslatableMarkup^ {#2695
      #string: "Resizing will make images an exact set of dimensions. This may cause images to be stretched or shrunk disproportionately."
      #arguments: []
      #translatedMarkup: null
      #options: []
      #stringTranslation: null
    }
  ]
  "image_scale_and_crop" => array:5 [
    "class" => "Drupal\image\Plugin\ImageEffect\ScaleAndCropImageEffect"
    "provider" => "image"
    "id" => "image_scale_and_crop"
    "label" => Drupal\Core\StringTranslation\TranslatableMarkup^ {#2696
      #string: "Scale and crop"
      #arguments: []
      #translatedMarkup: null
      #options: []
      #stringTranslation: null
    }
    "description" => Drupal\Core\StringTranslation\TranslatableMarkup^ {#2697
      #string: "Scale and crop will maintain the aspect-ratio of the original image, then crop the larger dimension. This is most useful for creating perfectly square thumbnails without stretching the image."
      #arguments: []
      #translatedMarkup: null
      #options: []
      #stringTranslation: null
    }
  ]
...

Creating a new Plugin Type

  • Create a plugin manager, and define it as a service (in service container)
  • Choose a discovery method. PHP attributes (new default), Annotations (previous default) or YAML file
  • Define an interface for plugin instances
  • Define an annotation class (Assuming Annotations are being used for discovery)
  • Provide a base class (Provides example for implementers)

Services and Dependency Injection

  # get list of services use Devel module
  drush devel:service
  • Dependency injection means that if a class needs an object or some configuration we pass it into (inject) the class (via constructor) instead of globally defining the object or configuration
  • Drupal controllers and plugins normally use the create() factory method for Dependency injection
use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ExampleService implements ContainerInjectionInterface {
   protected $database;
   
   public function __construct(Connection $database) {
     $this->database = $database;
   }
   
   public static function create(ContainerInterface $container) {
     return new static($container->get('database'));
   }
}

// The following will initialize an instance of ExampleService - this call will be made in the HTTP request processing flow
$example = ExampleService::create($container);
  • Setter Injection
    • Setter injection uses setter methods to set dependencies after object instantiation
    • This method is less common but useful in specific scenarios
use Drupal\Core\Logger\LoggerChannelInterface;

class AnotherExampleService {
   protected $logger;
   
   public function setLogger(LoggerChannelInterface $logger) {
     $this->logger = $logger;
   }
}
  • Using services (objects) and service container seems to add unnecessary complexity but benefits are -
    • Class files are auto-loaded only when object is instantiated - application memory footprint is reduced
    • Objects can be instantiated (and methods called) without service container but with service container one can easily modify or augment class methods
  • Services are defined in YAML files
    • A user can substitute the class that implements a particular service with another implementation
    • Core services are defined in CoreServiceProvider.php and core.services.yml
  • Steps to using a service
    • Create service : drush gen custom-service
    • In your controller use create() method to instantiate service
  • Useful "utility" services
    • logger.factory
    • keyvalue
  • class Drupal is available as a static service container wrapper for legacy and/or procedural code that can't use constructor injection or setter method injection
    • Example: \Drupal::getContainer()->get('service_name') OR shortcut method \Drupal::service('service_name')
    • Example: \Drupal::logger('module_name')->notice('Message using from global logger')

Menus

Menu Link Types

Menu Items and Links

  • Menu items
  • Menu items are individual entries in a menu, typically used for site navigation
    • Defined in <module_name>.links.menu.yml file
  • Local tasks (also known as Tabs)
    • Used to group together related links on a page
      • Examples
        • Content, Files, Media tabs at /admin/content
        • View, Edit, Delete, Revisions tabs at /node/{node}
    • Defined in <module_name>.links.task.yml file
    • Can be altered using hook_menu_local_tasks_alter()
  • Local actions
    • Local actions are links that relate to a given route
    • Used for operations
      • Example: +Add content button on /admin/content is an 'Action' link.
    • Defined in <module_name>.links.action.yml file
    • Can be altered using hook_menu_local_actions_alter()
  • Contextual links
    • Used by the Contextual module to provide links next to a given component (a render array).
    • Example: Icon with a dropdown that has the Configure block link for blocks
    • Contextual links are tied to Render Arrays
    • Defined in <module_name>.links.contextual.yml file
    • Can be altered by implementing hook_contextual_links_alter()

Links

Rendering and Render API

  • Render Array Overview
  • Render API Overview
  • Drupal.org | The Drupal 8 render pipeline
  • Article | The Drupal 8 Render Pipeline
  • Using CKEditor plugins in Drupal 8 | Agaric
  • Rendering in Drupal means turning "render" arrays into HTML or other output format for display on web page
  • Rendering is done using the Render API
  • Render API consist of two parts:
    • Render arrays - representation of web page content (HTML, JSON and other formats) in an array
    • Render pipeline - the process Drupal uses to:
      • Service a request and collate required data
      • Determine output format
      • Transform render array(s) into the desired format (HTML, JSON etc.)
  • Why Render API ?
    • Useful to defer rendering web page content for as long as possible so theme layer can control output
      • Theming gets the 'last say' of how data is actually output
    • Arrays are easier to manipulate than strings (HTML or other strings)
    • Using arrays enables data to be easily rendered into other formats JSON etc.

Render Array

  • An Introduction to Drupal's Render Arrays
  • Standard Drupal Render Array Properties
  • Video | Aha! Understanding and Using Render Arrays in Drupal 8
  • A render array is implemented as a hierarchical associative array containing -
    • HTML markup (using #markup property)
    • Render elements (using #type property)
      • Render elements are web page elements to be displayed on web page (links, tables, form element, etc.)
    • A theme hook (using #theme property) specifying a template file and variables (to render content)
  • Module developers use render arrays to represent web page content
  • Theme developers can alter render arrays in order to change the resulting HTML output
  • Render arrays are rendered by the renderer service (RendererInterface), which traverses the array and recursively renders each level

Render Array Properties

  • Render Array Properties
  • Properties (associative array keys) start with #
  • Two type of properties - value and callable
  • Value properties store strings, integers, boolean values
    • e.g. #markup, #title, #prefix, #weight
  • Callable properties store PHP callables (functions)
    • e.g. #theme, #pre_render, #post_render
  • Each render array needs to have either the
    • #markup
    • #plain_text
    • #type
    • #theme or
    • #lazy_builder property / key
  • The #lazy_builder is related to caching and is used to defer the building of a render array to a later stage
  • Partial list of render array properties / keys
    • #markup
    • #theme
    • #type
    • #title
    • #attached
    • #attributes
    • #pre_render / #post_render
    • #theme_wrappers
    • #cache
    • #weight

Basic Render Array

$build = [
  '#markup' => '<p>The simplest <b>render array</b> possible.</p>',
];

Render Elements

  • Form and Render Elements
  • Elements are 'under the hood' pre-packaged render arrays - expanded during rendering process
  • Example elements are links, buttons, fields, images, nodes, blocks, pages, pager, status messages
  • The #type property in a render array identifies the render element
  • Elements are implemented as Plugins
 // Example - link render element
 $build['examples_link'] = [
   '#title' => $this->t('Examples'),
   '#type' => 'link',
   '#url' => Url::fromRoute('examples.description')
  ];
  • Why Render Elements ?
  • Render elements allow for encapsulation of complex display logic into re-usable components
  • Using a library of elements to define common components on a page allows for consistent display and handling of those elements
  • Similar to a front-end developer pattern library, or a UX designer style guide.

Render Element Types

  • There are two render element types, Generic elements and Form input elements
  • Generic elements
    • Encapsulate logic for generating HTML and attaching relevant CSS and JavaScript to the page.
    • Examples - links, tables, drop buttons
  • Form input elements
    • Examples - text fields, password fields, file upload buttons, vertical tabs, checkboxes

Form API

  • Form API
  • Form Generation
  • Drupal's Form API (FAPI) is a comprehensive framework for managing forms within Drupa
  • The Form API extends the Render API, providing a structured approach to form creation, validation,
    and submission
  • It offers an abstraction layer over HTML forms and HTTP form handling, simplifying the process
    of capturing and processing user input securely and efficiently
  • The Form API extends the Render API, providing form-related render elements (#type) to handle all kinds of user input in a consistent manner
  • It uses render arrays to define, display, validate, and process forms in a Drupal site

Form Elements

  • Drupalize.me | Form Element Reference
  • Form and Render Elements
  • Form elements are a subset of render elements. They represent form HTML elements
  • Example Form render elements are button, checkbox, date, select list, radio button, textfield, textarea, table
    and many others
  • Example Form using Drupal\Core\Form\FormInterface:buildForm() method
public function buildForm(array $form, FormStateInterface $form_state) {

    $form['description'] = [
      '#type' => 'item', // item form element
      '#markup' => $this->t('This basic example shows a single text input element and a submit button'),
    ];

    $form['title'] = [
      '#type' => 'textfield', // textfield form element
      '#title' => $this->t('Title'),
      '#description' => $this->t('Title must be at least 5 characters in length.'),
      '#required' => TRUE,
    ];

    // Group submit handlers in an actions element with a key of "actions" so
    // that it gets styled correctly, and so that other modules may add actions
    // to the form. This is not required, but is convention.
    $form['actions'] = [
      '#type' => 'actions',
    ];

    // Add a submit button that handles the submission of the form.
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }
  • Example of form in a render array using the form_builder service
   $build = [
     'header' => [
       '#prefix' => '',
       '#suffix' => '',
       '#markup' => t('There is a form below this'),
     ],
     'form' => \Drupal::formBuilder()->getForm(NAMESPACE\OF\FORM),
     'footer' => [
       '#markup' => t('This comes after the form'),
     ],
   ];
   return $build;

Third Party Settings API

Access Control & Permissions

administer product:
  title: 'Administer product settings'
  restrict access: true
access product overview:
  title: 'Access product overview page'
delete product:
  title: Delete product
create product:
  title: Create product
view product:
  title: View product
edit product:
  title: Edit product
administer product_type:
  title: 'Administer product type'

Logging API

  • Logging API
  • Drupalize.me | How to log messages in Drupal 8
  • Monolog
  • A Logger is a service that implements the Psr\Log\LoggerInterface
    • Loggers typically use trait RfcLoggerTrait for helper methods
  • Out of the box, Drupal core comes with two modules that register and implement a logger
    • dblog logs messages to the site's database - see Drupal\dblog\Logger\DbLog
    • syslog logs messages to a servers syslog facility - see Drupal\syslog\Logger\SysLog
  • Other modules can register custom loggers - to log messages to different destinations (e.g email etc.)
  • Messages are logged via LoggerChannel log() method - this call invokes all registered loggers

Messenger Service

Data Modeling and Storage

  • Storing Data in Drupal 8
  • Drupal features several data storage types: State, TempStore, Content, Session, Configuration, Cache
    • State : key-value pair for temporary information e.g. last time cron job was run
    • TempStore: key-value pair for session-like storage - keeps temporary data across multiple requests
    • UserData: Implemented by user module - stores information related to a particular user
    • Configuration: Information that is not content but is meant to be persistent/permanent
      • e.g. site name, content types, taxonomy terms, views etc
    • Content: Information meant to be displayed on site (articles, blogs, images etc.)
    • Session: Information about user interaction with site (login status etc.)
    • Cache: Site information that is stored elsewhere - exists to speed up data retrieval

State API

  • State API
  • The State API is a key/value database storage and the simplest way to store data
  • The State API defines Drupal\Core\State\StateInterface and is implemented by the state service
    • Use \Drupal::state() or inject the state service

TempStore API

Settings API

  • Use settings service (defined in core.services.yaml) to access variables defined in site settings.php
  • Example use in a class constructor
  /**
   * @var \Drupal\Core\Site\Settings
   */
  protected $settings;
...
  public function __construct() {
    $this->settings = \Drupal::service('settings');
  }
...

$private_file_path = $this->settings->get('file_private_path');

Configuration API

  • Configuration API
  • Drupalize.me | Introducing the Configuration System
  • Drupal's configuration system is designed to move configuration between instances of the same site
    • e.g. Development site -> Production site
  • It provides a process by which configuration can be exported/imported between instances of the same site
  • The Configuration API is the programming interface used by the configuration system for exporting/imporing site configuration
  • Each site has a UUID - Universally Unique Identifier
  • When importing configuration, site UUIDs must match
    • New instances of a site must be created as a clone
      • Create dev site (files and database)
      • Copy dev site files to prod site and import dev site database
  • The configuration system doesn't replace Features module - Is Features still used ?
    • Use Features module for packaging configuration into re-usable "features" modules
    • Features leverages the configuration system
    • Don't use Features module to deploy configuration for entire site
  • Sites, not modules, own configuration
    • Once default configuration is imported, the site owns the configuration
    • Modules should use hook_update_N() to introduce change
  • Configuration Storage
    • Configuration is staged in YAML files but is stored (by default) in database
    • Storage is configurable - can be file based, can use Redis, MongoDB etc.
  • There are two types of configuration data - Simple Configuration and Configuration Entitied
  • Simple configuration - only one instance is allowed
    • Examples: core/modules/system/config/install/system.site.yml core/modules/user/config/install/user.mail.yml
    • Configuration Entities
      • Configuration Entities describe a type - more than one configuration entity may be created
      • Allows you to store multiple sets of configuration (beyond Simple Configuration)
      • Part of Entity API
        • Defines custom CRUD functions
        • Implemented as plugins: Uses Annotation, extends ConfigEntityBase
        • Structure is defined YAML files in module config/schema directory
      • Requires a module to implement (use Configuration API)
      • Examples of Configuration Entities
        • (search for ConfigEntityBase or 'type: config_entity' in schema files)
        • Content types
        • Views
        • Image styles (core/modules/image/config/install/*)
        • Code examples transcode_profile, site_announcement
  • Overriding Configuration
    • Global Overrides (highest priority)
      • Use $config variable in settings.php file - this data not stored in DB
      • Useful for storing sensitive data - API keys etc.
      • Cannot define a priority - last module to override wins
    • Module Overrides
      • Define a class the implements ConfigFactoryOverrideInterface
      • Can define a priority
    • Language Overrides (lowest priority)
      • Use language_manager service

Module Default Configuration

  • Default configuration locations
    • webroot/modules/{contrib,custom}/my_module/config/install
      • Only read once at module install time
    • webroot/modules/{contrib,custom}/my_module/config/optional
      • Use for configuration with dependencies
      • Scanned every time modules are installed
      • Configuration will be applied/installed when dependencies are met
        • Example: Only when Views module is installed will Node module config/optional views configuration be installed
  • Updating module default configuration
    • Update files in config/install and config/optional (handles new site install)
    • Implement hook_update_N() (handles updating existing sites)

Configuration Schema

  • Configuration schema/metadata
  • Schemas are a way to define configuration items and specify what kind of data (string, boolean integer) they store
  • There are three main reasons why configuration needs a schema definition
    • Multilingual support - Provides a standardized way of handling translation of configurations
    • Type enforcement - Enables enforcement of type consistency and validation
    • Configuration entity validation
  • See core.data_types.schema.yml for schema properties/types/keys
  • Modules that have configuration should implement a configuration schema
  • Module configuration schema is a YAML file located in <module_dir>/config/schema/<module>.schema.yml

Configuration Commands

# get all configuration with <prefix>
# most of this configuration will be defined in config/install/*.yml files

# get all configuration with "system" prefix
drush cli system
...
system.authorize
system.cron
system.date
system.diff
system.file
system.image
system.image.gd
system.logging
...
system.site  <==== key values defined in system.site.yml
system.theme
system.theme.global
...

# get all system.site values
drush cget system.site
uuid: 32c3e6d8-10b0-49ea-977f-544eabe6cbd7
name: d8dev.dev
mail: [email protected]
slogan: ''
...

# get specific value from system.site configuration
drush cget system.site name
'system.site:name': d8dev.dev

Entities

Content Entities

  • Drupalize.me | Content Entities and Fields
  • Content entities are field-able and support Bundles
  • A Bundle is the name given to an Content Entity with fields
  • The ability of adding fields to objects other than Node (a.k.a Content) has proven to be very useful
    • Several D6/D7 modules (Content Profile, Node Comments, Taxonomy Node) were written to treat objects as nodes
    • Follows Drupal-ism "Everything is a Node meme"
    • "Everything is a node" meme became "Everything is an Entity" meme
  • Content entities have two types of fields
    • Base Fields
      • Defined by Drupal\Core\Field\BaseFieldDefinition
      • Base fields exist on all bundles of a given Entity Type
    • Configurable Fields
      • Configurable fields are typically created through the UI
      • Attached to an entity type bundle, and exported to code
  • Both Base fields and Configurable fields are built on the Typed Data API
  • Typed Data API - See Typed Data API for a tutorial
    • PHP is a loosely typed language - this can lead to inconsistencies (e.g. adding an integer to a string etc.)
    • The Typed Data API
      • Wraps PHP data types - this enforces strict typing, adds data validation and useful metadata
      • Enable creation of compound data types
      • All Entity fields / data use Typed Data
    • There are two parts of the Typed Data API
      • DataType objects (Plugins)
        • Defines available data types (StringData, IntegerData, TimeStamp etc.)
      • DataDefinition objects
        • Data definitions are the objects used to store all that meaning about the underlying data
  • Content Entities support revisions and translations
  • Example Content Entities
    • Node (a.k.a Content Type)
    • Taxonomy
    • User
    • File
    • Comment
    • Block
    • Contact Form
  • Content Entities are not exportable.
    • Use Migration to transport content from/to different sites/environments

Terminology

  • Entity Type - Grouping of like entities
    • Node (Content) entity type - Drupal\node\Entity\Node
    • User entity type - Drupal\user\Entity\User
    • File entity type
    • Taxonomy term entity type
    • Image entity type
    • Custom entity type
    • Each entity type has an ID, but differ in their fields and may differ in access control and storage mechanisms
    • Entity types are defined as plugins
  • Entity
    • An Entity is an instance (individual unit of data) of a particular Entity Type or of a Bundle
  • Bundle
    • A Bundle is an implementation of an entity type to which fields can be attached
    • Most common "Bundles" are implementation of Node entity type (a.k.a Content Type)
      • Page
      • Article
      • Blog post
      • Custom content
  • Node Bundle (example)
  • Node <------ Entity type
    • Page (Id, Vid, status, Title, Author, Body) <------ Bundle
    • Article (Id, Vid, status, Title, Author, Body, Image, Tags <------ Bundle
    • Custom Content Type(Id, Vid, status, Title, Author, Caption, Image) <------ Bundle
  • Properties
    • Data/fields that are common to all Bundles of a given entity type
    • In above Node Entity type example, Id, Vid, status, Author, Title are Properties
  • Fields
    • Data (information) that's configurable per Bundle
    • For the Node entity type, by default the Article bundle has a body, image, tags, and image fields
  • Summary (in OOP terms)
    • An Entity Type is a base class
    • A Bundle is an extended class
    • A Field is a class member
    • An Entity is an object or instance of a base or extended class
  • When and why to define/use a new Entity Type
    • If faced with having to put information into a custom table consider defining an new Entity Type
    • New fields can easily be added

Configuration Entities

  • Drupal 8 introduced the concept of "Configuration Entities (Configurables)
    • Configuration Entities are not field-able and represent a configuration within a site
    • No revision support
    • Leverages the entity system's CRUD and pluggable storage functionality
    • Use Configuration system for storage
    • Can fire and respond to hooks
  • Example Configuration Entities
    • Content types
    • Views
    • Menus
    • Taxonomy vocabularies
    • Contact forms
    • Image styles
  • Configuration Entities are exportable sets of site configuration values

Entity API (D7)

  • How to use EntityFieldQuery for Drupal 7
  • Entity API allows
    • Definition of new entity types
    • Augmenting existing entity types
  • Example Modules
    • D7 Bean Module
      • Allows creation of blocks using Entity system
      • Block type is a Bundle of the "Bean" (Block) Entity type
    • Other (D7) modules that use Entity System
      • Drupal Commerce
      • File entity
      • Recurly
  • Use EntityFieldQuery API to retrieve entity info without writing SQL db query
  • Example use case
    • Video Entity (videoentity) - Displays YouTube video
    • Advantage over just using node entity (content type) with field for video
      • Videos are pulled in using API (not user created)
      • No fields needed for publish, status, author as with node - so custom entity is more efficient
      • With custom entity we have more control when saving and rendering the content
    • UI is similar to that for adding/deleting/editing/viewing content (article, page, blog etc.)
// API functions
entity_load()
entity_extract_ids() 
hook_entity_info()

// Classes

Entity API (D8+)

  • Entity API
  • Drupalize.me | Entity API Overview
  • Drupal 8 Entity API cheat sheet
  • EntityManager has been split into 11 classes
  • The Entity API provides a way to manage data with minimal code
  • The Entity API abstracts direct database access
    • Provides methods for performing CRUD (Create, Read, Update, Delete) operations on entities
    • Instead of writing SQL, you can use the Entity API to manipulate both content and configuration data
  • Entity validation happens at the Entity API level rather than the Form API level
    • This ensures that validation logic is applied regardless of whether the entity is created via an HTML form, a JSON:API request, or in code
  • Entities use the Plugin system
    • EntityTypeManager service is the Plugin manager and provides discovery and handling of entities
    • ConfigEntityStorage class interacts with the Configuration management system to load/save/delete config entities
  • Entity API integrates seamlessly with the multilingual system to bring fully translatable content and configuration entities
  • D8+ entity API is more robust than D7 - offers a layer that reduces the need to query the database directly
  • See entity.api.php for Entity related hooks

Entity CRUD

// Using EntityTypeManager service with dependency injection
$entity_type = 'node';
$query = $this->entityTypeManager->getStorage($entity_type)->getQuery();

// Using the Drupal global service.
$query = \Drupal::entityQuery($entity_type);
  • Example Query
$blogs = $query
  ->accessCheck(TRUE)
  ->condition('type', 'blog')
  ->condition('status', 1)
  ->sort('created', 'DESC')
  ->execute();

// Queries return an array of entity IDs
$blogs = [
  8 => '5',
  9 => '6',
];

// Load the complete entity objects using the entity type manager service
$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($blogs);

foreach ($nodes as $node) {
  // do something with $node
}
  • Example 1 - Get content nodes
/** @var \Drupal\Core\Entity\Query\QueryInterface $query */
$query = \Drupal::entityQuery('node')->condition('type', 'article');
$ids = $query->execute();

/** @var \Drupal\Core\Entity\EntityStorageInterface $node_storage */
$node_storage = \Drupal::entityTypeManager()->getStorage('node');

// Returns array of EntityInterface objects (in this case NodeInterface)
/** @var \Drupal\node\NoteInterface $nodes */
$nodes = $node_storage->loadMultiple($ids);
dsm($nodes);

foreach ($nodes as $n) {
  dsm($n->title->value, 'Title');
  // perform operation on node object (set fields, save, etc.)
  $n->set('title', "Article {$n->nid->value}");
  dsm($n->title->value, 'Title');
  $n->save();
}

  • Example 2 - Load content entities
// Returns array of EntityInterface objects (in this case NodeInterface)
/** @var \Drupal\node\NoteInterface $nodes */
$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($ids);

// Returns a single NodeInterface object
/** @var \Drupal\node\NoteInterface $node */
$node = \Drupal::entityTypeManager()->getStorage('node')->load(array_shift($id));

// loadByProperties() shorcut method - load all article nodes
/** @var \Drupal\node\NoteInterface $nodes */
$nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties(['type' => 'article']);
  • Example 3 - Reading content entities
//  Get node with id 3 - Use a _query_ to get the $id of the node you're interested in
/** @var \Drupal\node\Entity\Node $node */
$node = Node::load(3); // $id = 3

/** @var \Drupal\Core\Field\FieldItemListInterface $title */
$title = $node->get('title');
// This get the node title field
$value = $title->value;

/** @var \Drupal\Core\Field\FieldItemListInterface $body */
$body = $node->get('body');
// This gets the node body field
$value = $body->value;
  • Example 4 - Creating content entities
$values = [
  'type' => 'article',
  'title' => 'My title'
];

/** @var NodeInterface $node */
$node = \Drupal::entityTypeManager()->getStorage('node')->create($values);
$node->set('field_custom', 'some text');
$node->save();

  • Example 5 - Rendering content entities using EntityViewBuilder
// Get $nid with query
....
$node = \Drupal::entityTypeManager()->getStorage('node')->load($nid);
$view_mode = 'teaser';

// View builders convert entity objects to renderable arrays
$build = \Drupal::entityTypeManager()->getViewBuilder('node')->view($node, $view_mode);

  • Example 6 - Rendering content entities using theme hook
// The steps are:
* Define a theme hook with variables
* Query and load entities
* Reading the values of an entity
* Create a render array that uses the theme hook

  • Example 7 - hook_entity_view()
function module_name_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
  if ($entity->getEntityTypeId() == 'node' && $view_mode == 'full') {
    $build['disclaimer'] = [
      '#markup' => t('The content provided is for general information purposes only.'),
      '#weight' => 100
    ];
  }
}

Content Entity Validation

drush gen constraint

Configuration Entities CRUD

// ex 1 
/** @var NodeType $type */
$type = \Drupal::entityTypeManager()->getStorage('node_type')->load('article');
$type->set('name', 'News');
$type->save();

// Get keys/fields
$id = $type->id();
$label = $type->label();
$uuid = $type->uuid();
$bundle = $type->bundle();
$language = $type->language();

Creating a Custom Content Entity

// Update entity db tables - only use in development
drush entup
  • In production, use hook_update_N() to implement an update
// Update entity db tables - use in production
function <module_name>_update_9000(&$sandbox) {
  \Drupal::entityTypeManager()->clearCachedDefinitions();
  $entity_type = \Drupal::entityTypeManager()->getDefinition('<entity_id>');
  \Drupal::entityDefinitionUpdateManager()->installEntityType($entity_type);
}

Field Types API

  • Drupalize.me | Field API Overview
  • Field API
  • FieldTypes, FieldWidgets and FieldFormatters
  • Finding All FieldWidget Types
  • Video | Custom Compound Fields
  • Content entities can have fields
  • Field data is entered using Field widgets and displayed with Field formatters
  • The Field API provides developers with means to define/customize fields, widgets, and formatters
  • Out of the box Drupal core provides the following fields (field types)
    • Boolean
    • Date (datetime)
    • Email
    • Link
    • Number (integer, float, decimal)
    • File
    • Image
    • Text (plain, formatted, long with summary)
    • [ lots more.... ]
  • Field types are defined using the Plugin System
  • Each field type has its own class
    • This class provides schema and property information
  • Fields have three main components
    • Field Type
      • Defined by FieldType Annotation
      • Enables storage of different types of data (boolean, integer etc.)
      • Fields are added/deleted/configured via Manage fields
      • See core/lib/Drupal/Core/Field/Plugin/Field/FieldType/ for all core defined field types
    • Field Widget
      • Defined by FieldWidget Annotation
      • Determines how the field is displayed on Edit forms
        • Example: boolean field type is rendered as a checkbox
      • Field widgets are configured via Manage form display
      • See core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/ for all core defined field widgets
    • Field Formatter
      • Defined by FieldFormatter Annotation
      • Determines how field is displayed to user (on content page, block etc.)
      • Return a render array (subsequently processed by the theming layer)
      • Field formatters are configured via Manage display
      • See core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/ for all core defined field formatters
  • Drush field commands
drush | grep field:
field:                                                                                                                          
  field:base-info (fbi)                                  List all base fields of an entity type                                 
  field:base-override-create (bfoc)                      Create a new base field override                                       
  field:create (fc)                                      Create a new field                                                     
  field:delete (fd)                                      Delete a field                                                         
  field:formatters                                       Lists field formatters.                                                
  field:info (fi)                                        List all configurable fields of an entity bundle                       
  field:types                                            Lists field types.                                                     
  field:widgets                                          Lists field widgets. 
  • Field List is available at admin/reports/fields

Field CRUD

Drupal\Core\Field\FieldException: Attempted to create an instance of field with name bp_background on entity type paragraph when the field storage does not exist. in Drupal\field\Entity\FieldConfig->getFieldStorageDefinition() (line 316 of core/modules/field/src/Entity/FieldConfig.php)

Acessing Field Data

  • Use the entity type manager service to load entities and access field values
  • Load an entity
$entity_type = 'node';
$storage = \Drupal::entityTypeManager()->getStorage($entity_type);

// Load a single entity by its ID.
$node = $storage->load(1);
  • Access a field instance
$field = $node->get('field_vendor_contact_name');
  • Retrieve a raw field value
$contact_name = $node->get('field_vendor_contact_name')->value;
  • Accessing base fields
// Base fields may have separate helper methods for accessing their values.

/ Example:

$title = $node->getTitle();
  • Updating field values
// After loading an entity by its type, use set() to modify field values, then save() to update the database
// Load an entity of type 'node' with an ID of 1.
$node = \Drupal::entityTypeManager()->getStorage('node')->load(1);
// Set the value of the desired field 
$node->set('field_vendor_contact_name', "tester");
// Save the new value to the database.
try {
  $node->save();
}
catch (EntityStorageException $exception) {
  // Handle the error.
}

Update API

drush updatedb -y; drush config-import -y; drush cache-rebuild

Features

  • Features is a module that provides a UI and API for taking different site building components/configuration (content types, views, image styles, taxonomy, roles, permissions, custom modules etc.) and bundles them together into a single custom "features" module.
    • In essence Features takes a 'package' of site configuration and writes it into code
  • Originally written by "Development Seed" company to bundle functionality for its OpenAtrium 1 distro for D6
  • A "features" module contains information in its info file so that configuration can be checked, updated, or reverted programmatically.
  • Example features:
    • A blog
    • A pressroom
    • An image/photo gallery
      • Image content type
      • Image field
      • Gallery View
      • Image style(s)
      • Other misc config (possibly in variables table etc.)
  • Advantages of using Features
    • Since configuration is in code, it can be version controlled
    • Can easily deploy configuration from Dev site -> Staging site -> Prod site
    • Ability to create re-usable modules
  • Features was not designed to be a configuration management tool. It was designed to bundle functionality in re-usable modules.
  • Features Deficiencies
    • No consistent format for exporting configuration to code
      • Every module's export format is different - views/ctools uses a different export format than content types etc.
    • Exporting database data directly to code can lead to problems with data types (string vs integer etc.)
    • Problems with over-rides and reverts

Features (D7)

Features (D8)

  • Features for Drupal 8
  • An Introduction to the Drupal Features Module (D7)
  • Announcing Features for Drupal 8
  • What's new in Features for Drupal 8?
  • Features
  • The Role of Features in Drupal 8 with Mike Potter - Modules Unraveled Podcast
  • Stop Using Features: A Guide To Drupal 8 Configuration Management
  • Features vs CMI
    • Features
      • Exports configuration into (PHP) code
      • Allows version control
      • Allows deployment (Dev site -> Staging site -> Prod site)
      • Easy to bundle configuration/functionality and re-use
    • CMI
      • Exports/stores configuration to/in YAML files
      • Allows version control
      • Allows deployment (Dev site -> Staging site -> Prod site)
      • Cannot easily bundle configuration/functionality and re-use
  • Issues with CMI
    • Bundling configuration (functionality) and creating a module for it requires manual steps i.e
      • Using Configuration exporter, export views, content-type, fields etc. YAML files one-by-one
      • Above is tedious and error-prone
    • Module configuration may cause undesirable effects
      • D8 modules provide initial configuration "default" values
      • Module configuration changes are (still) done using hook_update_N()- this can lead to configuration being erroneously over-written or changed
      • Uninstalling a module may not remove all of the module's configuration
  • Key Features of "Features"
    • Can import, export and detect configuration changes
      • Import means to "revert" feature to match what's in code - overwrite database config
      • Export means take database settings and write to code
    • Assignment plugins (uses D8 Plugin system) for auto-packaging
      • Auto creates features for you based on "Assignment methods"
    • Use "Bundles" for name-spacing features (i.e all features start with acme, acme_blog, acme_photo_gallery)
      • Basically a way to group features
    • drush support (same as Drush 7)
    • Separate Features UI module - need not be enabled in PROD sites
  • Features violates CMI rules (by necessity)
    • Modules marked as a Feature can be enabled even if it contains existing configuration
    • Module (feature) configuration can be imported to overwrite database (active) configuration
  • Overrides
    • How do you merge active (in database) configuration with configuration changes in code ?
      • Currently can only use active configuration OR configuration in code
      • D7 solution was to use Features Overrides module
      • No current solution for D8 ?

Features (D8) Summary

  • Features is used to bundle functionality into re-usable modules
  • Features module in D8 is a "devel" module - it need not be installed and/or enabled in production site
  • Implements lots of new functionality for auto-creating "features" modules
  • drush commands
# list all features
drush features-list-packages
drush fl

# show detailed feature configuration
drush fl <feature_name>
drush fl oa_my_image

# show feature difference (code vs active/database)
drush fd oa_my_image
Legend: 
Code:    drush features-import will replace the active config with the displayed code.
Active:  drush features-export will update the exported feature with the displayed active config

Config views.view.my_gallery 
  display::default::display_options::style::options::row_class_default : 1
  display::default::display_options::style::type : grid
< display::default::display_options::title : My Image Gallery
---
> display::default::display_options::title : Our Image Gallery
  display::default::display_plugin : default
  display::default::display_title : Master

# over-ride (revert) all feature database (active) settings with code version
drush features-import-all -y
drush fia -y
drush fim-all -y
drush fra -y

# over-ride (revert) single feature database (active) settings with code version
drush features-import -y oa_my_image
drush fim -y oa_my_image
drush fr -y oa_my_image

# export (copy) feature database (active) settings to code
drush features-export -y oa_my_image
drush fex -y oa_my_image

Features Workflow

# Workflow for creating feature
# Use Drupal GUI to create content type, view, taxonomy etc for feature
# Use Features UI to export feature to code -> modules/feature/my_feature

# Workflow for updating feature
# on development site
# update to latest code
git pull

# revert feature - this make/forces component configuration match feature code
drush features-revert <feature> 
drush fr <feature>

# clear cache
drush cc all

# make change in a component (content type, views etc.) via UI or directly in code
#...
# update your features code to reflect change
drush features-update <feature>
# or click "Recreate Feature" button on Features UI
#...
# check diffs
# test changes
# commit newly generated code to source control
#...

# on production site
# update to latest code
git pull

# revert feature - this make/forces component configuration match feature code
drush features-revert <feature>  # single feature
drush fr <feature>
drush features-revert-all        # all features

# clear cache
drush cc all

Theming

Theme Hooks

  • A theme hook is used to specify how a particular piece of content (a list, a table etc.) should be rendered
  • Modules use theme hooks to connect content from the module to a Twig template
  • During page rendering, Drupal uses the theme hook associated with the content to determine the
    appropriate template file
  • Theme hooks are registered using hook_theme - Each hook maps to a Twig template file

Core Theme Hooks

  • These hooks are provided by the Drupal core system module
    • web/core/modules/system/system.module:system_theme() and
    • theme.inc:drupal_common_theme()
  • theme.inc also contains template_preprocess_xxx functions
    • These functions are used to create the variables that are defined by the theme hook and made available in the Twig template file
  • To build content, a module code can define a render array with the #theme property
  • The #theme property defines the theme hook (and thus the Twig template) of the content
// Example 1 - List of items
// item_list theme hook (defined in theme.inc)
'item_list' => [
   'variables' => ['items' => [], 'title' => '', 'list_type' => 'ul', 
    'wrapper_attributes' => [], 'attributes' => [], 
    'empty' => NULL, 'context' => []
   ]
]
// Controller constructs a render array for the list using the item_list theme hook
...
$build['list'] = [
  '#theme' => 'item_list',  // item_list is hook name
  '#title' => $this->t('A list of items'),  // #title is theme hook variable
  '#items' => [                            // #items is theme hook variable   
    $this->t('List item #1'),
    $this->t('List item #2'),
   ],
   '#attributes' => [ 'class' => ['my-list-item'] ],
];
...
return $build;  
  • web/core/modules/system/templates/item-list.html.twig is the default item_list template
    used to render the list
  • A theme or module can over-ride this template
  • theme.inc:template_preprocess_item_list creates the variables for item-list.html.twig
// Example 2 - Links
// links theme hook (defined in theme.inc)
'links' => [
  'variables' => ['links' => [], 'attributes' => ['class' => ['links']], 
   'heading' => [], 'set_active_class' => FALSE
  ]
]
// Controller constructs a render array for links using the links theme hook
...
$links = [
  [
    'title' => 'Link 1',
    'url' => Url::fromRoute('<front>'),
  ],
  [
     'title' => 'Link 1',
     'url' => Url::fromRoute('hello_world.hello'),
  ]
];
...
return [
  '#theme' => 'links',
  '#links' => $links,
  '#set_active_class' => true,
];
  • web/core/modules/system/templates/links.html.twig is the default links template
    used to render the links
  • A theme or module can over-ride this template
  • theme.inc:template_preprocess_links creates the variables for links.html.twig
// Example 3 - Table
// table theme hook (defined in theme.inc)
'table' => [
  'variables' => ['header' => NULL, 'rows' => NULL, 'footer' => NULL, 
    'attributes' => [], 'caption' => NULL, 'colgroups' => [],
    'sticky' => FALSE, 'responsive' => TRUE, 'empty' => ''
  ]
]
// Controller constructs a render array for links using the table theme hook
...
$header = ['Column 1', 'Column 2'];
$rows = [
  ['Row 1, Column 1', 'Row 1, Column 2'],
  ['Row 2, Column 1', 'Row 2, Column 2']
];
...
return [
  '#theme' => 'table',
  '#header' => $header,
  '#rows' => $rows,
];
  • web/core/modules/system/templates/table.html.twig is the default table template
    used to render the table
  • A theme or module can over-ride this template
  • theme.inc:template_preprocess_table creates the variables for table.html.twig

Custom Theme Hooks

  • Modules can register their own theme hooks by implementing hook_theme in the
    MODULE.module file
  • Themes can also define theme hooks in the THEME.theme file
// Example 1
/**
 * Implements hook_theme
 */
function MODULE_theme($existing, $type, $theme, $path) {
  return [
    'salutation_message' => [ // HOOK_name
      'variables' => [
        'message' => '', 
      ],
      'template' => 'salutation-message', // maps to salutation-message.html.twig Twig template
    ],
  ];
}
// Controller constructs render array
...
$message = 'This is the message';
...
return [
 '#theme' => 'salutation_message',
 '#message => this->t($message),
]
// web/custom/MODULE/templates/salutation-message.html.twig
<div {{ attributes }}>
  {{ message }}
</div>

Preprocess Functions

  • Modules can optionally also implement template_preprocess_HOOK_name()
  • This function allows data to be pre-processed before sending to the Twig template file
/**
 * Template preprocess function for salutation_message hook
 *
 * @param array $variables
 *   An associative array containing:
 *   - message
 */
function template_preprocess_salutation_message(&$variables) {
  $variables['message'] = strtoupper($variables['message']);
}

Theme Hook Suggestions

/**
 * Implements hook_theme().
 */
function MODULE_theme($existing, $type, $theme, $path) {
  return [
    'salutation_message' => [
      'variables' => [
        'message' => NULL, 'target' => NULL,
        'overridden' => FALSE,
        'attributes' => [],
      ],
    ],
    'salutation_message__overridden' => [
      'template' => 'salutation-message--overridden',
      'base hook' => 'salutation_message',   // <==== base hook key needed for hook_theme_suggestions_HOOK_alter() to work
    ],
  ];
}

/**
 * Implements hook_theme_suggestions_HOOK_alter().
 */
function MODULE_theme_suggestions_salutation_message_alter(&$suggestions, $variables) {
  if (isset($variables['overridden'])) {
    if ($variables['overridden'] === TRUE) {
      $suggestions[] = 'salutation_message__overridden';
    }
  }
}

  • Create template file MODULE/template/salutation-message--overridden.html.twig

Steps for creating new theme hook and template file

  • In MODULE.module file
    • Implement hook_theme() - declare new theme hook using variables
    • Implement optional preprocess function
    • function template_preprocess_<theme_hook_name>(&$variables) { ... }
  • Create a new Twig template file
  • Document variables in template file
  • Use #theme property / key in render array
  • See sample_module_marquee theme hook in sample_module

Assets and Libraries

Layouts

  • Modules can use the Layout API to define Layouts used by Layout Builder

JavaScript

Ajax

Caching & Cache API

Why Caching ?

  • Assembling and rendering all components of a typical page involves a lot of steps before the final HTML/CSS/JS is produced
  • Caching stores (in database or other storage) the final HTML/CSS/JS of a page and delivers it to a user without
    having to re-construct the page
  • By default, caches are stored in site's database - suitable for small sites
  • A common issue with caching systems is - When should the cached data be invalidated
  • In Drupal's case, this translates to when should a page or parts of the page be re-rendered / re-constructed
  • Three main cases -
    • Changes in content - Nodes/entities edited, added, deleted
    • Changes in context - Different user, user role, time zone etc
    • Passage of time - Every 5 min, 30 min, 24hrs etc
  • Cacheability Metadata (Cache tags, context and max-age) is used for cache invalidation

Cache Bins

  • DRUPAL 8 DEVELOPMENT: CACHING ON
  • Cache storage is separated into Bins, each containing various cache items.
  • If caches are stored in the database, each bin maps to a database table
  • Each bin can be configured separately; see Configuration.
  • The default bin is called default, with service name cache.default - used to store common and frequently used caches
  • Other common cache bins
    • bootstrap: Data needed from the beginning to the end of most requests that has a very strict limit on variations and is invalidated rarely.
    • render: Contains cached HTML strings like cached pages and blocks, can grow to large size
    • data: Contains data that can vary by path or similar context
    • discovery: Contains cached discovery data for things such as plugins, views_data, or YAML discovered
      data such as library info.
  • A module can define a custom cache bin by defining a service in its <module_name>.services.yml

Built-in Caches

  • Drupal out-of-the box comes with two caches
  • Internal Page Cache
    • Serves anonymous users with responses that are cached in their entirety
    • Core module enabled by default
    • Configure via admin/config/development/performance
  • Dynamic Page Cache
    • Handles caching of pages for anonymous and authenticated users
    • Caches parts of pages that can be served to all users and handles dynamic content that depends on a context separately
    • Core module enabled by default

Cache Backend Services

  • A cache backend service (cache.default and others) interacts with cache storage
  • A cache service can be used to cache custom data (from slow/expensive operations ) or interact with cache bins programmatically
  • See To use the Cache API

Classes and Interfaces

  • CacheBackendInterface - interface for cache implementations
    • Default back-end is DatabaseBackend - cached data is stored in the database
  • CacheableMetadata - represents cache metadata and methods to apply that metadata to a render array
  • CacheableDependencyInterface
  • RefinableCacheableDependencyInterface

Cacheability Metadata

Cache Tags

  • Cache Tags
  • Cache tags are basically labels attached to entities (nodes) and configuration data
  • These tags enable Drupal to invalidate cached entities / configuration once data dependencies change
  • A page that displays different content (nodes, blocks, views etc.) has multiple cache tags associcated with it
  • If the page displays article node 5 and that node is updated, the cached page would be flushed and the page
    would need to be re-rendered / re-constructed
  • Cache Tag Examples
    • node:article - clear from cache if article content type changes
    • node:1 - clear from cache id node 1 changes
    • node:list - clear from cache if any node has been updated - common cache tag for views
  • Cache tags are automatically provided by Entities
  • Cache tags (if desired) will need to be explicitly added to render arrays (via #cache key) in custom code / custom module

Cache Contexts

  • Cache contexts
  • Allows a different version of a particular piece of content to be cached based on a certain context
  • Context (or circumstances) can be:
    • Anonymous vs authenticated user
    • A particular user or user role
    • Time of day
    • TBD: What are some other contexts ?

Cache Max-age

  • Cache max-age
  • Cached content / item is valid for a certain period of time, 1hr, 24hrs etc.
  • Max-age set to 0 means content / item is never cached
  • Max-age set to -1 (CACHE_PERMANENT) means cacheable forever - content will only ever be invalidated due to cache tags
    See Cacheability of render arrays for cache tag example

Cacheability Metadata in Render Arrays

// Example 1: Add cache tag to a render array
...
$current_user = \Drupal::currentUser();
$build = [ 
  '#markup' => t('Hello, %name, welcome to the @site!', [
    '%name' -> $current_user->getUserName(),
    '@site' => $config->get('name'),
  ]),
]

// Set the context to user because content should be cacheable per user
$build['#cache']['contexts'] = ['user'];

// Merge cacheability information from dependent objects - uses renderer service
$renderer = \Drupal::service('renderer');
$renderer->addCacheableDependency($build, $config);
$renderer->addCacheableDependency($build, $current_user);

// $build now contains all cacheability information
...
// Example 2
...
$build = [
  '#markup' => some_dymamic_content(),
  '#cache' => [
     'tags' => ['node_list'], // invalidate when any node updates
     'contexts' => ['user'], // new cache item per individual user
     'max-age' => \Drupal\Core\Cache\Cache::PERMANENT,
   ],
];
...
// More example tags:
// 'tags' => ['node:1', 'term:2'], // invalidate if node 1 or term 2 changes
// 'max-age' => 60, invalidate cache after 60 seconds
// 'max-age' => 0, // never cache
...

Placeholders and Lazy Building

  • Auto-placeholdering is the process by which Drupal identifies the render arrays that cannot
    or should not be cached for the reasons we mentioned before, and replaces them with a placeholder
  • The placeholder is then replaced at the very last possible moment while allowing the rest of the
    page components to be cached - This is also called Lazy Building
  • Use #lazy_builder key in a render array to indicate lazy building
  public function build() {
    return [
      '#lazy_builder' => ['<module_service>.lazy_builder:outputSomeContent', []],
      '#create_placeholder' => TRUE,
    ];
  }

Security

Internationalization and Localization

  • Translation API
  • Drupalize.me | Drupal Code Standards: The t() Function
  • Drupalize.me | Make Strings Translatable
  • Dynamic strings with placeholders
  • $this->t() versus t()
  • Internationalization and Localization are terms used for a multilingual site
  • A multilingual site is any site that has content in more than 1 language - Users are able to switch between languages
  • Internationalization is the underlying structure that allows software to be adapted to different languages
  • Localization is the process of actually translating the software for use by a specific locale
  • When we write code that adds UI elements to the page, we're concerned with internationalization
    • We want to make sure that all user interface strings in our application are translatable.
    • As a rule, any text you add to templates, or strings you add in render arrays need to be translatable
  • Any time you display UI text using PHP code in a Drupal modul
    • Pass it through either the global t() function or the t() method on the class
// Example
// Simple string.
$build = [
  '#markup' => $this->t('Hello, World!')
];
  • For plurals (like 1 degree, 2 degrees) use
    • \Drupal\Core\StringTranslation\PluralTranslatableMarkup::createFromTranslatedString() global
    • or formatPlural() method on the class
    • Classes can use the \Drupal\Core\StringTranslation\StringTranslationTrait to get these methods
// Example
use Drupal\Core\StringTranslation\StringTranslationTrait;

class MyClass {
  use StringTranslationTrait;
  public function myMethod() {
    // Simple translation in a render array.
    $output_simple = [
      '#markup' => $this->t('Hello, World!')
    ];

    // With placeholders for dynamic content.
    $output_with_placeholder = [
      '#markup' => $this->t('Hello, @name!', ['@name' => $username])
    ];
    
    // To account for plural variation of the string.
    $output_plural = [
        '#markup' => $this->formatPlural($count, '@count degree', '@count degrees', ['@count' => $count])
    ];

  }
}

Placeholders

@variable

  • Use this style of placeholder for most use-cases. Special characters in the text will be converted to HTML entities
t('Hello @name, welcome back!', ['@name' => $user->getDisplayName()]);

// Hello Ray, welcome back!

%variable

  • Use this style of placeholder to pass text through drupal_placeholder() which will result in the text being HTML escaped, and then wrapped with <em> tags
t('The file was saved to %path.', ['%path' => $path_to_file ]);

// The file was saved to <em class="placeholder">sites/default/files/myfile.txt</em>.

:variable

  • Use this style of placeholder when substituting the value of an href attribute
  • Values will be HTML escaped and filtered for dangerous protocols
t('Hello <a href=":url">@name</a>', array(':url' => 'http://example.com', '@name' => $name));

// Hello <a href="http://example.com">Ray</a>
  • Pass any dates displayed in the UI through the 'date' service class' \Drupal\Core\Datetime\Date::format()
// Example
$date_service = \Drupal::service('date');
$formatted_date = $date_service->format($timestamp, 'custom', 'Y-m-d H:i:s', NULL, $langcode);

$output_date = [
  '#markup' => $formatted_date
];
  • For Twig templates, use the t or trans filters to indicate translatable text
  • Use the plural tag to define a plural variation of a variable's value
// Example
{# Simple translation #}
{{ 'Hello, World!'|t }}

{# With placeholders for dynamic content #}
{{ 'Hello, @name!'|t({'@name': username}) }}

{# Or use the set tag #}
{% set title = '@label Introduction'|t({'@label': node.title.value}) %}
<h1>{{ title }}</h1>

{# Use trans filter for longer texts or with HTML #}
{% trans %}
  Hello, <strong>@name</strong>!
{% endtrans %}

{# With plural variation #}
{% trans %}
There is 1 comment.
{% plural count %}
There are {{ count }} comments.
{% endtrans %}
  • In JavaScript code, use Drupal.t() and Drupal.formatPlural() to translate UI text
// Example
// For a simple translation of a string.
Drupal.t('Hello, World!');

// With placeholders for dynamic content.
Drupal.t('Hello, @name!', {'@name': username});

// Handling plurals.
Drupal.formatPlural(count, '@count apple', '@count apples', {'@count': count});
  • All these methods accept placeholders to allow for substituting dynamic content into a translated string
  • Just because your code allows user interface strings to be translated doesn't mean that they will be
  • By default, Drupal only uses the string as-entered in your code
  • Best practice is to use English
  • To allow users of another language to see the strings in their own language, you'll need to
    configure Drupal's localization modules

Files and Images

Drupal Filesystems

  • Drupal defines four main types of file storage (filesystems)
  • Each of these types use a Drupal defined Stream Wrapper
    • Public - uses public:// stream wrapper
    • Private - uses private:// stream wrapper
    • Temporary - uses temporary:// stream wrapper
    • Translation - uses translations:// stream wrapper
  • Streams allow file or remote data to be processed in "chunks" - this can alleviate memory issues when
    processing large amounts of data
  • Stream wrappers are an abstraction layer on top streams - they tell PHP how to handle specific types of data
  • See PHP Streams for a fuller explanation of Streams and Stream Wrappers

File API

  • file_save_data, file_copy and file_move are deprecated and replaced with a service
  • File Entity
  • Drupal\file\Entity\File
  • Interfaces
    • Drupal\file\FileInterface - File Entity interface
    • Drupal\Core\File\FileSystemInterface - Interface for helpers that operate on files and stream wrappers
  • Services
    • Drupal\file\FileRepositoryInterface file.repository service
    • Drupal\Core\File\FileSystem file_system service
    • Drupal\file\FileUsage\DatabaseFileUsageBackend file.usage service
  • For Managed files, use file.repository and file.usageservices
  • For Un-managed files, use file_system service

Schema API

  • Schema API
  • Schema API - D7 (Mostly applies to D8+ also)
  • The Schema API enables database tables to be defined using PHP code
  • In doing so, Drupal is providing an abstraction layer over various database
    implementations (MySQL, MariaDB, PostgreSQL etc.)
  • A schema definition is an array structure representing one or more tables and their related keys and indexes
  • Schema definitions are in implememted by hook_schema() in <module-name>.install file
  • NOTE: In most applications, using a Content Entity and Entity CRUD operations is sufficient for data needs
    In cases where it's not (rare) use the Schema API to manually define database tables and use the Database API

Database API

database:
  class: Drupal\Core\Database\Connection
  factory: Drupal\Core\Database\Database::getConnection
  arguments: [default]
  • Can also use $database = \Drupal::database();

Mail API

Token API

Batch API

  • Batch processing / Batch API allows processing of large amounts of data while avoiding PHP request timeouts

Batch-powered Update Hooks

  • hook_update_N(&$sandbox) and hook_post_update_NAME(&$sandbox) support batch processing
    via the $sandbox parameter

Batch Operations in Forms, Plugins and Services

Queue API and Cron

Installation Profiles

Views API

UUID

Web Services

URL/URI

Middleware API

Headless Drupal

Migrations, Migrate API and importing data into Drupal

TBD

Debugging

Developing for Drupal

Novice Issues

GitLab Links

  • TBD

GitHub Links (for comparison to GitLab)

Other Links

Steps to Fixing a Drupal Contributed Module Issue / Bug

  • Prerequistes
  • GitLab tutorial
  • Why Drupal decided to use Gitlab
  • Use Git version control system (for Drupal) as a guide
  • Follow the instructions in Introduction to Drupal Git and Setting up Git for Drupal to setup / enable Git access for your Drupal.org account
  • Using Git to Contribute to Drupal can be just skimmed at first
  • Step 1 - Select a project and project issue to work on
    • For this example we'll use the Block Background Style contrib module
    • The issue we'll work is Lost background images
    • NOTE: Step 5 - Create Issue Fork appears to not be necessary as Issue Fork is automatically created once issue has been created
  • Step 2 - Create new (clean) Drupal installation and clone project git repo
    • Create a new clean Drupal installation
    • TBD: Install code sniffer tools
    • Go to contrib module project page
      • On the right sidebar, under "Development", click on "Source code"
        • This will take you to the project GitLab git
      • Click on "Clone" button (on upper right), under "Clone with SSH", click "Copy URL" paste icon.
    • Go to <your_drupal_installation_directory>/web/modules/contrib directory
      • If you haven't installed any contrib modules, web/modules won't have a contrib sub-directory. In this case you can just create a contrib`` directory under web/modules```
    • In your <your_drupal_installation_directory>/web/modules/contrib directory, run git clone
    • Go to contrib module directory
      • git status should indicate what branch you're on and say "nothing to commit, working tree clean"
      • _TBD - execute git config commands git config user.name, user.email etc. See Configuring Git for Drupal
  • Step 3 - Should I do a rebase at this point to bring in latest commits ?
  • Step 4 - Enable the module, reproduce the issue
  • Step 5 - Create Issue Fork
    • Go to project issue page - Click "Create issue fork"
      • This will create ("fork") a new project repository (repo) in GitLab based on the current project repo
    • Click on "Show commands"
    • Run commands in contrib module dir
      • use git remote -v to verify
    • Clink on "Issue fork" link - this will take you the GitLab page showing the fork repo
    • TBD - show image of what repo should look like
  • Step 6 - Write and commit code to fix issue
    • Run code sniffer
  • Step 7 - Submit Merge Request
  • Step 8 - OPTIONAL - Create a patch from Merge Request

Automated Testing

PHPUnit Installation

Method 1 - Use drupal/core-dev

  • Install site using drupal/recommended-project template
  • If drush is installed, remove it by running composer remove drush/drush
  • Install drupal/core-dev
composer require drupal/core-dev --dev --update-with-all-dependencies
  • Re-install drush
composer require drush/drush
  • Install phpspec/prophecy-phpunit - required by PHPUnit 9
composer require --dev phpspec/prophecy-phpunit:^2

Method 2 - Install PHPUnit and other needed libraries manually

composer require --dev phpunit/phpunit
composer require --dev phpspec/prophecy-phpunit:^2
composer require --dev behat/mink-selenium2-driver
composer require --dev symfony/phpunit-bridge
composer require --dev friends-of-behat/mink-browserkit-driver:^1.3

Drupal Testing Frameworks

  • Drupal has 4 different types of testing frameworks
  • If you run tests by using PHPUnit with the --testsuite argument they're named
    unit, kernel, functional, functional-javascript
  • If you run tests with the run-tests.sh test runner with the --types argument, they're named PHPUnit-Unit, PHPUnit-Kernel, PHPUnit-Functional, PHPUnit-FunctionalJavascript Drupal Testing Frameworks
  • Unit Tests
    • Write a unit test when you know you'll be handling all of the dependencies
    • Unit tests are great for testing some very fine-grained behavior with few or no dependencies
    • If you don't need any of the behavior and convenience methods from the kernel test framework,
      write a unit test
    • Since unit tests don't have to set anything up, they run very quickly
  • Kernel Tests
    • Kernel tests should be used when we have some dependencies we can't or shouldn't mock.
    • Use when test doesn't require performing HTTP requests
  • Functional Tests
    • Functional tests use the Mink framework to provide a virtual HTTP client
    • The client can perform HTTP requests against an installed Drupal site
  • Functional JavaScript Tests
    • Same as Functional Tests but adds ability to test JavaScript
  • Before running tests copy web/core/phpunit.xml.dist to your site <doc-root>, one level above web/
  • See Configure PHPUnit for example phpunit.xml.dist configuration
  • See Drupal.org | Automated tests API for Testing Framework purpose and scope
    • Note: Functional Tests is the same as Browser Tests

Run Drupal Tests with the run-tests.sh script

  • Run one test class with --class
# Run from Drupal site <doc-root> - one level above web/
php ./web/core/scripts/run-tests.sh --sqlite /tmp/test.sqlite --url https://drupal.ddev.site --class 'Drupal\Tests\system\Functional\Batch\PageTest'
  • Run tests for a group
php ./web/core/scripts/run-tests.sh --sqlite /tmp/test.sqlite --url https://drupal.ddev.site Batch
  • Speed up execution with --concurrency
php ./web/core/scripts/run-tests.sh --sqlite /tmp/test.sqlite --url https://drupal.ddev.site --concurrency 5 Batch
  • Use --verbose output
php ./web/core/scripts/run-tests.sh --sqlite /tmp/test.sqlite --url https://drupal.ddev.site --concurrency 5 --verbose --non-html Batch
  • Run tests in a module or directory
php ./web/core/scripts/run-tests.sh --sqlite /tmp/test.sqlite --url https://drupal.ddev.site --module testing_example
php ./web/core/scripts/run-tests.sh --sqlite /tmp/test.sqlite --url https://drupal.ddev.site --directory modules/contrib/examples/modules/testing_example
  • Run different test types with --types
php ./web/core/scripts/run-tests.sh --sqlite /tmp/test.sqlite --url https://drupal.ddev.site --concurrency 5 --types PHPUnit-Unit Batch
php ./web/core/scripts/run-tests.sh --sqlite /tmp/test.sqlite --url https://drupal.ddev.site --concurrency 5 --types PHPUnit-Unit,PHPUnit-Functional Batch
  • Run Functional JavaScript tests
  • In order to run functional JavaScript tests the Selenium WebDriver and a browser-driver, such as ChromeDriver must be installed
  • Functional JavaScript tests can't run concurrently
php ./web/core/scripts/run-tests.sh --verbose --non-html --url --sqlite /tmp/test.sqlite http://drupal.ddev.site --concurrency 1 --types PHPUnit-FunctionalJavascript Ajax

Run Drupal Tests directly with PHPUnit

  • Run all tests
# Run from Drupal site <doc-root> - one level above web/
./vendor/bin/phpunit
  • Run a single test
./vendor/bin/phpunit web/core/tests/Drupal/Tests/Component/Plugin/DefaultFactoryTest.php
  • Run single test with browser output
./vendor/bin/phpunit web/core/tests/Drupal/Tests/Component/Plugin/DefaultFactoryTest.php --printer="\Drupal\Tests\Listeners\HtmlOutputPrinter"
  • Run all tests in directory
./vendor/bin/phpunit web/modules/contrib/examples/modules/phpunit_example/tests/src/Unit/
  • Run test with --filter
# Run testAdd method only 
./vendor/bin/phpunit --filter testAdd web/modules/contrib/examples/modules/phpunit_example/tests/src/Unit/AddClassTest.php
  • Run test with --filter
# Running tests this way is expensive as the whole Drupal codebase needs to be scanned
./vendor/bin/phpunit --filter DefaultFactoryTest
./vendor/bin/phpunit --filter DefaultFactoryTest::testGetPluginClassWithValidArrayPluginDefinition
  • Run all unit tests
./vendor/bin/phpunit --testsuite unit
./vendor/bin/phpunit --testsuite unit --filter DefaultFactoryTest

Miscellaneous Resources

https://packagist.drupal-composer.org
https://github.com/drupal-composer/drupal-project
https://www.drupal.org/node/2471553
https://github.com/geerlingguy/drupal-vm
http://www.slideshare.net/GetPantheon/lean-drupal-repositories-with-composer-and-drush
http://openconcept.ca/blog/mparker/conforming-coding-standards-linters
https://github.com/brigade/scss-lint
http://openconcept.ca/blog/mparker/automatically-running-linters-commit
http://editorconfig.org/
http://www.sitepoint.com/comparison-javascript-linting-tools/

⚠️ **GitHub.com Fallback** ⚠️