Nov 06 2018
Nov 06

Pattern Lab (PL), a commonly known pattern library, is an open-source project to generate a design system for your site. In the last two years it has gotten a lot of attention in the Drupal community. It's a great way to implement a design system into your front-end workflow.

The following post describes how our client (the City and County of San Francisco) began to implement a pattern library that will eventually be expanded upon and re-used for other agency websites across the SF.gov ecosystem.

USWDS.

Using the U.S. Web Design System (USWDS), until their own pattern library was ready for prime time, was a client requirement.

USWDS uses a pattern library system called Fractal. I think Fractal is a great idea, but it lacked support for Twig, Twig is the template engine used by Drupal. Fractal out of the box uses Handlebars (templating engine in JavaScript), and thought the template language in Fractal can be customized I wasn’t able to make it work with Twig macros and iterations even with the use of twig.js

Creating the Pattern Lab

Ultimately, I decided to start from scratch. In addition to the USWDS requirement, the client also needed to be able to reuse this pattern library on other projects. I used the Pattern Lab Standard Edition for Twig, among other things this means that you need PHP in the command line in order to "compile" or generate the pattern library.

I added a gulpfile that was in charge of watching for changes in the PL source folders. Once a Twig, Sass or JavaScript file was changed, the pattern library was re-generated.

Generating the Pattern Library

I also needed Gulp to watch the file changes.

The following is a SIMPLE example of the Gulp task that generates the PL watching the folders. the following code snippet shows the config object containing an array of the folder directories.


{
  "css": {
    "file" : "src/sass/_all.scss",
    "src": [
      "pattern-lab/source/_patterns/*.scss",
      "pattern-lab/source/_patterns/**/*.scss",
      "pattern-lab/source/scss/*.scss"
    ],
    "pattern_lab_destination": "pattern-lab/public/css",
    "dist_folder": "dist/css"
  },
  "js": {
    "src": [
      "pattern-lab/source/js/*.js",
      "pattern-lab/source/js/**/*.js"
    ]
  }
}

And the following one is a common watcher in gulp:


gulp.task('watch', function () {
    gulp.watch(config.js.src, ['legacy:js']);
    gulp.watch(config.css.src, ['pl:css']);
    gulp.watch(config.pattern_lab.src, ['generate:pl']);
    gulp.watch(config.pattern_lab.javascript.src, ['generate:pl']);
});


The following task is in charge of generating the pattern library with PHP:


gulp.task('pl:php', shell.task('php pattern-lab/core/console --generate'));

Please NOTE that this is an oversimplified example.

Sass

Having generated the Pattern Library, I figured out that in order to use this Pattern Lab into my Drupal theme, I needed to generate a single CSS file and single JavaScript (JS) file.
The main Sass file imports all Sass code from USWDS by using the `@import` statement.
I imported the source Sass code from USWDS which I required with npm and imported  the source file directly from the node_modules folder:



//pattern-lab/source/scss/components.scss

// Styles basic HTML elements
@import '../../../node_modules/uswds/src/stylesheets/elements/buttons';
@import '../../../node_modules/uswds/src/stylesheets/elements/embed';

Then I imported the scss files that were inside my pattern elements:

// Styles inside patterns.
@import "../_patterns/00-protons/*.scss";
@import "../_patterns/01-atoms/**/*.scss";
@import "../_patterns/02-molecules/**/*.scss";
@import "../_patterns/03-organisms/**/*.scss";
@import "../_patterns/04-templates/**/*.scss";
@import "../_patterns/05-pages/**/*.scss";


All the styles were dumped into a single file called components.css

Having this single CSS file created I was able to use USWDS CSS classes along with the new ones.
I had to add a /dist folder where the transpiled Sass would live and be committed for later use in the Drupal theme.

JavaScript

I did something similar for JavaScript. The biggest challenge was to compile the USWDS JavaScript files exactly as they were. I resorted to copying all the source for the JavaScript into the src folder of the pattern library and set a watcher specifically for the USWDS JavaScript, and added another watcher for the new Pattern Lab JavaScript:

Example:

In the following example I compile all the JS that lives inside the components into a single file.

Then the resulting file is copied  to: ./pattern-lab/public/js which is the folder that reads the Pattern Lab when working on Pattern Lab only.
The other copy of the file goes to the distribution folder ./dist/pl/js which is the one I use in my Drupal theme.


// Component JS.
// -------------------------------------------------------------------- //
// The following task concatenates all the JavaScript files inside the
// _patterns folder, if new patterns need to be added the config.json array
// needs to be edited to watch for more folders.

gulp.task('pl:js', () => {
    return gulp.src(config.pattern_lab.javascript.src)
        .pipe(sourcemaps.init())
        .pipe(babel({
            presets: ['es2015']
        }))
        .pipe(concat("components.js"))
        .pipe(sourcemaps.write())
        .pipe(gulp.dest('./pattern-lab/public/js'))
        .pipe(gulp.dest('./dist/pl/js'));
});

The resulting files:


--/dist/
--/dist/css/components.css
--/dist/js/components.js

Were included the HEAD of my Pattern Lab by editing pattern-lab/source/_meta/_00-head.twig 

I included the following lines:


<link rel="stylesheet" href="https://www.chapterthree.com/blog/decoupling-pattern-lab-from-your-theme-a-city-of-san-francisco-project/../../css/components.css" media="all">
<script src="https://www.chapterthree.com/blog/decoupling-pattern-lab-from-your-theme-a-city-of-san-francisco-project/../../js/dist/uswds.min.js"></script>
<script src="https://www.chapterthree.com/blog/decoupling-pattern-lab-from-your-theme-a-city-of-san-francisco-project/../../js/dist/components.js"></script>

Please refer to the repo if you need the details of the integration: GitHub - SFDigitalServices/sfgov-pattern-lab: SFGOV Pattern Lab

Integrating the Pattern Lab with Drupal.

Composer and libraries:

I used the following plugin:


composer require oomphinc/composer-installers-extender

This plugin allowed me to put the pattern library in a folder different than vendor
Then I added some configuration to the composer.json

Under extra I specified where composer should install the repository of type github:


"extra": {
        "installer-paths": {
            "web/libraries/{$name}": ["type:github"],
          }

Then under repositories I set the type:github


"repositories": {
        "github": {
            "type": "package",
            "package": {
                "name": "sf-digital-services/sfgov-pattern-lab",
                "version": "master",
                "type": "drupal-library",
                "source": {
                    "url": "https://github.com/SFDigitalServices/sfgov-pattern-lab.git",
                    "type": "git",
                    "reference": "master"
                }
            }
        }
    }

and required the package under require: As you can see the name matches the name in the previously declared github repo:


"require": {
   "sf-digital-services/sfgov-pattern-lab": "dev-master",
}

A composer update should clone the github repo and place the Pattern Lab inside relative to the Drupal web folder:

/web/libraries/sfgov-pattern-lab

Components Libraries

The Component Libraries module was especially important because it allowed me to map the Pattern Lab components easily into my theme.

Then I had to map my Pattern Lab components with the Drupal theme:

The Drupal Theme:

I created a standard Drupal theme:

The sfgovpl.info.yml file:

In the following part of the sfgovpl.info.yml file I connected the Pattern Lab Twig files to Drupal:


component-libraries:
  protons:
    paths:
      - ../../../libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/00-protons
  atoms:
    paths:
      - ../../../libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/01-atoms
  molecules:
    paths:
      - ../../../libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/02-molecules
  organisms:
    paths:
      - ../../../libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/03-organisms
  templates:
    paths:
      - ../../../libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/04-templates
  pages:
    paths:
      - ../../../libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/05-pages

libraries:
  - sfgovpl/sfgov-pattern-lab

The sfgovpl.libraries.yml file:

In the last line of the previous code example, you can see that I required sfgov-pattern-lab library,  which will include the files compiled by Gulp into my Pattern Lab.


sfgov-pattern-lab:
  css:
    base:
      '/libraries/sfgov-pattern-lab/dist/css/components.css': {}
  js:
    '/libraries/sfgov-pattern-lab/dist/pl/js/components.js': {}

Using the Twig templates in our theme:

The following is an example of how to use a molecule from the pattern library into the Drupal theme:

You can include the @molecules/08-search-results/03-topic-search-result.twig twig like this:

Pattern Lab twig:

node--topic--search-index.html.twig


<div class="topic-search-result">
<div class="topic-search-result--container">
<div class="content-type"><i class="sfgov-icon-filefilled"></i><span>{{ content_type }}</span></div>
<a class="title-url" href="https://www.chapterthree.com/blog/decoupling-pattern-lab-from-your-theme-a-city-of-san-francisco-project/{{ url }}"><h4>{{ title }}</h4></a>
<p class="body">{{ body|striptags('<a>')|raw }}</p>
</div>
</div>

Drupal template:

The following example calls the Pattern lab molecule originally located at: web/libraries/sfgov-pattern-lab/pattern-lab/source/_patterns/02-molecules/08-search-results/03-topic-search-result.twig but thanks to the Components module we just call it as: @molecules/08-search-results/03-topic-search-result.twig


{# Set variables to use in the component. #}
{% set url = path('entity.node.canonical', {'node': elements['#node'].id()  }) %}
{% set description = node.get('field_description').getValue()[0]['value'] %}
{% set type = node.type.entity.label %} {# content type #}
{# Icluding the molecule in our Pattern Lab.#}

{% include "@molecules/08-search-results/03-topic-search-result.twig" with {
  "content_type": type,
  "url": url,
  "title": elements['#node'].get('title').getString(),
  "body": description
} %}

Recommendations

SFGOV Pattern Lab, Initial development was made in large part for Chapter Three and this post is intended to show the core concepts of decoupling your Pattern Lab from your Drupal theme.

You can find the full code implementation for the Pattern library and Drupal in the following Urls:

SFDigitalServices/sfgov and the Pattern Lab here: SFDigitalServices/sfgov-pattern-lab

You should try the value module, it is great for extracting values in the Drupal twig templates and connect them with your Pattern Lab twig templates.

Give a try to the UI Patterns module, looks promising and a great solution for decoupled Pattern Libraries.

May 14 2018
May 14

Commenting system giant Disqus powers reader conversations on millions of sites, including large publishers like Rolling Stone and the Atlantic. So when Disqus quietly introduced ads into their free plans last year, there was some understandable frustration.

Why did @disqus just add a bunch of ads to my site without my permission? https://t.co/CzXTTuGs67 pic.twitter.com/y2QbFFzM8U

— Harry Campbell (@TheRideshareGuy) February 1, 2017

I might be late but now getting tacky ads injected into my comments on my site so saying goodbye @disqus . I refuse to pay for an ad-free experience when it has been free all this time. #baitedandswitched

— KenyaRae (@IamKenyaRae) April 18, 2018 React comments is responsive and slick on mobile devices, as shown here on an iPhone X.

Around this time, one of our clients, Military.com, decided to move their content out of Disqus, not only to avoid licensing fees to avoid ads, but also so they could own their comment content. Aside from those two issues, Military.com was satisfied with the user experience of Disqus. An interactive component like comments presented a perfect opportunity to progressively decouple using React.js. So we began work on the React Comments module.

React Comments is a drop-in replacement for Drupal Core’s Comment module. It has all the same features, but with a few added benefits. Because it was built using React, user interactions appear on the page immediately, with no page refresh needed.

Editors can moderate comments in context from the node’s front-end display, rather than digging through the Drupal Admin UI. And regular users can flag comments as inappropriate, too, so you can identify repeat offenders and block them from commenting. Coupled with the Comment Admin Notify module, site administrators can get emails about new comments on their site, so they can take action right away.

When looking at the front-end, anyone can flag a comment as inappropriate. Then, through the same interface, privileged users will be able to delete problematic comments as needed.

And, of course, there are no ads.

Military.com has been using this module on their site since launch in December 2017. With over 10 million active users, they amassed over two thousand comments within just a couple weeks of launch.

Our site uses it as well, as you can see right on this blog post!

In the future, we’re looking to add the ability to improve the theming experience by including an option to wrap the comments section in an iframe with an optional custom stylesheet. Also, setting up notifications for commenters who receive replies is on the horizon.

Try the React Comments module on your site and see what you think. We’d love to get a comment from you with your thoughts on it!

Apr 26 2018
Apr 26

Most corporate websites have a career section where people can find out what jobs are open. These sections can range from just a few standing openings to hundreds of vacant positions across multiple job types that open and close frequently. Larger companies traditionally use HR software systems to manage this ebb and flow.

One popular and powerful HR system is Taleo. Many companies simply link offsite to the taleo.com listing, which serves as the company’s only place to see and apply to openings. Other times, that same listing gets embedded on the company’s career section. As you can see in the user journey below for our client MemorialCare, this got the job done, but didn’t really convey a company’s brand or culture.

Step 1: Candidate finds the careers section of MemorialCare’s website. Step 2: Candidate is linked to the unbranded MemorialCare Taleo job listing.

 

Step 3: Candidate sees the job description on the unbranded Taleo site, and decides whether or not to apply.

You need a delightful candidate user journey to attract amazing candidates, and this begins with a branded careers section. So, we are giving Taleo customers the ability to display their job postings within their Drupal site, and branding them to match.

MemorialCare saw a dramatic improvement when we redesigned their careers section as a separate website and built an integration with Taleo. The result is a beautiful candidate journey that captures MemorialCare’s brand and culture.

Step 1: Candidates can begin searching by location and job title right away, without clicking additional obscure links. Step 2: Searching will produce a list of jobs. Step 3: A detail page gives you an easy to read overview of what to expect and a clear link to apply.

With this Taleo integration, the HR department has full control of the look and feel for their careers content. If they desire a change, there’s no need to contact Taleo– their developers (or Chapter Three, if they prefer!) can make the change right away. When job descriptions or details are updated in Taleo, it automatically gets updated on the new careers site, too! And, Taleo meta-tags give Drupal job listings an extra SEO boost. Jobs listed on the new site are ready to be ingested by a variety of job aggregators.

The way you present your organization matters to prospective talent. Building a Taleo integration with your Drupal site and making it look beautiful will give your organization a competitive edge with quality candidates. If this is something your company is currently struggling with, please feel free to contact us to find out how we can help. 

Apr 11 2018
Apr 11

I presented at DrupalCon Nashville about working with the City of San Francisco to make a better transaction experience for residents. Moving beyond a simple content site where we tell users how to do things, we are now developing a brand new city website in Drupal 8, where residents can actually do those things online. The presentation covers how to run an agile project of this scale in a government environment, what we did as a part of discovery, where we're going, and how our foray into design & development is progressing so far.

Here are the slides to digest. Don't hesitate to reach out if you have any questions!

Mar 19 2018
Mar 19

With all the changes in Drupal 8, it’s no wonder the landscape for access control modules is adapting. While the port of Organic Groups has started, there are several major issues to resolve before it’s ready for use. There are, however, a couple promising new options.

Permissions By Term

If you don’t need group management, Permissions by Term is the Drupal 8 substitute for the Taxonomy Access Control module. It controls node visibility based on the how a node is tagged. Term access is granted by role, and individual users can be whitelisted for term access permissions.

Each term allows you to select by role who can access it from the taxonomy term edit page.

 

Moderation

If your content has drafts or other moderation states, this module will play nicely. Permissions by Term handles access control to published nodes, while moderation modules control access to unpublished moderation states.

Multilingual

This module works with translations if all translations have the same "access control" (they are tagged with same terms). If not, the latest translation of the node gets saved to the node access table, and the other translations get lost. There is work in progress on this issue. 

Shortcomings

  • At present, this module does not control edit and delete permissions since it only works for published content. 
  • This is not the right module if content and users need to be organized by groups. It can be used, but inefficiently, as it would require each user group to be a role, resulting in duplication of "term group A" and role "group A". A workaround could be to whitelist the users to the term, but that might create overhead in the node_access table. If this is a requirement, the Group module is a better choice. 

Technical Overview

Setup is very straightforward: Enable it and configure role access for each taxonomy term on the term edit page. This module uses the node grants API and has 2 custom tables. The codebase looks modern and contains tests.  Node grants built for the current user object list every node the user can access, which could be thousands, and does not seem efficient. This might require attention if your site will has many nodes.

Group

What it does

Group allows content to be organized into groups that users can join. Access is controlled by group roles. The default roles are anonymous, outsider (logged-in but not member), and member, administrators can create additional group roles. An optional module provides out of the box CRUD access control for nodes. 

Group provides an interface to show you what belongs to it.

 

Moderation

The core Content Moderation module permissions take precedence for editing (not for viewing/deleting) so site editors can edit all content even if they are not members or editors of a group. This potentially unexpected behavior presents a moderately complex issue that probably has a custom solution for each site’s unique set of business rules. This, and other similar conflicts, can be solved with a custom module defining the permissions priorities.

Multilingual

This works module works ok with translations. All translations automatically belong to the same groups as the original node and this cannot be changed.

Shortcomings

  • We didn’t find a straightforward way to add a node to a group through the node/add form. Instead, we needed to create the node through a "create content" link in the group page, or manually associate it on another form.
  • If groups are required to control access to nodes, this module works as-is. Associating non-node entities with groups for access control is possible, but requires additional work.

Technical Overview

This module comes with good documentation for site builders and developers. It’s user interface looks similar to Organic Groups, but seems more intuitive. Site administrators can create multiple “group types,” and all groups, group types, user/group memberships, and content/group relationships are fieldable entities (meaning they get all the entity goodness!). Group roles and permissions are configurable per group type, and the module uses the node grants API.

Summary

Drupal 8’s architecture changes mean we may say goodbye to some beloved (and not-so-beloved) module versions. This creates an opportunity for our community to revisit how we solve problems, and better serve our users. Permissions by Term and Group are another indicator that the Drupal 8 community is alive, well and, perhaps most importantly, adapting to change.

Jan 29 2018
Jan 29

Dreditor is a beloved and indispensable tool, in the form of a browser extension, that enhances project issue pages on Drupal.org. When it comes to reviewing patches, it turns what would just be a plain text file into a feature rich interface for reviewing patches, allowing users to easily select and comment on lines of code, which then get pasted into the comment form, as properly formatted HTML.

I've been lucky to have some extra community time here at Chapter Three over the past couple of weeks. While perusing the core issue queue, I decided to resurrect an old User Style I created for Dreditor back in 2009, and give Dreditor a little refresh. You can install the Dreditor Refresh style with Stylish here. Make sure you also have Dreditor installed, and have logged into Drupal.org.

Happy contributing! Enjoy!

Screenshot of Dreditor Refresh while reviewing a patch

UPDATE: Work is happing to implement the enhancements Dreditor brings to Drupal.org directly, which is great.  You can follow along and help out here: https://www.drupal.org/node/1673278

Oct 16 2017
Oct 16

Last week at DrupalCamp Quito, I presented an updated, Spanish-language version of my DrupalCon session. If you would like to view the presentation in English, you can find it on my DrupalCon blog post.

Las estructuras orientadas a objetos han reemplazado a nuestros queridos "hooks" que nos permitían extender Drupal con nueva funcionalidad sin necesidad de hackear core (u otros módulos de contrib). Pero, ¿cómo funciona esto? En esta charla revisamos cómo extender un módulo para implementar single sign-on (SSO), y al hacerlo nos adentramos a cómo la programación orientada a objetos hace magia en nuestros módulos, haciéndolos más fáciles de escribir, entender y depurar. Adicionalmente, se describen algunos de los patrones de diseño de Drupal, cómo utilizar event listeners, sobreescribir rutas y otras herramientas.

[embedded content]

Slides: https://goo.gl/QgskXw
Código de ejemplo: https://github.com/arlina-espinoza/openid_fb

Sep 07 2017
Sep 07

Recently I had to generate term-specific aliases (aliases that are different from the default alias pattern set for Article entities). This is how to do it:

1. Enable the Pathauto module
2. Set the default URL alias pattern for your content type in order to fire the hook
3. Implement hook_pathauto_alias_alter() in your .module file.

Example module structure:

mymodule/
  - mymodule.info.yml
  - mymodule.module
  - src/
    - ArticlePathAlias.php

I like to keep .module clean and simple and because of that I store the main logic in src/ArticlePathAlias.php file.

The mymodule.info.yml this is just a regular .info file.

4. Add the following to your mymodule.module file:

use Drupal\mymodule\ArticlePathAlias;

/**
 * Implements hook_pathauto_alias_alter().
 */
function mymodule_pathauto_alias_alter(&$alias, array &$context) {
  if ($new_alias = (new ArticlePathAlias())->generate($context)) {
    $alias = $new_alias;
  }
}

5. Add the following to your src/ArticlePathAlias.php file:

<?php

namespace Drupal\mymodule;

use Drupal\Component\Utility\Html;
use Drupal\taxonomy\Entity\Term;

/**
 * Generate URL aliases for articles.
 */
class ArticlePathAlias {

  protected $terms = [
    'Term name 1' => 'custom/alias',
    'Term name 2' => 'custom2/alias',
    'Term name 3' => 'custom3/alias',
  ];
  protected $pattern = '/%term%/%year%/%monthnum%/%day%/%postname%';

  public function generate($context) {
    if ($context['bundle'] === 'article' && ($context['op'] == 'insert' || $context['op'] == 'update')) {
      return $this->assembleAlias($context['data']['node']);
    }
  }

  protected function assembleAlias($entity) {
    $date = new \DateTime(date('c', $entity->getCreatedTime()));
    $parameters = [
      '%year%'     => $date->format('Y'),
      '%monthnum%' => $date->format('m'),
      '%day%'      => $date->format('d'),
      '%postname%' => Html::cleanCssIdentifier($entity->getTitle()),
      '%term%'     => $this->findTermAlias($entity),
    ];
    if (!empty($parameters['%term%'])) {
      return str_replace(array_keys($parameters), array_values($parameters), $this->pattern);
    }
  }

  protected function findTermAlias($entity) {
    // Make sure to change `field_keywords` to the field you would like to check.
    if ($keywords = $entity->get('field_keywords')->getValue()) {
      foreach ($keywords as $data) {
        $term = Term::load($data['target_id']);
        $name = $term->getName();
        if (in_array($name, array_keys($this->terms))) {
          return $this->terms[$name];
        }
      }
    }
  }

}

The code above will generate /%term%/%year%/%monthnum%/%day%/%postname% alias or (/custom/alias/2017/07/21/test-title) depending on the term.

Make sure you change field_keywords to your own taxonomy term reference field. Also change $context['bundle'] === 'article' to entity type that will trigger custom alias.

Aug 26 2017
Aug 26

Sometimes you might want to display additional data in the autocomplete results, for instance add content language next to the title, or display entity type or any other related data. In this blog post I will demonstrate how to alter suggestions in autocomplete fields in Drupal 8. The project is available for download from github, see the link at the bottom of the page.

Link autocomplete results

Here is the module structure I will be using:

alter_entity_autocomplete/
  - alter_entity_autocomplete.info.yml
  - alter_entity_autocomplete.services.yml
  - src/
    - EntityAutocompleteMatcher.php
    - Controller/
      - EntityAutocompleteController.php
    - Routing/
      - AutocompleteRouteSubscriber.php

Contents of the alter_entity_autocomplete.info.yml file:

name: Alter Entity Autocomplete
description: The module alters entity autocomplete suggestion list.
type: module
core: 8.x

Contents of the alter_entity_autocomplete.services.yml file:

services:

  alter_entity_autocomplete.route_subscriber:
    class: Drupal\alter_entity_autocomplete\Routing\AutocompleteRouteSubscriber
    tags:
      - { name: event_subscriber }

  alter_entity_autocomplete.autocomplete_matcher:
    class: Drupal\alter_entity_autocomplete\EntityAutocompleteMatcher
    arguments: ['@plugin.manager.entity_reference_selection']

Contents of the src/EntityAutocompleteMatcher.php file. This is the file where you would change the output of the sugesstions/autocomplete results:

<?php

namespace Drupal\alter_entity_autocomplete;

use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Tags;

class EntityAutocompleteMatcher extends \Drupal\Core\Entity\EntityAutocompleteMatcher {

  /**
   * Gets matched labels based on a given search string.
   */
  public function getMatches($target_type, $selection_handler, $selection_settings, $string = '') {

    $matches = [];

    $options = [
      'target_type'      => $target_type,
      'handler'          => $selection_handler,
      'handler_settings' => $selection_settings,
    ];

    $handler = $this->selectionManager->getInstance($options);

    if (isset($string)) {
      // Get an array of matching entities.
      $match_operator = !empty($selection_settings['match_operator']) ? $selection_settings['match_operator'] : 'CONTAINS';
      $entity_labels = $handler->getReferenceableEntities($string, $match_operator, 10);

      // Loop through the entities and convert them into autocomplete output.
      foreach ($entity_labels as $values) {
        foreach ($values as $entity_id => $label) {

          $entity = \Drupal::entityTypeManager()->getStorage($target_type)->load($entity_id);
          $entity = \Drupal::entityManager()->getTranslationFromContext($entity);

          $type = !empty($entity->type->entity) ? $entity->type->entity->label() : $entity->bundle();
          $status = '';
          if ($entity->getEntityType()->id() == 'node') {
            $status = ($entity->isPublished()) ? ", Published" : ", Unpublished";
          }

          $key = $label . ' (' . $entity_id . ')';
          // Strip things like starting/trailing white spaces, line breaks and tags.
          $key = preg_replace('/\s\s+/', ' ', str_replace("\n", '', trim(Html::decodeEntities(strip_tags($key)))));
          // Names containing commas or quotes must be wrapped in quotes.
          $key = Tags::encode($key);
          $label = $label . ' (' . $entity_id . ') [' . $type . $status . ']';
          $matches[] = ['value' => $key, 'label' => $label];
        }
      }
    }

    return $matches;
  }

}

Contents of the src/Controller/EntityAutocompleteController.php file:

<?php

namespace Drupal\alter_entity_autocomplete\Controller;

use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\alter_entity_autocomplete\EntityAutocompleteMatcher;
use Symfony\Component\DependencyInjection\ContainerInterface;

class EntityAutocompleteController extends \Drupal\system\Controller\EntityAutocompleteController {

  /**
   * The autocomplete matcher for entity references.
   */
  protected $matcher;

  /**
   * {@inheritdoc}
   */
  public function __construct(EntityAutocompleteMatcher $matcher, KeyValueStoreInterface $key_value) {
    $this->matcher = $matcher;
    $this->keyValue = $key_value;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('alter_entity_autocomplete.autocomplete_matcher'),
      $container->get('keyvalue')->get('entity_autocomplete')
    );
  }

}

Here is contents of the src/Routing/AutocompleteRouteSubscriber.php file:

<?php

namespace Drupal\alter_entity_autocomplete\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;

class AutocompleteRouteSubscriber extends RouteSubscriberBase {

  public function alterRoutes(RouteCollection $collection) {
    if ($route = $collection->get('system.entity_autocomplete')) {
      $route->setDefault('_controller', '\Drupal\alter_entity_autocomplete\Controller\EntityAutocompleteController::handleAutocomplete');
    }
  }

}

This module was developed with help of my co-workers and I hope you will find this useful.

Download from Github

Aug 25 2017
Aug 25

Sometimes you might want to display additional data in the autocomplete results, for instance add content language next to the title, or display entity type or any other related data. In this blog post I will demonstrate how to alter suggestions in autocomplete fields in Drupal 8. The project is available for download from github, see the link at the bottom of the page.

Link autocomplete results

Here is the module structure I will be using:

alter_entity_autocomplete/
  - alter_entity_autocomplete.info.yml
  - alter_entity_autocomplete.services.yml
  - src/
    - EntityAutocompleteMatcher.php
    - Controller/
      - EntityAutocompleteController.php
    - Routing/
      - AutocompleteRouteSubscriber.php

Contents of the alter_entity_autocomplete.info.yml file:

name: Alter Entity Autocomplete
description: The module alters entity autocomplete suggestion list.
type: module
core: 8.x

Contents of the alter_entity_autocomplete.services.yml file:

services:

  alter_entity_autocomplete.route_subscriber:
    class: Drupal\alter_entity_autocomplete\Routing\AutocompleteRouteSubscriber
    tags:
      - { name: event_subscriber }

  alter_entity_autocomplete.autocomplete_matcher:
    class: Drupal\alter_entity_autocomplete\EntityAutocompleteMatcher
    arguments: ['@plugin.manager.entity_reference_selection']

Contents of the src/EntityAutocompleteMatcher.php file. This is the file where you would change the output of the sugesstions/autocomplete results:

<?php

namespace Drupal\alter_entity_autocomplete;

use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Tags;

class EntityAutocompleteMatcher extends \Drupal\Core\Entity\EntityAutocompleteMatcher {

  /**
   * Gets matched labels based on a given search string.
   */
  public function getMatches($target_type, $selection_handler, $selection_settings, $string = '') {

    $matches = [];

    $options = [
      'target_type'      => $target_type,
      'handler'          => $selection_handler,
      'handler_settings' => $selection_settings,
    ];

    $handler = $this->selectionManager->getInstance($options);

    if (isset($string)) {
      // Get an array of matching entities.
      $match_operator = !empty($selection_settings['match_operator']) ? $selection_settings['match_operator'] : 'CONTAINS';
      $entity_labels = $handler->getReferenceableEntities($string, $match_operator, 10);

      // Loop through the entities and convert them into autocomplete output.
      foreach ($entity_labels as $values) {
        foreach ($values as $entity_id => $label) {

          $entity = \Drupal::entityTypeManager()->getStorage($target_type)->load($entity_id);
          $entity = \Drupal::entityManager()->getTranslationFromContext($entity);

          $type = !empty($entity->type->entity) ? $entity->type->entity->label() : $entity->bundle();
          $status = '';
          if ($entity->getEntityType()->id() == 'node') {
            $status = ($entity->isPublished()) ? ", Published" : ", Unpublished";
          }

          $key = $label . ' (' . $entity_id . ')';
          // Strip things like starting/trailing white spaces, line breaks and tags.
          $key = preg_replace('/\s\s+/', ' ', str_replace("\n", '', trim(Html::decodeEntities(strip_tags($key)))));
          // Names containing commas or quotes must be wrapped in quotes.
          $key = Tags::encode($key);
          $label = $label . ' (' . $entity_id . ') [' . $type . $status . ']';
          $matches[] = ['value' => $key, 'label' => $label];
        }
      }
    }

    return $matches;
  }

}

Contents of the src/Controller/EntityAutocompleteController.php file:

<?php

namespace Drupal\alter_entity_autocomplete\Controller;

use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\alter_entity_autocomplete\EntityAutocompleteMatcher;
use Symfony\Component\DependencyInjection\ContainerInterface;

class EntityAutocompleteController extends \Drupal\system\Controller\EntityAutocompleteController {

  /**
   * The autocomplete matcher for entity references.
   */
  protected $matcher;

  /**
   * {@inheritdoc}
   */
  public function __construct(EntityAutocompleteMatcher $matcher, KeyValueStoreInterface $key_value) {
    $this->matcher = $matcher;
    $this->keyValue = $key_value;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('alter_entity_autocomplete.autocomplete_matcher'),
      $container->get('keyvalue')->get('entity_autocomplete')
    );
  }

}

Here is contents of the src/Routing/AutocompleteRouteSubscriber.php file:

<?php

namespace Drupal\alter_entity_autocomplete\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;

class AutocompleteRouteSubscriber extends RouteSubscriberBase {

  public function alterRoutes(RouteCollection $collection) {
    if ($route = $collection->get('system.entity_autocomplete')) {
      $route->setDefault('_controller', '\Drupal\alter_entity_autocomplete\Controller\EntityAutocompleteController::handleAutocomplete');
    }
  }

}

This module was developed with help of my co-workers and I hope you will find this useful.

Download from Github

Aug 18 2017
Aug 18

In this post I will show you a technique to fix HTML issues, import images or perform content operations during migrations.

We have to fix source content before most content migrations. This can be challenging if there are many entries in the source database. The powerful Drupal 8 Migration API provides elegant ways to solve this type of problem.

To solve HTML issues, I always create my own process plugin. Here is an example how you would call your own process plugin to fix HTML issues in the body field:

  'field_body/value':
    -
      plugin: fix_html_issues
      images_source: '/minnur/www/source-images'
      images_destination: 'public://body-images/'
      source: post_content
    -
      plugin: skip_on_empty
      method: row

As you can see, I am piling up several process plugins for field_body/value field migration. You may also pass custom parameters to your process plugin (in my example, params are: images_source and images_destination ). You may add any number of process plugins depending on your needs.

Now let's view the plugin code. Please note all of the process plugins are stored in the src/Plugin/migrate/process directory in your migration module.

The plugin imports images into Drupal as media entities and replaces <img> tags with Drupal entity embed tags <drupal-entity data-embed-button="embed_image"></drupal-entity>. Below is the source code of the plugin:

<?php

namespace Drupal\wp_migration\Plugin\migrate\process;

use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\file\FileInterface;
use Drupal\migrate\Row;
use Drupal\media_entity\Entity\Media;
use Drupal\Core\Database\Database;
use Drupal\Component\Utility\Unicode;

/**
 * @MigrateProcessPlugin(
 *   id = "fix_html_issues"
 * )
 */
class FixHTMLissues extends ProcessPluginBase {

  /**
   * {@inheritdoc}
   */
  public function transform($html, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {

    // Values for the following variables are specified in the YAML file above.
    $images_source = $this->configuration['images_source'];
    $destination = $this->configuration['images_destination'];

    preg_match_all('/<img[^>]+>/i', $html, $result);

    if (!empty($result[0])) {

      foreach ($result as $img_tags) {
        foreach ($img_tags as $img_tag) {

          preg_match_all('/(alt|title|src)=("[^"]*")/i', $img_tag, $tag_attributes);

          $filepath = str_replace('"', '', $tag_attributes[2][1]);

          if (!empty($tag_attributes[2][1])) {

            // Create file object from a locally copied file.
            $filename = basename($filepath);

            if (file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {

              if (filter_var($filepath, FILTER_VALIDATE_URL)) { 
                $file_contents = file_get_contents($filepath);
              }
              else {
                $file_contents = file_get_contents($images_source . $filepath);
              }
              $new_destination = $destination . '/' . $row->getSourceProperty('id') . '-' . $filename;

              if (!empty($file_contents)) {

                if ($file = file_save_data($file_contents, $new_destination, FILE_EXISTS_REPLACE)) {

                  // Create media entity using saved file.
                  $media = Media::create([
                    'bundle'      => 'image',
                    'uid'         => \Drupal::currentUser()->id(),
                    'langcode'    => \Drupal::languageManager()->getDefaultLanguage()->getId(),
                    'status'      => Media::PUBLISHED,
                    'field_image' => [
                      'target_id' => $file->id(),
                      'alt'       => !empty($tag_attributes[2][0]) ? Unicode::truncate(str_replace('"', '', $tag_attributes[2][0]), 512) : '',
                      'title'     => !empty($tag_attributes[2][0]) ? Unicode::truncate(str_replace('"', '', $tag_attributes[2][0]), 1024) : '',
                    ],
                  ]);

                  $media->save();
                  $uuid = $this->getMediaUuid($file);
                  $html = str_replace($img_tag, '<p><drupal-entity
                    data-embed-button="embed_image" 
                    data-entity-embed-display="entity_reference:media_thumbnail"
                    data-entity-embed-display-settings="{"image_style":"large","image_link":""}"
                    data-entity-type="media"
                    data-entity-uuid="' . $uuid . '"></drupal-entity>></p>', $html);
                }

              }

            }
          }
        }
      }
    }
    return $html;
  }

  /**
   * Get Media UUID by File ID.
   */
  protected function getMediaUuid(FileInterface $file) {
    $query = db_select('media__field_image', 'f', ['target' => 'default']);
    $query->innerJoin('media', 'm', 'm.mid = f.entity_id');
    $query->fields('m', ['uuid']);
    $query->condition('f.field_image_target_id', $file->id());
    $uuid = $query->execute()->fetchField();
    return $uuid;
  }

}

The process plugin code can get really nasty, and that's fine. Since this could be just a small portion of the overall migration, you don't want spend time to make it look nice and optimized. The best way to improve your code is to write more migrations and optimize it over time.

I hope this was helpful and I would love to hear about your techniques and solutions for content migration issues.

Aug 03 2017
Aug 03

In this post I will show a custom process plugin that I created to migrate taxonomy terms. The plugin handles the creation of new terms and prevents duplicates.

Below is a portion of the migration template. In the example, I am migrating new terms into keywords vocabulary via field_keywords field.

  field_keywords:
    -
      plugin: existing_term
      # Destination (Drupal) vocabulary name
      vocabulary: keywords
      # Source query should return term name
      source: term_name
    -
      plugin: skip_on_empty
      method: row

This is the source code for the process plugin.

<?php

namespace Drupal\my_module\Plugin\migrate\process;

use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\MigrateSkipProcessException;
use Drupal\migrate\Row;
use Drupal\taxonomy\Entity\Term;

/**
 * Check if term exists and create new if doesn't.
 *
 * @MigrateProcessPlugin(
 *   id = "existing_term"
 * )
 */
class ExistingTerm extends ProcessPluginBase {

  /**
   * {@inheritdoc}
   */
  public function transform($term_name, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    $vocabulary = $this->configuration['vocabulary'];
    if (empty($term_name)) {
      throw new MigrateSkipProcessException();
    }
    if ($tid = $this->getTidByName($term_name, $vocabulary)) {
      $term = Term::load($tid);
    }
    else {
      $term = Term::create([
        'name' => $term_name, 
        'vid'  => $vocabulary,
      ])->save();
      if ($tid = $this->getTidByName($term_name, $vocabulary)) {
        $term = Term::load($tid);
      }
    }
    return [
      'target_id' => is_object($term) ? $term->id() : 0,
    ];
  }

  /**
   * Load term by name.
   */
  protected function getTidByName($name = NULL, $vocabulary = NULL) {
    $properties = [];
    if (!empty($name)) {
      $properties['name'] = $name;
    }
    if (!empty($vocabulary)) {
      $properties['vid'] = $vocabulary;
    }
    $terms = \Drupal::entityManager()->getStorage('taxonomy_term')->loadByProperties($properties);
    $term = reset($terms);
    return !empty($term) ? $term->id() : 0;
  }

}

The logic of the plugin is very simple. Please let me know in comments or questions. Also, please share any problems you've had during your content migrations and how you solved them.

Jul 28 2017
Jul 28

WordPress to Drupal 8

In this post I will show you how to migrate thumbnail content from Wordpress to Drupal 8. My goals are to help you better understand the content migration process, give you starting point for future migrations, and teach you how to write process plugins and migration sources. Taxonomy terms and users migration is more straightforward so I won't cover it here.

This migration example contains templates to migrate thumbnails content. For this post, I assume the image/thumbnail field is using the Media module field. I will be using the Migrate drush module to run migrations.

First, make sure to configure your connection in your settings.php file. Add the following with proper credentials:

$databases['migrate']['default'] = [
    'driver'   => 'mysql',
    'database' => 'wordpress_dbname',
    'username' => 'wordpress_dbuser',
    'password' => 'wordpress_dbpassowrd',
    'host'     => '127.0.0.1',
];

Here is the module structure I will be using:

wp_migration/
  - wp_migration.info.yml
  - wp_migration.module
  - migration_templates/
    - wp_content.yml
    - wp_thumbnail.yml
    - wp_media.yml
  - src/
    - Plugin/
      - migrate/
        - process/
          - AddUrlAliasPrefix.php
          - DateToTimestamp.php
        - source/
          - SqlBase.php
          - Content.php
          - Thumbnail.php

Contents of the wp_wordpress.info.yml file:

name: Wordpress Migration
type: module
description: Migrate Wordpress content into Drupal 8.
core: 8.x
dependencies:
  - migrate
  - migrate_drupal
  - migrate_drush

Contents of the migration_templates/wp_thumbnail.yml file:

id: wp_thumbnail
label: 'Thumbnails'
migration_tags:
  - Wordpress
source:
  plugin: wordpress_thumbnail
  # This is WP table prefix  (custom variable)
  # DB table example: [prefix]_posts
  table_prefix: wp
  constants:
    # This path should point ot WP uploads directory.
    source_base_path: '/path/to/source/wp/uploads'
    # This is directory name in Drupal where to store migrated files
    uri_file: 'public://wp-thumbnails'
process:
  filename: filename
  source_full_path:
    -
      plugin: concat
      delimiter: /
      source:
        - constants/source_base_path
        - filepath
    -
      plugin: urlencode
  uri_file:
    -
      plugin: concat
      delimiter: /
      source:
        - constants/uri_file
        - filename
    -
      plugin: urlencode
  uri:
    plugin: file_copy
    source:
      - '@source_full_path'
      - '@uri_file'
  status: 
    plugin: default_value
    default_value: 1
  changed: 
    plugin: date_to_timestamp
    source: post_date
  created: 
    plugin: date_to_timestamp
    source: post_date
  uid: 
    plugin: default_value
    default_value: 1
destination:
  plugin: 'entity:file'
migration_dependencies:
  required: {}
  optional: {}

Contents of the migration_templates/wp_media.yml file:

id: wp_media
label: 'Media'
migration_tags:
  - Wordpress
source:
  plugin: wordpress_thumbnail
  # This is WP table prefix  (custom variable)
  # DB table example: [prefix]_posts
  table_prefix: wp
  constants:
    bundle: image
process:
  bundle: 'constants/bundle'
  langcode:
    plugin: default_value
    default_value: en
  'field_image/target_id':
    -
      plugin: migration
      migration: wp_thumbnail
      source: post_id
    -
      plugin: skip_on_empty
      method: row
destination:
  plugin: 'entity:media'
migration_dependencies:
  required: {}
  optional: {}

Contents of the migration_templates/wp_content.yml file:

id: wp_content
label: 'Content'
migration_tags:
  - Wordpress
source:
  plugin: wordpress_content
  # Wordpress post type (custom variable)
  post_type: post
  # This is WP table prefix  (custom variable)
  # DB table example: [prefix]_posts
  table_prefix: wp
process:
  type:
    plugin: default_value
    default_value: article
  'path/pathauto':
    plugin: default_value
    default_value: 0
  'path/alias':
    # This will add the following to URL aliases in Drupal
    plugin: add_url_alias_prefix
    # url/alias/prefix/2017/07/21/[post-title]
    prefix: url/alias/prefix
    source: path_alias
  promote: 
    plugin: default_value
    default_value: 0
  sticky: 
    plugin: default_value
    default_value: 0
  langcode:
    plugin: default_value
    default_value: en
  status: 
    plugin: default_value
    default_value: 1
  title: post_title
  created: 
    plugin: date_to_timestamp
    source: post_date
  changed: 
    plugin: date_to_timestamp
    source: post_modified
  field_image:
    -
      plugin: migration
      migration: wp_media
      source: thumbnail
    -
      plugin: skip_on_empty
      method: row
  'body/summary': post_excerpt
  'body/format':
    plugin: default_value
    default_value: full_html
  'body/value': post_content
destination:
  plugin: 'entity:node'
migration_dependencies:
  required: {}
  optional: {}

Contents of the src/Plugin/migrate/process/AddUrlAliasPrefix.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\process;

use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;

/**
 * Add prefix to URL aliases.
 *
 * @MigrateProcessPlugin(
 *   id = "add_url_alias_prefix"
 * )
 */
class AddUrlAliasPrefix extends ProcessPluginBase {

  /**
   * {@inheritdoc}
   */
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    $prefix = !empty($this->configuration['prefix']) ? '/' . $this->configuration['prefix'] : '';
    return $prefix . $value;
  }

}

Contents of the src/Plugin/migrate/process/DateToTimestamp.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\process;

use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;

/**
 * Date to Timetamp conversion.
 *
 * @MigrateProcessPlugin(
 *   id = "date_to_timestamp"
 * )
 */
class DateToTimestamp extends ProcessPluginBase {

  /**
   * {@inheritdoc}
   */
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    return strtotime($value . ' UTC');
  }

}

I like to keep my module code clean and organized so I use base classes that I later extend in individual migration source files.

Here is contents of the src/Plugin/migrate/source/SqlBase.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\source;

use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Row;

class SqlBase extends DrupalSqlBase {

  /**
   * Get database table prefix from the migration template.
   */
  protected function getPrefix() {
    return !empty($this->configuration['table_prefix']) ? $this->configuration['table_prefix'] : 'wp';
  }

  /**
   * Get Wordpress post type from the migration template.
   */
  protected function getPostType() {
    return !empty($this->configuration['post_type']) ? $this->configuration['post_type'] : 'post';
  }

  /**
   * Generate path alias via pattern specified in `permalink_structure`.
   */
  protected function generatePathAlias(Row $row) {
    $prefix = $this->getPrefix();
    $permalink_structure = $this->select($prefix . '_options', 'o', ['target' => 'migrate'])
      ->fields('o', ['option_value'])
      ->condition('o.option_name', 'permalink_structure')
      ->execute()
      ->fetchField();
    $date = new \DateTime($row->getSourceProperty('post_date'));
    $parameters = [
      '%year%'     => $date->format('Y'),
      '%monthnum%' => $date->format('m'),
      '%day%'      => $date->format('d'),
      '%postname%' => $row->getSourceProperty('post_name'),
    ];
    $url = str_replace(array_keys($parameters), array_values($parameters), $permalink_structure);
    return rtrim($url, '/');
  }

  /**
   * Get post thumbnail.
   */
  protected function getPostThumbnail(Row $row) {
    $prefix = $this->getPrefix();
    $query = $this->select($prefix . '_postmeta', 'pm', ['target' => 'migrate']);
    $query->innerJoin($prefix . '_postmeta', 'pm2', 'pm2.post_id = pm.meta_value');
    $query->fields('pm', ['post_id'])
      ->condition('pm.post_id', $row->getSourceProperty('id'))
      ->condition('pm.meta_key', '_thumbnail_id')
      ->condition('pm2.meta_key', '_wp_attached_file');
    return $query->execute()->fetchField();
  }

}

Contents of the src/Plugin/migrate/source/Thumbnail.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\source;

use Drupal\migrate\Row;

/**
 * Extract content thumbnails.
 *
 * @MigrateSource(
 *   id = "wordpress_thumbnail"
 * )
 */
class Thumbnail extends SqlBase {

  /**
   * {@inheritdoc}
   */
  public function query() {
    $prefix = $this->getPrefix();
    $query = $this->select($prefix . '_postmeta', 'pm', ['target' => 'migrate']);
    $query->innerJoin($prefix . '_postmeta', 'pm2', 'pm2.post_id = pm.meta_value');
    $query->innerJoin($prefix . '_posts', 'p', 'p.id = pm.post_id');
    $query->fields('pm', ['post_id']);
    $query->fields('p', ['post_date']);
    $query->addField('pm2', 'post_id', 'file_id');
    $query->addField('pm2', 'meta_value', 'filepath');
    $query
      ->condition('pm.meta_key', '_thumbnail_id')
      ->condition('pm2.meta_key', '_wp_attached_file')
      ->condition('p.post_status', 'publish')
      ->condition('p.post_type', 'post');
    return $query;
  }

  /**
   * {@inheritdoc}
   */
  public function fields() {
    return [
      'post_id'   => $this->t('Post ID'),
      'post_date' => $this->t('Media Uploaded Date'),
      'file_id'   => $this->t('File ID'),
      'filepath'  => $this->t('File Path'),
      'filename'  => $this->t('File Name'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getIds() {
    return [
      'post_id' => [
        'type'  => 'integer',
        'alias' => 'pm2',
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function prepareRow(Row $row) {
    $row->setSourceProperty('filename', basename($row->getSourceProperty('filepath')));
  }

}

Contents of the src/Plugin/migrate/source/Content.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\source;

use Drupal\migrate\Row;

/**
 * Extract content from Wordpress site.
 *
 * @MigrateSource(
 *   id = "wordpress_content"
 * )
 */
class Content extends SqlBase {

  /**
   * {@inheritdoc}
   */
  public function query() {
    $prefix = $this->getPrefix();
    $query = $this->select($prefix . '_posts', 'p');
    $query
      ->fields('p', [
        'id',
        'post_date',
        'post_title',
        'post_content',
        'post_excerpt',
        'post_modified',
        'post_name'
      ]);
    $query->condition('p.post_status', 'publish');
    $query->condition('p.post_type', $this->getPostType());
    return $query;
  }

  /**
   * {@inheritdoc}
   */
  public function fields() {
    return [
      'id'            => $this->t('Post ID'),
      'post_title'    => $this->t('Title'),
      'thumbnail'     => $this->t('Post Thumbnail'),
      'post_excerpt'  => $this->t('Excerpt'),
      'post_content'  => $this->t('Content'),
      'post_date'     => $this->t('Created Date'),
      'post_modified' => $this->t('Modified Date'),
      'path_alias'    => $this->t('URL Alias'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getIds() {
    return [
      'id' => [
        'type'  => 'integer',
        'alias' => 'p',
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function prepareRow(Row $row) {
    // This will generate path alias using WP alias settings.
    $row->setSourceProperty('path_alias', $this->generatePathAlias($row));
    // Get thumbnail ID and pass it to the wp_media migration plugin.
    $row->setSourceProperty('thumbnail', $this->getPostThumbnail($row));
  }

}

IMPORTANT: You must run migrations in their proper order. In this example you have to run wp_thumbnail first, wp_media second and wp_content last.

Comments Not Loading?

Due to some temporarily SSL cert issue please refresh the page using this link in order to be able to leave comments.

Jul 27 2017
Jul 27

WordPress to Drupal 8

In this post I will show you how to migrate thumbnail content from Wordpress to Drupal 8. My goals are to help you better understand the content migration process, give you starting point for future migrations, and teach you how to write process plugins and migration sources. Taxonomy terms and users migration is more straightforward so I won't cover it here.

This migration example contains templates to migrate thumbnails content. For this post, I assume the image/thumbnail field is using the Media module field. I will be using the Migrate drush module to run migrations.

First, make sure to configure your connection in your settings.php file. Add the following with proper credentials:

$databases['migrate']['default'] = [
    'driver'   => 'mysql',
    'database' => 'wordpress_dbname',
    'username' => 'wordpress_dbuser',
    'password' => 'wordpress_dbpassowrd',
    'host'     => '127.0.0.1',
];

Here is the module structure I will be using:

wp_migration/
  - wp_migration.info.yml
  - wp_migration.module
  - migration_templates/
    - wp_content.yml
    - wp_thumbnail.yml
    - wp_media.yml
  - src/
    - Plugin/
      - migrate/
        - process/
          - AddUrlAliasPrefix.php
          - DateToTimestamp.php
        - source/
          - SqlBase.php
          - Content.php
          - Thumbnail.php

Contents of the wp_wordpress.info.yml file:

name: Wordpress Migration
type: module
description: Migrate Wordpress content into Drupal 8.
core: 8.x
dependencies:
  - migrate
  - migrate_drupal
  - migrate_drush

Contents of the migration_templates/wp_thumbnail.yml file:

id: wp_thumbnail
label: 'Thumbnails'
migration_tags:
  - Wordpress
source:
  plugin: wordpress_thumbnail
  # This is WP table prefix  (custom variable)
  # DB table example: [prefix]_posts
  table_prefix: wp
  constants:
    # This path should point ot WP uploads directory.
    source_base_path: '/path/to/source/wp/uploads'
    # This is directory name in Drupal where to store migrated files
    uri_file: 'public://wp-thumbnails'
process:
  filename: filename
  source_full_path:
    -
      plugin: concat
      delimiter: /
      source:
        - constants/source_base_path
        - filepath
    -
      plugin: urlencode
  uri_file:
    -
      plugin: concat
      delimiter: /
      source:
        - constants/uri_file
        - filename
    -
      plugin: urlencode
  uri:
    plugin: file_copy
    source:
      - '@source_full_path'
      - '@uri_file'
  status: 
    plugin: default_value
    default_value: 1
  changed: 
    plugin: date_to_timestamp
    source: post_date
  created: 
    plugin: date_to_timestamp
    source: post_date
  uid: 
    plugin: default_value
    default_value: 1
destination:
  plugin: 'entity:file'
migration_dependencies:
  required: {}
  optional: {}

Contents of the migration_templates/wp_media.yml file:

id: wp_media
label: 'Media'
migration_tags:
  - Wordpress
source:
  plugin: wordpress_thumbnail
  # This is WP table prefix  (custom variable)
  # DB table example: [prefix]_posts
  table_prefix: wp
  constants:
    bundle: image
process:
  bundle: 'constants/bundle'
  langcode:
    plugin: default_value
    default_value: en
  'field_image/target_id':
    -
      plugin: migration
      migration: wp_thumbnail
      source: post_id
    -
      plugin: skip_on_empty
      method: row
destination:
  plugin: 'entity:media'
migration_dependencies:
  required: {}
  optional: {}

Contents of the migration_templates/wp_content.yml file:

id: wp_content
label: 'Content'
migration_tags:
  - Wordpress
source:
  plugin: wordpress_content
  # Wordpress post type (custom variable)
  post_type: post
  # This is WP table prefix  (custom variable)
  # DB table example: [prefix]_posts
  table_prefix: wp
process:
  type:
    plugin: default_value
    default_value: article
  'path/pathauto':
    plugin: default_value
    default_value: 0
  'path/alias':
    # This will add the following to URL aliases in Drupal
    plugin: add_url_alias_prefix
    # url/alias/prefix/2017/07/21/[post-title]
    prefix: url/alias/prefix
    source: path_alias
  promote: 
    plugin: default_value
    default_value: 0
  sticky: 
    plugin: default_value
    default_value: 0
  langcode:
    plugin: default_value
    default_value: en
  status: 
    plugin: default_value
    default_value: 1
  title: post_title
  created: 
    plugin: date_to_timestamp
    source: post_date
  changed: 
    plugin: date_to_timestamp
    source: post_modified
  field_image:
    -
      plugin: migration
      migration: wp_media
      source: thumbnail
    -
      plugin: skip_on_empty
      method: row
  'body/summary': post_excerpt
  'body/format':
    plugin: default_value
    default_value: full_html
  'body/value': post_content
destination:
  plugin: 'entity:node'
migration_dependencies:
  required: {}
  optional: {}

Contents of the src/Plugin/migrate/process/AddUrlAliasPrefix.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\process;

use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;

/**
 * Add prefix to URL aliases.
 *
 * @MigrateProcessPlugin(
 *   id = "add_url_alias_prefix"
 * )
 */
class AddUrlAliasPrefix extends ProcessPluginBase {

  /**
   * {@inheritdoc}
   */
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    $prefix = !empty($this->configuration['prefix']) ? '/' . $this->configuration['prefix'] : '';
    return $prefix . $value;
  }

}

Contents of the src/Plugin/migrate/process/DateToTimestamp.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\process;

use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;

/**
 * Date to Timetamp conversion.
 *
 * @MigrateProcessPlugin(
 *   id = "date_to_timestamp"
 * )
 */
class DateToTimestamp extends ProcessPluginBase {

  /**
   * {@inheritdoc}
   */
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    return strtotime($value . ' UTC');
  }

}

I like to keep my module code clean and organized so I use base classes that I later extend in individual migration source files.

Here is contents of the src/Plugin/migrate/source/SqlBase.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\source;

use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Row;

class SqlBase extends DrupalSqlBase {

  /**
   * Get database table prefix from the migration template.
   */
  protected function getPrefix() {
    return !empty($this->configuration['table_prefix']) ? $this->configuration['table_prefix'] : 'wp';
  }

  /**
   * Get Wordpress post type from the migration template.
   */
  protected function getPostType() {
    return !empty($this->configuration['post_type']) ? $this->configuration['post_type'] : 'post';
  }

  /**
   * Generate path alias via pattern specified in `permalink_structure`.
   */
  protected function generatePathAlias(Row $row) {
    $prefix = $this->getPrefix();
    $permalink_structure = $this->select($prefix . '_options', 'o', ['target' => 'migrate'])
      ->fields('o', ['option_value'])
      ->condition('o.option_name', 'permalink_structure')
      ->execute()
      ->fetchField();
    $date = new \DateTime($row->getSourceProperty('post_date'));
    $parameters = [
      '%year%'     => $date->format('Y'),
      '%monthnum%' => $date->format('m'),
      '%day%'      => $date->format('d'),
      '%postname%' => $row->getSourceProperty('post_name'),
    ];
    $url = str_replace(array_keys($parameters), array_values($parameters), $permalink_structure);
    return rtrim($url, '/');
  }

  /**
   * Get post thumbnail.
   */
  protected function getPostThumbnail(Row $row) {
    $prefix = $this->getPrefix();
    $query = $this->select($prefix . '_postmeta', 'pm', ['target' => 'migrate']);
    $query->innerJoin($prefix . '_postmeta', 'pm2', 'pm2.post_id = pm.meta_value');
    $query->fields('pm', ['post_id'])
      ->condition('pm.post_id', $row->getSourceProperty('id'))
      ->condition('pm.meta_key', '_thumbnail_id')
      ->condition('pm2.meta_key', '_wp_attached_file');
    return $query->execute()->fetchField();
  }

}

Contents of the src/Plugin/migrate/source/Thumbnail.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\source;

use Drupal\migrate\Row;

/**
 * Extract content thumbnails.
 *
 * @MigrateSource(
 *   id = "wordpress_thumbnail"
 * )
 */
class Thumbnail extends SqlBase {

  /**
   * {@inheritdoc}
   */
  public function query() {
    $prefix = $this->getPrefix();
    $query = $this->select($prefix . '_postmeta', 'pm', ['target' => 'migrate']);
    $query->innerJoin($prefix . '_postmeta', 'pm2', 'pm2.post_id = pm.meta_value');
    $query->innerJoin($prefix . '_posts', 'p', 'p.id = pm.post_id');
    $query->fields('pm', ['post_id']);
    $query->fields('p', ['post_date']);
    $query->addField('pm2', 'post_id', 'file_id');
    $query->addField('pm2', 'meta_value', 'filepath');
    $query
      ->condition('pm.meta_key', '_thumbnail_id')
      ->condition('pm2.meta_key', '_wp_attached_file')
      ->condition('p.post_status', 'publish')
      ->condition('p.post_type', 'post');
    return $query;
  }

  /**
   * {@inheritdoc}
   */
  public function fields() {
    return [
      'post_id'   => $this->t('Post ID'),
      'post_date' => $this->t('Media Uploaded Date'),
      'file_id'   => $this->t('File ID'),
      'filepath'  => $this->t('File Path'),
      'filename'  => $this->t('File Name'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getIds() {
    return [
      'post_id' => [
        'type'  => 'integer',
        'alias' => 'pm2',
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function prepareRow(Row $row) {
    $row->setSourceProperty('filename', basename($row->getSourceProperty('filepath')));
  }

}

Contents of the src/Plugin/migrate/source/Content.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\source;

use Drupal\migrate\Row;

/**
 * Extract content from Wordpress site.
 *
 * @MigrateSource(
 *   id = "wordpress_content"
 * )
 */
class Content extends SqlBase {

  /**
   * {@inheritdoc}
   */
  public function query() {
    $prefix = $this->getPrefix();
    $query = $this->select($prefix . '_posts', 'p');
    $query
      ->fields('p', [
        'id',
        'post_date',
        'post_title',
        'post_content',
        'post_excerpt',
        'post_modified',
        'post_name'
      ]);
    $query->condition('p.post_status', 'publish');
    $query->condition('p.post_type', $this->getPostType());
    return $query;
  }

  /**
   * {@inheritdoc}
   */
  public function fields() {
    return [
      'id'            => $this->t('Post ID'),
      'post_title'    => $this->t('Title'),
      'thumbnail'     => $this->t('Post Thumbnail'),
      'post_excerpt'  => $this->t('Excerpt'),
      'post_content'  => $this->t('Content'),
      'post_date'     => $this->t('Created Date'),
      'post_modified' => $this->t('Modified Date'),
      'path_alias'    => $this->t('URL Alias'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getIds() {
    return [
      'id' => [
        'type'  => 'integer',
        'alias' => 'p',
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function prepareRow(Row $row) {
    // This will generate path alias using WP alias settings.
    $row->setSourceProperty('path_alias', $this->generatePathAlias($row));
    // Get thumbnail ID and pass it to the wp_media migration plugin.
    $row->setSourceProperty('thumbnail', $this->getPostThumbnail($row));
  }

}

IMPORTANT: You must run migrations in their proper order. In this example you have to run wp_thumbnail first, wp_media second and wp_content last.

Comments Not Loading?

Due to some temporarily SSL cert issue please refresh the page using this link in order to be able to leave comments.

Jul 18 2017
Jul 18

AppNexus + Drupal 8

One of our clients requested to integrate AppNexus ads with their Drupal 8 site. The requirements were very simple:

  • Ability to display ads as blocks.
  • Ad blocks should be configurable: tagId, Sizes. And pass those variables via apntag.defineTag().
  • Ability to pass global page options via apntag.setPageOpts().

Since there wasn't any module that would support this I decided to create a module that will be very easy to use and at the same time will be extendable and useful in other projects.

The module comes with a simple PHP class that helps to generate pageOpts from hook_page_attachments(). You may create your own class using that class as an example.

use Drupal\appnexus\PageOpts;

/**
 * Implements hook_page_attachments().
 */
function mymodule_page_attachments(array &$page) {
  $page_opts = (new PageOpts())
    ->setKeywordQuotes()
    ->setMember('YOUR_MEMBER_ID')
    ->setKeyword('var1', 'value')
    ->setKeyword('var2', ['val1', 'val2'])
    ->setKeyword('var3', 1234);
  if ($opts = $page_opts->build()) {
    $page['#attached']['drupalSettings']['appnexus']['opts'] = json_encode($opts);
  }
}

Please make sure you specify Member ID in order to properly pass all the parameters.

Download AppNexus

Jul 15 2017
Jul 15

Amp and Drupal 8

Adding AMP (Accelerated Mobile Pages) support could be very tricky and complex depending on your project needs. In this blog post I won't be using AMP module. Instead I created something more simpler and easier to use for AMP integration. The Simple AMP module is still using Lullabot's AMP PHP library. The module is a starter module and most likely won't be very useful out of the box, but it can get you going very fast. You may need to extend it and this shouldn't as hard as in other modules.

Download Simple AMP

Installation

First thing you need to do is install composer require lullabot/amp, the module won't work without it.

How to Use

  • Create new Display Mode for your entity that will support AMP. New View modes can be created via /admin/structure/display-modes/view.
  • Enable newly created View mode on entity Manage Display page. Example path: /admin/structure/types/manage/article/display
  • Open Entity Types page and enable entity type that supports AMP and choose Display Mode for it. /admin/config/services/simple-amp.
  • Open Components page and and enable default components. /admin/config/services/simple-amp/components.
  • Enable Access AMP pages permission.
  • Hit Save configuration and you're all set.

URL alias

If you would like to have URL alias in the path /node-alias/amp instead of /node/[nid]/amp please install Sub-pathauto (Sub-path URL Aliases)

Theming

Easy to extend and alter AMP templates per content type: amp--[ENTITY_TYPE].html.twig or amp--[ENTITY_TYPE]--[BUNDLE].html.twig

Extend Simple AMP module

The module is extendable, there are two components: AmpComponent and AmpMetadata. I built them using Drupal 8 plugin system which will let anyone extend it to match your project needs.

AmpComponent

  • All plugins stored in src/Plugin/AmpComponent/*. currently the module doesn't support all available AMP components, but can be easily extended from your own module.

Here is example:

The key variables here are:

  • name - plugin name
  • description - plugin description.
  • regexp - array of regular expressions to match in HTML body.
<?php

namespace Drupal\simple_amp\Plugin\AmpComponent;

use Drupal\simple_amp\AmpComponentBase;

/**
 * Youtube AMP component.
 *
 * @AmpComponent(
 *   id = "amp-youtube",
 *   name = @Translation("YouTube"),
 *   description = @Translation("YouTube Component"),
 *   regexp = {
 *     "/<amp\-youtube.*><\/amp\-youtube>/isU",
 *   }
 * )
 */
class Youtube extends AmpComponentBase {

  /**
   * {@inheritdoc}
   */
  public function getElement() {
    return '<script async custom-element="amp-youtube" src="https://cdn.ampproject.org/v0/amp-youtube-0.1.js"></script>';
  }

}

AmpMetadata

  • All plugins stored in src/Plugin/AmpMetadata/*. provide Metadata for specific entity.

Here is example:

The key variables here are:

  • entity_types - array of entity type names. This will tell the module to generate AMP Metadata for entity types specified in this variable.
<?php

namespace Drupal\simple_amp\Plugin\AmpMetadata;

use Drupal\simple_amp\AmpMetadataBase;
use Drupal\simple_amp\Metadata\Metadata;
use Drupal\simple_amp\Metadata\Author;
use Drupal\simple_amp\Metadata\Publisher;
use Drupal\simple_amp\Metadata\Image;

/**
 * Example AMP metadata component.
 *
 * @AmpMetadata(
 *   id = "default",
 *   entity_types = {
 *     "example_article"
 *   }
 * )
 */
class ExampleEntity extends AmpMetadataBase {

  /**
   * {@inheritdoc}
   */
  public function getMetadata($entity) {
    $metadata = new Metadata();
    $author = (new Author())
      ->setName('Test Author');
    $logo = (new Image())
      ->setUrl('http://url-to-image')
      ->setWidth(400)
      ->setHeight(300);
    $publisher = (new Publisher())
      ->setName('MyWebsite.com')
      ->setLogo($logo);
    $image = (new Image())
      ->setUrl('http://url-to-image')
      ->setWidth(400)
      ->setHeight(300);
    $metadata
      ->setDatePublished($entity->getCreatedTime())
      ->setDateModified($entity->getChangedTime())
      ->setDescription('test')
      ->setAuthor($author)
      ->setPublisher($publisher)
      ->setImage($image);
    return $metadata->build();
  }

}

Support

Feel free to create pull requests, help is always appreciated.

Comments Not Loading?

Due to some temporarily SSL cert issue please refresh the page using this link in order to be able to leave comments.

Jul 13 2017
Jul 13

Adding AMP support could be very tricky and complex depending on your project needs. In this blog post I won't be using AMP module. Instead I created something more simple and easier to use for simple AMP integration. The Simple AMP module is still using Lullabot's AMP PHP library. The module is a starter module and most likely won't be very useful out of the box, but it can get you going very fast. It is available for download here https://github.com/chapter-three/simple_amp.

First thing you need to do is to install composer require lullabot/amp, the module won't work without this library.

Once you install the module modify few lines of code in the module:

  • Modify Drupal\simple_amp\AmpBase::getMetadata() to reflect data that you would like to show in AMP metadata.
  • Modify template in simple_amp/templates/amp.html.twig to match your design needs.
  • You may also have custom template per content type: amp--node.html.twig or amp--node--article.html.twig

By default Simple AMP module includes only basic AMP JS files and in case if you need to add other AMP JS libraries you would have to add some code.

In order to add additional AMP JS files you will have to modify Drupal\simple_amp\AmpBase::detect() method. You would need to write some regular expressions to detect ads, youtube videos, vimeo, instagram or any other libraries. Once detected you would add that library to protected $scripts = [] array in the AmpBase class.

Example:

protected function detect() {
    $youtube = [
      '/youtube\.com\/watch\?v=([a-z0-9\-_]+)/i',
      '/youtube\.com\/embed\/([a-z0-9\-_]+)/i',
      '/youtu.be\/([a-z0-9\-_]+)/i',
      '/youtube\.com\/v\/([a-z0-9\-_]+)/i',
    ];
    foreach ($youtube as $regexp) {
      if (preg_match($regexp, $this->html)) {
        $this->scripts[] = '<script async custom-element="amp-youtube" src="https://cdn.ampproject.org/v0/amp-youtube-0.1.js"></script>';
      }
    }
  }

Roadmap

  • Improve UI
  • Eliminate any need for code modifications.

Feel free to create pull requests, help is always appreciated.

Jun 01 2017
Jun 01

The editorial experience is a critical and often overlooked part of website development. A weak editorial experience means slower site updates. Without a robust and intuitive content creation process, editors lose motivation to keep their site up to date. Our modular content creation process makes it quick and easy for editors to create content while providing front-end users with a rich visual experience.

What Are Modular Content Components?

Modular Content Components are a powerful way for website editors to quickly add new sections and send article content to multiple distribution channels in a single step. Drupal sites with Modular Content Components make it easy to add new distribution channels and send custom formats to each.

Add new sections Drupal WYSIWYGQuickly add new sections with easeDistribution Partners Drupal WYSIWYGSend content to multiple distribution channels in a single step

Should My Drupal Site Use Modular Content Components?

If your editors need to easily update article content or send articles to multiple distribution partners, it should use Modular Content Components.

Editors can add new components on the fly, with drag and drop functionality, in a fully responsive presentation that looks great across all devices.

Rich content tagging lets editors cross-promote content, populate landing page, and target advertisers.

Customizable bylines have built-in sponsorship capabilities and editors can easily insert rich, branded excerpts of relevant content with only a URL. Removing embed code from article text offers better security, control and maintainability, along with ease of use.

Content distribution for AOL, Apple News, Facebook, MSN and Yahoo are currently available, along with Accelerated Mobile Pages (AMP) and OpenGraph integration.

Customize Bylines Drupal WYSIWYGCustomizable bylines

Tailoring Content to Distribution Partners

Partners often have restrictions around certain types of content. You may have a partner who does not allow links, so articles with related content links need to be modified for that partner. When an article is created from Modular Content Components, it’s simple to make exceptions and modifications so editors can focus on article content rather than distribution rules.

Consistent Branding and a Scalable Architecture

Article content published using inconsistent layouts, carried over from legacy site designs, create a confusing user experience. Using Modular Content Components with Drupal means your site will have a solid architecture with consistent branding and an intuitive user experience. Stakeholders and editors benefit from a future-forward platform that is scalable and free of technical debt. This means your organization can expand and redesign its website, either in part or as a whole, without re-inventing the wheel. As branding changes, the site can be easily updated to reflect the new look and feel.

A well-defined set of Modular Content tools provides the same advantages at the article content level. Instead of fragmented HTML dumped into a WYSIWYG editor, you have a structured, upgradeable, and maintainable platform.

Advertising Power

The DoubleClick for Publishers (DFP) module integrates with your existing DoubleClick setup, allowing custom ad placement. Smart Targeting lets your site architecture drive advertising campaigns powerfully and consistently. Flexible ad tags make implementing creative campaigns simple and manageable. Along with good targeting parameters, it’s easier to measure what works best and where. Unique, built-in identifiers allow you to target one-off campaigns and header bidding capability is also available.

Learn More About Modular Content Development For Your Site

We know that each organization has a unique situation and unique requirements. Get in touch to find out how Chapter Three can tailor this powerful functionality for your site. Call John Faber at 415.218.1351 or email [email protected].

Apr 21 2017
Apr 21

Wouldn't it be great if???

Configuration management is one of the most useful site development features in Drupal 8. It makes a site's configuration exportable, importable and manageable in git. Whilst building the configuration management feature, a thought that often occurred was "Wouldn't it be great if you can take an existing set of configuration and install a new site from it?". Every Drupal developer has turned up to a new project and had to learn a different way to build a development site. Do you get the code from github? Download a database from production or some other special location? And is that database sanitised? I often wonder how many Drupal developers there are in the world wandering around with production databases on their laptops.

Install Drupal 8 from Configuration CMI

We started to explore the ability to install from configuration in https://www.drupal.org/node/1613424. Why hasn't this made it into Drupal 8 yet? It transpires that changing the installer is hard and it was not a top priority in the run up to the 8.0.0 release. To fix the feature gap, I created the contrib project Configuration Installer. This is a special installation profile that allows you to install from a configuration export.

So problem solved in contrib right? Well, no. Profiles are special weird magic modules. They can do everything a module can do and more. But things don't work the same. You can uninstall the Contact module after installing the Standard profile even though the contact module is listed as a dependency. This causes problems for the Configuration Installer because Standard's hook_install() implementation will error if Contact is not installed. The Configuration Installer can work around that with ugly code but its luck runs out if the install profile has custom or contrib modules included. In order to work, the Configuration Installer swaps the currently active profile during install so Drupal can't find modules or themes buried inside the install profile.

Your site's install profile could contain all the configuration

In an ideal world, I want to get a project's code and use the Drupal installer to select install profile for the site I'm working on and and install it. I want to make some configuration changes, export them, and commit them to git. I want to throw away that site and forget about it. I want to come back six months later and update the code, re-install a fresh site that includes any changes other developers made in the meantime, and continue to work on the site. All without having to think particularly hard. I want to use the standard Drupal tools like the installer, configuration admin UI, and Drush.

The good news is that we can have all this! This is precisely what the patch available at https://www.drupal.org/node/2788777 does. We can change the installer to read from its config/sync directory and run a configuration import instead of the regular install. It builds on the Configuration Installer project but does not require the magic of swapping around the profile, so is much more robust. The final piece of the puzzle is to change the configuration sync directory to the profile's config/sync in settings.php, and it just works. As a happy developer said trying out this for the first time:

I love this workflow :)

How to have a rebuildable and reinstallable Drupal today!

  1. Use https://github.com/drupal-composer/drupal-project to manage your codebase using composer.
  2. Use https://github.com/cweagans/composer-patches to apply the latest patch on https://www.drupal.org/node/2788777 as part of composer install.
  3. Create an install profile with a config/sync directory. The new Install Profile Generator module can do this for you with a simple Drush command.

Now I can update my install profile after making configuration changes by running drush config-export. I can share those changes by pushing the install profile. I can pull in changes from other developers by pulling and running drush config-import. I can rebuild the site from scratch by checking out the codebase and running drush site-install.

Afterthoughts

If you are thinking "Wouldn't it be nice if we could also create some content at the same time?" have a look at the Default Content project. You will need the patch from https://www.drupal.org/node/2833562.

If you are wondering whether applying a patch to core is safe - in this instance I would argue it really is. The only changes the patch makes are to the installer and makes no runtime changes.

Isn't the obfuscated config sync directory generated by core important? Well I'm not a big believer in security through obscurity. But also if you are using Apache then core checks for the presence of a .htaccess in your config sync directory that should prevent access. Other webservers should also be denying access to .yml files by default too.

Further reading

Thanks

Eli-T for helpful suggestions on this blogpost and working with me on the Install Profile Generator.

Catch for helpful suggestions on this blogpost.

Apr 10 2017
Apr 10

In the previous post I covered how to use the Component Libraries module and Twig to create simple reusable components, using an SVG sprite icon template as an example.

Part 2: Applying Twig Concepts to Write Better Code

When learning Drupal theming, overriding templates is one of the key topics of interest. It’s a simple thing. Copy the source template into your theme, modify it and clear the cache. Easy! However, doing just that over and over again, can lead to a mess of unmaintainable code.

A good example of this is the page.html.twig template. This template is the main layout driver of a theme. Changes to the page template can have far reaching consequences, though. When I inherit a site to maintain, one of the first things I look for is how many page templates the theme contains. If there are more than a handful, or even more than one that seem unnecessary, I start to cringe. For example, if I’m looking at a theme with multiple page templates, which a client requests a change to, no matter how big or small that change is, I now need to get familiar with all templates and try to find out the intentions behind their creation, and how I can implement that change successfully, or risk a botched deployment. This usually involves comparing the files using a diff app, looking back and commit history, and also searching through CSS. It can turn a seemingly simple task into one that takes hours.

Why does this sort of thing happen? At a basic level, this is what we were taught to do. There’s a ton of ground to cover with markup coming from many different sources, and blindly overriding is often faster and gets the job done. Beyond theme hook suggestions, which provide some clues about intent, I find that there’s generally been an inability to express clear and concise intentions in code alone, in Drupal themes. What I mean by that specifically: I want to immediately understand why a variant of page.html.twig exists without having to spend much time thinking about it, and comparing by trudging through a sea of code.

Twig is a lifesaver in this regard.

In this post, I’ll provide details about how and when to use Twig’s include, block, extends and embed tags, and in a way that helps makes your intentions for overriding templates clearer to anyone else that might work with them the future.

Include

The Twig include tag is similar to PHP include function, in that the purposes is to include another file. The include tag also provides access to that template’s variables. It’s pretty straightforward. In the Part I’s icon component implementation example, I used it twice. First, to inline the contents of our icons.svg file in html.html.twig:

{% include active_theme_path() ~ '/img/icons.svg' %}

Second, to demonstrate using the icon component template, using the with keyword to pass a variable:

{% include '@components/icon.twig' with { icon: 'twitter' } %}

Summary

Allows for including one file into another, with ability to manipulate variables and context via special keywords.

  • No special structure is required for the contents of the include file.
  • You may include as many files as you want in a given template.
  • Variables can be accessed using the with {} keyword, and also directly in the file being included.
  • Context can be disabled or limited with the only keyword.
  • Missing templates/files can be made fail silently with the ignore missing keyword.

Block

Twig’s documentation describes them like this: “Blocks are used for inheritance and act as placeholders and replacements at the same time. They are documented in detail in the documentation for the extends tag.” That’s a bit confusing, and they are documented in much more detail, but I’ll try to explain.

I find that the concept of Twig Blocks is similar to the concept of theme regions in Drupal. Blocks are essentially sections of code that as the template designer, you decide should be changeable from some other template that implements them. Let’s look at a real world example of how we might take advantage of that, in the context of page.html.twig.

First, here’s a very simplified version of what you might find in page.html.twig. It’s got a wrapper, basic header, logo, title, main area with a column and sidebar, and a footer:

{% set classes = ['page'] %}
<div{{ attributes.addClass(classes) }} id="page">
  <header role="banner">
    <a href="https://www.chapterthree.com/blog/twig-concepts-drupal-8-themes-part-ii/{{ path('<front>') }}" title="{{ 'Home'|t }}" rel="home" class="logo">
      {% include '@components/icon.twig' with { icon: ‘logo’ } %}
    </a>
    {{ page.header }}
  </header>
  <main role="main">
    <a id="main-content" tabindex="-1"></a>
    <div class="content">
      {{ page.breadcrumb }}
      {{ page.highlighted }}
      {{ page.help }}
      {{ page.content }}
    </div>
    {% if page.sidebar_first %}
      <aside class="sidebar" role="complementary"> {{ page.sidebar_first }} </aside>
    {% endif %}
  </main>
  {% if page.footer %}
    <footer role="contentinfo"> {{ page.footer }} </footer>
  {% endif %}
</div>

Note: I’m not using the Site branding block, which normally comes through the header region.

As is, I cannot extend this template, so if I need another page template for whatever reason, the answer is to copy and paste, which I’d prefer not to do. Instead, let’s wrap what we might want to override elsewhere in Twig blocks and make this template into something that can be changed or built upon from its variants:

{% set classes = ['page'] %}
<div{{ attributes.addClass(classes, add_classes) }} id="page">
  <header role="banner">
    {% block logo %}
      <a href="https://www.chapterthree.com/blog/twig-concepts-drupal-8-themes-part-ii/{{ path('<front>') }}" title="{{ 'Home'|t }}" rel="home" class="logo">
        {% include '@components/icon.twig' with { icon: 'twitter' } %}
      </a>
    {% endblock %}
    {% block header %} {{ page.header }} {% endblock %}
  </header>
  <main role="main">
    <a id="main-content" tabindex="-1"></a>
    {% block main %}
    <div class="content">
      {{ page.breadcrumb }}
      {{ page.highlighted }}
      {{ page.help }}
      {% block content %} {{ page.content }} {% endblock %}
    </div>
    {% if page.sidebar_first %}
      <aside class="sidebar" role="complementary">
        {% block sidebar %} {{ page.sidebar_first }} {% endblock %}
      </aside>
    {% endif %}
    {% endblock %}
  </main>
  {% if page.footer %}
    <footer role="contentinfo">
      {% block footer %} {{ page.footer }} {% endblock %}
    </footer>
  {% endif %}
</div>

In the code above I defined the following 6 areas as Twig blocks: header, logo, main, content, sidebar, and footer. By creating these blocks, I’ve made this template extendable in areas I’ve decided are “variable” based on the design requirements of a given site. Twig blocks have nothing to do with Drupal, and Drupal has no means of interacting with them.

I’ve purposely defined the blocks inside the HTML wrappers of these areas because I’d like extending templates to easily change the contents, not the wrappers themselves. I could have taken this a step further, creating blocks for both the outer and inner areas, of these same sections for times when changing the outer wrapper is needed.

The addition of these blocks by don’t actually “do” anything in the context of this template. The markup contained within block tags, prints just as if those tags didn’t exist. If you are reading and thinking WTF, stick with me. The power of this functionality comes into play when using Twig Blocks in combination with Extends or Embed...

Extends

Twig’s extends tag exists to facilitate extending one template to another, with the use of Twig blocks and a parent/child relationship. The parent template contains the structure in the form of blocks (like our example code above) and the child template uses the template via the extends tag, from a “child” template implementation. When combined with theme hook suggestions, this opens up some really cool possibilities, as we can see with some simple examples below:

Example: Remove the sidebar from the homepage.

Using theme hook suggestions, create a template called page--front.html.twig, and use the extends tag to reference the page.html.twig template. Then, underneath, create a block with the same name of the Twig block (sidebar), created in page.html.twig, and don’t put anything inside it. This has the effect of overriding and emptying the area.

{#
/**
 * @file
 * Overrides the homepage, removing the sidebar.
 */
#}
{% extends 'page.html.twig' %}
{% block sidebar %}{% endblock %}

Example: Wrap a DIV around the content and sidebar.

Using theme hook suggestions, create a template called page--front.html.twig, and use the extends tag to reference the page.html.twig template. Then, underneath, create a block with the same name of the Twig block (main), created in page.html.twig. Inside this block, create the DIV, and use the parent() function to print the contents of the Twig block as defined in the current template.

{#
/**
 * @file
 * Overrides the homepage, wrapping the main area in a div.
 */
#}
{% extends 'page.html.twig' %}
{% block main %}
  <div class="another-div">
    {{ parent() }}
  </div>
{% endblock %}

Summary
Extends facilitates a parent/child relationship with templates, using Twig blocks to manage the structure in the parent template. The child template may then choose to modify, override or inherit from the parent as desired by implementing block tags with the same name.

  • Can be used once per rendering.
  • Child templates have access to variables in both the outer and block level scopes.
  • Child templates can override or “empty” blocks, as shown in the first example.
  • Child templates can partially override a block’s content, or move it to a different location, without duplicating code, using the parent() function, as shown in the second example.
  • Code is not allowed outside of block tags in a child template. You can add new blocks to get around this, however that will not help in all cases, and if you run into this, chances are you probably want to use embed (see below). For example, if you add a new Twig block (one that doesn’t exist in the template you’re extending) with HTML it will work just fine, but if you try to use attach_library() in that same block, the library will not load.
  • You cannot pass variables into the extends tag using with {}, like you can with include or embed.

Embed

The Twig Embed tag combines the functionality of include and extend, giving the best of both worlds. The syntax is a bit more verbose, but it’s a lot more flexible. As you can probably gather from the descriptions of Include and Extends, there are some pretty clear differences of between two:

  • Include is very basic, limited functionality. The most complex thing you can do with it is reference a file to include, and pass it keywords and/or variables.
  • Extend is more complex, since it works with blocks. While its’ purpose is to allow you extend one template from another, there are limitations that make it difficult in some common use cases, such as only being able to use it once per template, and limiting content to inside blocks.

However, there are use cases where you want to be able to combine the functionality of these two tags, and that’s what the Embed tag is for. It gives the best of both worlds. It removes the limitations set in extends, allowing you to use it multiple times in a file, provides more flexibility around structure (or lack thereof), and it allows you to control the variables using with {}, only, and/or ignore missing keywords.

All of the examples I’ve provided in this post can be done with the Embed tag:

{# Include example using embed. #}
{% embed '@components/icon.twig' with { icon: 'twitter' } %}{% endembed %}

{# page--front.html.twig that overrides sidebar using embed. #}
{% embed 'page.html.twig' %}
  {% block sidebar %}{% endblock %}
{% endembed %}

{# page--front.html.twig that adds wrapper div ands uses parent() with embed. #}
{% embed 'page.html.twig' %}
{% block main %}
  <div class="another-div">
    {{ parent() }}
  </div>
{% endembed %}

Summary
Embed combines the utility of include and extends.

  • The structure of the embedded file is flexible; blocks are optional.
  • You can use embed and include tags as often as you need in a given file.
  • You can nest embed tags, e.g. one embed inside another.
  • Variables can be accessed in the embed tag using with {} keyword.
  • Variables can be limited in the embed tag using the only or ignore missing keyword.
  • Twig blocks can be overridden inside the Embed tag.
  • The parent() function can be used via blocks inside the embed tag to partially or fully override a block’s content.
  • Embedded templates have access to variables in both the outer and block level scopes.

These patterns are useful in Drupal contexts, beyond small components and the page template. They're just as useful for everything in between, from content types that have a relatively similar design, down to field and form level. As you start to get deeper into using Twig, you'll find that you can combine these techniques together to create more complex components that fit your use case.

I hope this post helps you to start thinking about ways you might write your Twig templates in a DRY, clear and concise manner. Beyond that, I hope you become inspired to take some control over architecture back. You no longer have to work within the constraints of a box that Drupal has provided for architecture, so get excited about that, and challenge yourself to create something great.

Theo Ballew, and Gabe Guevara contributed to this post.

Apr 04 2017
Apr 04

Twig is a powerful templating engine with many useful features. In the context of Drupal, especially when coming from a Drupal 7 way of doing things, it can be easy to overlook many of these features in the interest of just getting the job done quickly.  However, taking the time to learn Twig beyond just printing variables is worthwhile, because it can help you solve common problems.  

In this series, we will explore some of the tools Twig itself provides, within the context of a Drupal 8 theme to improve the quality of the code we write. Each post will identify a common problem, and provide details about Twig concepts that can be used to help solve them.

Part I: Creating Custom Components

Drupal literally throws the kitchen sink at its front end, generating every piece of HTML from a bunch of different sources: templates provided by modules, Render API, Form API, Field Formatters, and more hidden stages like Pre/Post render. Dealing with this in the theme layer has always been, and still is difficult. Twig alone does not and cannot solve this.

Given all of the above, what do you do if you just want to create something new? Something really simple that Drupal does not provide? It’s such a basic concept, yet one that is rarely encountered in custom themes, because the control in this area is weighted toward the backend first. As web developers, we’re taught and encouraged to keep our code DRY, and we want to, but Drupal makes this very difficult. Defining a custom Drupal template that you can reuse throughout your theme requires:

  • General PHP knowledge.
  • A basic understanding of how Drupal hook implementations work.
  • Knowledge of how to implement hook_theme() inside a theme to create a hook.
  • Knowledge of how to invoke that theme hook in practice. 

When faced with a very basic use case: “I have a bit of code that I want to be able to use all over this theme,” you kinda get slapped in the face with the reality of needing to learn a bunch of PHP/Drupal concepts. In prior versions of Drupal, you either learned how to do these things, or threw your hands up in frustration, and proceeded to duplicate code all over the place.

The bad news is, this is still hard in Drupal 8 with Drupal templates. The good news is, with the help of Twig, its native functionality, and the Component Libraries module, in many cases you can sidestep Drupal templates and go with Twig templates instead where it makes sense, and easily begin to weave them into your theme.

As a result of this, we’re finally starting to see some exciting prospects in the front end Drupal community with contributions like Pattern Lab Starter and Emulsify. These themes are essentially toolkits that take advantage of functionality provided by Twig and the Component Libraries module to implement a Drupal specific version of Pattern Lab.

But what if you’re not yet comfortable with, or just prefer not to use Pattern Lab? With smaller projects, and smaller budgets, there simply may not be enough time to learn about and work one of these into your project. In any case, it’s helpful to learn the concepts driving this functionality because you can apply those concepts to create reusable components in your own themes, at a level and pace that makes sense for you.

Component Libraries Module

The Component Libraries module provides the needed functionality to use plain Twig templates in our themes. It’s important to clarify what I mean by Twig template vs Drupal templates, so let’s get that out of the way.

Template Suffix Details Drupal .html.twig Originates from Drupal in form of theme hook implementation and is tracked in the theme registry. Can be used in the PHP files of modules and themes. Twig .twig Originates from a theme directory defined in .info.yml as Component Libraries module namespace. Has no theme hook implementation, and isn’t tracked in the theme registry. Cannot be used in PHP files.

To get started, we need to define a namespace and a path to directory where the components will live, within our theme, outside of the templates directory (which is reserved for Drupal templates). This is done in the theme’s .info.yml file. There can be multiple namespaces, and directories, but let’s keep this as simple as possible and use the “components” namespace and directory:

component-libraries:
  components:
    paths:
      - components

With that in place, after we clear the cache, creating new Twig templates, or components, is as simple as creating files inside the directory. For example, to create a component template called foo, you simply create a file named /themes/themename/components/foo.twig.

The foo.twig component template is ready to use, and can be used in any theme template via Twig include, extends and embeds tags. I’ll cover each of these tags in more detail in Part II. To use this template from another Twig template, you would use the include tag:

{% include '@components/foo.twig' %}

That’s all there is to it. For the majority of use cases (namely those that don’t involve working with PHP code), this allows you to gain the benefits of a custom theme hook, without PHP or Drupal knowledge.

Creating a Simple Icon Component

Now that we know how to define a Component Library directory, let’s delve deeper into a real life example. One common use case is the implementation of icons.  In this case we’ll go through the details of implementing SVG sprites.  There are different ways to approach the particulars here, and I won’t get into much detail on them, since they’re off topic, but this generally involves:

  • A directory in the theme that contains the icons.svg files, e.g:

    build/img/icons/{icon}.svg
    
  • A step in the build process that transforms the individual files into a single sprite file (here I’m using gulp-svgmin and gulp-svgstore in a pretty standard gulp task):

    gulp.task('img:icons', function() {
      return gulp.src(['img/icons/*.svg'])
        .pipe(rename({ prefix: 'icon-' }))
        .pipe(svgmin())
        .pipe(svgstore({ inlineSvg: true }))
        .pipe(gulp.dest('../img'));
    }); 
    
  • Printing the contents of the SVG file on each page of the Drupal site via html.html.twig: 

    {% include active_theme_path() ~ '/img/icons.svg' %}
    
  • Our custom Twig component template, which we use to print the HTML needed to reference a single icon from the sprite. This file lives in our theme component directory as themes/themename/components/icon.twig:

    <svg class="icon icon--{{ icon }}"><use xlink:href="#icon-{{ icon }}"></use></svg>
    

For the purpose of this post, let’s start with the 4th bullet point and create the icon template file at themes/themename/components/icon.twig, placing the markup below it inside. Once that is done, the component is complete.

As you can see, it uses a variable, “icon” which serves as the both name of the original icon file without the extension, and the symbol id reference in the sprite.  When using this component template where we want this icon to appear,  we’ll call the template using Twig’s include tag, and set that variable as the icon we want to use:

{% include '@components/icon.twig' with { icon: 'twitter' } %}

The above code will result in the following markup.

<svg class="icon icon-twitter"><use xlink:href="#icon-twitter"></use></svg>

Scratching the Surface

I hope this post was a helpful introduction for how you might approach getting started with creating your own components. The component templates that you create, how you structure them, how you name them, and what you do in them are totally up to you, and there are so many options.

I recommend checking out the Zen theme for more examples of basic components, and how you might organize them with CSS, and also what has been done in the Pattern Lab space, linked near the top of this post.

The ability to do this opens up a ton of possibilities, and the most exciting aspect of it for me, is that it actually gives some real power to themes from an architectural standpoint. This is the first time in the history of Drupal, that we are able to make these kinds of decisions, and I’m excited to see where we take it over the course of the Drupal 8 cycle.

If you enjoyed this post, stay tuned for Part II, where I’ll cover Twig include, block, extends, and embed tags in-depth.

Theo Ballew, and Gabe Guevara contributed to this post.

Mar 08 2017
Mar 08

Acquia announced Monday that Chapter Three was one of its top three growth partners in 2016!

Chapter Three has developed and hosted multi-site installations for large companies like CooperVision and Memorial Care on the Acquia platform for over four years, and we partner with Acquia to host many more sites.

"Chapter Three partners with Acquia to create enterprise class solutions," explains Chapter Three managing partner, John Faber. "We use the Acquia platform for our Drupal 8 sites, supporting our clients by supporting each other."

Acquia is a certified Drupal 8 hosting provider and is especially beneficial for Drupal 8 sites. Drupal 8’s Configuration Management system and the Acquia platform dramatically simplify multi-site installations and management. Each module’s configuration uses a shared API, meaning that Chapter Three site builders can move configuration reliably from one environment or site to another as easily as they can move code.

"Acquia partners are thought leaders, storytellers, and tastemakers who continue to amaze me with their ability to think ahead of the curve," said Acquia director Meg Fitzgerald. "They are defining what a customer journey looks like and ultimately sculpting what the next version of digital engagement should be."

The Growth Partner Award that we recently won from Acquia shows we’re not like any other partner, we’re Drupal 8 growth. We can step up to the plate and say, we know this stuff. That’s how Drupal works and that’s how open source operates.

Dec 09 2016
Dec 09

Using hook_views_query_alter to alter a queryWhen you first hear of Drupal's hook_views_query_alter you think you get it. You understand Drupal's hook system (presumeably, otherwise start here), you've used other hooks before, maybe you even experience a twinge of guilty excitement that it's one of the hooks that hasn't been removed from Drupal 8. You think to yourself, okay, this function will be passed a query argument, and I'll alter it -- and you're right! But it turns out it's a little more complicated than it sounds. First of all, your typical support system is conspicuously absent: most of the comments on api.drupal.org are about just getting the hook to fire; there's very little on stackoverflow about what to actually do once you get that $query object; and what little information there is out there is about adding new conditions/sorting/etc to your query, not changing what's already there. Then, once you give up on finding a meaningful example and dig into that $query object, you're confronted with an intimidating mess of arrays nested inside of objects nested inside of arrays. In this blog post we will be looking at how to use the $query object passed in to hook_views_query_alter to alter existing where conditions in a Views-generated sql query.

Use Case

In a side project I'm working on, I have a View that uses multiple filters, one of which is the Search Keywords filter. This filter works almost exactly as I'd like it to, except that it doesn't allow for partial word searches, e.g. if someone were to search for "burger" I would want results containing the word "cheeseburger" to show up, but they don't. Inspecting the query (clicking the "show the sql query" checkbox in admin/structure/views/settings should let you view the query on the view edit page) we see the following WHERE condition:

WHERE (((node_field_data.status = '1')
AND (node_field_data.type IN ('dish'))
AND ((node_search_index.type = 'node_search')
AND (node_search_dataset.data LIKE '% burger %' ESCAPE '\\')
AND ((node_search_index.word = 'burger')))))

If you're familiar with SQL syntax you might have spotted our issues: 1) There are spaces around the word "burger" in our LIKE statement. A SQL LIKE statement interprets the % symbol as a wildcard representing an arbitrary string of arbitrary length. So this clause will match any text containing the literal string ' burger ' (with spaces) somewhere in the middle. It will match 'good burger ' and ' burger good', but not 'goodburger' or 'cheeseburger'. So we'll want to alter this statement to remove the spaces around our search term.

2) The node_search_index.word clause is using = instead of LIKE. This clause will only accept words that exactly match the search string, and since the search index only contains complete words, this means no partial word search. Solution: swap out the = for a LIKE, and wrap the search string in % wildcard.

Okay so we've got a plan! Let's dig into some code.

DISCLAIMER: A SQL LIKE operation is far more computationally expensive than an = operation; altering this query as described here could result in prohibitive performance regressions, especially when executed on large tables. Since my side project currently has very little data to search through, and premature optimization is the root of all evil, I chose to make this compromise in the name of getting stuff done. If you are working on a medium to large site and/or a View with lots of content, I might recommend against this approach, or at least break out a profiler (see here or here). That said, the approach we're using here can easily be adapted to other alterations, so I'm gonna keep talkin!

Solution

First things first, let's take a look at that $query object in a debugger:

query object

Wow! What a load of garbage. Okay so, we've got our WHERE clause with a group of conditions, and each condition can either be a bottom level condition, or itself another condition group. Now, our first instinct might be to just find the condition that we want to edit, and just get to drupalin' with something like this:

$value = &$query->where[1]['conditions'][2]['field']->conditions[1]['value'];

But let's not do that. First of all, it's fragile. What if I add a filter to the View later on? Will my condition still be in the exact same position in the query tree? Probably not. Secondly, if we went this route, we'd have to specifically target each condition we want to edit, but (spoiler alert) new conditions are added for each extra word in the search phrase.

So, we're faced with a tree structure of arbitrary size, where each item could potentially be a condition or a new condition group, and we want to check each condition to see if it matches the portion of the query we want to alter. How do we do this? Enter recursion! If you're not familiar with recursion it might sound scary, but really all we're doing is creating a helper function that calls itself in certain circumstances.

Here's how it will work in our example: we'll start by looping over all the top level conditions, then if a condition is actually a condition group (represented by a Drupal\Core\Database\Query\Condition object), we'll call our helper function on it. The helper function will then loop over all conditions in the condition group object, and if one of those conditions happens to be a condition group object, we'll call the helper function again. This will repeat until we reach a condition group that doesn't contain other condition groups.

That was a lot of words, let's take a look at some pseudo code:

foreach top level condition
    if condition is a condition group
        call helper function on it
    else
        check if we want to alter the condition and if so alter it
        
helper function: (takes in conditions group argument)
    foreach condition in condition group
        if condition is itself a condition group
            call helper function on it
        else
            check if we want to alter the condition and if so alter it

Hopefully it's relatively clear that this code would eventually touch every condition in the query tree. If not, refer back to this overview of recursion, or ask me some questions in the comments.

Okay we're almost there. I'm going to inflict one more block of pseudocode on you, and then we'll look at the actual Drupal implementation. Why more pseudocode? Well if you look at what we have written above, you probably noticed that the helper function and the original foreach loop have a lot in common, and when you don't keep your code "DRY" @bollskis kills a kitten. Okay that's not true, but if you don't stay DRY you will definitely get a code review from some smartass on the internet. So here we go:

foreach top level condition group
    call helper function to recursively alter conditions
    
helper function: (takes in conditions group argument)
    foreach condition in condition group
        if condition is itself a condition group
            call the helper function on it
        else
            check if we want to alter the condition and if so alter it

Okay, finally, some real code!

function MYMODULE_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
  // make sure we're altering the correct view
  if ($view->storage->get('id') === 'MY_VIEW' && $view->current_display === 'MY_VIEW_DISPLAY') {
    // foreach top level condition group
    foreach ($query->where as &$condition_group) {
      // call helper function to recursively alter conditions
      _recursively_alter_query_conditions($condition_group['conditions']);
    }
  }
}

// helper function: (takes in conditions group argument)
function _recursively_alter_query_conditions(&$conditions) {
  // foreach condition in condition group
  foreach ($conditions as &$condition) {
    // if condition is itself a condition group
    if (isset($condition['field']) && is_a($condition['field'], 'Drupal\Core\Database\Query\Condition')) {
      // call the helper function on it
      _recursively_alter_query_conditions($condition['field']->conditions());
    }
    else {
      // check if we want to alter the condition and if so alter it
      _alter_query_condition($condition);
    }
  }
}

// separate helper function to determine if the condition is one we want to alter
function _alter_query_condition(&$condition) {
  if (isset($condition['field']) && ($condition['field'] === 'node_search_index.word')) {
    $condition['value'] = "%{$condition['value']}%";
    $condition['operator'] = 'LIKE';
  }
  if (isset($condition['field']) && ($condition['field'] === 'node_search_dataset.data')) {
    // here we're using trim to eliminate both the unwanted whitespace and the '%' symbols,
    // which then get added back via string concatenation
    $condition['value'] = "%" . trim($condition['value'], " \t\n\r\0\x0B%") . "%";
  }
}

Phew, we did it! Here's a recursion themed gif to celebrate:

whoa

As you can see, once we got our pseudocode sorted out, porting it over to Drupal was pretty straightforward -- just a matter of figuring out the implementation details, like how to determine whether a condition is an instance of the Drupal\Core\Database\Query\Condition class, and how to get the conditions list from said object.

The final piece of the puzzle was another helper function to alter bottom level query conditions, which simply checked to see if the condition field was one we were interested in and then altered the condition as described earlier. Take note that we were only able to act directly on the condition because we made sure to pass everything by reference -- you can see the & operator in both function argument lists and foreach loops in the code above.

I hope this was helpful, please let me know about any questions, typos, code reviews, or general snark in the comments.

Oct 31 2016
Oct 31

Our minds and hearts were full as BADCamp 2016 came to a close. It was overflowing with excellent presentations and the best of the community. Our team led a Drupal 8 training, presented in the Front End Summit, and held a few presentations and sessions along the way. Here are a few of the recordings:

The Slice Template

Zakiya Khabir and Nica Lorber

[embedded content]

This talk EXPLAINS what the Slice Template is, different development tactics for creating it, and challenges we’ve seen crafting content for it.

Deep Dive into Drupal 8

With Arlina Espinosa

[embedded content]

Object-oriented structures are replacing our all too familiar hooks, enabling us to extend Drupal with new functionality without hacking core (or other contrib modules). But how does this all work? Arlina's talk explains the details.

Golang for PHP Devs

With Mark Ferree

[embedded content]

Much like PHP, Go is a programming language designed for the Internet. Unlike PHP, Go is designed for the Internet in the second decade of the 21st century.

Desktop to Mobile: Why Your Themer Cries

With Zakiya Khabir

[embedded content]

By now we should all know that a mobile-first approach is ideal for a successful responsive website. But, out there in the real world, despite all the blog posts and best intentions, some projects require converting an existing desktop site or design into a responsive website.

Oct 11 2016
Oct 11

How to use Drush CommandsIn this blog post I will provide an example of a method I use to deploy changes on the projects that I work on my own time. This method is very useful when there is a need to make a lot of manual changes to the site when there is a new release. You could use this method to automate deployment process of your Drupal sites. In fact this method can be used for any other non-Drupal sites too.

For this blog post I am using Drush commands.

Lets start form a very simple example. I will use Acquia's directory structure to descibe where I am storing release scripts.

acquia-utils/
docroot/
hooks/
...
releases/
  - code/
    - release-2/
      - permissions.php
      - users.php
  - release-1.sh
  - release-2.sh
  - release-3.sh

As you can see releases/ directory is on the same level a docroot/. That's where the scripts would locate.

release-1.sh - A very simple release script that runs database updates and reverts features.

#!/bin/bash

# Database updates.
drush updb -y

# Revert features.
drush fr my_feature1 my_feature2 my_feature3 -y


release-2.sh - Release script with examples of how to run SQL queries and run PHP code.

#!/bin/bash

# Change weight of the module.
drush sql-query 'UPDATE system SET weight = 100 WHERE name = "my_custom_module"' -y

# Execute PHP code

# Make changes to permissions
drush php-script 'permissions.php' --script-path="../releases/code/release-2/" --user=[Drupal Admin Username]

# Make changes to users.
drush php-script 'users.php' --script-path="../releases/code/release-2/" --user=[Drupal Admin Username]


release-3.sh - Release script with additional examples.

#!/bin/bash

# Set variables
drush vset cache_lifetime 86400 -y
drush vset page_cache_maximum_age 86400 -y
drush vset error_level 0 -y
drush vset --always-set cache 1

# Execute inline PHP code

# Set permissions
drush php-eval "user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array('my_module_name' => 'permission name'));"

# Set variable
drush php-eval "variable_set('my_variable_name', array('key' => 'value'));" -y

# Clear Drupal cache
drush cc all


And finally, you would need to ssh to the server and run each release script.

What else you could do:

  • You could add backup logic so every time you run the script you would create database and files backups.
  • Make clean Drupal install locally, download database from production and restore it locally with Drush.
  • Run content migrations.

Use your imagination and please share with us the methods you use to improve your deployment process.

Oct 04 2016
Oct 04

Using Jupyter with Drupal 8Jupyter notebooks are used in data science for exploring / documenting / presenting data. If you haven't heard of Jupyter maybe you have by it's former name, iPython notebook. Since there are multiple "kernels" supported it was renamed to Jupyter ( for Julia-Python-R ). There is support for 3rd party kernels such as the Ruby kernel and thanks to the Jupyter-PHP project on github we can add PHP to the list.

Notebook results can be hosted on github as static HTML pages or hosted on a `nbviewer` server like https://nbviewer.jupyter.org. You can even host your own nbviewer server if you like.

Here is a brief walk through on getting it set up on a mac with homebrew.

Prerequisites

First you need a copy of php with the Zero MQ php library:

# Stick a tap in the homebrew php repository:
brew tap homebrew/homebrew-php

# Install a local version of php.
brew install php56

# Install the Zero MQ extension
brew install php56-zmq

# Install these extras if you like.
brew install php56-mcrypt php56-apcu php56-opcache php56-xdebug php56-xhprof php56-twig

Installing Jupyter

The easiest way to install Jupyter is with anaconda.

# Tap caskroom
brew tap caskroom/cask
# Install anaconda
brew cask install anaconda

Installing Jupyter-PHP

# Grab the installer.
wget https://litipk.github.io/Jupyter-PHP-Installer/dist/jupyter-php-installer.phar
# Get the ssh 512 signature via the browser or with:
curl https://litipk.github.io/Jupyter-PHP-Installer/dist/jupyter-php-installer.phar.sha512
# Compare the sha 512 digest of the download with the hosted hash.
openssl dgst -sha512 jupyter-php-installer.phar
# If they match you can install with:
./jupyter-php-installer.phar install

I also had to manually install the Jupyter-PHP library dependencies:

# Go to the jupyter-php folder.
cd ~/Library/jupyter-php/pkgs/
composer install

WhooHoo! Now you ready to run Jupyter notebook with Jupyter-PHP.

Starting Jupyter Notebook

# launch Jupyter notebook
jupyter notebook

If everything went well, it will open a browser browser window that looks like this:

Jupyter start screen

For an overview of using Jupyter notebooks see http://jupyter.readthedocs.io/en/latest/.

Exploring Drupal in a Jupyter notebook

Bootstrapping Drupal 8

Here we will go over bootstrapping Drupal 8. See the footnotes for bootstrapping Drupal 7.

// Load the autoloader.
$autoloader = isset($autoloader) ? $autoloader : require_once '/path/to/drupal_root/autoload.php';

You should see:

jupyter notebook 1

// Bootstrap drupal.
$request = \Symfony\Component\HttpFoundation\Request::createFromGlobals();
$kernel = \Drupal\Core\DrupalKernel::createFromRequest($request, $autoloader, 'prod');
// There are a couple of files not loaded by the autoloader.
$kernel->loadLegacyIncludes();
$kernel->boot();

jupyter notebook 2

Loading the entity type manager Drupal service.

$entity_manager = \Drupal::service('entity_type.manager');

jupyter notebook 3

Exporting entity data to a CSV file

// Load some entities.
$entities = $entity_manager->getStorage('node')->loadByProperties(['type'=>'page']);
// Open a file to write out a csv of entity data.
$file_path = __DIR__.'/entity-data.csv';
$f = fopen($file_path, 'w');
// Add a header to the file.
fputcsv($f, ['nid', 'type', 'created', 'owner']);
// Loop over each entity and add it's data to the file.
foreach ($entities as $entity) {
  fputcsv($f, [
   'nid' => $entity->id(),
   'type' => $entity->getType(),
   'created' => $entity->getCreatedTime(),
   'owner' => $entity->getOwner()->getUsername(),
  ]);
}
fclose($f);

jupyter notebook 4

Now you are ready to load the CSV in a python notebook for data exploration with:

import numpy as np
f = open("entity-data.csv")
# Skip the header
f.readline()
# Read the data into a Numpy array:
data = np.loadtxt(f)

Footnotes

Special thanks to open source software contributors

Installing php-zmq in php70 (experimental)

brew tap jenssegers/homebrew-custom
brew install --HEAD jenssegers/custom/php70-zmq

Bootstrapping Drupal 7

if(!defined('DRUPAL_ROOT')) { 
  define('DRUPAL_ROOT', '/path/to/drupal_root');
}
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
Sep 06 2016
Sep 06

Custom Drush Commands for Drupal 8 and Drupal 7It is very easy to create your own custom Drush commands. In this blog post I will show two examples for Drupal 7 and Drupal 8.

Creating custom Drush commands could be very useful when you need to import data into Drupal or export data from Drupal. You would have to create a separate bash script where you would execute your custom command and add the bash script to your crontab.

Drupal 8 example

Drupal 8 module structure:

drush_example/
  - drush_example.info.yml
  - drush_example.drush.inc

drush_example.info.yml:

name: Drush Example command
description: Drupal 8 custom drush command example
type: module
core: 8.x

drush_example.drush.inc

/**
 * Implements hook_drush_command().
 */
function drush_example_drush_command() {

  $commands['my-example-command'] = [
    'description' => 'This is my example command.',
    'aliases' => ['mec'],
    'arguments' => [
       'arg1' => 'My custom argument 1.',
       'arg2' => 'My custom argument 2.',
     ],
     'options' => [
       'opt1' => 'My custom option.',
     ],
     'examples' => [
       'drush mec' => 'Print my example command.',
       'drush mec myargument' => 'Print my example command with an argument "myargument".',
       'drush mec myargument --opt1=myoption' => 'Print my example command with an argument "myargument" and an option "myoption".',
     ],
  ];

  return $commands;
}

/**
 * Drush command logic.
 * drush_[MODULE_NAME]_[COMMAND_NAME]().
 */
function drush_drush_example_my_example_command($arg1 = 'N/A', $arg2 = 'N/A') {
  $opt1 = drush_get_option('opt1', 'N/A');
  $tokens = ['@arg1' => $arg1, '@opt1' => $opt1];
  drush_print(dt('My custom command. Argument: @arg1 Option: @opt1', $tokens));
}

Drupal 7 example

I will use the following structure for the D7 example module:

drush_example/
  - drush_example.info
  - drush_example.module (empty .module file)
  - drush_example.drush.inc

drush_example.info:

name = Drush Example command 
description = Drupal 7 custom drush command example
core = 7.x

drush_example.drush.inc

/**
 * Implements hook_drush_command().
 */
function drush_example_drush_command() {

  $commands['my-example-command'] = array(
    'description' => 'This is my example command.',
    'aliases' => array('mec'),
    'arguments' => array(
       'arg1' => 'My custom argument 1.',
       'arg2' => 'My custom argument 2.',
     ),
     'options' => array(
       'opt1' => 'My custom option.',
     ),
     'examples' => array(
       'drush mec' => 'Print my example command.',
       'drush mec myargument' => 'Print my example command with an argument "myargument".',
       'drush mec myargument --opt1=myoption' => 'Print my example command with an argument "myargument" and an option "myoption".',
     ),
  );

  return $commands;
}

/**
 * Drush command logic.
 * drush_[COMMAND_NAME]().
 */
function drush_my_example_command($arg1 = 'N/A', $arg2 = 'N/A') {
  $opt1 = drush_get_option('opt1', 'N/A');
  $tokens = array('@arg1' => $arg1, '@opt1' => $opt1);
  drush_print(dt('My custom command. Argument: @arg1 Option: @opt1', $tokens));
}

Aug 07 2016
Aug 07

(This module was tested on drupal 8.1.8)

Twig Extensions, Views and Drupal 8

Problem to solve

Sometimes working with Views can be a challenge, in some cases you want to modify the values or process the content of the field.

Many developers use the preprocess functions, or alter the query of the view, however there is another option: twig filters.

The Goals:

Being able to calculate the reading time of any given node (body field).

The field needs to output: "5 min read", "9 min read", depending on the word count of the node.

Twig Filters:

Twig filters give us the ability to modify variables on twig template.

{% set salutevariable = "Hello Friend" %}"

If we output the variable {{ salutevariable }} is going to show: Hello Friend

If we want to make the variable lowercase, we use a filter.

{{ salutevariable | lower }} the "|" indicates that we are going to use a filter in our case "lower", so the value "Hello Friend" is going to be parsed by the filter lower.

The output will be: hello friend

Official Documentation:

Creating A Module:

Create a new module in: modules/custom/

Our module is going to have the following structure:

|readingtime/
|-- readingtime.info.yml
|-- readingtime.services.yml
|-- src/TwigExtension/ReadingTimeExtension.php

readingtime.info.yml contains:

name: readingtime
type: module
description: Provides a Twig filter that calculates the time of reading for any given string.
core: 8.x
package: Custom

Create a services file: readingtime.services.yml

services:
  readingtime.twig_extension:
    class: Drupal\readingtime\TwigExtension\ReadingTimeExtension
    tags:
      - { name: twig.extension }

The ReadingTimeExtension.php will contain the following:

<?php

namespace Drupal\readingtime\TwigExtension;

use Drupal\Core\Render\Markup;

class ReadingTimeExtension extends \Twig_Extension {
  /**
   * Here is where we declare our new filter.
   * @return array
   */
  public function getFilters() {
    return array(
      'readingtime' => new \Twig_Filter_Function(
        array('Drupal\readingtime\TwigExtension\ReadingTimeExtension', 'readingTimeFilter') // Here we are self referencing the function we use to filter the string value.
      )
    );
  }

  /**
   * This is the same name we used on the services.yml file
   * @return string
   */
  public function getName() {
    return "readingtime.twig_extension";
  }

  /**
   * @param $string
   * @return float
   */
  public static function readingTimeFilter($string) {

    if($string instanceof Markup) { // we check if the $string is an instance of Markup Object.

      // strip_tags help us to remove all the HTML markup.

      // if $string is an instance of Markup we use the method __toString() to convert it to string.

      $striped_markup = strip_tags($string->__toString()); 

      // On avarage we read 130 words per minute.

      // the PHP function str_word_count counts the number of words of any given string.

      $wpm = str_word_count($striped_markup)/130; // $wpm = Words Per Minute.

      $reading_time = ceil($wpm); // Round the float number to an integer.

      return $reading_time; // we return an integer with the number of minutes we need to read something.

    } else {

      return $string;

    }

  }

}

Testing our brand new filter:

Now we install our new module in admin/modules.

It's also helpful to generate dummy content with the devel module.

The View:

Then I proceed to create a simple view of articles:

View configuration:

FORMAT:

show: fields

FIELDS:

Content: Title
Content: Body ( this is the one we are going to apply our new twig filter )
Content: Body

We access the configuration of the second Content: Body

and under the REWRITE RESULTS option we are going to override the output of the field: 

overriding field views drupal 8

and the result is the following:

twig filters views result

Jun 27 2016
Jun 27

Paragraphs has become a popular site building tool for Drupal. In the feedback to our recent blog post, some asked why the Chapter Three team has not fully embraced the module. Most of our Drupal 8 sites use Entity Construction Kit with Inline Entity Form (ECK/IEF) to achieve what others do with Paragraphs.

Slice TemplateThe Slice Template design could be created using Paragraphs or Inline Entity Form.

This post seeks to explain why one might choose one module (or set of modules) over the other for semi-structured content in Drupal 8.

We'll be using our colloquial term "slice" to refer to a paragraph-like entity created with the ECK/IEF approach.

Similarities

Both approaches allow for the creation of horizontal chunks of structured content that require no technical knowledge to create, and allow for flexible displays. Both work with Drupal's multilingual system. Both use Drupal's Entity API system, and are therefore fieldable. The theming experience of working with ECK and Paragraphs is similar; both return render elements and provide some template suggestions for bundles and view modes.

Site Builder Experience

Paragraphs
After installation, Paragraphs are immediately available to be added to nodes. If you neglect to add paragraph types, you are given a convenient link to go do so.

ECK/IEF 
The difference between a bundle and an entity can be a bit confusing the first time you use ECK/IEF.

First, set up ECK Entity types then Bundle types. Then add references to the Bundle type as a field on each Content type. Finally configure the form display on each Content type that uses Inline Entity Form. Additionally, each time you add a new Bundle type, you'll have to add it to each field that the type needs to make it available. The learning curve is small, but it exists nonetheless.

Winner: Paragraphs


Content Editing Experience

Paragraphs
On the form configuration page you can choose whether (1) all form items are opened completely, (2) each paragraph is accessed by accordion, or (3) users see exactly what they see on the front end. You can also choose the display of the paragraph type selector. 

ECK/IEF
While you'll have to use the Inline Entity Form Preview (IEFP) module if you want preview display, all the other form options provided by Paragraphs (and more) are available using ECK/IEF. IEFP also provides the ability to choose a configurable "Preview" view mode. While this is extra site building work, it facilitates a better content editing experience. Rich content parts can be condensed and rendered in a more "edit-mode" appropriate way. For example, images can be set to render at smaller sizes, and entity references can be shown as labels, instead of rendering directly inline, etc. This can be managed on a case by case basis.

Both 
Working with many attached entities can be a slow and downright painful experience. Drupal's TableDrag functionality allows editors to re-order content parts, but doesn't perform very well with many pieces of content. All of these modules, and Drupal in general, would benefit from an investment in improving TableDrag functionality.

Winner: Tie
Comparison of Slice vs Paragraph Content Editing ExperiencesInline Entity Form next to Paragraphs in "Preview" mode. The content editing experiences of Paragraphs and ECK/IEF can be very similar.


Revisions

Paragraphs
Entity Reference Revisions is a requirement for Paragraphs so it is innately revision-friendly without any extra configuration.

ECK/IEF 
Alas, there isn't any out-of-the-box support for revisions using ECK/IEF on Drupal 8 at the time of this posting. Contributors to this Drupal.org issue for Inline Entity Form, many of whom are Chapter Three employees, are working toward a solution. 

Winner: Paragraphs


Search

Paragraphs 
Drupal's default search indexes Paragraph entities as part of their parent nodes without any extra configuration.

ECK/IEF
ECK entities are also indexed by core Drupal search. Just make sure that fields are being displayed, and users have permission to view the custom entities.

Winner: Tie

Reusable Entities

Paragraphs
This issue sums it up. If you want to reuse the same paragraph on multiple pages, you're out of luck. It's possible, though, to reference other kinds of entities inside a paragraph.

ECK/IEF 
You can easily reuse slices with the ICK/IEF set up. You can just as easily restrict that functionality from users. It's important to have strong naming conventions if your users can reuse entities. A potential drawback of the Entity system is that each entity/slice has a required title field whether you plan to display that title or reuse the content.

Winner: ECK/IEF
 

Flexibility

Paragraphs
Configuration options are limited to exactly what is expected of the module. Use the included Paragraphs Type Permissions module for more user-level control.

ECK/IEF 
With the ECK/IEF you have lots of options. You can limit which slice types are available to a field; you can change naming conventions in the UI; you can set permissions per type; you can choose whether or not the user can use existing slices; and much much more.

Additionally, ECK and IEF are useful for structured content that is not tied to a node nor should be classified as a paragraph type. In a recent project, we created Entities called "chunks" to hold things like accordion sets, newsletter links, calls-to-action, and featured content.

Winner: ECK/IEF
 

Unique URL

Paragraphs
Paragraphs are only accessible by visiting the referenced container (like the node).

ECK/IEF
Each Entity has its own url. But in most cases, there's no need for these types of entities to have one and it's best to use a module like Rabbit Hole to obscure it.

Winner: Paragraphs


Support

Paragraphs
There are many blog posts, tutorials, and Drupal camp sessions dedicated to using the Paragraphs module as a site builder.

ECK/IEF
Beginner tutorials about ECK/IEF aren't as prevalent. If you get stuck on a custom feature, you may have to spend more time searching for answers. 

Winner: Paragraphs
 

Conclusion

The Paragraphs module is great for what it was designed. It is an easy-to-use solution to a common configuration problem. It is also the only Drupal contrib choice if you need structured content to be revisionable. Site builders without a development team, and those without the time or desire to dig in and customize the experience beyond Paragraphs' default offerings, will find it especially attractive.

Nearly the exact same functionality, however, can be achieved by using Entity Construction Kit and Inline Entity Form. While the initial set up may not be intuitive, and will take more time upfront to configure, it is also more extensible and customizable.

For smaller sites with simpler specs, Paragraphs might be all you need. If you're looking a more custom solution that can be extended in the future, Entity Construction Kit + Inline Entity Form is the way to go.

At Chapter Three, we build relationships with clients that often last for years. We know that priorities and technical specifications change. We typically choose ECK/IEF over Paragraphs either because of a particular specification in the initial build, or because we know we'll need more flexibility in the long-term. With a little more effort up-front, we deliver a first-rate content editing experience and meet the ever-changing needs of our clients.

Jacine LuisiJaesin MulenexDaniel Wehner, and Mark Feree contributed to this post.

Jun 13 2016
Jun 13

For the past two to three years, we’ve been evolving a flexible design solution for marketing pages. We call it the Slice Template.

What is the Slice Template?

The Slice Template is kind of like a layer cake. It’s  a design structure that enables site authors  to create web pages out of modular, horizontal components that span the width of the browser. Popularized by responsive design, this pattern has become a web standard. That’s because it can support a myriad of content.

Our team started calling these components slices early on. You could call them that too. We can make it our little thing :)

What is it for? 

The Slice Template empowers marketers to create pages that look elegant and sophisticated without the assistance of a designer or developer. Specifically, marketers can easily build pages from slices that have two, three or four columns, and varying background colors and image placement options. They can add other slices to support a specific strategy, such as quote carousels, product highlights or calls to action. 

Improvements with Drupal 8

Early versions of this solution worked on the front end, but were hard to administer on the back end. With Drupal 8, I’m thrilled to announce that the authoring experience (AX) has improved dramatically. I literally shrieked with delight the first time I used it. This is largely due to two things:

  1. The AX is faster and snappier in Drupal 8 (a lot faster)
  2. Uploading images is quicker and easier because there is only one method. A user uploads from his or her computer. There is no need to connect to the server. (Note: You should monitor the size and number of uploaded images to prevent performance issues.)

How it works on the back end

Naming the slices something obvious helps authors know what to expect. Slices can be used once or be reused on multiple pages.

Slice Back End

Slices are edited from a single interface. Selecting "edit" expands the slice. From there, the author can save changes and collapse it again. The collapsable feature keeps the interface clean and user friendly.

What do the Slices look like?

The slices can look however you like, however we've identified a baseline set that are useful for most sites. These are shown below.

One Column

One Column

Image Left Text Right

Image Left Text Right

Image Right Text Left

Image Right Text Left

Three Column

3 Column
 

Four Column

4 Column

Additional Custom Slices

Featured Content

Featured Content

Accordions

Accordions

CTA (Call to Action)

CTA

Customer Testimonial

Customer Quote

 

The takeaway

Drupal 8 has made our popular, time-tested Slice Template much easier to administer. We can now provide flexible, elegant design options without sacraficing the back end experience. 

Examples of pages built with the Slice Template:

Jun 09 2016
Jun 09

Basic Configuration ManagementWhat is CMI?

One of the major features of Drupal 8 is the Configuration Management Initiative (CMI). I’ll briefly touch on why CMI is so important, but our own Alex Pott has written extensively on the creation, principles and importance of this feature.

Previous to Drupal 8, it was cumbersome to trade configuration between sites and environments. Moving configuration changes from a developmental environment into production meant either making the changes manually through the UI, or utilizing an advanced option like Features. A third option would be full-database swapping, but that can be quite risky and is generally considered poor-form.

Configuration Management (CMI) allows us to export our site’s configuration settings into files. This means that we are now able to capture configuration changes in code. Pushing configuration through code means we can utilize and take advantage of Git’s features to manage our configuration in our repos! No more database swaps, no more manual checkbox clicking. This is a major win.

While CMI has a UI component, I strongly suggesting using Drush. The UI is a fine option when importing/exporting full-configurations, but the individual or “cherry-picked” changes take quite a bit of navigation. CMI is new to Drupal 8, and while it’s very powerful it’s also undergoing development - this includes the UI.

Database In, Database Out

The basic concept behind CMI is this:
Either you’re spitting out a site’s configuration into YML files from its database, or you’re sucking up YML files into a site’s database. It’s a basic import or export of configuration. You can consider the current database's configuration to be the "Active" configuration, and the exported YML configuration to be considered the "Staged" configuration. These configuration files typically live in the /sites/default/config directory, but this can change from site-to-site depending on what’s configured in settings.php.

You might be wondering “What does CMI capture, exactly?”. CMI is engineered to capture only the configuration aspects of a site. Configuration includes Views, Fields, Content Types, site settings, which Modules are enabled or disabled, individual Module settings, even Block placement. However, it does not capture site content – an important distinction to keep in mind.

Drush CEX

You can use this Drush command to export configuration: drush cex

$~ drush cex

Drush will then prompt you with a list of items in your existing configuration YML files that will be overwritten with configuration from your database:

Drush CEX Screenshot

Drush CIM

You can use this command to import configuration: drush cim

$~ drush cim

Drush will then prompt you with a list of items in your site’s database that will be overwritten with configuration from the files present in your configuration directory:

Drush CIM Screenshot

Typical Workflow

Your environment setup might vary, but this is the workflow I typically work with: I’ll make configuration changes locally and run the drush cex command. I’ll then add the resulting changed YML files to Git, and commit those changes to the project’s repository. Once the changes are in pulled into the development environment I’ll run the drush cim command. This imports the new configuration changes I’ve made from the YML files into the Development environment’s database.

May 31 2016
May 31

We Give a HootPerformance

As our team of impassioned front-end developers hammer their anvils in harmony, they’re putting influential principles in motion. We code with grace. We stride through our text editors with elegance and poise. Our minds and fingers fly with synchronicity in a display you could only describe as “truly inspiring”.

Maybe that’s not exactly the case, but we’re certainly passionate about what we do. Our team cares about the quality of work we produce, and about the principles we employ. We craft with great discretion and intention. Sometimes with grace, and sometimes with gallons of cold brew and thrash metal.

When we’re not producing sleek, performant projects, then we’re learning and exploring new methods. We're exercising forward-looking principles to use in future projects. For Front-end development, our work has a strong emphasis on performance. Almost everything we ship has a golden rule of keeping performance in tip top shape, and for good reason:

The faster and more performant your site or system is, the happier your users are - and that’s immensely important.

Performance is Important to Your Business Goals

A slow site means unhappy users, and unhappy users are lost conversions, lost leads, and lost potential customers. There are plenty of experiments, writings, articles, blog posts, and other evidence on the web to show exactly how poor performance negatively affects your business. Not only that, it directly affects the efforts of your Marketing Team. Performance is the double-edged blade that can make or break your marketing efforts. Performance is the difference between a user signing up for your trial, or leaving before the site has a chance to load. The Marketing Team needs performance to succeed.

Marketing can be the Performance Killer

Once our team has a project primed and ready for launch, glistening with sleek and innovative performance optimizations, it doesn’t take long before we’re asked to “add scripts to the footer” (or similar). These marketing scripts are usually javascript, and typically track visitor behavior. Other variations might deliver ads or capture information about the user so targeted ads could be delivered.

Each script requires the visitor’s browser to fetch data from the script’s originating server. This action causes delays in a user’s load time. These scripts also often manipulate the structure of the HTML, causing even more delays. Sometimes they load even more scripts, affecting the performance exponentially. These delays aren’t insignificant - these tracking scripts effectively extinguish our performance efforts causing sites to plummet in load speed, and increase visitor exits.

The bottom line is that these scripts dramatically hurt performance, and exponentially so for each script added.

It’s not uncommon to see our page load-times increase by 100-300% or more when these tracking scripts are implemented. That can mean a lot of unhappy users, many lost conversions, and many more missed opportunities for your Marketing Team to accomplish their goals.

We All Want the Same Thing

It’s easy for us front-enders to perpetuate a burning rage for marketing tracking scripts. It's even easier to throw our hands in the air and “let the Marketing Team do whatever they want”. However, this misses the bigger point: the Marketing Team, the front-end developers, and especially our larger teams overall, we all want the same thing. We want the project to succeed.

These tools and tracking scripts provide insight to information not readily accessible through other means. They have essentially become a staple to online-marketing. We understand they’re not going anywhere, they’re useful to the Marketing Team, and they’re (gradually) getting better. With that in mind, we understand their place in our ecosystem.

Can’t Force It

We’re always working with performance in mind, but some contracts have an explicit performance element to target specific business goals. Rarely are these requirements truly hellish. However, when it comes to the marketing integrations and the effects they have on a project’s performance, our hands are tied. A site’s performance is at the full-mercy of several layers of technology: DNS resolution, server delay and delivery, script optimization, structure, DOM manipulation and much more.

Even the best developers’ hands are tied when it comes to improving the performance of marketing scripts.

The Solution

I’m not suggesting we sever ties with all tracking scripts. The easiest way to retain the performance, the happy users, and the higher conversion rates would be to get choosey with that tracking and marketing system you utilize. It might seem like using as many marketing services as your budget allows will improve conversions and inflate your overall return, but the ramifications are unavoidable - your site’s performance will drop severely, and your goals will suffer. Also, most of these marketing services offer the same (or similar) functionalities. Having several is definitely overkill.

Google Analytics is certainly the most commonly used service, and it’s fairly performant. With all that Google Analytics provides, how many more services do you need? Knowing that your performance will suffer (and knowing your site’s performance is directly related to your marketing success) what specific functionality do you absolutely need?

I should also state that we’re more than happy to help you answer these questions. Please ask us for insight on what services have been troublesome or performant and easy to implement. Chances are we’ve worked with the services you’re considering or currently using.

Segment

We’ve recently caught wind of a service that aggregates tracking actions and feeds them to various tracking sources via an API. In other words, it’s a central service that speaks to multiple marketing and tracking services. Its primary feature could retain performance by only existing as a single service while feeding data to a plethora of other services. This behavior is distinctly different than the Google Tag Manager. It’s a service called Segment, and it looks quite promising.

May 30 2016
May 30

How to Improve Performance with HTTP cache headersIn this blog post I will show you a simple technique to improve your web application performance by modifying headers. Please keep in mind, if you're using HTTP reverse proxy caching applications such as Varnish you might harm your application performance or your settings could be ignored.

This technique could help improve page loads for authenticated users where reverse proxy caching disabled.

Important: Drupal already already provides all the required headers when Performance settings properly configured. This is information is generic and could be very helpful for decoupled projects or any other frameworks.

There are two primary cache headers, Cache-Control and Expires. You can set caching either time-based, content-based or on Expire date.

Cache-Control

The Cache-Control general-header field is used to specify directives that MUST be obeyed by all caching mechanisms along the request/response chain.

Settings

Example header:
Cache-Control: max-age=900, public

public - Indicates that the response MAY be cached by any cache, even if it would normally be non-cacheable or cacheable only within a non- shared cache.

private - Indicates that all or part of the response message is intended for a single user and MUST NOT be cached by a shared cache. This allows an origin server to state that the specified parts of the response are intended for only one user and are not a valid response for requests by other users. This parameter doesn't provide the same level of privacy as SSL does.

max-age - Indicates that the client is willing to accept a response whose age is no greater than the specified time in seconds. Unless max- stale directive is also included, the client is not willing to accept a stale response. (The value is in seconds)

See all available options RFC 2616

Expires

Note: If both Expires and max-age are set max-age will take precedence.

This header parameter tells the browser when to next retrieve the resource from the network.

Example:

Cache-Control:public
Expires: Mon, 25 May 2016 11:31:12 GMT

Time-based

Last-Modified - The Last-Modified entity-header field indicates the date and time at which the origin server believes the variant was last modified.

Example:
Last-Modified: Wed, 25 May 2016 11:45:26 GMT

If-Modified-Since - The If-Modified-Since request-header field is used with a method to make it conditional: if the requested variant has not been modified since the time specified in this field, an entity will not be returned from the server; instead, a 304 (not modified) response will be returned without any message-body.

Example:
If-Modified-Since: Wed, 25 May 2016 11:45:26 GMT

Content-based

ETag - Short for "entity-tag", the ETag is a unique identifier for the resource being requested, typically comprised of the hash of that resource, or a hash of the timestamp the resource was updated. Basically, this lets a client ask smarter questions of the CDNs, like "give me X if it's different than the ETag I already have."

Note: This tag is useful when for when the last modified date is difficult to determine.

Cache-Control:public, max-age=604800
ETag: "4345717de182e49e8d7bd9994af537ed" 

On subsequent browser requests the If-None-Match request header is sent with the ETag value of the last requested version of the resource.

If-None-Match: "4345717de182e49e8d7bd9994af537ed"

Drupal 8 Example

/**
 * Example Even Subscriber.
 */
class AddCustomHTTPheaders implements EventSubscriberInterface {

  /**
   * Sets extra HTTP headers.
   */
  public function onRespond(FilterResponseEvent $event) {
    $response = $event->getResponse();
    $current_user = \Drupal::currentUser();
    // Set Cache-Control for authenticated users
    if ( !$current_user->isAnonymous() ) {
      $response->headers->set('Cache-Control', 'public, max-age: 604800');
    }
  }

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

}

Please read How to Register an Event Subscriber in Drupal 8 to learn more about the code example. And download Drupal 8 module to see how this can be implemented.

Useful links

  1. Increasing Application Performance with HTTP Cache Headers
  2. HTTP caching - Web Fundamentals - Google Developers
  3. Analysis of HTTP Performance problems
  4. Bloated Request & Response Headers
  5. Improve Performance with Cache-Control Headers
  6. Caching Tutorial
  7. A Guide to Caching with NGINX
  8. RFC 2616 - 14.9 Cache-Control
May 27 2016
May 27

Easy Drupal Development with Drush ScriptingWhether you work on one Drupal site or multiple, it is often necessary for your local dev environment to be slightly different from your site's server.  Perhaps you need to disable Secure Pages because you don't want to set up SSL on your local environment, or there are modules specific to your website's server config.  If you work on multiple sites in a sporadic fashion its possible you need to synchronize your local database with the dev server between tasks, that way you aren't missing any updated configurations.

Sure, you can pull this off manually by grabbing the database, reloading your local, and updating your Drupal site's config; but why not add a little automation to help out?

Using a Shell Script with Drush

Recently I was setting up a local environment and had the opportunity to use a script created by Colan Schwartz. If you are running Linux or OSX then you can easily use this script and modify it to your own needs.  If you haven't done much shell scripting, or you use an alternative OS such as Windows, you may want to get a quick overview from howtogeek.com

Now that you're an expert on shell scripting you should check out Colan's very well documented script on gitlab. You may need to update the command path variables

# Set command paths.
DRUSH=/usr/local/bin/drush
GZIP=/bin/gzip
CUT=/usr/bin/cut
GREP=/bin/grep
ECHO=/bin/echo
DATE=/bin/date
SUDO=/usr/bin/sudo
CHOWN=/bin/chown
ID=/usr/bin/id
MKDIR=/bin/mkdir

Also, the default USER_WEB settings may be incorrect for you setup and your web server user is not one of: php-fpm, nginx, apache, or www-data.

The MODULES_DISABLE variable controls what modules are disabled on your local environment, and you can modify MODULES_ACQUIA and MODULES_ADVAGG with your own variables and module lists.  

If you'd like to test out the script make sure you have a backup of your code and databases.  Once you get the base settings adjusted for your setup, updating the settings for subsequent sites will be quite easy.  Running the script executes the following commands:

  1. Saves a cache-cleared dump of the destination database (DB) as a backup.
  2. Overwrites the destination DB with the source's.
  3. Updates the DB schema.
  4. Imports the latest configuration saved in code.
  5. Disables modules that shouldn't be enabled during development.
  6. Enables modules that are helpful for development.
  7. Clears all caches.
  8. Overwrites the destination's files directory with the source's.
  9. Runs cron.

I hope this helps your development workflow, if you were able to adjust the script for your specific setup please share your results.  Happy scripting!

Some additional items:

If all you need to do is rebuild your local dev environment then check out this cool project which turns the process into a simple drush command: Drush Rebuild.

Here are a few other scripts that you might find useful or inspirational:

May 26 2016
May 26

How to Register an Event Subscriber in Drupal 8Events in Drupal 8 allow for different components of the system to interact and communicate with each other. One system component dispatches the event at an appropriate time; many events are dispatched by Drupal core and the Symfony framework in every request. Other system components can register as event subscribers; when an event is dispatched, a method is called on each registered subscriber, allowing each one to react.

Most of the hooks from previous versions of Drupal were removed in Drupal 8 in favor of Events. Example: hook_init() or hook_boot() which now can be done by registering an event subscriber.

I will use the following structure for the example module:

my_event_subscriber/
  - my_event_subscriber.info.yml
  - my_event_subscriber.services.yml
  - src/
    - EventSubscriber/
      - MyEventSubscriber.php

Standard my_event_subscriber.info.yml file for the module:

name: Register an Event Subscriber
type: module
description: 'Example: How to Register an Event Subscriber in Drupal 8'
core: 8.x
package: Other

Define a service tagged with 'event_subscriber' in the my_event_subscriber.services.yml.

services:
  my_event_subscriber:
    class: '\Drupal\my_event_subscriber\EventSubscriber\MyEventSubscriber'
    tags:
      - { name: 'event_subscriber' }

src/EventSubscriber/MyEventSubscriber.php contains a class that implements \Symfony\Component\EventDispatcher\EventSubscriberInterface

/**
 * @file
 * Contains \Drupal\my_event_subscriber\EventSubscriber\MyEventSubscriber.
 */

namespace Drupal\my_event_subscriber\EventSubscriber;

use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Event Subscriber MyEventSubscriber.
 */
class MyEventSubscriber implements EventSubscriberInterface {

  /**
   * Code that should be triggered on event specified 
   */
  public function onRespond(FilterResponseEvent $event) {
    // The RESPONSE event occurs once a response was created for replying to a request.
    // For example you could override or add extra HTTP headers in here
    $response = $event->getResponse();
    $response->headers->set('X-Custom-Header', 'MyValue');
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    // For this example I am using KernelEvents constants (see below a full list).
    $events[KernelEvents::RESPONSE][] = ['onRespond'];
    return $events;
  }

}

Here is a list of KernelEvents constants:

KernelEvents::CONTROLLER; // The CONTROLLER event occurs once a controller was found for handling a request.
KernelEvents::EXCEPTION; // The EXCEPTION event occurs when an uncaught exception appears.
KernelEvents::FINISH_REQUEST; //The FINISH_REQUEST event occurs when a response was generated for a request.
KernelEvents::REQUEST; // The REQUEST event occurs at the very beginning of request dispatching.
KernelEvents::RESPONSE; // The RESPONSE event occurs once a response was created for replying to a request.
KernelEvents::TERMINATE; // The TERMINATE event occurs once a response was sent.
KernelEvents::VIEW; // The VIEW event occurs when the return value of a controller is not a Response instance.

There are more events available. More event constants.

How to dispatch an event

$dispatcher = \Drupal::service('event_dispatcher');
$dispatcher->dispatch($Event_Name, $Optional_Event_Object);

Useful links

May 24 2016
May 24

In this blog post I will briefly overview some of the very useful HTTP response header parameters that will help to secure any website. HTTP Response headers are name-value pairs of strings sent back from a server with the content you requested. More information can be found on the internet.

How to Secure Drupal HTTP Headers

I will cover some of the most important security-related HTTP parameters. The original blog post was written by Scott Helme who is the creator of SecurityHeaders.io. This is a brief overview of his blog post to introduce this technique to our readers.

Most of the header parameters can be set either in your .htaccess file (Apache) or webserver configuration files (preferred).

It is always good to review your response HTTP headers and remove some of them (if the system allows). For instance PHP version or webserver version.

Important: Make sure to QA your website after making the changes to the HTTP headers. Headers such as X-Frame-Options and Access-Control-Allow-Origin may break your site if incorrectly configured.

Also as a result of this blog post I built Drupal 8 module. The module is similar to Security Kit Drupal 7 module.

X-Xss-Protection

This response header can be used to configure a user-agent's built in reflective XSS protection. Currently, only Microsoft's Internet Explorer, Google Chrome and Safari (WebKit) support this header.

Set this in Nginx:
add_header X-Xss-Protection "1; mode=block" always;

Set this in Apache:
Header always set X-Xss-Protection "1; mode=block"

X-Powered-By

The X-Powered-By header gives information on the technology that's supporting the Web Server. This parameter specifies the technology (e.g. PHP etc..) supporting the web application (version details are often in X-Runtime, X-Version, or X-AspNet-Version

To disable this parameter edit your php.ini file. Set expose_php = Off and restart your webserver.

X-Content-Type-Options

This header parameter prevents Google Chrome and Internet Explorer from trying to mime-sniff the content-type of a response away from the one being declared by the server. It reduces exposure to drive-by downloads and the risks of user uploaded content that, with clever naming, could be treated as a different content-type, like an executable.

Set this in Nginx:
add_header X-Content-Type-Options "nosniff" always;

Set this in Apache:
Header always set X-Content-Type-Options "nosniff"

X-Frame-Options

Clickjacking protection. Valid values include DENY meaning your site can't be framed, SAMEORIGIN which allows you to frame your own site or ALLOW-FROM https://example.com/ which lets you specify sites that are permitted to frame your own site.

Set this in Nginx:
add_header X-Frame-Options "SAMEORIGIN" always;

Set this in Apache:
Header always set X-Frame-Options "SAMEORIGIN"

Access-Control-Allow-Origin

Access-Control-Allow-Origin is apart of the Cross Origin Resource Sharing (CORS) specification. This header is used to determine which sites are allowed to access the resource by defining either a single origin or all sites (denoted by a wildcard value). It should be noted that if the resource has the wildcard value set, then the Access-Control-Allow-Credentials option will not be valid and the user-agent's cookies will not be sent in the request. Valid values are:

  • * - Wildcard value allowing any remote resource to access the content of the resource which returned the Access-Control-Allow-Origin header.
  • http://www.example.com - A single serialized origin (http://[host], or https://[host]).

Content Security Policy

This HTTP header parameter allows you to define a whitelist of approved sources of content for your site. By restricting the assets that a browser can load for your site you will have extra level of protection from XSS attacks.

Set this in Nginx:
add_header Content-Security-Policy "default-src https: data: 'unsafe-inline' 'unsafe-eval'" always;

Set this in Apache:
Header always set Content-Security-Policy "default-src https: data: 'unsafe-inline' 'unsafe-eval'"

Read more Content Security Policy - An Introduction

HTTP Strict Transport Security

This policy will enforce TLS on your site and all subdomains for a year.

Set this in Nginx:
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains" always;

Set this in Apache:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

Read more HSTS - The missing link in Transport Layer Security

Drupal modules

Download Drupal 8 module (github)

Useful links

  1. List of HTTP header fields
  2. Security HTTP Response Headers
  3. Security through HTTP response headers
  4. Hardening your HTTP response headers
  5. Introduction to HTTP Response Headers for Security
  6. Guidelines for Setting Security Headers
May 18 2016
May 18

OctopusOn the frontlines of Drupal support the team here at Chapter Three has seen a lot of different ways you can complicate or overengineer a Drupal build. These can range from sites with 350 unique modules, to PHP-heavy Frankensites that do everything they can to avoid using core APIs.

Drupal's built-in flexibility and array of complicated contrib modules gives you ample opportunity to build your own unique monster if you aren't careful. Unfortunately for those who end up maintaining Drupal sites, a lot of modules and distributions don't focus much on the idea of maintainability. Instead primarily focusing on the speed and ease with which the site can be built. This may mean less up front cost, but will most definitely end up adding additional maintenance cost down the road. Site-building debt is just as real as technical debt on a traditional software project.

If you think you might be saddled with one of these beasts, I have some good news: There may still be a chance to reverse the transformation.

Monsters aren't created in a vacuum. My experience has been that there are almost always organizational issues larger than the website that help contribute to a things spinning out of control. Identifying these issues and dealing with them before you tackle the next phase of development can make the process much more productive, and spare your new team that extra chaos. You need to identify at least one stakeholder with final decision-making power and someone with the time to deal with understanding all the detailed nitty gritty of the issues on the site. In a perfect world, and on smaller projects, this can be the same person. A large committee where no member takes ownership over decisions or the results is a worst-case scenario for a successful web project.

Once organizational issues have been removed from the equation, the main thing for a site owner to start to quantify are the trade-offs between continuing to feed the monster and starting with a blank slate.

Chapter Three goes through an extensive auditing process for all of the sites we take on, but if you decide to go it alone the following questions usually are enough to trigger that 'site building smell' of a poorly built site.

Some questions to look into:

  • The number of modules (particularly custom modules).
  • The number of unique content types relative to how complex your content is.
  • The number of users assigned to each role and the permissions associated with those roles.
  • Use of complexity-adding modules like Panelizer, Context, Organic Groups, Domain especially in combination.

With a better sense of the breadth and depth of the rats nest it is crucial to re-evaluate the goals of the original project and whether they still apply. Even the largest monsters can be kept patched and secure (and locked in a dungeon) without a huge amount of effort. However if we want to build a lot of new functionality on this site we may break the bank.

Every piece of functionality (especially custom) in the current site needs to be evaluated against how it meets (or fails to meet) these original goals. The only way to start removing technical debt is by developing a better financial picture. Organizations change, marketing trends move, and website goals should change accordingly. Deleting unnecessary functionality is the most powerful tool at your disposal in a cleanup effort.

With a clearer picture of the current goals for this site and which business rules are still relevant, future efforts need to be prioritized based on what we've learned. There may be a big chunk of cleanup work necessary before we can re-start any new feature development. If new features are at the top of our list of goals, then this cleanup effort is where we should start. If we have decided to put the site on life support pending a rebuild, coming up with a clean and repeatable workflow to apply security updates and ensure stability after those pushes should be our path forward.

The bottom line is that the more organized you can be about the current state of your site the better equipped you will be to make business-driven decisions about the best path forward. As always the experts here are Chapter Three are happy to help your business navigate the path ahead.
 

Apr 29 2016
Apr 29

These are instructions on how to setup DigitalOcean droplet to host your personal website. DigitalOcean is a very affordable cloud hosting for developers (starting from $5 for a very simple droplet 512MB Memory / 1 CPU and 20GB disk).

How to Host Drupal 8 on DigitalOcean

DigitalOcean provides great documentation with step by step instruction about how to configure your servers to do what you need.

Certainly by building your own server you won't have the tools that Drupal sepcific hostings provide, such as Acquia or Pantheon and certainly I am recommending to use Drupal specific hosting for your clients because they provide much better support on a various levels, starting from server issues all the way to Drupal-specific issues.

While they have great support and pricing for businesses they don't have affordable plans for personal websites and that is the reason why I am writing this blog post. By following these instructions and configuring the servers yourself you will better understand how webservers work.

I won't rewrite all the instructions, instead I will include links to DigitalOcean's manuals with all the steps.

Note: When you create a droplet (server) you may choose preconfigured droplet with Drupal 8 already installed on it. However I prefer installing everything myself by following the steps you will learn new things.

We will be using Ubuntu 14.04, Ngnix, PHP7, MySQL 5.6. (LEMP stack). Please follow the instructions in the following order:

  1. Initial Server Setup with Ubuntu 14.0
  2. Once you complete initial server configuration make sure your server is secured. Please follow firewall configuration instructions: How To Set Up a Firewall with UFW on Ubuntu 14.04. Basically only allow 80 (http), 443 (https) and 22 (SSH) ports. Please read also UFW Essentials: Common Firewall Rules and Commands
  3. How To Install Linux, nginx, MySQL, PHP (LEMP) stack on Ubuntu 14.04
  4. The link above will only install PHP5. In order to upgrade it to PHP7 use this: How To Upgrade to PHP 7 on Ubuntu 14.04
  5. If you would like to use Memcached with PHP7 follow this instructions: Installing PHP-7 with Memcached
  6. Once you have your server configured begin installing Drupal 8. Please see Nginx configuration settings for Drupal 8

If you would like to use Apache (LAMP stack) for your webserver please use this:

  1. Follow the same server conifguraiton from above.
  2. How To Install Linux, Apache, MySQL, PHP (LAMP) stack on Ubuntu

Once you complete all the steps above you should be able to see your site by accessing http://Your_Server_Public_IP.

There are a great number of instructions how to build high availability servers, of course this will cost more since you have to create multiple droplets, however for a personal site this is not needed. Make sure your servers are in the same datacenter region:

  1. How To Create a High Availability Setup with Corosync, Pacemaker, and Floating IPs on Ubuntu 14.04 read also about Floating IPs: How To Use Floating IPs on DigitalOcean
  2. How To Create a High Availability HAProxy Setup with Corosync, Pacemaker, and Floating IPs on Ubuntu 14.04 (complete the first instruction before starting this)
  3. How To Set Up MySQL Master-Master Replication

More interesting instructions:

  1. How To Set Up Automatic Deployment with Git with a VPS
  2. How To Set Up a Host Name with DigitalOcean
  3. How To Use the DigitalOcean Docker Application
  4. Any many more tutorials.
  5. How To Create an SSL Certificate on Nginx for Ubuntu 14.04
  6. Using SSL Certificates with HAProxy also read How can I use SSL behind a load balancer and still get the client IP?

Other similar cloud hosting providers:

  1. Linode
  2. Amazon EC2
Apr 25 2016
Apr 25

This is a very simple tutorial that could help you with the performance of your custom modules. I will show you how to use Cache API in Drupal 8 and Drupal 7.

Learn the Cache API in Drupal 7 and 8

You don't have to perform heavy calculations every time you need to pull data either from third-party API or from database. Instead run it once and cache it. I personally use caching when I need to run complex SQL queries and third-party integrations (Example: get a list of available forms from Hubspot, or available campaign lists from Mailchimp etc).

In Drupal 8 use the following code structure:

public function mymodule_example_cache() {
  $data = &drupal_static(__FUNCTION__); // Can be replaced with the `__METHOD__`
  $cid = 'mymodule_example:' . \Drupal::languageManager()->getCurrentLanguage()->getId();

  if ($cache = \Drupal::cache()->get($cid)) {
    $data = $cache->data;
  }
  else {
    // This is where you would add your code that will run only
    // when generating a new cache.
    $data = my_module_complicated_calculation();
    \Drupal::cache()->set($cid, $data);
  }
  return $data;
}

In Drupal 7 the following:

function mymodule_example_cache() {
  $data = &drupal_static(__FUNCTION__);
  if (empty($data)) {
    if ($cache = cache_get('mymodule_example')) {
      $data = $cache->data;
    }
    else {
      // This is where you would add your code that will run only
      // when generating a new cache.
      $data = my_module_complicated_calculation();
      cache_set('mymodule_example', $data, 'cache');
    }
  }
  return $data;
}

Useful links

  1. Drupal 8 API: Cache API
  2. Drupal 8 API: function drupal_static
  3. Drupal 7 API: function cache_set
  4. Drupal 7 API: function cache_get
  5. Drupal 7 API: function drupal_static
Apr 21 2016
Apr 21

Its been a big week for Drupal 8 here at Chapter Three.

At Chatper Three we fly rocket ships to other planets to pick apples. What are you guys up to?As mentioned previously, at the beginning of the year Chapter Three made the decision to build all new projects with Drupal 8. Knowing we had the help and Drupal 8 expertise of Alex and Daniel to back us up if we encountered any errors or incomplete contrib ports gave us the confidence to leave Drupal 7 in the dust. Having already had a few successful client projects on 8 last year let me know I wouldn't have a developer revolt on my hands as a result of that decision.

The fruits of that decision are starting to ripen.

Earlier this week we were able to launch Ixia's new site on Drupal 8. Unlike our previous Drupal 8 sites where we leaned heavily on core and built small single-purpose utility modules to take the place of missing contrib we were able to leverage contrib. The big thing we noticed as a result of this sea change was that the previous site's 175 contrib modules were still reduced to just under 30. Not having 20,000 modules to choose from can be a good thing when one of the projects goals is to reduce complexity. More details on this project will be coming soon in a full-fledged case study, but everyone at Chapter Three and the client team are thrilled to be running on the latest and greatest version of Drupal and take full advantage of the many improvements it brings.

In addition to Ixia we were able to re-launch our own site on Drupal 8 last week. This port was promised and started many months ago, and work has been ongoing in fits and starts in the two years since that comment. Luckily for us we were able to start some Drupal 8 client projects in the interim but unfortunately that didn't leave us much time to work on our own site. For this project we purposefully tried to leave out any new features or functionality and went through the exercise of building one to one port from the Drupal 7 site. Similarly to Ixia we saw a steep reduction in the number of contrib modules needed to accomplish the same tasks. On the new site we have 33 contrib and custom modules to replace the previous sites 89. Overall we are very happy with the results. Attempting a migration from Drupal 7 to Drupal 8 while the migration path was still in flux was probably the most frustrating part of the whole process. Luckily for the rest of you the migration landscape is now much clearer than when we started two years ago.

The major takeaway I'd like to share from these two projects are:

  • Don't let the availability of a contrib module hijack your plans to work in Drupal 8.
  • Drupal 8 is production ready, and the functionality is stable for many different types of sites, not just brochure sites.

With three more Drupal 8 sites already in development we've jumped with both feet into the Drupal 8 swimming pool, and based on our experience I would encourage everyone else in the Drupal community to do the same.

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