Jul 26 2018
Jul 26
July 26th, 2018

Intro

In this post, I’m going to run through how I set up visual regression testing on sites. Visual regression testing is essentially the act of taking a screenshot of a web page (whether the whole page or just a specific element) and comparing that against an existing screenshot of the same page to see if there are any differences.

There’s nothing worse than adding a new component, tweaking styles, or pushing a config update, only to have the client tell you two months later that some other part of the site is now broken, and you discover it’s because of the change that you pushed… now it’s been two months, and reverting that change has significant implications.

That’s the worst. Literally the worst.

All kinds of testing can help improve the stability and integrity of a site. There’s Functional, Unit, Integration, Stress, Performance, Usability, and Regression, just to name a few. What’s most important to you will change depending on the project requirements, but in my experience, Functional and Regression are the most common, and in my opinion are a good baseline if you don’t have the capacity to write all the tests.

If you’re reading this, you probably fall into one of two categories:

  1. You’re already familiar with Visual Regression testing, and just want to know how to do it
  2. You’re just trying to get info on why Visual Regression testing is important, and how it can help your project.

In either case, it makes the most sense to dive right in, so let’s do it.

Tools

I’m going to be using WebdriverIO to do the heavy lifting. According to the website:

WebdriverIO is an open source testing utility for nodejs. It makes it possible to write super easy selenium tests with Javascript in your favorite BDD or TDD test framework.

It basically sends requests to a Selenium server via the WebDriver Protocol and handles its response. These requests are wrapped in useful commands and can be used to test several aspects of your site in an automated way.

I’m also going to run my tests on Browserstack so that I can test IE/Edge without having to install a VM or anything like that on my mac.

Process

Let’s get everything setup. I’m going to start with a Drupal 8 site that I have running locally. I’ve already installed that, and a custom theme with Pattern Lab integration based on Emulsify.

We’re going to install the visual regression tools with npm.

If you already have a project running that uses npm, you can skip this step. But, since this is a brand new project, I don’t have anything using npm, so I’ll create an initial package.json file using npm init.

  • npm init -y
    • Update the name, description, etc. and remove anything you don’t need.
    • My updated file looks like this:
{ "name": "visreg", "version": "1.0.0", "description": "Website with visual regression testing", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" } }   "name":"visreg",  "version":"1.0.0",  "description":"Website with visual regression testing",  "scripts":{    "test":"echo \"Error: no test specified\" && exit 1"

Now, we’ll install the npm packages we’ll use for visual regression testing.

  • npm install --save-dev webdriverio chai wdio-mocha-framework wdio-browserstack-service wdio-visual-regression-service node-notifier
    • This will install:
      • WebdriverIO: The main tool we’ll use
      • Chai syntax support: “Chai is an assertion library, similar to Node’s built-in assert. It makes testing much easier by giving you lots of assertions you can run against your code.”
      • Mocha syntax support “Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun.”
      • The Browserstack wdio package So that we can run our tests against Browserstack, instead of locally (where browser/OS differences across developers can cause false-negative failures)
      • Visual regression service This is what provides the screenshot capturing and comparison functionality
      • Node notifier This is totally optional but supports native notifications for Mac, Linux, and Windows. We’ll use these to be notified when a test fails.

Now that all of the tools are in place, we need to configure our visual regression preferences.

You can run the configuration wizard by typing ./node_modules/webdriverio/bin/wdio, but I’ve created a git repository with not only the webdriver config file but an entire set of files that scaffold a complete project. You can get them here.

Follow the instructions in the README of that repo to install them in your project.

These files will get you set up with a fairly sophisticated, but completely manageable visual regression testing configuration. There are some tweaks you’ll need to make to fit your project that are outlined in the README and the individual markdown files, but I’ll run through what each of the files does at a high level to acquaint you with each.

  • .gitignore
    • The lines in this file should be added to your existing .gitignore file. It’ll make sure your diffs and latest images are not committed to the repo, but allow your baselines to be committed so that everyone is comparing against the same baseline images.
  • VISREG-README.md
    • This is an example readme you can include to instruct other/future developers on how to run visual regression tests once you have it set up
  • package.json
    • This just has the example test scripts. One for running the full suite of tests, and one for running a quick test, handy for active development. Add these to your existing package.json
  • wdio.conf.js
    • This is the main configuration file for WebdriverIO and your visual regression tests.
    • You must update this file based on the documentation in wdio.conf.md
  • wdio.conf.quick.js
    • This is a file you can use to run a quick test (e.g. against a single browser instead of the full suite defined in the main config file). It’s useful when you’re doing something like refactoring an existing component, and/or want to make sure changes in one place don’t affect other sections of the site.
  • tests/config/globalHides.js
    • This file defines elements that should be hidden in ALL screenshots by default. Individual tests can use this, or define their own set of elements to hide. Update these to fit your actual needs.
  • tests/config/viewports.js
    • This file defines what viewports your tests should run against by default. Individual tests can use these, or define their own set of viewports to test against. Update these to the screen sizes you want to check.

Running the Test Suite

I’ll copy the example homepage test from the example-tests.md file into a new file /web/themes/custom/visual_regression_testing/components/_patterns/05-pages/home/home.test.js. (I’m putting it here because my wdio.conf.js file is looking for test files in the _patterns directory, and I like to keep test files next to the file they’re testing.)

The only thing you’ll need to update in this file is the relative path to the globalHides.js file. It should be relative from the current file. So, mine will be:

const visreg = require('../../../../../../../../tests/config/globalHides.js'); constvisreg=require('../../../../../../../../tests/config/globalHides.js');

With that done, I can simply run npm test and the tests will run on BrowserStack against the three OS/Browser configurations I’ve specified. While they’re running, we can head over to https://automate.browserstack.com/ we can see the tests being run against Chrome, Firefox, and IE 11.

Once tests are complete, we can view the screenshots in the /tests/screenshots directory. Right now, the baseline shots and the latest shots will be identical because we’ve only run the test once, and the first time you run a test, it creates the baseline from whatever it sees. Future tests will compare the most recent “latest” shot to the existing baseline, and will only update/create images in the latest directory.

At this point, I’ll commit the baselines to the git repo so that they can be shared around the team, and used as baselines by everyone running visual regression tests.

If I run npm test again, the tests will all pass because I haven’t changed anything. I’ll make a small change to the button background color which might not be picked up by a human eye but will cause a regression that our tests will pick up with no problem.

In the _buttons.scss file, I’m going to change the default button background color from $black (#000) to $gray-darker (#333). I’ll run the style script to update the compiled css and then clear the site cache to make sure the change is implemented. (When actively developing, I suggest disabling cache and keeping the watch task running. It just makes things easier and more efficient.)

This time all the tests fail, and if we look at the images in the diff folder, we can clearly see that the “search” button is different as indicated by the bright pink/purple coloring.

If I open up one of the “baseline” images, and the associated “latest” image, I can view them side-by-side, or toggle back and forth. The change is so subtle that a human eye might not have noticed the difference, but the computer easily identifies a regression. This shows how useful visual regression testing can be!

Let’s pretend this is actually a desired change. The original component was created before the color was finalized, black was used as a temporary color, and now we want to capture the update as the official baseline. Simply Move the “latest” image into the “baselines” folder, replacing the old baseline, and commit that to your repo. Easy peasy.

Running an Individual Test

If you’re creating a new component and just want to run a single test instead of the entire suite, or you run a test and find a regression in one image, it is useful to be able to just run a single test instead of the entire suite. This is especially true once you have a large suite of test files that cover dozens of aspects of your site. Let’s take a look at how this is done.

I’ll create a new test in the organisms folder of my theme at /search/search.test.js. There’s an example of an element test in the example-tests.md file, but I’m going to do a much more basic test, so I’ll actually start out by copying the homepage test and then modify that.

The first thing I’ll change is the describe section. This is used to group and name the screenshots, so I’ll update it to make sense for this test. I’ll just replace “Home Page” with “Search Block”.

Then, the only other thing I’m going to change is what is to be captured. I don’t want the entire page, in this case. I just want the search block. So, I’ll update checkDocument (used for full-page screenshots) to checkElement (used for single element shots). Then, I need to tell it what element to capture. This can be any css selector, like an id or a class. I’ll just inspect the element I want to capture, and I know that this is the only element with the search-block-form class, so I’ll just use that.

I’ll also remove the timeout since we’re just taking a screenshot of a single element, we don’t need to worry about the page taking longer to load than the default of 60 seconds. This really wasn’t necessary on the page either, but whatever.

My final test file looks like this:

const visreg = require('../../../../../../../../tests/config/globalHides.js'); describe('Search Block', function () { it('should look good', function () { browser .url('./') .checkElement('.search-block-form', {hide: visreg.hide, remove: visreg.remove}) .forEach((item) => { expect(item.isWithinMisMatchTolerance).to.be.true; }); }); }); constvisreg=require('../../../../../../../../tests/config/globalHides.js');describe('Search Block',function(){  it('should look good',function(){    browser      .url('./')      .checkElement('.search-block-form',{hide:visreg.hide,remove:visreg.remove})      .forEach((item)=>{        expect(item.isWithinMisMatchTolerance).to.be.true;      });

With that in place, this test will run when I use npm test because it’s globbing, and running every file that ends in .test.js anywhere in the _patterns directory. The problem is this also runs the homepage test. If I just want to update the baselines of a single test, or I’m actively developing a component and don’t want to run the entire suite every time I make a locally scoped change, I want to be able to just run the relevant test so that I don’t waste time waiting for all of the irrelevant tests to pass.

We can do that by passing the --spec flag.

I’ll commit the new test file and baselines before I continue.

Now I’ll re-run just the search test, without the homepage test.

npm test -- --spec web/themes/custom/visual_regression_testing/components/_patterns/03-organisms/search/search.test.js

We have to add the first set of -- because we’re using custom npm scripts to make this work. Basically, it passes anything that follows directly to the custom script (in our case test is a custom script that calls ./node_modules/webdriverio/bin/wdio). More info on the run-script documentation page.

If I scroll up a bit, you’ll see that when I ran npm test there were six passing tests. That is one test for each browser for each test. We have two test, and we’re checking against three browsers, so that’s a total of six tests that were run.

This time, we have three passing tests because we’re only running one test against three browsers. That cut our test run time by more than half (from 106 seconds to 46 seconds). If you’re actively developing or refactoring something that already has test coverage, even that can seem like an eternity if you’re running it every few minutes. So let’s take this one step further and run a single test against a single browser. That’s where the wdio.conf.quick.js file comes into play.

Running Test Against a Subset of Browsers

The wdio.conf.quick.js file will, by default, run test(s) against only Chrome. You can, of course, change this to whatever you want (for example if you’re only having an issue in a specific version of IE, you could set that here), but I’m just going to leave it alone and show you how to use it.

You can use this to run the entire suite of tests or just a single test. First, I’ll show you how to run the entire suite against only the browser defined here, then I’ll show you how to run a single test against this browser.

In the package.json file, you’ll see the test:quick script. You could pass the config file directly to the first script by typing npm test -- wdio.conf.quick.js, but that’s a lot more typing than npm run test:quick and you (as well as the rest of your team) have to remember the file name. Capturing the file name in a second custom script simplifies things.

When I run npm run test:quick You’ll see that two tests were run. We have two tests, and they’re run against one browser, so that simplifies things quite a bit. And you can see it ran in only 31 seconds. That’s definitely better than the 100 seconds the full test suite takes.

Let’s go ahead and combine this with the technique for running a single test to cut that time down even further.

npm run test:quick -- --spec web/themes/custom/visual_regression_testing/components/_patterns/03-organisms/search/search.test.js

This time you’ll see that it only ran one test against one browser and took 28 seconds. There’s actually not a huge difference between this and the last run because we can run three tests in parallel. And since we only have two tests, we’re not hitting the queue which would add significantly to the entire test suite run time. If we had two dozen tests, and each ran against three browsers, that’s a lot of queue time, whereas even running the entire suite against one browser would be a significant savings. And obviously, one test against one browser will be faster than the full suite of tests and browsers.

So this is super useful for active development of a specific component or element that has issues in one browser as well as when you’re refactoring code to make it more performant, and want to make sure your changes don’t break anything significant (or if they do, alert you sooner than later). Once you’re done with your work, I’d still recommend running the full suite to make sure your changes didn’t inadvertently affect another random part of the site.

So, those are the basics of how to set up and run visual regression tests. In the next post, I’ll dive into our philosophy of what we test, when we test, and how it fits into our everyday development workflow.

Web Chef Brian Lewis
Brian Lewis

Brian Lewis is a frontend engineer at Four Kitchens, and is passionate about sharing knowledge and learning new tools and techniques.

Web Chef Dev Experts
Development

Blog posts about backend engineering, frontend code work, programming tricks and tips, systems architecture, apps, APIs, microservices, and the technical side of Four Kitchens.

Read more Development
Mar 08 2018
Mar 08

This post was originally published on May 22, 2013 and last updated March 8, 2018 thanks to some helpful input by Steve Elkins.

Drupal 7 is a haus at combining CSS & JS files. This can help boost page performance & optimization easily, but if not used right, can do the complete opposite. In this post, we’ll go over how to load JS & CSS files based on conditionals like URL, module, node, views and more.

Before we dive in, get somewhat familiar with the drupal_add_js and drupal_add_css functions. We’ll use these to load the actual JS and CSS files.

hook_init – runs on every page

/**
 * Implements hook_init()
 *
 * @link https://api.drupal.org/api/drupal/modules%21system%21system.api.php/function/hook_init/7.x
 */
function HOOK_init() {

  // Using the equivalent of Apache's $_SERVER['REQUEST_URI'] variable to load based on URL
  // @link https://api.drupal.org/api/drupal/includes!bootstrap.inc/function/request_uri/7
  if (request_url() === 'your-url-path') {
    drupal_add_js( /* parameters */ );
    drupal_add_css( /* parameters */ );
  }
}

Using hook_init is one of the simplest methods to load specific JS and CSS files (don’t forget to replace HOOK with the theme or module machine name).

Be careful, this method get’s ran on every page, so it’s best to use this method only when you actually need to check every page for your conditional. A good example, loading module CSS and JS files. A bad example, loading node-specific CSS and JS files. We’ll go over that next.

There’s also a similar preprocess function, template_preprocess_page you could use, but it too get’s ran on every page and is essentially the same as hook_init.

template_preprocess_node – runs on node pages

/**
 * Implements template_preprocess_node()
 *
 * @link https://api.drupal.org/api/drupal/modules%21node%21node.module/function/template_preprocess_node/7.x
 */
function TEMPLATE_preprocess_node(&$vars) {
  // Add JS & CSS by node type
  if( $vars['type'] == 'your-node-type') {
    drupal_add_js( /* parameters */ );
    drupal_add_css( /* parameters */ );
  }
   
  // Add JS & CSS to the front page
  if ($vars['is_front']) {
    drupal_add_js( /* parameters */ );
    drupal_add_css( /* parameters */ );
  }
   
  // Given an internal Drupal path, load based on node alias.
  if (drupal_get_path_alias("node/{$vars['#node']->nid}") == 'your-node-id') {
    drupal_add_js( /* parameters */ );
    drupal_add_css( /* parameters */ );
  }
}

Using template_preprocess_node is perfect when loading JS and CSS files based on nodes (don’t forget to replace TEMPLATE with the theme machine name). Since it only get’s run on nodes, it’s great to use when you want to load CSS and JS files on specific node types, front pages, node URLs, etc.

template_preprocess_views_view – runs every view load

/**
 * Implements template_preprocess_views_view()
 *
 * @link https://api.drupal.org/api/views/theme%21theme.inc/function/template_preprocess_views_view/7.x-3.x
 */
function TEMPLATE_preprocess_views_view(&$vars) {
  // Get the current view info
  $view = $vars['view'];

  // Add JS/CSS based on view name
  if ($view->name == 'view_name') {
    drupal_add_js( /* parameters */ );
    drupal_add_css( /* parameters */ );
  }

  // Add JS/CSS based on current view display
  if ($view->current_display == 'current_display_name') {
    drupal_add_js( /* parameters */ );
    drupal_add_css( /* parameters */ );
  }
}

Using template_preprocess_node is useful when loading JS and CSS files when a particular view is being used (don’t forget to replace TEMPLATE with the theme machine name).

Helpful Methods for Conditionals

Here’s a few helpful Drupal methods you can use for your conditionals. Have one you use often? Let me know in the comments below.

  • request_uri – Returns the equivalent of Apache’s $_SERVER[‘REQUEST_URI’] variable.
  • drupal_get_path_alias – Given an internal Drupal path, return the alias set by the administrator.

Looking like a foreign language to you?

Not a developer or just lost looking at the code snipplets above? Shoot me a question in the comments below, or give these ‘plug-and-play’ modules a try for a GUI alternative:

Like this:

Like Loading...

Author: Ben Marshall

Red Bull Addict, Self-Proclaimed Grill Master, Entrepreneur, Workaholic, Front End Engineer, SEO/SM Strategist, Web Developer, Blogger

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

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.

Mar 01 2017
Mar 01
    To improve SEO, we need to clean our URLs. By default in drupal, we've an option called clean URLs at the configuration. In drupal 7, we can also manage the URLs. For instance, you have a content type called services. You wanted to each service page have url like services/page-name. To do that, we've a pathauto module in drupal 7. The pathauto module allow us to manage the URLs for every content types, files, taxonomy & users and also we can remove some unnecessary words from URL like an, the and so on.

   The pathauto module can remove some unnecessary words like a, an, the and so on & also remove special characters like !, @, $ and so on. Unfortunately, it doesn't included some other symbols like copyright(©), trademark(™), registered(®) and so on. But it provide a hook to add new symbols into the punctuation settings called hook_pathauto_punctuation_chars_alter. After created a content with some symbols which are represented above, your page URL looks like below image:

Drupal 7 - remove special characters from url using pathauto module

/**
 * Implements hook_pathauto_punctuation_chars_alter().
 */
function phponwebsites_pathauto_punctuation_chars_alter(array &$punctuation) {
  $punctuation['copyright']          = array('value' => '©', 'name' => t('Copyright'));
  $punctuation['registered']         = array('value' => '®', 'name' => t('Registered trademark'));
  $punctuation['trademark']          = array('value' => '™', 'name' => t('Trademark'));
}
   After implemented above code into your module, you cold see added symbols are listing on Pathauto module's settings page at /admin/config/search/path/settings. If You didn't get these symbols, clear cache & test it again. It looks like below image:

Drupal 7 - pathauto settings after hook_pathauto_punctuation_chars_alter

Now you can create a content with those symbols. The pathauto module didn't added those symbols into the URL.

Now I hope you know how to remove some special characters from URL alias using pathauto module in drupal 7.

Related articles:
Add new menu item into already created menu in Drupal 7
Add class into menu item in Drupal 7
Create menu tab programmatically in Drupal 7
Add custom fields to search api index in Drupal 7
Clear views cache when insert, update and delete a node in Drupal 7
Create a page without header and footer in Drupal 7
Login using both email and username in Drupal 7
How to set multiple URL alias for a node using pathauto module in Drupal 7
Update multiple fields using #ajax in Drupal 7
Mar 01 2017
Mar 01
   As we discussed in my previous post, clean URL is one of the option to improve SEO. We've module called pathauto to clean URLs in drupal 7. It can allow us to set alias for content types, files, users & taxonomies. But we can set only one URL alias for a content type in drupal 7. You can set URL alias for a content type at admin/config/search/path/patterns. It looks like below image:

Pathauto module patterns in drupal 7

   Suppose you need two path for a content. For instance, the URL alias for a article need to node title and also article/node-title. Is it possible to set multiple path alias for a content type in drupal 7? Yes it is possible in drupal 7. We can set multiple URL alias for a conten type programmatically using pathauto module in drupal 7. We need to insert our path alias into the "url_alias" table while inserting & updating a node and remove path alias When delete a node.

Add URL alias programmatically when insert and update a node using pathauto module in drupal 7:


    For instance, I've choosen article content type. We need to insert & update a URL alias into the "url_alias" table using hook_node_insert() & hook_node_update() in drupal 7.


/**
 * Implements hook_node_insert()
 */
function phponwebsites_node_insert($node) {
  if ($node->type == 'article') {
    //save node alias
    _phponwebsites_insert_update_alias($node);
  }
}

/**
 * Implements hook_node_update()
 */
function phponwebsites_node_update($node) {
  if ($node->type == 'article') {
    //update node alias
    _phponwebsites_insert_update_alias($node);
  }
}

/**
 * Insert and update alias for course
 */
function _phponwebsites_insert_update_alias($node) {
  module_load_include('inc', 'pathauto');
  $title = pathauto_cleanstring($node->title);

  $values['source'] = 'node/' . $node->nid . '/article';
  $values['alias'] = 'article/' . $title;

  $all_values = array($values);

  foreach ($all_values as $all) {
    $query = db_merge('url_alias')
      ->fields(array('source' => $all['source'], 'alias' => $all['alias'], 'language' => LANGUAGE_NONE))
      ->key(array('source' => $all['source']))
      ->execute();
  }
}

Where,
 pathauto_cleanstring is obey the pathatuo module's rules which is mentioned at admin/config/search/path/settings. To know more details of pathauto_cleanstring, please visit http://www.drupalcontrib.org/api/drupal/contributions!pathauto!pathauto.inc/function/pathauto_cleanstring/7

After added the above code into your custome module(clear cache), you will create a article. You just test your url at admin/config/search/path in the pathauto's list. It looks like below image:

Pathauto module URL alias list in drupal 7

Now you could access the article by both node-title as well as article/node-title.

Multiple URL alias for a node using pathauto module in drupal 7

Delete URL alias programmatically when delete a node using pathauto module in drupal 7:


     We've inserted 2 URL alias for a node. So we need to delete those from "url_alias" table when delete a node. We can trigger it using hook_node_delete() in drupal 7. Consider the below code:


/**
 * Implements hook_node_delete()
 */
function arep_node_delete($node) {
  if ($node->type == 'article') {
    //delete node alias for ceu and non-ceu course
    module_load_include('inc', 'pathauto');
    $source[0] = 'node/' . $node->nid . '/article';

    foreach ($source as $s) {
      $path = path_load(
        array('source' => $s)
      );
      path_delete($path['pid']);
    }

  }
}

Where,
  path_load returns the details of a URL alias like source, alias, path id  & language. To know more details of path_load(), please visit https://api.drupal.org/api/drupal/includes!path.inc/function/path_load/7.x.

After added the above code into your customer module(clear cache), you will delete a node and check your URL alias at admin/config/search/path. Now tt should not be displayed here.

Now I've hope you know how to set multiple URL alias for a content type.

Related articles:
Remove speical characters from URL alias using pathauto module in Drupal 7
Add new menu item into already created menu in Drupal 7
Add class into menu item in Drupal 7
Create menu tab programmatically in Drupal 7
Add custom fields to search api index in Drupal 7
Clear views cache when insert, update and delete a node in Drupal 7
Create a page without header and footer in Drupal 7
Login using both email and username in Drupal 7
Update multiple fields using #ajax in Drupal 7
Mar 01 2017
Mar 01
     This blog describes how to disable future dates in the Drupal 7. One of the features in the date  module is displayed the date in the pop-up.

Disable future dates in the date pop up - drupal 7

The use case is if you want to display only past & current date rather than all the dates in the pop-up, then how to do it in Drupal 7. Actually, the date module provides API called hook_date_popup_process_alter to alter the date_popup widget elements.

Example for disabling future dates in Drupal 7:


   For instance, I am going to disable future dates in the article content type. Please consider the following code snippet.

/**
 * Implement hook_date_popup_process_alter().
 */
function phponwebsites_date_popup_process_alter(&$element, &$form_state, &$context) {

  if ($form_state['complete form']['#form_id'] == 'article_node_form' && $element['#field']['field_name'] == 'field_date') {
    $max = 0;
  }

  if (isset($element['#datepicker_options']['maxDate'])) {
    $max = $element['#datepicker_options']['maxDate'];
  }

  if (isset($max)) {
    $element['#datepicker_options'] = array(
      'maxDate' => "+$max D",
    );
  }
  $element['date'] = date_popup_process_date_part($element);
}


   I've disabled the dates only if the form is article & the field name is field_date. After added the above code to your module, you could see disabled future dates in the date pop up. It looks like the below image:


Disable future dates in the date pop-up in drupal 7

   Now I've hope you know how to disable the future dates at the date module in Drupal 7.

Related articles:
Remove speical characters from URL alias using pathauto module in Drupal 7
Add new menu item into already created menu in Drupal 7
Add class into menu item in Drupal 7
Create menu tab programmatically in Drupal 7
Add custom fields to search api index in Drupal 7
Clear views cache when insert, update and delete a node in Drupal 7
Create a page without header and footer in Drupal 7
Login using both email and username in Drupal 7
Update multiple fields using #ajax in Drupal 7
Mar 01 2017
Mar 01
     We know how to replace a field using #ajax in a form. Can we update multiple fields using #ajax in a form? Yes, we can update multiple fields using #ajax in the Drupal 7. We can achieve it using ajax_command_replace() in Drupal 7. For more details about ajax commands on Drupal 7, please visit https://api.drupal.org/api/drupal/7.x/search/ajax_command.

Update fields using #ajax in a Drupal form:


    Consider the following example. In this example, I've created a form with fields First, Second & Third name. I tried to update the Second & Third name fields when focus out on the First name field.
/**  * Implments hook_form()  */ function phponwebsites_ajax_form($form, &$form_state) {   $form['firstname'] = array(     '#title' => t('First name'),     '#type' => 'textfield',     '#ajax' => array(       'callback' => '_generate_textfield',       'wrapper' => 'copied-text-field',     )   );
  $form['secondname'] = array(     '#title' => t('Second name'),     '#type' => 'textfield',     '#prefix' => '<div id="copied-secondname">',     '#suffix' => '</div>',   );
  $form['thirdname'] = array(     '#title' => t('Third name'),     '#type' => 'textfield',     '#prefix' => '<div id="copied-thirdname">',     '#suffix' => '</div>',   );
  $form['submit'] = array(     '#type' => 'submit',     '#value' => 'Submit'   );
  return $form; }
function _generate_textfield($form, &$form_state) {   if (!empty($form_state['values']['firstname'])) {     $form['secondname']['#value'] =  $form_state['values']['firstname'];     $form['thirdname']['#value'] =  $form_state['values']['firstname'];   }   $commands = array();   $commands[] = ajax_command_replace('#copied-secondname', drupal_render($form['secondname']));   $commands[] = ajax_command_replace('#copied-thirdname', drupal_render($form['thirdname']));   return array('#type' => 'ajax', '#commands' => $commands); }
When you tried to execute the above codes, it'll populate the same name on the other 2 fields. It looks likes the below image:
Update multiple form fields using #ajax in Drupal 7

Now I hope you know how to populate multiple fields using #ajax in Drupal 7 forms. Related articles:
Remove speical characters from URL alias using pathauto module in Drupal 7
Add new menu item into already created menu in Drupal 7
Add class into menu item in Drupal 7
Create menu tab programmatically in Drupal 7
Add custom fields to search api index in Drupal 7
Clear views cache when insert, update and delete a node in Drupal 7
Create a page without header and footer in Drupal 7
Login using both email and username in Drupal 7
Disable future dates in date pop-up calendar Drupal 7
Mar 01 2017
Mar 01
     This blog describes how to add date pop-up calender to a custom form in the Drupal 7.

Use date pop-up calendar in custom form - drupal 7

     The use case is if you want to use date pop-up calendar in a custom form, then how you can do it in the drupal 7. Actually, the drupal 7 form API provides lots of form types like textfield, checkbox, checkboxes etc to create a custom form. Similarly, the date module  also provides the form type called date_popup. We can use it in the custom form in order to display the date pop-up in the custom form.

Use date pop-up calendar with the custom form in drupal 7:


   Let consider the below code snippet:

 
function phponwebsites_menu() {
  $items = array();

  $items['customform'] = array(
    'title' => t('Custom Form'),
    'type' => MENU_CALLBACK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array('phponwebsites_display_date_popup_form'),
    'access callback' => TRUE,
  );

  return $items;
}

function phponwebsites_display_date_popup_form($form, &$form_state) {
  $form['date'] = array(
    '#type' => 'date_popup',
    '#default_value' => date('Y-m-d'),
    '#date_format'   => 'Y-m-d',
    '#date_year_range' => '0:+5',
    '#datepicker_options' => array('minDate' => 0, 'maxDate' => 0),
  );

  return $form;
}


    Where,
      '#date_format'   => 'Y-m-d' if you need to display only date
      '#date_format'   => 'Y-m-d H:i:s' if you need to display date & time
      '#date_year_range' => '0:+5' if you need to display only future 5 years
      '#datepicker_options' => array('minDate' => 0, 'maxDate' => 0) if you want to display only current date. We can hide the future & past dates using this option.

   Please add the above code into your module file and look into the "customform" page. It looks like the below image:

Display only current date in date -pop-up - drupal 7

   Now I've hope you know how to add date pop-up calendar with custom form in the drupal 7.

Related articles:
Remove speical characters from URL alias using pathauto module in Drupal 7
Add new menu item into already created menu in Drupal 7
Add class into menu item in Drupal 7
Create menu tab programmatically in Drupal 7
Add custom fields to search api index in Drupal 7
Clear views cache when insert, update and delete a node in Drupal 7
Create a page without header and footer in Drupal 7
Login using both email and username in Drupal 7
Disable future dates in date pop-up calendar Drupal 7
Update multiple fields using #ajax in Drupal 7
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:

Oct 26 2016
Oct 26

One of my biggest pet-peeves is creating Drupal 7 empty menu link titles since there’s no out-of-the-box solution. As a result it can be difficult to create stylized links, such as icons or background images. After many frustrating sessions I finally sat down to find a way to make this happen. Consequently, I began to think this was an impossibility and was unable to find a solution already in existence that did exactly what I needed it to do. However, Drupal 7 empty menu link titles are absolutely possible with just this one little snippet! Have no fear, theme_menu_link to the rescue!

Using <none> to Create Drupal 7 Empty Menu Link Titles

drupal 7 empty menu link titles

First of all, you must start by using the snippet provided below and will need to enter <none> as the link title in order to render it empty. To accomplish this, in your theme’s template.php file add:

/**
 * Implements theme_menu_link().
 *
 * @link https://api.drupal.org/api/drupal/includes!menu.inc/function/theme_menu_link/7.x
 */
function your_theme_menu_link($vars)
{
  $element = $vars['element'];
  $sub_menu = '';

  if ($element['#below']) {
    $sub_menu = drupal_render($element['#below']);
  }

  if ( '<none>' === $element['#title'] )
  {
    $element['#title'] = '';
  }

  $output = l($element['#title'], $element['#href'], $element['#localized_options']);

  return '<li' . drupal_attributes($element['#attributes']) . '>' . $output . $sub_menu . "</li>\n";
}

theme_menu_link returns HTML for menu and submenu links, so that it can be used to alter the menu’s output. In the above, we’re checking to see if the menu link title has been set to <none>. If this has been done correctly it removes the title text while leaving the link intact. Be sure to check your work before moving on because sometimes the simplest mistakes in the beginning will cause you grief in the future.

Finally, be sure to clear your cache if you’re not seeing the change!

Have you already added this snippet and yet not seen anything change? It’s probably due to Drupal not yet seeing the new hook in your code. In order to fix this, you’ll need to clear your cache so that Drupal will register the changes you’ve made. Most of the time, this will fix issues like these quickly rather than spending hours trying to debug something that truly isn’t broken.

You can also use this hook to alter other menu link attributes. For instance, if you wanted to avoid using the Menu attributes module you’re able to use this hook to add or remove classes.

For more information on theme_menu_link, see https://api.drupal.org/api/drupal/includes!menu.inc/function/theme_menu_link/7.x.

Is there a Drupal module that can handle this?

Of course there is! Icon API accomplishes this in addition to some extra features you may find useful. I’ve never used it personally, and I’d rather stay away from modules that don’t produce production releases. Consequently, if you have created or know of a module that can handle this feel free to shoot me a line in the comments below and I’ll gladly take a look at it to potentially include in this post as a footnote. In conclusion, don’t ever believe something is impossible because you’re often just a few days of banging your head against the wall away from a breakthrough!

Like this:

Like Loading...

Author: Ben Marshall

Red Bull Addict, Self-Proclaimed Grill Master, Entrepreneur, Workaholic, Front End Engineer, SEO/SM Strategist, Web Developer, Blogger

Jul 06 2016
Jul 06

Google Summer of Code (GSoC) is into the next phase of coding after the mid-Term evaluations which got over by June 27th. This also reminds students to speed up the coding activities to complete the projects within the schedules provided in the proposal.

I am porting Search Configuration module to Drupal 8 as part of this year’s summer of code. GSoC is definitely turning out to be a venue for the young students from universities around the world to work on real-world projects under the experience of well-known developers, learning new technologies, making use of version control systems, with regular meetings and finally building up a software which is going to help a large section of the society.

I blog regularly, sharing my project progress. If you would like to have a glimpse of my past activities on this port, please visit this link.

Drupal 8 has introduced the concept of Html twigs in place of the PHP templates. So, the PHP template files have to be now ported to the Html environment. The .tpl.php template file is replaced by the .html.twig file for the module templates.  Templates are simply text files which can give outputs in Html, Latex, CSV or XML formats.

To print some data, we usually take the service of echo statements in PHP.  The print statements are replaced by {{ }} in Html twigs.

<?php echo t(‘Let’s start the process.’); ?>

is replaced by:

{{ ‘Le’s start the process’|t }}

The variable names have to be converted to simple names. For instance,

$page[‘title’]

becomes

{{  title }}

The PHP logics have to be replaced by {% %} syntax. This is applicable to all the logical statements.

<?php if ($page[‘title]): ?>

…..

<?php endif; ?>

is transformed as:

{% if form %}

……

{% endif %}

Also, the variables are replaced by simple names.

<?php if ($logo): ?>

is transformed as:

{% if logo %}

These were some of the basic transformations to get started into created the HTML twigs.The use of the Html twigs has made the templates look very simple and easily understandable. It is really easy to get the templates converted to the Html twigs. This is always one of the crucial requirements of porting modules from Drupal 7 to Drupal 8.

Stay tuned for further updates on this port process.

Jun 21 2016
Jun 21

Google Summer of Code (GSoC), has entered into the mid-Term evaluation stage. This is a 1 week period from 21- 27 June, were students and mentors present the progress of their projects. Based on the reports submitted, students are made pass/ fail.

I have been working on porting Search Configuration to Drupal 8 in the past few weeks. If you would like to have a quick glimpse of my past activities on this port process, please go through these posts.

last week, I could learn some Drupal concepts which were really helpful for my project. In the previous versions of Drupal, the role permissions were stored in a role_permissions table in the Database. But now, in Drupal 8, the role permissions are directly stored in the role configuration entity.

So, as described above, in D7 and its preceding versions, role permissions were stored in a role_permissions database which had the role Id and the corresponding permissions. The permissions distributed to a role was retrieved in D7 using:

$permissions = role->getPermissions();

But, in D8, this is done by the

$permissions = role->getPermissions();

Another instance is that, to grant certain permissions to roles.

In D7 it was controlled by,

user_role_grant_permissions($rid, array(‘ access content’));

The role configuration entity remodels this functionality in D8 to:

$role->grantPermission(‘ access content’);

In connection with the term permissions, the most important aspect in Drupal is a hook: hook_permissions(). This hook, obviously as you might have guessed, distributes the permissions to various users; decides whether a particular user should be allowed to access a page or a content, granting and restricting the access.

This hook has been replaced in Drupal 8 by a module.permissions.yml file. This file contains the permissions and its specifications. We can write a driver function in a php file to add the dynamic permissions. This can be achieved by making a driver class in the php file and adding the behaviour of the permission we need in the member functions of the class. We also have to link this PHP file with our yml file to keep it active. This is done by adding a callback function in the yml file which references this php file.

To display special characters in a plain text string for display as HTML format, Drupal earlier versions used the function check_plain.  This had the general syntax:

check_plain($text); // where $text was the string to be processed.

This function has got deprecated in Drupal 8. This has been replaced by the \Drupal\Compoent\Utility\Html::escape($text).

May 31 2016
May 31

The Google Summer of Code 2016 (GSoC’ 16) coding period kicked off from last week, May 23rd. Each selected student had a one-month long community bonding period with their respective organisation. I had written my community bonding experience with Drupal in my previous blog post.

I am working on porting search configuration module to Drupal 8 under the mentorship of Naveen Valecha, Neetu Morwani and Karthik Kumar. I learnt the basics of Drupal, implementing Drupal forms and other fundamentals required for my project in the initial phase.

I was really thrilled to get into the coding rounds of GSoc’ 16. I was successful in making a good progress in the initial week. I could start the coding phase effectively by porting some important features. I had regular discussions with my mentors throughout the previous week. They were really helpful in guiding me in the proper way by reviewing the code and sharing their valuable feedback.

Here I would like to share with you some of the changes which I have come across in Drupal 8 from its preceding versions which I have worked out in the previous week for my project.

  • In D8, the .info files are replace by .info.yml files. The .info files contained the basic information regarding the respective module. The .info.yml generally contains the following attributes of the module:
    • name
    • description
    • core
  • The Drupal 7 variables have been converted to the Drupal 8 state system. The previous versions had variable_get and variable_set functions to store variables and their default configurations. They were used to store and retrieve data. They are presently removed in D8.
  • Moreover, we need to store the default configuration of the variables. While developing a new module, it comes in module/config/schema/ will contain the default variable configurations.
    • The default configuration is the default property of the system with its type and the corresponding default value linked to it.
  • Another transformation that has taken place is that the global $user has been substituted by the current_user service.
    • So, the previous definition,
      • global $user;
    • is replaced by
      • $user = \Drupal::currentUser();

These were some of the important concepts that I had to deal with in the previous week for the porting process. It was really a wonderful learning time for me. Hope to enjoy the coming weeks to learning new concepts and implementing it for my porting.

Hope all the selected students had a wonderful start to their coding period. All the best for the coming weeks.

May 20 2016
May 20

I have been selected for the Google summer of Code’ 16 for Drupal for the project, Port search configuration module to Drupal 8. Thanks to all the developers in #Drupal IRC channel for guiding me into this summer project by sharing their ideas and suggestions.

The search configuration feature is presently available in Drupal 7 and its preceding versions. This is really a cool feature which helps us a lot in improving the search and enhancing it for better search results. This summer, I will be engaged in porting this module to Drupal 8.

The GSoC projects were announced on April 22, 2016. All selected students have a community bonding period till May 22nd. This is the time when students get closer to the organisation, learn the organisation code base, interact with mentors, plan more about the project and its deadline for the coding period which starts soon  after this community bonding.

I have been blessed with three experienced mentors from Drupal- Naveen Valecha, Neetu Morwani and Karthik Kumar. I have been discussing with them regarding the project plan for the last few weeks. Meanwhile, I was also asked to learn some of the basic concepts of Drupal like hooks, hook permissions, forms in Drupal which are the real components of my project. This helped me a lot to understand more about the coding methodologies I need to adopt.  I could go through the code base of the module in Drupal 7 which has helped me collect more ideas for the project.

I also got the opportunity to hack with some simple modules by creating a sandbox project in Drupal and pushing commits on sample module developments I did to learn the basics of the module. I have created a project in Drupal for the search configuration port and has added the tasks I need to complete in association with this process.

I  will be posting regular updates regarding my GSoC project here.

Best of luck to all the selected students.

Looking for a bright summer ahead, coding for Drupal.

Thank you.

Apr 28 2016
Apr 28
      This blog describes about how to create a new node programmatically in Drupal 7. If you want to add a new node, you can done at node/add by default. In Drupal, you can also add a node programmatically. Let see the below code.

<?php
// create object
  $node = new stdClass();
  // set title for a node
  $node->title = t('Created node programmatically');
  // set node type
  $node->type = 'article';
  // set node language
  $node->language = LANGUAGE_NONE;
  // set value to node body
  $node->body[LANGUAGE_NONE][0]['value'] = t('This node has been created programmatically in Drupal 7');
  // set value to node body summary
  //$node->body[LANGUAGE_NONE][0]['summary'] = text_summary(t('This node has been created programmatically in Drupal 7'));
  // set node body format like plain_text, filtered_html, full_html
  $node->body[LANGUAGE_NONE][0]['format'] = 'filtered_html';
  node_object_prepare($node);
  // author for a node
  $node->uid = 1;
  // status of node  0 - unpublished, 1 - published
  $node->status = 1;
  // promoted to front page or not
  $node->promote = 0;
  // sitcky at top of tha page
  $node->sticky = 0;
  // comments 0 - hidden, 1 - closed, 2 - opened
  $node->comment = 1;

  // add term
  $node->field_tags[$node->language][]['tid'] = 1;

  // get the file path
  $file_path = drupal_get_path('module', 'phponwebsites') . '/Desert.jpg';
  // create file object
  $file = (object) array(
    'uid' => 1,
    'uri' => $file_path,
    'filemime' => file_get_mimetype($file_path),
    'status' => 1,
  );
  // Save the file to the public directory. You can specify a subdirectory, for example, 'public://images'
  $file = file_copy($file, 'public://');
  // assign the file object into image field
  $node->field_image[LANGUAGE_NONE][0] = (array)$file;
  // Prepare node for a submit
  $node = node_submit($node);
  //save the node
  node_save($node);


    After ran this code, you can see newly created node at admin/content. When you view that node, it looks like below image:

Create a new node programmatially in Drupal 7 at Phponwebsites

     Now I’ve hope you know how to create a new node programmatically in Drupal 7.
Related articles:
Add new menu item into already created menu in Drupal 7
Add class into menu item in Drupal 7
Create menu tab programmatically in Drupal 7
Add custom fields to search api index in Drupal 7
Clear views cache when insert, update and delete a node in Drupal 7
Create a page without header and footer in Drupal 7
Login using both email and username in Drupal 7
Redirect users into any page after logged into a site in Drupal 7
Apr 21 2016
Apr 21
  This blog describes about how to add class into menu item that is created programmatically  using hook_menu() in drupal 7.
        We know how to add custom menu item into already created menu in drupal7. Is it possible to add class to that menu item in durpal 7? Yes you can add custom classes into menu item using hook_menu() in drupal 7.

Add class into menu item in drupal 7:

     
       Consider below program to add class into menu item in drupal 7.

/**
 * Implement hook_menu()
 */
function phponwebsites_menu() {
  $items['sample'] = array(
    'title' => t('Sample page'),
    'type' => MENU_NORMAL_ITEM,
    'menu_name' => 'main-menu',
    'page callback' => 'samplepage',
    'access callback' => TRUE,
    'options' => array(
      'attributes' => array(
        'class' => array('drupal-menu-class')
      )
   ),
  );

  return $items;
}

/**
 * Implement samplepage()
 */
function samplepage() {
  $str = t('Hi this is sample page');
  return $str;
}


Where,
     type – MENU_NORMAL_ITEM
     menu-name – name of the menu to add new link
     options – add any attributes like class, id

You need to clear cache to see created menu items with custom class into main menu.

Add multiple classes into menu item in drupal 7:


     Similarly you can add multiple classes into menu item using hook_menu() in drupal 7. Consider below program to add multiple
classes into menu item.

function phponwebsites_menu() {
  $items['sample'] = array(
    'title' => t('Sample page'),
    'type' => MENU_NORMAL_ITEM,
    'menu_name' => 'main-menu',
    'page callback' => 'samplepage',
    'access callback' => TRUE,
    'options' => array(
      'attributes' => array(
        'class' => array('drupal-menu-class' ,  'drupal-menu-new-class')
      )
   ),
  );

  return $items;
}


       Now i’ve hope you know how to add class programmatically to menu item using hook_menu() in drupal 7.
Related articles:
Add new menu item into already created menu in Drupal 7
Create menu tab programmatically in Drupal 7
Add custom fields into apache solr search api index in Drupal 7
Redirect users after login in Drupal 7
Apr 21 2016
Apr 21

     This blog describes about how to add a new menu item into menu like main menu, user menu in drupal 7.

drupal 7 - add link into menu programmatically using hook_menu()
     
We can create a menu item using hook_menu in drupal 7. Can we add menu item into already created menu in drupal7? Yes you can add a link into menu using hook_menu().

Add new menu item into main menu in drupal 7:


       Consider below program to add new menu item into main menu in drupal 7.
/**
 * Implement hook_menu()
 */
function phponwebsites_menu() {
  $items['sample'] = array(
    'title' => t('Sample page'),
    'type' => MENU_NORMAL_ITEM,
    'menu_name' => 'main-menu',
    'page callback' => 'samplepage',
    'access callback' => TRUE,
  );

  return $items;
}

/**
 * Implement samplepage()
 */
function samplepage() {
  $str = t('Hi this is sample page');
  return $str;
}



Where,      type – MENU_NORMAL_ITEM      menu-name – name of the menu to add new link         You need to clear cache to see created new menu item in main menu. Now i’ve hope you know how to add new link programmatically to already created menu in drupal 7.

Related articles:
Add class into menu item in Drupal 7
Create menu tab programmatically in Drupal 7
Add custom fields into apache solr search api index in Drupal 7
Redirect users after login in Drupal 7
Apr 21 2016
Apr 21
     This is blog describes about how to add custom field to Search API solr index in Drupal 7.

     Suppose we need a new field, we can add new fields for a content type at /admin/structure/content/ types. Then all fields are showed at /admin/config/search/search_ api/index/default_node_index/ fields. Now you can add desired fields to solr index.

     Suppose you want to show custom field to Search API results but that field is not created for any specific content types. Is it possible in Search API? Yes you can done this with use of hook_entity_property_info_ alter().

/**
 * Implement phponwebsites_get_nodecountviews_nid()
 *
 * Return count of views by particular nid
 */
function phponwebsites_get_nodecountviews_nid($ nid) {
  $result = db_query("SELECT COUNT(*) as count FROM {nodeviewcount} WHERE nid=:nid", array(':nid' => $nid))->FetchAssoc();
  return $result['count'];
}

/**
 * Implements hook_entity_property_info_ alter().
 */
function phponwebsites_entity_property_info_ alter(&$info) {
  $info['node']['properties'][' is_nodeviewcount'] = array(
    'type' => 'integer',
    'label' => t('Node view count'),
    'description' => t("Number of views."),
    'sanitized' => TRUE,
    'getter callback' => 'phponwebsites_get_is_nodeviewcount_ callback',
  );
}

/**
 * Implement phponwebsites_get_is_nodeviewcount_ callback()
 */
function phponwebsites_get_is_nodeviewcount_ callback($item) {
  $count = phponwebsites_get_nodecountviews_nid( $item->nid);
  $total = (int) $count;
  return $total;
}


          After added above code into your custom module, go to /admin/config/search/search_ api/index/default_node_index/ fields. Now you could see new custom field is displayed as in below images.

Add custom fields to search API solr index in Drupal 7

         Now you can add your custom field into search api solr index and index that field. The custom field is listed in views add field section. Now you can add custom field into search results.

Related articles:
Add new menu item into already created menu in Drupal 7
Add class into menu item in Drupal 7 Create menu tab programmatically in Drupal 7
Redirect users after login in Drupal 7
Apr 21 2016
Apr 21
    This blog describes how to hide "Promoted to front page" and "Sticky at top of lists" options from node form in drupal 7. When adding or editing a node, you can see "Publishing options" at bottom of the page which contains 'Published', 'Promoted to front page' and 'Sticky at top of lists' checkbox options. It should look like below image:

Promoted to front page & Sticky at top of lists in Drupal 7
       The "Published" option is used to publish the content. The "Promoted to front page" option is used to display content in the front page. The 'Sticky at top of lists' option is used to keep the content sticked to the top of front page. If you don't want to show "Promoted to front page" and "Sticky at top of lists" options, then you can hide those options using hook_form_alter(), hook_form_FORM_ID_alter() and hook_form_BASE_FORM_ID_alter().

Hide Promoted to front page & Sticky at top of lists options in single node form:


       If you want to hide "Promoted to front page" and "Sticky at top of lists" options only in single node form, then you can remove those options from node form using either hook_form_alter() or hook_form_FORM_ID_alter() in drupal 7.  For example, we go to hide those options from article node form.
/**
 * Implement hook_form_alter().
 */
function phponwebsites_form_alter(&$form, &$form_state, $form_id) {
  // to hide promoted to front page option
  if (isset($form['options']['promote'])) {
    $form['options']['promote']['#access'] = FALSE;
  }

  // to hide sticky at top of lists option
  if (isset($form['options']['sticky'])) {
    $form['options']['sticky']['#access'] = FALSE;
  }
}


     Now you go to article node form and check whether "Promoted to front page" and "Sticky at top of lists" options are hidden or not. You couldn’t see those options in article node form. It should look like below image:
Hide Promoted to front page & Sticky at top of lists in drupal 7

Hide Promoted to front page & Sticky at top of lists options in multiple node forms:


     If you want to hide "Promoted to front page" and "Sticky at top of lists" options in all node forms, then you can remove those options using  hook_form_BASE_FORM_ID_alter() in drupal 7.
/**
 * Implement hook_form_BASE_FORM_ID_alter().
 */
function phponwebsites_form_node_form_alter(&$form, &$form_state, $form_id) {
  // to hide promoted to front page option
  if (isset($form['options']['promote'])) {
    $form['options']['promote']['#access'] = FALSE;
  }

  // to hide sticky at top of lists option
  if (isset($form['options']['sticky'])) {
    $form['options']['sticky']['#access'] = FALSE;
  }
}


     Now you could not see those options in all node forms. Now you know how to hide "Promoted to front page" and "Sticky at top of lists" options from node form in drupal 7. Related articles:
Add new menu item into already created menu in Drupal 7
Add class into menu item in Drupal 7
Create menu tab programmatically in Drupal 7
Add custom fields to search api index in Drupal 7
Redirect users after login in Drupal 7
Apr 21 2016
Apr 21
   This blog describes about how to login using both email and username in Drupal 7. All of you know we could login using only username in Drupal 7.

Login using mail address and usename in Drupal 7

       I've tried to login using email without any contrib modules. Finally i got the code. First alter form to add custom form validation. In custom form validation, get the name from user table by email and set that value into name field in form.  Let see the code:

<?php
/**
 * Implement hook_form_alter().
 */
function phponwebsites_form_alter(&$form, &$form_state, $form_id) {

  if ($form_id == "user_login" || $form_id == "user_login_block") {
    $form['name']['#title'] = t('Username or E-mail Address');
    // Ensure a valid validate array.
    $form['#validate'] = is_array($form['#validate']) ? $form['#validate'] : array();
    // login using username or email address
    array_unshift($form['#validate'],'phponwebsites_user_login_validate');
  }
}

 /**
 * Implement phponwebsites_user_login_validate()
 *
 * Return name by its email address
 */
function phponwebsites_user_login_validate($form, &$form_state) {
  if (isset($form_state['values']['name']) && strpos($form_state['values']['name'], '@') !== false) {
      $name = db_query("SELECT name FROM {users} WHERE LOWER(mail) = LOWER(:name)", array(':name' => $form_state['values']['name']))->fetchField();
    }
  if (isset($name)) {
    form_set_value($form['name'], $name, $form_state);
  }
}


   Now you can login using both username and email. I've hope you know how to login using both username and email in Drupal 7.

Related articles:
Add new menu item into already created menu in Drupal 7
Add class into menu item in Drupal 7
Create menu tab programmatically in Drupal 7
Add custom fields to search api index in Drupal 7
Clear views cache when insert, update and delete a node in Drupal 7
Create a page without header and footer in Drupal 7
Redirect users after login in Drupal 7
Apr 21 2016
Apr 21
This blog describes how to clear views cache while inserting, updating and deleting a node in Drupal 7. If we want to improve site performance, then views caching is one of the options.

   For example, you have views which display list of records. It will update occasionally. Then we can render views data from cache rather than server if we set cache for views. We can set views cache at its settings page. Suppose you have cached views for 5 mins. Then it didn't display updated data until 5 mins even if new node is added to that views. It displays updated data only after 5 mins because the views is cached for 5 mins. In that situation, the user can't view new data in cached views. So we need to clear views cache when add , update and delete a node. So only we can see new data in views and also data is rendered from cache.

Clear views cahce when insert, update and delete a node in drupal 7

Clear views cache when insert a new node in Drupal 7:

   The newly added node has not been displayed in views list if the cache is applied to a views. So we need to clear views cache when insert a new node using hook_node_insert(). Lets see the code for clear views cache while inserting a node:

 <?php
 /**
  * Imeplement hook_node_insert().
  */
 function phponwebsites_node_insert($node) {
   if ($node->type == 'tasks') {
     //clear views cache
     $viewsname = 'activity';
     cache_clear_all($viewsname, 'cache_views_data', TRUE);
   }
 }

Clear views cache when update a node in Drupal 7:

   When you tried to update a node, the updated data in that node has not been displayed in views. So we need to clear views cache when update a node using hook_node_update(). Lets see the code for clear views cache while updating a node:

 <?php
 /**
  * Imeplement hook_node_update().
  */
 function phponwebsites_node_update($node) {
   if ($node->type == 'article') {
     //clear views cache
     $viewsname = 'articles';
     cache_clear_all($viewsname, 'cache_views_data', TRUE);
   }
 }

Clear views cache when delete a node in Drupal 7:

   After delete a node, you could see the deleted node is displayed in the views. So we need to clear views when delete a node using hook_node_delete(). Lets see the code for clear views cache while deleting a node:

 <?php
 /**
  * Imeplement hook_node_delete().
  */
 function phponwebsites_node_delete($node) {
   if ($node->type == 'article') {
     //clear views cache
     $viewsname = 'articles';
     cache_clear_all($viewsname, 'cache_views_data', TRUE);
   }
 }
   You can see the performance of views page will be increased and you can see changes in your views. Now I've hope you know how to clear views cache when insert, update and delete a node in Drupal 7.

Related articles:
Add new menu item into already created menu in Drupal 7
Add class into menu item in Drupal 7
Create menu tab programmatically in Drupal 7
Add custom fields to search api index in Drupal 7
Login using both email and username in Drupal 7
Create a page without header and footer in Drupal 7
Redirect users after login in Drupal 7
Apr 21 2016
Apr 21
    This blog describes about create only page contents without header and footer in Drupal 7. All of you know almost all of the pages in Drupal have header and footer. Suppose you want to create a page without header and footer in Drupal 7. Is it possible? Yes, it is possible in Drupal 7. You can create a page without header and footer using 'delivery callback' in hook_menu.

Render a page without header and footer in Drupal 7:


     Drupal provide a option to create page without header and footer. Let see the below code for render a page without header and footer in Drupal 7.

/**
 * Implement hook_menu().
 */
function phponwebsites_menu() {
  $items['sample-wo-header-footer'] = array(
    'title' => 'A page without header and footer in Drupal 7',
    'access callback' => TRUE,
    'page callback' => 'phponwebsites_without_header_footer',
    'type' => MENU_CALLBACK,
    'delivery callback' => 'deliver_plain',
  );
  return $items;
}

function deliver_plain($page_callback_result) {
  print $page_callback_result;
}

/**
 * Implement phponwebsites_without_header_footer().
 */
function phponwebsites_without_header_footer() {
  return 'This is the page without header and footer';
}

   You could see the page without any header and footer when you view page in a browser. Now I've hope you how to render a page without header and footer in Drupal 7.


Related articles:
Add new menu item into already created menu in Drupal 7
Add class into menu item in Drupal 7
Create menu tab programmatically in Drupal 7
Add custom fields to search api index in Drupal 7
Login using both email and username in Drupal 7
Clear views cache when insert, update and delete a node in Drupal 7
Redirect users after login in Drupal 7

Pages

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web