Jan 07 2019
Jan 07
The home automation system is a combination of software and hardware devices which handles home routine such as lights, temperature/humidity (microclimate) and entertainment systems (TV, audio etc.) automatically based on some scenarios. This post is about building such system based on Domoticz server, Xiaomi and Broadlink devices. I'll try to cover some topics such as system set up and useful scenarios. Agenda:
  1. Hardware
    1. Gateway
    2. Signaling sensors
    3. Data provider sensors
    4. Controlling sensors
    5. Smart lamps
    6. Server and wifi router
  2. Software
    1. Domoticz
    2. Mini DLNA
  3. Adding devices to Domoticz
    1. Gateway
    2. ZigBee devices
    3. Yeelight Smart bulbs
    4. Broadlink RM 3 mini and RM Pro+ remote controls
    5. Naming convention, groups, and scenes
  4. Network
  5. Scenarios
    1. Network devices state
      1. Are you at home?
    2. Notifications
      1. Motion activity detected
      2. Doors/windows activity detected
      3. Fire detected (high temperature)
      4. ZigBee devices batteries level report
    3. Room occupation
      1. Motion trigger
      2. Windows and doors trigger
    4. Room presence detection
      1. TV
      2. Laptop
      3. Room locking
    5. Light
      1. Room occupation trigger
      2. Button trigger
    6. Microclimate
    7. Alarm clock
  6. Remote access to your Domoticz
  7. Monitoring
    1. Service uptime
    2. Containers state
  8. Conclusion

Hardware


Let's overview hardware which will be used in the system. I distinguish 5 types of devices/sensors for building home automation systems: gateways, signaling sensors, data providers, controlling sensors and "smart" light. Most of Xiaomi/Aqara devices are working over ZigBee protocol, but there are some which are working over wifi like lamps. In a nutshell, a gateway is a central place of a smart home which receives all the data from other sensors/devices which are connected to it. Signaling sensors tell about some event. Data sensors provide environment information. Controlling sensors send commands to home devices. Smart lamps can be controlled over wifi in order to switch light.
There are a lot more devices and sensors on the market for different purposes but in this post, I'll cover only that which are used for my basic scenarios.

Gateway

  • Gateway (Xiaomi, WiFi/ZigBee) - a central point of ZigBee network. Receives events from other sensors. A system can run scenarios based on provided information.

Signaling sensors

  • Motions sensor (Xiaomi or Aqara, ZigBee) - emits a signal when motion is detected.
  • Door/window state sensor (Xiaomi or Aqara, ZigBee) - emits a signal when door/window is being opened or closed.

Data provider sensors

  • Temperature and humidity sensor (Xiaomi or Aqara, ZigBee) - provides current temperature and humidity.
  • Lux sensor (Aqara, ZigBee) - provides current luminosity. Usually, this sensor combined with a motion sensor (Aqara).

Controlling sensors

  • Button (Xiaomi, ZigBee) - sends commands (single click, double click and long click).
  • Socket (Xiaomi or Aqara, ZigBee) - power control.
  • Remote control RM Pro + (Broadlink, WiFi) - sends commands to IR and RF controlled devices such as conditioner, TV etc.
  • Remote control RM Mini (Broadlink, WiFi) - sends commands only to IR controlled devices.

Dec 26 2018
Dec 26
It's not possible to use list values in a conditional operator. For example, if you try:
resource "aws_elasticsearch_domain" "es_domain" {
  ...

  vpc_options {
    ...
    subnet_ids = [ "${var.es_zone_awareness_enabled ? list("subnet-1", "subnet-2") : list("subnet-1")}" ]
  }

  ...
}
it will fail with "conditional operator cannot be used with list values" message. This is because terraform cannot assert that list's element types are consistent.

The workaround is to join list values into a string to bypass type check and then split string back to list:

resource "aws_elasticsearch_domain" "es_domain" {
  ...

  vpc_options {
    ...
    subnet_ids = [ "${split(",", var.es_zone_awareness_enabled ? join(",", list("subnet-1", "subnet-2")) : join(",", list("subnet-1", "")))}" ]
  }

  ...
}
You might've mentioned that there is an empty string as a second list value in second join function. This is needed to make result string contain a "," character for splitting by final split function.

Of course, it's just a synthetic example with list functions. In real life, those lists come from variables. For instance, I had a map variable which looked like

variable "private_subnet_ids" {
  type = "map"
  default = { "zone-1,zone-2" = "subnet-1,subnet-2" }
}
and I wanted to get a list which contains one or both subnet ids depending on bool variable. Possible steps were:
  1. Turn a map into a list.
  2. If we need both subnets:
    1. Get the first list value (string, "subnet-1,subnet-2").
  3. If we need only one subnet:
    1. Get the first list value (string, "subnet-1,subnet-2").
    2. Split by "," character (list, ["subnet-1", "subnet-2"]).
    3. Get needed list element (string, "subnet-1").
    4. Put it to a list along with an empty string (list, ["subnet-1", ""]).
    5. Join this list into a string by "," character (string, "subnet-1,").
  4. Split by "," character.
Result resource definition looked as:
resource "aws_elasticsearch_domain" "es_domain" {
  ...

  vpc_options {
    ...
    subnet_ids = [ "${split(",", var.es_zone_awareness_enabled ? element(values(var.private_subnet_ids), 0) : join(",", list(element(split(",", element(values(var.private_subnet_ids), 0)), 0), "")))}" ]
  }

  ...
}
Jan 27 2018
Jan 27
Netflix Eureka is a REST service that is primarily used in the AWS cloud for locating services. It comes with a Java-based client. But if you need a PHP-based client - welcome under the cut.

PHP Eureka client supports all the Eureka operations: register, de-register, send heartbeat, take instance out, put instance back, update metadata etc. How to use:

1. Create Eureka app instance definition:
// We will use app name and instance id for making requests below.
$appName = 'new_app';
$instanceId = 'test_instance_id';

// Create app instance metadata.
$metadata = new Metadata();
$metadata->set('test_key', 'test_value');

// Create data center metadata.
$dataCenterMetadata = new Metadata();
$dataCenterMetadata->set('data_center_test_key', 'data_center_test_value');

// Create data center info (Amazon example).
$dataCenterInfo = new DataCenterInfo();
$dataCenterInfo
  ->setName('Amazon')
  ->setClass('com.netflix.appinfo.AmazonInfo')
  ->setMetadata($dataCenterMetadata);

// Create Eureka app instance.
$instance = new Instance();
$instance
  ->setInstanceId($instanceId)
  ->setHostName('test_host_name')
  ->setApp($appName)
  ->setIpAddr('127.0.0.1')
  ->setPort(80)
  ->setSecurePort(433)
  ->setHomePageUrl('http://localhost')
  ->setStatusPageUrl('http://localhost/status')
  ->setHealthCheckUrl('http://localhost/health-check')
  ->setSecureHealthCheckUrl('https://localhost/health-check')
  ->setVipAddress('test_vip_address')
  ->setSecureVipAddress('test_secure_vip_address')
  ->setMetadata($metadata)
  ->setDataCenterInfo($dataCenterInfo);

2. Create Eureka client (it is based on Guzzle HTTP client):
// Eureka client usage example.
// Create guzzle client.
$guzzle = new Client();

// Create eureka client.
$eurekaClient = new EurekaClient('localhost', 8080, $guzzle);

3. Communicate with Eureka server:
try {
  // Register new application instance.
  $response = $eurekaClient->register($appName, $instance);

  // Send application instance heartbeat.
  $response = $eurekaClient->heartBeat($appName, $instanceId);

  // De-register application instance.
  $response = $eurekaClient->deRegister($appName, $instanceId);
}
catch (Exception $e) {
  echo $e->getMessage() . PHP_EOL;
}

See repository for more information.
Jan 21 2018
Jan 21
Using monolg library and monolog-cascade extension you can't configure the "namespaced" loggers. What does it mean? Imagine you have tons of classes and you need to log information from them into a log file. There is nothing special in this. Just define loggers with the needed handler(s) and instantiate them directly in a place where you want them to use with a help of monolog-cascade. It means in your monolog-cascade config file you have to define needed loggers in advance and you have to reference needed loggers by their names. But what if you need an additional logger (with absolutely different handlers/processors) for some of the classes? Will you go through all the classes and change logger names where you instantiate them? I think it doesn't look like a good idea when a small requirement (for instance, change the log file name for records from a bunch of classes) leads to edits in an application code. It's something that must be configurable and that's why I decided to write a tiny library called monolog-cascade-namespaced.

Monolog Cascade Namespaced is actually an extension of a monolog-cascade library (please take a look at this library first if you're not familiar with it). The main idea is simple - pass a full class name instead of logger name to the static method which instantiates loggers. Just do this:
CascadeNamespaced::getLogger(get_called_class())->info('...');
instead of this:
Cascade::getLogger('my_logger_name')->info('...');
In other words, you don't have to think which logger must be instantiated in this place. You will be able to configure needed logger in your config file instead of hardcoding the name here.

How does it work? First, it looks for a logger specific to your class. If there is no such logger then it looks for a logger for a namespace the class is located in. If there is no even this logger then it tries to find "root" logger - default logger for your application. And finally, if there is no even root logger defined in your config file then simple logger with null handler will be returned as a fallback behavior. Loggers specific to a class have more priority than loggers for a namespace.

Let's dive deeper and consider an example. You have next classes:

  • MyNamespace\Class1
  • MyNamespace\Class2
  • MyNamespace\SubNamespace\Class3
  • MyNamespace\SubNamespace\Class4
  • AnotherNamespace\Class5

and you want to log messages from MyNamespace\SubNamespace namespace classes into the stdout, but messages from MyNamespace\Class1 and MyNamespace\Class2 classes into two different files. It can be done with next configuration:
handlers:
    file1:
        class: Monolog\Handler\StreamHandler
        stream: log_file_1.log

    file2:
        class: Monolog\Handler\StreamHandler
        stream: log_file_2.log

    console:
        class: Monolog\Handler\StreamHandler
        stream: php://stdout

loggers:
    # Will be used for `AnotherNamespace\Class5` class as a default logger
    # because there are no specific loggers defined for this class.
    root:
        handlers: [console]

    # Will be used for all the classes from `MyNamespace\SubNamespace` namespace.
    MyNamespace\SubNamespace:
        handlers: [console]

    # Will be used for `MyNamespace\Class1` class.
    MyNamespace\Class1:
        handlers: [file1]

    # Will be used for `MyNamespace\Class2` class.
    MyNamespace\Class2:
        handlers: [file2]
If you need to change the logger for some class just define new one specific to a needed class in your config file. If you don't want to use any specific loggers then just leave root logger.

You may ask me "So, how does it differ from actually monolog-cascade"? The answer is:

  1. You don't have to define needed loggers in advance.
  2. You can define a logger for a set of classes located in a namespace.
  3. You can define a logger for a specific class.
  4. You don't have to hardcode a logger name. Just pass the full class name and configure logger in the future if needed.
Additional information on how to install, configure and use monolog-cascade-namespaced can be found here
Nov 26 2017
Nov 26
Let's imagine you want to send log records from your Drupal 8 site to your email box, 3rd party service or to some other destination in order to know about warnings, errors etc. Most probably you don't want to send an email each time when some action happens as you don't want to decrease page performance. So, in this case, you should write a "buffered logger" which will keep all log entries in a buffer and send them only when it's overflown or on shutdown function. So let's write it.

Define a Drupal logger


Let's say we have a custom module called logger_example, so create a directory src/Logger and put there a file BufferLogger.php (you can choose any name you want, it's just an example) with next content:
<?php

namespace Drupal\logger_example\Logger;

use Psr\Log\LoggerInterface;
use Psr\Log\LoggerTrait;

/**
 * Class BufferLogger
 *
 * @package Drupal\logger_example\Logger
 */
class BufferLogger implements LoggerInterface {

  use LoggerTrait;

  /**
   * @var array
   */
  private $buffer;

  /**
   * @var int
   */
  private $bufferLimit;

  /**
   * BufferLogger constructor.
   */
  public function __construct($bufferLimit = 10) {
    $this->buffer = [];
    $this->bufferLimit = $bufferLimit;

    drupal_register_shutdown_function([$this, 'flush']);
  }

  /**
   * Logs with an arbitrary level.
   *
   * @param mixed $level
   * @param string $message
   * @param array $context
   *
   * @return void
   */
  public function log($level, $message, array $context = []) {
    $this->buffer[] = [
      'level' => $level,
      'message' => $message,
      'context' => $context,
    ];

    // Flush buffer when it's full.
    if (count($this->buffer) == $this->bufferLimit) {
      $this->flush();
    }
  }

  /**
   * Log messages into needed destination.
   */
  public function flush() {
    // It's not "full buffer" case.
    // If buffer is empty it means it's usual call of shutdown function
    // and there is nothing to log.
    if (empty($this->buffer)) {
      return;
    }

    // TODO: Log buffered log entries here.

    // Reset the buffer.
    $this->buffer = [];
  }

}

Let's find out how does it work. First of all, it has $buffer and $bufferLimit properties. First one is an array which will contain all buffered records and the second one is a setting which means "how many log records do you want to keep before sending?".

In a constructor, we register a public method called flush as a PHP shutdown function which will actually send buffered records to the needed destination. We need this shutdown function to handle the case when the buffer isn't full but it isn't empty as well and we want to send those buffered records anyway.

The BufferLogger::log() method saves records in an array and flushes them if needed. So we do not send any log entries each time when log() is called.

The last method is BufferLogger::flush(). It does two things: sends buffered records into needed destinations and resets the buffer array. All sending logic must be implemented in this method.

Tell the Drupal about your logger


Define a service inside your logger_example.services.yml file and tag it with logger name:
services:
  ...
  logger.logger_example:
    class: Drupal\logger_example\Logger\BufferLogger
    arguments: [100]
    tags:
      - { name: logger }
  ...
You can set up buffer size as big/small as you wish. For example, you want to send logs only if the buffer is 100 messages full (or anyway it will flush the buffer on a shutdown). Rebuild Drupal cache and after that, Drupal will log messages through all defined loggers including yours and all buffered records will be sent to the needed destination.

That's all, now you have a simple buffered logger. Of course, it can be improved. For example, you could implement a filter for channels or/and log levels you want to listen to. It's useful when you need to be notified only, let's say, about errors from "system" channel. It's all up to you.

Nov 05 2017
Nov 05
If you need to install the latest version of a composer you can use next bash snippet:
EXPECTED_SIGNATURE=$(wget -q -O - https://composer.github.io/installer.sig) && \
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php -r "if (hash_file('SHA384', 'composer-setup.php') === '${EXPECTED_SIGNATURE}') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" && \
php composer-setup.php && \
php -r "unlink('composer-setup.php');" && \
mv composer.phar /usr/local/bin/composer
Using EXPECTED_SIGNATURE variable with the latest available signature value you don't have to hardcode a specific one for comparison on 3rd line.
Jul 29 2017
Jul 29
This is a simple API module for Drupal 7 and Drupal 8 which allows developers to react on changed fields in a node when it was updated. For example, you want to modify node object depends on its field values. Or you just want to know what fields were changed. Or finally, you need to check the difference between old and new field values and do some other thing depending on this difference.

Changed Fields API supports all the core's field types both for Drupal 7 and Drupal 8. But for Drupal 7 it supports even more. Please visit the project page to find out more information.

Changed Fields API is built on "Observer" pattern. The idea is pretty simple: attach observers to a node subject which will notify all of them about changes in node fields. If you are not familiar with this pattern yet then I suggest you read about it and consider this simple example. So let's find out how to use this API. An example below is based on changed_fields_basic_usage and changed_fields_extended_field_comparator demo modules that are the part of Changed Fields API module.

Observer


The first thing we need is an observer. Define a class that implements ObserverInterface interface. This interface provides two methods: ObserverInterface::getInfo() and ObserverInterface::update(SplSubject $nodeSubject). The first method should return an associative array keyed by content type names which in turn contain a list of field names you want to observe. A second method is a place where you can react on changed fields and do something with a node. It will be called only if some of the listed fields were changed.

<?php

/**
 * @file
 * Contains BasicUsageObserver.php.
 */

namespace Drupal\changed_fields_basic_usage;

use Drupal\changed_fields\ObserverInterface;
use SplSubject;

/**
 * Class BasicUsageObserver.
 */
class BasicUsageObserver implements ObserverInterface {

  /**
   * {@inheritdoc}
   */
  public function getInfo() {
    return [
      'article' => [
        'title',
        'body',
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function update(SplSubject $nodeSubject) {
    $node = $nodeSubject->getNode();
    $changedFields = $nodeSubject->getChangedFields();

    // Do something with $node depends on $changedFields.
  }

}

So here we defined that we want to listen to article content type and we want to check only title and body fields.

Node presave


In order to detect changed fields in a node we need to define hook_node_presave and there do several steps:
  1. Wrap a node object into an instance of NodeSubject class and set up the field comparator. Field comparator is an object which checks needed fields and returns differences between old and new field values. Default comparator is default_field_comparator but you can define your own by extending default one. 
  2. Then attach your observer BasicUsageObserver to instance of NodeSubject class.
  3. Finally, execute NodeSubject::notify() method and react on this event in BasicUsageObserver::update(SplSubject $nodeSubject) if some of the field values of registered node types have been changed.

<?php

/**
 * @file
 * Contains changed_fields_basic_usage.module.
 */

use Drupal\changed_fields\NodeSubject;
use Drupal\changed_fields_basic_usage\BasicUsageObserver;
use Drupal\node\NodeInterface;

/**
 * Implements hook_node_presave().
 */
function changed_fields_basic_usage_node_presave(NodeInterface $node) {
  // Create NodeSubject object that will check node fields by DefaultFieldComparator.
  $nodeSubject = new NodeSubject($node, 'default_field_comparator');

  // Add your observer object to NodeSubject.
  $nodeSubject->attach(new BasicUsageObserver());

  // Check if node fields have been changed.
  $nodeSubject->notify();
}

Basically, that's all but you can say "what about field types that is not supported by Changed Fields API. How can I handle them?". Well, you're right, API supports a limited bunch of field types but it's easy to extend it.

By default, Changed Fields API comes with default_field_comparator class which supports all the core Drupal's field types. But if you have a field type from a contrib module you have to tell the API how to compare that fields. In order to do that you have to write a custom field comparator.

Custom field comparator


In Drupal 8 version of the module field comparators are plugins and they should be placed into module_name/src/Plugin/FieldComparator directory. So we need to define a class, extend it from DefaultFieldComparator and override method DefaultFieldComparator::getDefaultComparableProperties(FieldDefinitionInterface $fieldDefinition). All that you need is return field properties to compare depends on a field type. For example for text_with_summary fields API returns array('value', 'summary').

<?php

/**
 * @file
 * Contains ExtendedFieldComparator.php.
 */

namespace Drupal\changed_fields_extended_field_comparator\Plugin\FieldComparator;

use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\changed_fields\Plugin\FieldComparator\DefaultFieldComparator;

/**
 * @Plugin(
 *   id = "extended_field_comparator"
 * )
 */
class ExtendedFieldComparator extends DefaultFieldComparator {

  /**
   * {@inheritdoc}
   */
  public function getDefaultComparableProperties(FieldDefinitionInterface $fieldDefinition) {
    $properties = [];

    // Return comparable field properties for extra or custom field type.
    if ($fieldDefinition->getType() == 'some_field_type') {
      $properties = [
        'some_field_property_1',
        'some_field_property_2',
      ];
    }

    return $properties;
  }

}

Here we assume that we have a custom field type some_field_type with properties some_field_property_1 and some_field_property_2. In order to compare such fields, we need to tell node subject that it should use our newly defined field comparator:

<?php

/**
 * @file
 * Contains changed_fields_extended_field_comparator.module.
 */

use Drupal\changed_fields\NodeSubject;
use Drupal\changed_fields_extended_field_comparator\ExtendedFieldComparatorObserver;
use Drupal\node\NodeInterface;

/**
 * Implements hook_node_presave().
 */
function changed_fields_extended_field_comparator_node_presave(NodeInterface $node) {
  // Create NodeSubject object that will check node fields by your ExtendedFieldComparator.
  $nodeSubject = new NodeSubject($node, 'extended_field_comparator');
  ...
}

Cool, now we know how to add support for custom or additional contrib fields. But there might be cases when you want, for example, compare text_with_summary fields only by value property (by default API compares it by format and summary properties as well). In this case you need to override DefaultFieldComparator::extendComparableProperties(FieldDefinitionInterface $fieldDefinition, array $properties) method like this one:

public function extendComparableProperties(FieldDefinitionInterface $fieldDefinition, array $properties) {
    if ($fieldDefinition->getType() == 'text_with_summary') {
      unset($properties[array_search('summary', $properties)]);
      unset($properties[array_search('format', $properties)]);
    }

    return $properties;
  }

When it's done changes in field's summary or format properties will not be taken into consideration by the API.

Additional info

Jul 15 2017
Jul 15

On 10-11 of June in Kyiv there was an annual all-Ukrainian event - Drupal Camp Kyiv 2017. This is a place where experienced back-end, front-end developers, DevOps and managers share their knowledge. Traditionally Drupal Camp took place in two days: a conference day which includes 5 streams of presentations and code sprint where passionate developers can work together for improving Drupal and developing community. A few interesting statistics about this year’s event: 403 attendees, 5 streams of lectures, 42 speaker, 10+ international speakers, 70+ code sprint participants and 100+ patches made during code sprint.

I'm working on the Smartling company where my main projects are connectors: Smartling (for Drupal 7), TMGMT Smartling and TMGMT Extension Suite (for Drupal 8). The main goal both of these connectors is to send content for translation into Smartling service, get it back when translations are ready for publishing and correctly apply them into Drupal entities (such as nodes, taxonomy terms etc.), menus, blocks, locale strings and configurations.

So I took a part in this event with a presentation “How to outsource the pain of Drupal translation to Smartling” and I was talking about localization process in Drupal using Smartling translation service. I’ve also reviewed Translation Management Tool (a base for our connector), TMGMT Smartling and TMGMT Extension Suite modules.

In the session I've considered next key points:

  • How to configure your Drupal 8 site and TMGMT module
  • How to configure TMGMT Smartling plugin
  • What is visual context and why it’s so important for translators
  • How do we automate all the translation workflow and track changes of the source content
  • How did we manage to avoid a Drupal 8 core bug with bulk actions
I hope my session was interesting for those who wanted to know how to get rid of the pain of Drupal translation process and who was interested in other alternatives for translation workflow aside from common copy-pasting process from Excel sheets.

Drupal Camp Kyiv is the biggest Drupal event in Ukraine. It’s a great opportunity for developers to learn something new and grow up as a specialist. I’m looking forward to taking part in next events.


Session details


Jun 18 2017
Jun 18
Recently I set out to make a simple instrument for running simpletest tests without having LAMP stack installed on your local environment. I needed this for two reasons:
  1. for running tests locally
  2. for running tests on CI server
I've decided to use Docker and create monolith container with Drupal and all the LAMP stuff inside and here what I've got: docker-tester.


How to use


Before running container you have to setup next ennvironment variables:
  1. KEEP_RUNNING - specify  yes  if you want to keep container running when tests will be executed. Use for debugging purposes only. Default value is  no .
  2. DRUPAL_VERSION - specific version of Drupal. Supported Drupal 7 and Drupal 8. Example:  8.3.2 .
  3. MODULES_DOWNLOAD - a list of modules to download (by Drush) separated by comma. Example:  module_name-module_version,[...] .
  4. MODULES_ENABLE a list of modules to enable (by Drush) separated by comma. Example:  module_name,[...] .
  5. SIMPLETEST_GROUPS - a list of simpletest groups to run separated by comma. Example:  Group 1,[...] .
  6. SIMPLETEST_CONCURRENCY - amount of test runners to test code in parallel. Default value is  1 .
Then you need to build an image for container:
docker build -t drupal-tester .
Next you have two options: either run container with docker-compose tool or run container manualy with docker command.
For local usage I prefere to use docker-compose because it's easier than write all the CLI docker command manualy. Just specify what module you want to test inside of docker-compose.yml file and run:
docker-compose up && docker-compose down
It will run the container, install Drupal inside of it and run tests. That's all.

For running tests on CI server I use docker command and specify all the needed environment variables manualy:

docker run -v $(pwd)/test_results:/var/www/html/test_results -v $(pwd)/custom_scripts:/var/www/html/custom_scripts -e KEEP_RUNNING=no -e DRUPAL_VERSION=8.3.2 -e MODULES_DOWNLOAD=module-version -e MODULES_ENABLE=module -e SIMPLETEST_GROUPS=module_test_group -e SIMPLETEST_CONCURRENCY=1 drupal-tester
It allows you to override environment variables and volumes that you want to mount inside of the container. So you can setup different jobs on your CI server to test different modules on different Drupal versions with the help of this one container.

When docker finished the process all test results by default will be placed into test_results directory but you can easily override this by mounting some other directory inside of a container.

Setup and customization


Sometimes you need to do something before running tests. For example override some module specific settings or setup some Drupal variables etc. You can get it done with custom *.sh scripts. Just write sh file with all needed actions/commands and put it inside custom_scripts folder. All the files inside of this directory will be executed before running tests.
Mar 26 2017
Mar 26
Last time we've created a style plugin. Now we will learn how to cache panel panes with custom cache plugins.

In this tutorial we will create a panel cache plugin which will cache panel pane for a given time (5, 10 or 15 seconds). For now we have a module with next file structure:
example_panels_module
  |__ plugins
  |  |__ styles
  |  |  |__ example_panels_module.styles_plugin.inc
  |__ example_panels_module.info
  |__ example_panels_module.module
All that we need to do is put file example_panels_module.cache_plugin.inc into plugins/cache directory with next content:
<?php

/**
 * @file
 * Example panels cache plugin.
 */

$plugin = [
  // Plugin title.
  'title' => t('Example panel cache'),
  // Plugin description.
  'description' => t('Example panel cache.'),
  // Cache get callback.
  'cache get' => 'example_panels_module_cache_get_cache',
  // Cache set callback.
  'cache set' => 'example_panels_module_cache_set_cache',
  // Cache clear callback.
  'cache clear' => 'example_panels_module_cache_clear_cache',
  // Settings form.
  'settings form' => 'example_panels_module_cache_settings_form',
  // Settings form submit.
  'settings form submit' => 'example_panels_module_cache_settings_form_submit',
  // Default values.
  'defaults' => [
    'lifetime' => 5,
  ],
];

/**
 * Get cached content.
 */
function example_panels_module_cache_get_cache($conf, $display, $args, $contexts, $pane = NULL) {
  $cache = cache_get('example_panels_module_cache:' . $pane->pid, 'cache_panels');

  // No cached data available.
  if (!$cache) {
    return FALSE;
  }

  // Cache is expired.
  if ((REQUEST_TIME - $cache->created) > $conf['lifetime']) {
    return FALSE;
  }

  return $cache->data;
}

/**
 * Set cached content.
 */
function example_panels_module_cache_set_cache($conf, $content, $display, $args, $contexts, $pane = NULL) {
 cache_set('example_panels_module_cache:' . $pane->pid, $content, 'cache_panels');
}

/**
 * Clear cached content.
 */
function example_panels_module_cache_clear_cache($display) {
  cache_clear_all('example_panels_module_cache:', 'cache_panels', TRUE);
}

function example_panels_module_cache_settings_form($conf, $display, $pid) {
  // Cache lifetime.
  $form['lifetime'] = [
    '#title' => t('Lifetime'),
    '#type' => 'select',
    '#options' => drupal_map_assoc([5, 10, 15], 'format_interval'),
    '#default_value' => $conf['lifetime'],
  ];

  return $form;
}
So now module structure looks like:
example_panels_module
  |__ plugins
  |  |__ styles
  |  |  |__ example_panels_module.styles_plugin.inc
  |__|__ cache
  |__|__|__ example_panels_module.cache_plugin.inc
  |__ example_panels_module.info
  |__ example_panels_module.module
Clear cache (on enable module if it isn't enable yet) and navigate to panel pages settings (for this tutorial I've created a test panel page for displaying a single node). Choose a panel pane to cache and set up a cache type as Example panel cache (for this tutorial I've created a panel pane that shows a REQUEST_TIME value and put it into node/view panel page):

Set up cache for a given panel pane

Then chose cache lifetime:

Cache lifetime

Save settings and create some test node. For example if you set up cache life time as 5 seconds then value will be static for 5 seconds and only after this time will be refreshed.

Key notes:


Source code of all examples are available here.

Feb 25 2017
Feb 25
Panels style plugins are made for wrapping panel panes and panel regions into extra markup. 99% of your needs are covered by a "Panels Extra Styles" module. Please look at that module if you need extra styles for panels. But if you need some specific style you can easily implement it.

In this tutorial we will create plugin style for region or pane. It will allow site builders to wrap region or pane into a custom markup entered in settings form.

1. Create a directory example_panels_module and put there a file example_panels_module.info:

name = Example panels module
description = Provides example implementation of panels plugins
core = 7.x

; Since we're using panels for creating
; plugins we have to define this
; dependency.
dependencies[] = panels
2. Tell ctools where it has to search plugins (file example_panels_module.module):
<?php

/**
 * Implements hook_ctools_plugin_directory().
 *
 * Integrate our module with Ctools. Tell where
 * Ctools has to search plugins. Please note that
 * we should return path to plugins directory only
 * if $module equals 'panels'.
 */
function example_panels_module_ctools_plugin_directory($module, $plugin) {
  if ($module == 'panels' && !empty($plugin)) {
    return "plugins/$plugin";
  }
}
3. Create directory plugins/styles and put there a file example_panels_module.style_plugin.inc:
<?php
/**
 * @file
 * 'Example panel style' style.
 */

$plugin = [
  // Plugin title.
  'title' => t('Example panel style'),
  // Plugin description.
  'description' => t('Raw HTML wrapper.'),
  // Render region callback.
  'render region' => 'example_panels_module_raw_wrapper_render_region',
  // Render pane callback
  'render pane' => 'example_panels_module_raw_wrapper_render_pane',
  // Region settings form.
  'settings form' => 'example_panels_module_raw_wrapper_region_settings_form',
  // Pane settings form.
  'pane settings form' => 'example_panels_module_raw_wrapper_pane_settings_form',
];

/**
 * Region settings form callback.
 */
function example_panels_module_raw_wrapper_region_settings_form($settings) {
  // Define a settings form with prefix and suffix text areas
  // for region style.
  $form['wrapper_region_prefix'] = [
    '#type' => 'textarea',
    '#title' => t('Region wrapper prefix'),
    '#default_value' => !empty($settings['wrapper_region_prefix']) ? $settings['wrapper_region_prefix'] : '',
  ];

  $form['wrapper_region_suffix'] = [
    '#type' => 'textarea',
    '#title' => t('Region wrapper suffix'),
    '#default_value' => !empty($settings['wrapper_region_suffix']) ? $settings['wrapper_region_suffix'] : '',
  ];

  return $form;
}

/**
 * Region render callback.
 *
 * Please note that it's a theme function
 * and has to start with 'theme_' prefix.
 */
function theme_example_panels_module_raw_wrapper_render_region($vars) {
  $output = '';

  // Variable $vars['panes'] contains an array of all
  // panel panes in current region. Collect them into
  // variable.
  foreach ($vars['panes'] as $pane) {
    $output .= $pane;
  }

  // Variable $vars['settings'] contains settings
  // entered in settings form. Wrap region content
  // into custom markup.
  return $vars['settings']['wrapper_region_prefix'] . $output . $vars['settings']['wrapper_region_suffix'];
}

/**
 * Pane settings form callback.
 */
function example_panels_module_raw_wrapper_pane_settings_form($settings) {
  // Define a settings form with prefix and suffix text areas
  // for pane style.
  $form['wrapper_pane_prefix'] = [
    '#type' => 'textarea',
    '#title' => t('Pane wrapper prefix'),
    '#default_value' => !empty($settings['wrapper_pane_prefix']) ? $settings['wrapper_pane_prefix'] : '',
  ];

  $form['wrapper_pane_suffix'] = [
    '#type' => 'textarea',
    '#title' => t('Pane wrapper suffix'),
    '#default_value' => !empty($settings['wrapper_pane_suffix']) ? $settings['wrapper_pane_suffix'] : '',
  ];

  return $form;
}

/**
 * Pane render callback.
 *
 * Please note that it's a theme function
 * and has to start with 'theme_' prefix.
 */
function theme_example_panels_module_raw_wrapper_render_pane($vars) {
  // Variable $vars['settings'] contains settings
  // entered in settings form. Variable
  // $vars['content']->content is a renderable array
  // of a current pane. Wrap pane content
  // into custom markup.
  return $vars['settings']['wrapper_pane_prefix'] . render($vars['content']->content) . $vars['settings']['wrapper_pane_suffix'];
}
For now we have a module with next structure:
example_panels_module
  |__ plugins
  |  |__ styles
  |  |  |__ example_panels_module.styles_plugin.inc
  |__ example_panels_module.info
  |__ example_panels_module.module
When it's enabled you will be able to setup style for a region or a pane. We've created style for both of them but you can define style only for region or only for pane.

Let's test our style. I want to edit node/%node page: wrap content region into div tag with a class "content-region-wrapper". And wrap node_beind_viewed pane into span tag with id "pane-node-view-wrapper". That's how can I do that:

Change style for a region

Select newly created style

Set up prefix and suffix for a region

The same way I've set up prefix and suffix for a panel pane and that's what I've got:

Result markup

Key notes:

Feb 14 2017
Feb 14
Relationships plugins are "bridge" between existing context (that is already set up in a panel) and a context which you want to get from existing one. Let's say your panel contains "Node" context and you want to get a node author (user from node:uid property). To do that you can just set up "Node author" relationship in a panel (under a "contexts" tab) and that's all. That's why relationships plugins are so important - they provide easy way for getting context from existing contexts. Please have a look at this post before continue reading - there is described how to create module integrated with ctools API which allows us to define own plugins.

As you remember we've created a custom server_info context plugin that provides information from $_SERVER superglobal php variable. All that you can grab from that context are strings. Ctools comes with a simple context plugin string. It provides three ways of string representation: html_safe, raw and uppercase_words_html_safe. In this tutorial we will create a relationship plugin that will convert a string from server_info to a string context.

Let's add new plugin to our existing example_module which for now looks like:

example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |  |  |__ example_module.example_access_plugin.inc
  |  |__ contexts
  |  |  |__ example_module.example_context_plugin.inc
  |  |  |__ example_module.example_context_plugin.node_status.inc
  |  |__ arguments
  |     |__ example_module.example_argument_plugin.inc
  |__ example_module.info
  |__ example_module.module
1. Create file example_module/plugins/arguments/example_module.example_argument_plugin.inc:
<?php


/**
 * @file
 *
 * Plugin to provide a string relationship from server_info context.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('String from "Server info" context'),
  // Plugin description.
  'description' => t('Adds a string context from existing server_info context.'),
  // Keyword.
  'keyword' => 'string_from_server_info',
  // We want to create string context from server_info context.
  // It means that server_info has to be already set up into
  // a panel page. So server_info context is required for
  // this relationship.
  'required context' => new ctools_context_required(t('Server info'), 'server_info'),
  // Context builder function.
  'context' => 'example_relationship_plugin',
  // Settings form. We will provide a property from
  // server_info context we want to present as string context.
  'edit form' => 'example_relationship_plugin_settings_form',
  // Default values for settings form.
  'defaults' => [
    'server_info_property' => 'HTTP_HOST',
  ],
];

/**
 * Return a new context based on an existing context.
 */
function example_relationship_plugin($context = NULL, $conf) {
  $string_context = NULL;

  // If empty it wants a generic, unfilled context, which is just NULL.
  if (empty($context->data)) {
    $string_context = ctools_context_create_empty('string', NULL);
  }
  else {
    if (!empty($conf['server_info_property'])) {
      // Create the new string context from server_info parent context.
      $string_context = ctools_context_create('string', $context->data[$conf['server_info_property']]);
    }
  }

  return $string_context;
}

/**
 * Settings form for the relationship.
 */
function example_relationship_plugin_settings_form($form, $form_state) {
  $conf = $form_state['conf'];
  $keys = array_keys($_SERVER);

  $form['server_info_property'] = [
    '#required' => TRUE,
    '#type' => 'select',
    '#title' => t('"Server info" property'),
    '#options' => array_combine($keys, $keys),
    '#default_value' => $conf['server_info_property'],
  ];

  return $form;
}

/**
 * Configuration form submit.
 */
function example_relationship_plugin_settings_form_submit($form, &$form_state) {
  foreach ($form_state['plugin']['defaults'] as $key => $default) {
    $form_state['conf'][$key] = $form_state['values'][$key];
  }
}
Module structure:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |  |  |__ example_module.example_access_plugin.inc
  |  |__ contexts
  |  |  |__ example_module.example_context_plugin.inc
  |  |  |__ example_module.example_context_plugin.node_status.inc
  |  |__ arguments
  |  |  |__ example_module.example_argument_plugin.inc
  |  |__ relationships
  |     |__ example_module.example_relationship_plugin.inc
  |__ example_module.info
  |__ example_module.module
2. Clear cache. After that you will see a new relationship on panel settings page under contexts tab (please note that you need to set up server_info context first before be able to set up a relationship based on server_info):

New relationship is available for this panel

3. Set up newly created relationship:

Choose a value from server_info context you want to represent as a string context

4. At this point you will see additional context on a panel - "String from 'Server info' context".

A string context available from server_info context

5. It means you can use, for example, %string_from_server_info:uppercase_words_html_safe token as a substitution for a page title. Let's try to set up title as "%server_info:HTTP_HOST : %string_from_server_info:uppercase_words_html_safe" and see what will happen:
Set up page title from contexts values

My host is "drpual.loc" so page title looks like "drupal.loc : Drupal.loc" where first part (before ":") is a value grabbed from server_info context and second part is a value from string_from_server_info context in "uppercase_words_html_safe" format.
Page title consists of context and relationship values

The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:


Feb 12 2017
Feb 12
This time we will consider an argument plugin. Arguments are pretty similar to contexts. Actually arguments are context objects loaded from url. By default ctools provides a full set of needed arguments such as "Node: ID", "User: ID", "User: name" etc. But what if you've created a custom context? You might need to create a custom argument for your context (if you want to use your context as an argument of course). I advise you to read previous articles from "Ctools custom plugin" series such as "Ctools: custom access plugin" and "Ctools: custom context plugin". It's also required to read "Ctools: custom content type plugin" before reading this post because there I've described how to create a module integrated with ctools API which can contain ctools plugins.

For this tutorial we need a custom context (because there is no sense to copy paste default ctools argument plugins for creating default ctools context plugins - let's be creative) so I've created one: node_status. It's pretty simple and similar to default 'node' context by it provides extra node properties: 'status' and 'promote' (default node context doesn't provide them). It behaves similar to 'node' context. I mean it has a 'nid' option in settings form. Once you've entered a node id you will be able to use %argument_name:status and %argument_name:promote values. Context definition looks like:
<?php

/**
 * @file
 *
 * Plugin to provide a node_status context.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('Node status'),
  // Plugin description.
  'description' => t('Node status.'),
  // A function that will return context.
  'context' => 'example_context_node_status_plugin',
  // Context keyword to use for
  // substitution in titles.
  'keyword' => 'node_status',
  // Context machine name.
  'context name' => 'node_status',
  // Settings form callback.
  'edit form' => 'example_context_node_status_plugin_settings_form',
  // Default values for settings form.
  'defaults' => [
    'nid' => 1,
  ],
  // Array of available context values.
  'convert list' => [
    'status' => t('Node status'),
    'promote' => t('Node promote'),
  ],
  // A function gets data from a given
  // context and returns values defined
  // in 'convert list' property.
  'convert' => 'example_context_node_status_plugin_convert',
];

/**
 * Context callback.
 */
function example_context_node_status_plugin($empty, $data = NULL, $conf = FALSE) {
  // Create context object.
  $context = new ctools_context('node_status');

  // This property should contain file name where
  // plugin definition is placed.
  $context->plugin = 'example_module.example_context_plugin.node_status';

  if (empty($empty)) {
    // Define context data.
    // Variable $data can be an array or an object.
    // It depends on how this context is created.
    // If it's created by putting context directly to
    // a panel page then $data - array containing
    // settings from form.
    if (is_array($data) && !empty($data['nid'])) {
      $node = node_load($data['nid']);
    }

    // If context is created by
    // an argument then $data - object.
    if (is_object($data)) {
      $node = $data;
    }

    if (!empty($node)) {
      $context->data = $node;
      $context->title = $node->title;

      // Specify argument value - node id.
      $context->argument = $node->nid;
    }
  }

  return $context;
}

/**
 * Returns property value by property type.
 */
function example_context_node_status_plugin_convert($context, $type) {
  $result = '';

  // Return node property (status or promote).
  if (!empty($context->data)) {
    $result = $context->data->{$type};
  }

  return $result;
}

/**
 * Settings form for cookies context.
 */
function example_context_node_status_plugin_settings_form($form, &$form_state) {
  // Node id option.
  $form['nid'] = [
    '#type' => 'textfield',
    '#title' => t('Node nid'),
    '#default_value' => $form_state['conf']['nid'],
    '#element_validate' => [
      'element_validate_integer_positive',
    ],
  ];

  return $form;
}

/**
 * Settings form submit.
 */
function example_context_node_status_plugin_settings_form_submit($form, &$form_state) {
  // Save submitted value.
  $form_state['conf']['nid'] = $form_state['values']['nid'];
}
I won't describe what goes here because we've already discussed custom context theme last time. Just put this code into example_module/plugins/contexts/example_module.example_context_plugin.node_status.inc file. Make sure that your module file structure now looks like this one:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |  |  |__ example_module.example_access_plugin.inc
  |  |__ contexts
  |     |__ example_module.example_context_plugin.inc
  |     |__ example_module.example_context_plugin.node_status.inc
  |__ example_module.info
  |__ example_module.module
Ok, finally we've got a context which we want to use as argument. For this tutorial I've created a custom page with a "node-view-page/%nid" url.

Page with required argument

Let's move on. Now we will create an argument plugin.

1. Create file example_module/plugins/arguments/example_module.example_argument_plugin.inc:

<?php

/**
 * @file
 *
 * Plugin to provide an argument handler for a node_status context.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('Node status argument'),
  // Plugin description.
  'description' => t('Creates a node status context from a node ID argument.'),
  // Keyword.
  'keyword' => 'node_status_argument',
  // Context builder function.
  'context' => 'example_argument_plugin',
];

/**
 * Discover if this argument gives us the node we crave.
 */
function example_argument_plugin($arg = NULL, $conf = NULL, $empty = FALSE) {
  $context = FALSE;

  // If empyt it wants a generic, unfilled context.
  if ($empty) {
    $context = ctools_context_create_empty('example_module.example_context_plugin.node_status');
  }
  else {
    // We can accept either a node object or a pure nid.
    if (is_object($arg)) {
      $context = ctools_context_create('example_module.example_context_plugin.node_status', $arg);
    }
    elseif (is_numeric($arg)) {
      $node = node_load($arg);

      if (!empty($node)) {
        $context = ctools_context_create('example_module.example_context_plugin.node_status', $node);
      }
    }
  }

  return $context;
}
2. Clear cache and navigate to panel page settings: arguments. Assign "Node status argument" to %nid.

Assigned argument

3. Navigate to panel page settings: contexts. You will see that argument provides properties from defined context.
Context loaded from an argument

4. Now you can use context properties for example for page title:
Context properties are used in a page title

Now if you open node-view-page/1, for example, you will see:
Custom argument plugin in action

If you unpublish a node then title will be "Status: 0 Promoted: 1". That's all.

The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:

Feb 11 2017
Feb 11
In previous post we created an access ctools plugin which can be used as a selection or visibility rule in panels. It's time to learn how to create another important custom plugin - a context. It provides additional information for a panel page. For example if you've put a node context to the page you will be able to use node properties as substitutions for a page title. Moreover you will be able to put node fields as panes to a page. By default ctools module provides useful contexts (node, user, taxonomy_term, entity etc) but you can define your own. Please, read first post of "Ctools custom plugins" series before continue reading this. There we've created a module integrated with ctools.

In this tutorial we will create a context plugin which will provide information about a server from superglobal php variable $_SERVER. As you remember for now we have an example_module with next file structure:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |     |__ example_module.example_access_plugin.inc
  |__ example_module.info
  |__ example_module.module
To create a context plugin we need to create contexts directory and put there a file with plugin definition. Let's start:

1. Create example_module/plugins/contexts/example_module.example_context_plugin.inc file:

<?php

/**
 * @file
 *
 * Plugin to provide a server_info context.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('Server info'),
  // Plugin description.
  'description' => t('Available information about a server.'),
  // A function that will return context.
  'context' => 'example_context_plugin',
  // Settings form callback.
  'edit form' => 'example_context_plugin_settings_form',
  // Default values for settings form.
  'defaults' => [
    'decode_request_uri' => FALSE,
  ],
  // Context keyword to use for
  // substitution in titles.
  'keyword' => 'server_info',
  // Context machine name.
  'context name' => 'server_info',
  // A function returns a list of
  // available values from a context.
  // By the way it can be just an array.
  'convert list' => 'example_context_plugin_get_convert_list',
  // A function gets data from a given
  // context and returns values defined
  // in 'convert list' property.
  'convert' => 'example_context_plugin_convert',
];

/**
 * Context callback.
 */
function example_context_plugin($empty, $data = NULL, $conf = FALSE) {
  // Create context object.
  $context = new ctools_context('server_info');

  // This property should contain file name where
  // plugin definition is placed.
  $context->plugin = 'example_module.example_context_plugin';

  if (empty($empty)) {
    // Define context data.
    $context->data = $_SERVER;

    // Decode property if selected.
    // Just for demonstration how to use
    // plugin settings.
    if (!empty($conf) && !empty($data['decode_request_uri'])) {
      $context->data['REQUEST_URI'] = urldecode($context->data['REQUEST_URI']);
    }
  }

  return $context;
}

/**
 * Returns available properties list.
 */
function example_context_plugin_get_convert_list() {
  $list = [];

  // Get all $_SERVER properties and return them.
  foreach ($_SERVER as $property_name => $property_value) {
    $list[$property_name] = t('$_SERVER["!name"]', [
      '!name' => $property_name,
    ]);
  }

  return $list;
}

/**
 * Returns property value by property type.
 */
function example_context_plugin_convert($context, $type) {
  // Return $_SERVER property value.
  return $context->data[$type];
}

/**
 * Settings form for server_info context.
 */
function example_context_plugin_settings_form($form, &$form_state) {
  // Demo setting.
  $form['decode_request_uri'] = [
    '#type' => 'checkbox',
    '#title' => t('Decode $_SERVER["REQUEST_URI"] value'),
    '#default_value' => $form_state['conf']['decode_request_uri'],
  ];

  return $form;
}

/**
 * Settings form submit.
 */
function example_context_plugin_settings_form_submit($form, &$form_state) {
  // Save submitted value.
  $form_state['conf']['decode_request_uri'] = $form_state['values']['decode_request_uri'];
}
Updated module structure looks like:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |  |  |__ example_module.example_access_plugin.inc
  |  |__ contexts
  |     |__ example_module.example_context_plugin.inc
  |__ example_module.info
  |__ example_module.module
2. Clear cache and set up Server info context for a panel page:

Newly created context

Plugin settings form

Available context properties

Let's test context and set up page title as %server_info:REQUEST_URI. By the way it's a node/%nid page on screeshot so to test it we should open a node view page (but you can set up this context to any panel page you want):
Page title will contain value from $_SERVER['REQUESTED_URI'] variable

Request uri as a page title

The last thing we have to test is an option "Decode $_SERVER["REQUEST_URI"] value". When it's checked uri will be decoded. Check this checkbox and open node view page with a query parameter such as "?test=parameter to decode" and update page. Browser will encode spaces in url and it will look like "?test=string%20to%20decode". But page title will contain original (decoded) value.

The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:

Feb 05 2017
Feb 05
Last time we've learned how to create custom ctools content type plugin. In that post we've already created a module example_module where we defined the plugin. This time we will learn how to create custom ctools access plugin. This type of ctools plugins can be used as a selection rule for a panel variant or as a visibility rule for a panel pane. Please, read previous post before continue reading this. There is described how to create a module and integrate it with ctools.

Let's say we have a node/%node panel page where are placed "Powered by Drupal" and "Node being viewed" panes.

Node view panel page with "Powered by Drupal" and "Node being viewed" panes

And we want first panel pane be shown only if node title equals some specific string. For example "Test article". Well, for this case we have to create an access plugin and apply it to the panel pane. Let's start.
For now if you remember we have an example_module with next structure:
example_module
  |__ plugins
  |  |__ content_types
  |     |__ example_module.example_content_type_plugin.inc
  |__ example_module.info
  |__ example_module.module
1. Create file example_module/plugins/access/example_module.example_access_plugin.inc:
<?php

/**
 * @file
 * File with access plugin definition.
 */

/**
 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
 */
$plugin = [
  // Plugin title.
  'title' => t('Example access rule'),
  // Plugin description.
  'description' => t('Example access rule'),
  // Access rule callback. This function has to return
  // TRUE of FALSE.
  'callback' => 'example_access_plugin',
  // A required context for this pane. If there is no required
  // context on a panel page than you will not be able to apply
  // this access rule to a pane or page variant. In this example
  // we need a 'Node' context.
  'required context' => new ctools_context_required(t('Node'), 'node'),
  // Settings form constructor callback. If you need custom submit/validate
  // callback you can define it by 'settings form submit' and
  // 'settings form validate' properties.
  'settings form' => 'example_access_plugin_settings_form',
  // Default values for edit form inputs.
  'default' => [
    'example_option' => 'example_value',
  ],
  // Summary callback. Returns plugin summary text which is visible
  // in drop-down setting menu of a pane.
  'summary' => 'example_access_plugin_summary',
];

/**
 * Settings form a pane.
 */
function example_access_plugin_settings_form(array $form, array $form_state, $conf) {
  // Please note that all custom form elements
  // should be placed in 'settings' array.
  $form['settings']['example_option'] = [
    '#type' => 'textfield',
    '#title' => t('Example text option'),
    '#default_value' => $conf['example_option'],
    '#required' => TRUE,
  ];

  return $form;
}

/**
 * Access rule callback.
 */
function example_access_plugin($conf, $context) {
  $access = FALSE;

  // Variable $context contains a context grabbed by a panel page.
  // In this case it contains a node object in a 'data' property.
  if (!empty($context->data)) {
    $node_from_context = $context->data;

    // Access will be granted only if node title equals a string
    // set up in settings form.
    $access = $node_from_context->title == $conf['example_option'];
  }

  return $access;
}

/**
 * Summary callback.
 */
function example_access_plugin_summary($conf, $context) {
  return t('Example access plugin');
}
Now our module structure looks like:
example_module
  |__ plugins
  |  |__ content_types
  |  |  |__ example_module.example_content_type_plugin.inc
  |  |__ access
  |     |__ example_module.example_access_plugin.inc
  |__ example_module.info
  |__ example_module.module
2. Clear cache. Apply newly created access rule for the 'Node being viewed' pane in node view panel page:

Set up visibility rule for a pane

Select 'Example access rule'

Set up string to match with a node title

It's time to test our access plugin. Create a node with title "Test" and look at it's page. There will be no "Powered by Drupal" widget because node title doesn't equal "Test article":

Node view page without "Powered by Drupal" widget

And now edit node and change title to "Test article". See what will happen:
Node view page with "Powered by Drupal" widget

Access plugin works correctly.

P.S.
As you could notice there is a little difference between content_type and access plugin definition:

  • 'edit form' VS 'settings form' properties.
  • 'defaults' VS 'default' properties.
  • you have to define custom inputs for access plugin settings form inside 'settings' array. At the same time there is no so strict rule for defining settings inputs for content type plugin.
  • in a form callback for content type plugins configuration is available in $form_state variable, but for access plugins it isn't there. It available in an extra variable $conf.


The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:

Feb 04 2017
Feb 04
Ctools content types are an alternative to standard Drupal blocks. They are more comfortable and powerfull than blocks. Ctools content type plugins also known as panel panes. In this post you will learn how to create a configurable ctools content pane plugin.

For example we will create a custom content type plugin that will render a user name and user email. Let's start.

1. Create a module which will contain all defined plugins. File example_module/example_module.info:

name = Example module
description = Provides ctools content type plugin
core = 7.x

; Since we're using ctools api for creating
; ctools plugins we have to define this
; dependency.
dependencies[] = ctools
File example_module/example_module.module:
<?php

/**
 * Implements hook_ctools_plugin_directory().
 *
 * Integrate our module with Ctools. Tell where
 * Ctools have to search plugins. Usually a place
 * where developers store all defined plugins is
 * "module_name/plugins" directory. Variable
 * $plugin contains a name of a plugin type. It can be
 * content_types, context, argument, cache, access. So all
 * of listed plugin types should be located in corresponding
 * subdirectories (plugins/content_types if you create a content
 * type plugin).
 */
function example_module_ctools_plugin_directory($module, $plugin) {
  if ($module == 'ctools' && !empty($plugin)) {
    return "plugins/$plugin";
  }
}
2. Create a file which will contain plugin definition. File example_module/plugins/content_types/example_module.example_content_type_plugin.inc:
<?php

/**
 * @file
 * File with plugin definition.
 */

$plugin = [
  // Pane title.
  'title' => t('Example pane'),
  // Pane description. 
  'description' => t('Example pane'),
  // Tell ctools that it's not a child of another pane.
  'single' => TRUE,
  // You can categorize all defined panes by
  // defining a category name for a pane.
  'category' => [t('Example panes')],
  // A machine name of your pane.
  'content_types' => ['example_content_type_plugin'],
  // Function that will render markup of this pane.
  'render callback' => 'example_content_type_plugin_render',
  // A required context for this pane. If there is no required
  // context on a panel page than you will not be able to add
  // this pane to the panel page.
  'required context' => new ctools_context_required(t('User'), 'user'),
  // Edit form constructor callback.
  'edit form' => 'example_content_type_plugin_edit_form',
  // Default values for edit form inputs.
  'defaults' => [
    'example_option' => 'example_value',
  ],
];
A file name is just a combination of a module name and plugin name joined by a dot. But actually you can name it as you want. Please note that you can omit 'required context' property if you want to be able to add this pane to all existing panel pages. If you want to add your pane only to pages that have a context just use required context like described above. If you want to add your pane to all pages (but in some cases you need a context) just define it as optional:
'required context' => new ctools_context_optional(t('User'), 'user'),
Let's define plugin configuration form:
/**
 * Configuration form for a pane.
 */
function example_content_type_plugin_edit_form($form, $form_state) {
  $form['example_option'] = [
    '#type' => 'textfield',
    '#title' => t('Example text option'),
    '#default_value' => $form_state['conf']['example_option'],
    '#required' => TRUE,
  ];

  return $form;
}

/**
 * Configuration form submit.
 */
function example_content_type_plugin_edit_form_submit($form, &$form_state) {
  foreach ($form_state['plugin']['defaults'] as $key => $default) {
    $form_state['conf'][$key] = $form_state['values'][$key];
  }
}
The last thing we shoud define is render callback:
/**
 * Render callback.
 */
function example_content_type_plugin_render($subtype, $conf, $args, $context) {
  $block = new stdClass();

  // Variable $context contains a context grabbed by a panel page.
  // In this case we will put this pane on user/%uid page which
  // has a user context.
  if (!empty($context->data)) {
    $user_from_context = $context->data;

    // Just output user's name and email.
    $block->content = 'User name: ' . $user_from_context->name . '</br>User mail: ' . $user_from_context->mail . '</br>Example option: ' . $conf['example_option'];
  }

  return $block;
}
3. Clear cache. Edit user view panel page and put this pane into some region:

Example ctools content type pane

Enter settings for a pane:

Configuration form

Open user/1 page and here's a result:

Rendered example ctools content type pane

The above is true for:

  • Ctools: 7.x-1.12
  • Panels: 7.x-3.8

Key notes:

Jan 29 2017
Jan 29
Drush stands for "Drupal shell" which means a powerful tool for managing Drupal installation from command line interface. Drush provides a lot of useful commands for dealing with a cache, modules, cron, database etc. But some of contrib modules also provide some extra drush commands for specific functionality (like features module comes with commands for managing features). Here's a bunch of a useful drush commands which I use every day.


Cache

# Clear all caches.
drush cc all

# Clear menu cache.
drush cc menu

# Select cache bin for clearing.
drush cc

Modules

# Download module by name. You can grab module
# name from module's page on drupal.org.
drush dl module_name

# Download module by name.
# into sites/default/modules/custom
# directory.
drush dl module_name --destination=sites/default/modules/custom

# Enable module by name.
drush en module_name

# Disable module by name.
drush dis module_name

# Uninstall module by name.
# This commands will call hook_name_uninstall().
drush pmu module_name

# Get list of available modules
# in your Drupal installation.
drush pml

# Check if module module_name
# is available in your Drupal
# installation.
drush pml | grep module_name

User

# Get an one-time login link
# for user by his/her uid.
drush uli uid

# Get an one-time login link
# for user by his/her email.
drush uli [email protected]

# Block user by his/her username.
drush ublk test_user

# Unblock user by his/her username.
drush uublk test_user

# Change password for user by his/her name.
drush upwd --password="password" user_name

Pending database updates

# Run all available database updates
# implemented in hook_update_N() in
# *.install files. 
drush updb

Cron

# Run cron.php. It will call all hook_cron()
# defined in modules and process cron queues.
drush cron

Variables

# Get variable value by name.
drush vget variable_name

# Set variable value by name.
drush vset variable_name "Test value"

# Delete variable by name.
drush vdel variable_name

Registry

# Install registry_rebuild drush extension.
drush dl registry_rebuild

# Rebuild Drupal registry. Usefull when some of
# modules was moved into another folder.
drush rr

Features

# Get list of available features.
drush fl

# Get list of overridden features. Please
# note if you have multilingual site and
# site active language is not english then
# you have to replace word "Overridden" by
# it's translation.
drush fl | grep Overridden

# Show diff of overridden feature by name.
drush fd feature_name

# Update feature by name. Put changes from
# database into a code.
drush fu feature_name

# Revert feature by name. Restore database
# state from a code.
drush fr feature_name

Sql interface

# Show information about current database connection
# that defined in settings.php file.
drush sql-connect

# Login into sql console interface.
drush sqlc

# Execute a query.
drush sqlq "SELECT * from node;"

# Restore database dump from dump_name.sql file.
drush sqlc < dump_name.sql

# Save database dump into dump_name.sql.gz file.
drush sql-dump --gzip --result-file=dump_name.sql.gz

Other

# Run custom php code.
drush eval "variable_set('site_name', 'Test site name');"

# Get all dblog messages in realtime (tail style).
# Useful when you want to debug a remote Drupal
# installation where you have ssh access.
drush ws --tail

Key notes:

Jan 24 2017
Jan 24
Drupal provides a powerfull API for building different kind of forms. One of the most cool thing in this API, I think, it's a #states feature. It allows developers to create form elements that can change their state depending on some conditions. For instance you can create a text input which will be visible only if checkbox is checked. Or even make multiple conditions for an element.

Using this feature you can create OR, AND and XOR conditions. Let's consider an example. We have a form with two checkboxes and one text input:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
  ];

  return $form;
}
Let's say we want to show text_input_1 only if check_box_1 is checked. This example shows a SIMPLE CONDITION:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    // #states array describes element states depends on conditions.
    '#states' => [
      // State: this input will be visible only if checkbox_1 is checked.
      'visible' => [
        // Condition: selector of a checkbox this input depends on.
        'input[name="checkbox_1"]' => [
          // Condition value.
          'checked' => TRUE,
        ],
      ],
    ],
  ];

  return $form;
}
A complete list of existing states and condition values you can find here. Next example is AND CONDITION: let's show text_input_1 only if checkbox_1 AND checkbox_2 are checked:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    '#states' => [
      'visible' => [
        // Just provide a few conditions in one state array.
        'input[name="checkbox_1"]' => [
          'checked' => TRUE,
        ],
        'input[name="checkbox_2"]' => [
          'checked' => TRUE,
        ],
      ],
    ],
  ];

  return $form;
}
OR CONDITION. In this case we will show text_input_1 only if checkbox_1 OR checkbox_2 is checked:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    '#states' => [
      'visible' => [
        // OR condition: provide a few conditions each wrapped into a
        // state array.
        [
          'input[name="checkbox_1"]' => [
            'checked' => TRUE,
          ],
        ],
        [
          'input[name="checkbox_2"]' => [
            'checked' => TRUE,
          ],
        ]
      ],
    ],
  ];

  return $form;
}
And a final example: XOR CONDITION. Show text_input_1 only if checkbox_1 OR checkbox_2 is checked but not both:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    '#states' => [
      'visible' => [
        [
          'input[name="checkbox_1"]' => [
            'checked' => TRUE,
          ],
        ],
        'xor',
        [
          'input[name="checkbox_2"]' => [
            'checked' => TRUE,
          ],
        ]
      ],
    ],
  ];

  return $form;
}
Of course you can combine conditions and make them really complex.

Be careful with 'required' state


If you make field required depends on a condition you have to a validate this case on a back-end side manually:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  // If checkbox is checked then text input
  // is required (with a red star in title).
  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    '#states' => [
      'required' => [
        'input[name="checkbox_1"]' => [
          'checked' => TRUE,
        ],
      ],
    ],
  ];

  $form['actions'] = [
    'submit' => [
      '#type' => 'submit',
      '#value' => t('Submit'),
    ],
  ];

  return $form;
}

/**
 * Form validate callback.
 */
function module_form_validate($form, $form_state) {
  // if checkbox is checked and text input is empty then show validation
  // fail message.
  if (!empty($form_state['values']['checkbox_1']) &&
    empty($form_state['values']['text_input_1'])
  ) {
    form_error($form['text_input_1'], t('@name field is required.', [
      '@name' => $form['text_input_1']['#title'],
    ]));
  }
}

Key notes:

Jan 21 2017
Jan 21
You have to cache results of heavy functions, sql queries and markup if it's possible because it reduces load on a system in general. If you have a block which renders a lot of items shiped from a database - cache it. If you have a function that performs heavy calculations and this function is called many times during a script execution - cache a result of this function.

Static cache


It's recommended to implement static cache for functions that are called many times during a script execution. It's easy to do: just store a function result first time when function is called and skip heavy calculation next time and return saved value from a static variable.
<?php

/**
 * Some function performs heavy calculations.
 *
 * @return mixed
 *   A result.
 */
function module_heavy_function() {
  // Function drupal_static() returns
  // a static variable by name. Usually
  // variable name equals a function
  // name.
  $result = &drupal_static(__FUNCTION__);

  if (!isset($result)) {
    // Perform heavy calculation here.
    ...

    // After that assign result to a
    // static variable.
    $result = $result_of_heavy_calculations;
  }

  return $result;
}

Database cache


If you want to save a function result for some period of time you have to implement database cache. You can specify a cache lifetime (when it will be expired) or invalidate cache manually when do you need.
<?php

/**
 * Some function performs heavy calculations.
 *
 * @return mixed
 *   A result.
 */
function module_heavy_function() {
  $cache_name = 'module:module_heavy_function';
  $result_of_heavy_calculations = NULL;

  // Cache miss: If there is no cached data or
  // cache have expired or you invalidated cache
  // manually do heavy calculations and save
  // a result into a cache.
  if (!cache_get($cache_name)) {
    // Perform heavy calculation here.
    ...

    // After that set cache. Data will be saved
    // into cache bin 'cache' for one hour.
    cache_set($cache_name, $result_of_heavy_calculations, 'cache', time() + 60 * 60);
  }
  // Cache hit: load cached data from a database
  // and return it.
  else {
    $cache = cache_get($cache_name);
    $result_of_heavy_calculations = $cache->data;
  }

  return $result_of_heavy_calculations;
}
Manual cache invalidating:
<?php

// You can invalidate all cached data
// that matches a wildcard cid 'module:*';
cache_clear_all('module:', 'cache', TRUE);

// Or you can invalidate cache by specific
// cid.
cache_clear_all('module:content', 'cache');
You can also cache a data of a renderable array by specifying #cache key.
<?php

$renderable_array['content'] = [
  '#cache' => [
    'cid' => 'module:content',
    'bin' => 'cache',
    'expire' => time() + 60 * 60,
  ],
  '#markup' => 'This markup will be cached',
];

Key notes:

Jan 15 2017
Jan 15
I've already written about how to use native Drupal ajax mechanism on front-end side in this post. There I've described how to send requests immediately or by clicking (or by any other event) on any DOM element. It works when javascript is enabled in user's browser. But how to make your menu callback work both with ajax and GET (when js is disabled) requests?

1. Menu router. Have to contain additional argument %ctools_js:
<?php

/**
 * Implements hook_menu().
 *
 * Just for example we will render a node form.
 */
function module_menu() {
  $items = [];

  $items['some/%node/%ctools_js/path'] = [
    'title' => 'Test router',
    'page callback' => 'module_test_router_callback',
    'page arguments' => [1, 2],
    // For ajax requests first.
    'delivery callback' => 'ajax_deliver',
    'type' => MENU_CALLBACK,
  ];

  return $items;
}
2. Menu callback:
<?php

/**
 * Menu callback for test router.
 *
 * Just for example we will render a node form
 * and if it's ajax request then replace body element
 * with a form. Otherwise reload page and show
 * node form.
 */
function module_test_router_callback($node, $ajax) {
  // Don't forget to include node specific file
  // for an example.
  module_load_include('inc', 'node', 'node.pages');
  $form = drupal_get_form($node->type . '_node_form', $node);

  // If it's ajax request then $ajax will be equals 1.
  if ($ajax) {
    // Set up command for replacing body content
    // with a rendered form.
    $commands[] = ajax_command_html('body', render($form));

    return [
      '#type' => 'ajax',
      '#commands' => $commands,
    ];
  }
  else {
    // If it's not ajax request just deliver
    // html markup.
    drupal_deliver_html_page($form);
  }
}
3. Link with a use-ajax class:
<?php

// Make sure you've added ajax library
// before printing a link.
drupal_add_library('system', 'drupal.ajax');

// Main point here is 'use-js' class for a link.
// Also take a look at 'nojs' argument. If js is
// enabled in a browser then this string will
// be replaced with 'ajax' and link will be look
// like 'some/NID/ajax/path'.
print l(t('Get node form'), 'some/NID/nojs/path', [
  'attributes' => [
    'class' => [
      'use-ajax',
    ],
  ],
]);
So what's happening here?

  • First. Clicking by the link with 'some/NID/nojs/path' href and 'use-ajax' class. If js is enabled in user's browser then 'nojs' substring will be replaced by 'ajax'.
  • Second. You need to find out what is %ctools_js. Ctools module provides a function ctools_js_load() that determines if it's ajax request or not ('ajax' or 'nojs' string passed as an argument to page callback).
  • Third. Page callback takes argument $ajax which equals 1 if ajax request and 0 otherwise (thanks to ctools_js_load() function). We're returning ajax commands for ajax requests and delivering render array with drupal_deliver_html_page() function otherwise. Since we've defined delivery callback for menu router as ajax_deliver we need to handle non ajax requests with drupal_deliver_html_page() function manually.

Key notes:

Jan 13 2017
Jan 13
Default entities (node, user, term etc) in Drupal 7 are instances of a stdClass class. It means you can't determine entity type directly. Let's say you have an object and you want to ensure that it's a node and only then do something node specific. Entity API doesn't provide such possibility out of the box.

When it can be helpful? Well, real life example: custom ctools context plugin which returns some value depends on an object loaded from current panel's url.
<?php

/**
 * API function for context determination.
 */
function module_get_context() {
  ...

  // Load object from current page (page manager).
  // Path could be node/%node, user/%user or term/%term.
  $current_menu_object = menu_get_object('pm_arg', 1);

  // Is it a node? If yes we need to do one operation.
  // If it's a user we need to do something else.
  $object = $current_menu_object->data;

  ...
}
To solve this problem you can use next functions:
<?php

/**
 * API function to detect if passed object is a node.
 *
 * @param object $object
 *   Object.
 *
 * @return bool
 *   TRUE if it's a node FALSE otherwise.
 */
function module_is_node($object) {
  return !empty($object->nid);
}

/**
 * API function to detect if passed object is a term.
 *
 * @param object $object
 *   Object.
 *
 * @return bool
 *   TRUE if it's a term FALSE otherwise.
 */
function module_is_term($object) {
  return !empty($object->tid);
}

/**
 * API function to detect if passed object is an user.
 *
 * @param object $object
 *   Object.
 *
 * @return bool
 *   TRUE if it's an user FALSE otherwise.
 */
function module_is_user($object) {
  return !empty($object->uid);
}
I usually put helper functions into separate module that I create for each project.
So having this functions now you can determine entity type and do some specific actions:
<?php

/**
 * API function for context determination.
 */
function module_get_context() {
  ...

  // Load object from current page (page manager).
  // Path could be node/%node, user/%user or term/%term.
  $current_menu_object = menu_get_object('pm_arg', 1);

  // Is it a node? If yes we need to do one operation.
  // If it's a user we need to do something else.
  $object = $current_menu_object->data;

  if (module_is_node($object)) {
    // Do node specific actions here.
  } elseif (module_is_term($object)) {
    // Do term specific actions here.
  }

  ...
}
Of course you can extend this function set for other entities like file, etc. Or even write one universal function for checking all entity types.

Key notes:

Jan 10 2017
Jan 10
Drupal 7 offers a mechanism to render entities which called view mode. For example you can set up how your entity will look as a full page or as a teaser. By default Drupal provides only two modes for node entity (default and teaser) and one default mode for taxonomy term and user entities. But you can easily add your custom view mode.

First option is setup display suite module. It's powerfull and provides a lot of features like a layout for entity that can be set up from admin UI. But if you need just to add a view mode to an entity and don't need any other functionality you can do it manually.

In this example we will add a new view mode to a node entity:

<?php

/**
 * Implements hook_entity_info_alter().
 */
function module_entity_info_alter(&$entity_info) {
  $entity_info['node']['view modes']['custom_view_mode'] = [
    'label' => t('Custom view mode title'),
    // Use default display settings from `default` view mode.
    // It means that all fields labels settings and fields
    // formatters settings will be exactly the same as in
    // `default` view mode.
    'custom settings' => FALSE,
  ];
}
Now clear the cache and you will get a new view mode.

Basically that's all. You can set up fields formatters, weights and etc. But you might want to assign a specific template for nodes per view mode. Well it's not hard to do. Just define a hook_preprocess_node in theme's template.php file:

<?php

/**
 * Implements theme_preprocess_node().
 */
function theme_preprocess_node(&$variables) {
  $variables['theme_hook_suggestions'][] = 'node__' . $variables['type'] . '__' . $variables['view_mode'];
}
Then define node--node-type--view-mode.tpl.php template in your theme folder and provide needed markup there. I suggest to get the default markup from node.tpl.php file and modify it. Clear the cache once again and you will get a rendered through defined template node.

Key notes:

Jan 08 2017
Jan 08
Throbber is an element with a message that shows us that something is running in background. Drupal renders this element each time when ajax request is running (for example when you submit form via ajax). Sometimes we need to disable throbber at all or at least remove/change message. That's how you can do this.

Disable throbber at all:
<?php

// Add 'progress' settings into '#ajax'
// element of a submit button. Define progress
// type as 'none'.
$form['actions']['submit']['#ajax']['progress'] => [
  'type' => 'none',
];
Remove/change throbber message:
<?php

// Add 'progress' settings into '#ajax'
// element of a submit button. Change progress
// message to NULL (delete message) or any other
// string (change message).
$form['actions']['submit']['#ajax']['progress'] => [
  'message' => NULL,
];
If you  send ajax requests as described in this post you should modify settings variable and add progress property. Disable throbber:
var $link = $('#link-element', context);

new Drupal.ajax($link.attr('id'), $link, {
  url: '/ajax/callback/path',
  event: 'click',
  progress: {
    type: 'none'
  }
});
Remove/change throbber message:
var $link = $('#link-element', context);

new Drupal.ajax($link.attr('id'), $link, {
  url: '/ajax/callback/path',
  event: 'click',
  progress: {
    type: 'throbber',
    // Change progress message to NULL
    // (delete message) or any other
    // string (change message).
    message: null
  }
});

Key notes:

Jan 08 2017
Jan 08
If you are using jQuery.ajax() or jQuery.post() for background requests you should have a look at native Drupal ajax implementation. Drupal provides a js object Drupal.ajax that performs all needed actions for you (like processing ajax commands that came from back-end side). All that you need to do is to create instance of this object and configure it.

Send ajax request by event triggering of a DOM object:
// By clicking on this link we will
// send an ajax request.
var $link = $('#link-element', context);

new Drupal.ajax($link.attr('id'), $link, {
  url: '/ajax/callback/path',
  event: 'click',
  // Pass some data to back-end side
  // if you need.
  submit: {
    some_data: 'some_data_value'
  }
});
Send ajax request immediately:
// If we want to send request immediately
// we can omit first two parameters.
var ajax = new Drupal.ajax(false, false, {
  url: '/ajax/callback/path'
});

// To send request call this function.
// You shouldn't think how to process
// response. Be sure that all commands
// will be processed correctly.
ajax.eventResponse(ajax, {});

Key notes:

Jan 07 2017
Jan 07
Drupal 7 offers a lot of default ajax commands which are the part of Drupal Ajax Framework. Furthermore, ctools module extends a set of ajax commands and provides own commands. But what if you need some specific command? First, you need to write a wrapper function which returns ajax command array. Should looks like this one:
<?php

/**
 * Ajax command for rendering canvas data.
 *
 * @param array $data
 *   Settings array.
 *
 * @param mixed $some_other_parameter
 *   Some other parameter.
 *
 * @return array
 *   Ajax command array.
 */
function specific_ajax_command(array $data, $some_other_parameter) {
  return [
    // Name of a js function.
    'command' => 'specificAjaxCommand',

    // Data will be available in js function.
    'specific_data' => $data,

    // You can define as many parameters
    // for your command as you want.
    'some_other_parameter' => $some_other_parameter,
  ];
}
Second step is js command function implementation :
(function ($) {
  'use strict';

  // Ensure that Drupal.ajax object is set up.
  if (Drupal.ajax) {
    Drupal.ajax.prototype.commands.specificAjaxCommand = function (ajax, response, status) {
      // Check response status.
      if (status == 'success') {
        // Get data from back-end side.
        var specificData = response.specific_data,
            someOtherParameter = response.some_other_parameter;
        
        // Perform specific actions dependent on a data.
      }
    };
  }

}(jQuery));
Third, ensure that js file contains code above is added to a page before returning ajax command. When it's done you are ready to go with a new ajax command:
<?php

/**
 * Ajax page callback.
 *
 * @return array
 *   Array contains Drupal ajax commands.
 */
function module_ajax_page_callback() {
  $some_data = [];
  $some_other_parameter = 'Some other parameter value';
  $commands[] = specific_ajax_command($some_data, $some_other_parameter);
 
  return [
    '#type' => 'ajax',
    '#commands' => $commands,
  ];
}

Key notes:

Jan 07 2017
Jan 07
If you want to submit node form (or any form) via ajax request you need to follow next steps:
  1. Alter needed form: add container for validation messages and ajax callback for submit action.
  2. Implement ajax callback that returns ajax commands.
<?php

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function module_form_node_form_alter(&$form, &$form_state, $form_id) {
  // Add container for validation messages.
  $form['#prefix'] = '
';

  // Add ajax callback for submit action.
  $form['actions']['submit']['#ajax'] = [
    'callback' => 'module_node_form_ajax_submit',
  ];

  // Don't forget to include node.pages.inc file. Without it Drupal will not be
  // able to find and call node_form_validate() function.
  form_load_include($form_state, 'inc', 'node', 'node.pages');
}

/**
 * Form ajax submit callback.
 *
 * @return array
 *   Array contains Drupal ajax commands.
 */
function module_node_form_ajax_submit() {
  // Remove all status messages from $_SESSION['messages'] variable
  // because we want to render only error messages.
  drupal_get_messages('status');

  // Check form for possible validation messages.
  if (form_get_errors()) {
    // Validation failure: return rendered validation messages.
    $commands[] = ajax_command_html('#validation-messages', [theme('status_messages')]);
  }
  else {
    // Validation success: do something, for instance redirect user.
    ctools_include('ajax');
    $commands[] = ctools_ajax_command_redirect('some/page');
  }

  return [
    '#type' => 'ajax',
    '#commands' => $commands,
  ];
}

Key notes:

Jan 07 2017
Jan 07
If you have redirect functionality implemented with drupal_goto() function on hook_init() probably you aren't able to run drush commands because they crashe with a message:
Drush command terminated abnormally due to an unrecoverable error.  [error]
It happens because drush bootstraps Drupal application but can't perform redirect. To avoid this you need to check whether script runs from cli or not:
<?php

/**
 * Implements hook_init().
 */
function module_init() {
  if (!drupal_is_cli()) {
    drupal_goto('some/path');
  }
}

Key notes:

Jan 07 2017
Jan 07
When you build sql select queries using Drupal database API often you need to debug them. I mean to copy sql string, paste into sql editor, run it and see a result. If you cast query object to string you simply get a sql query with placeholders instead of their values. To get a string with replaced placeholders by values you can use this function:

You can also use devel's dpq() function but I prefer have a small helper module with needed functions only. Usage example:


Key notes:

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