Halp! I'm Stuck In Drupal 7!

http://slides.com/lawrencemiller/halp/live

Lawrence Miller

Team Lead at the American College of Physicians

@ldpm (twitter and drupal.org)

Should I Stay or Should I Go?

What we will (and will not) talk about

  • Patching contrib modules

  • Testing and Monitoring

  • Headless Drupal

Clean up the Admin UI by setting platform-specific variables from outside the Admin Toolbar

 

(also disable the "-UI" modules on prod, like Views UI and Rules UI)

But First:

Identify the Problem:

Use the devel module's variable editor to determine which variables you have set right now, and how they differ from platform to platform.

dev, stage, prod all have their own settings.local.php files

all platforms share one settings.php file

# DEV settings.local.php
$conf['my_setting'] = "My DEV setting";
# STAGE settings.local.php
$conf['my_setting'] = "My STAGE setting";
# PRODUCTION settings.local.php
$conf['my_setting'] = "My PROD setting";
# In your regular settings.php file
/**
 * Include a local settings file if it exists.
 */
$local_settings = dirname(__FILE__) . '/settings.local.php';
if (file_exists($local_settings)) {
  include $local_settings;
}

Chapter 1:

How to cope with absentee module maintainers

How to patch modules yourself in a manageable way:

  1. Open an issue for your problem
  2. Submit a patch for your issue
  3. Treat your patch as any other patch

Step 3: Treat your patch just like any other patch

drush patch-add

  • you specify an issue node or a patch file
  • drush downloads that patch and applies it...
  • ...and updates the patches.make file

patches.make

  • later when you run drush dl or drush up, drush will check this file and re-apply any patches it finds.
  • An error message could mean you need to re-roll, or it could mean that the patch is now included.

Step 1: The Issue Queue

It's never the wrong thing to open an issue, even if it seems nobody is paying attention.

Step 2: Let's fix it ourselves

  • Look in the "version control" tab of the module for instructions on how to clone the module and submit a patch.
  • By submitting the patch to this issue, you're guaranteeing that the patch will be available at the same URL for at least as long as the Drupal Association Exists.

Chapter 2:

Find problems quickly by increasing your automated testing and monitoring coverage

Honorable Mention:

Visual Regression Testing

https://github.com/mojoaxel/awesome-regression-testing

Behat

Behat is a Behavior-Driven Development tool written for PHP. It allows you to write human-readable test cases using a language called gherkin.

 

By using the Drupal Behat Extension, installation is easy and drupal-specific tests are exposed.

https://www.drupal.org/project/drupalextension

CasperJS to log in as a user

casper.start("https://drupal7dev-ldpm.c9users.io", function() {
    this.viewport(1200,900);
    this.test.assertHttpStatus(200);
    this.test.assertTitle('Drupal 7 Demo Site');
    this.fill('form#user-login', {
        'name': 'alice'
        'pass': 'password'
    }, true);
});

casper.then(function() {
    this.test.assertTextExists('Welcome, Alice');
});

casper.run();

The same Test in gherkin

Scenario: Logging in as Alice
  Given I am logged in as 'alice'
  And I am on the homepage
  Then I should see the heading "Drupal 7 Demo Site"
  And I should see the text "Welcome, Alice"

Sample "blackbox" Feature

Feature: Regression Tests
  In order to verify that a maintenance update did not change anything
  As a site-runner
  I want to run the following tests
  
  Scenario: Finding the Site Name Heading in the Header
    Given I am on the homepage
    Then I should see the heading "Drupal 7 Demo Site" in the "header" region

  Scenario: There shouldn't be errors on the homepage
    Given I am on the homepage
    Then I should not see the text "error"

Sample "Drupal API" Feature

Feature: Regression Tests
  In order to verify that a maintenance update did not change anything
  As a site-runner
  I want to run the following tests
  
  Scenario: Finding the Site Name Heading in the Header
    Given I am on the homepage
    Then I should see the heading "Drupal 7 Demo Site" in the "header" region

  Scenario: There shouldn't be errors on the homepage
    Given I am on the homepage
    Then I should not see the text "error"

@api
  Scenario: Run cron
    Given I am logged in as a user with the "administrator" role
    When I run cron
    And am on "admin/reports/dblog"
    Then I should see the link "Cron run completed"

  Scenario: Create many nodes
    Given "page" content:
    | title    |
    | Page one |
    | Page two |
    And I am logged in as a user with the "administrator" role
    When I go to "admin/content"
    Then I should see "Page one"
    And I should see "Page two"

Monitoring

  • Nagios
  • New Relic
  • Um...

Making sure Drupal stays healthy

"I mean we're getting 200 OK"

Nagios monitoring module

https://www.drupal.org/project/nagios

Nagios monitoring module

Creating the Synthetic

New Relic Synthetic

var myUniqueKey = 'my_random_string';
var myUri = 'https://dx7o.ply.st/nagios'

var assert = require('assert');
var options = {
    uri: myUri,
    headers: {
      'Accept': 'text/html'
    },
    qs: {
      'unique_id': myUniqueKey
    }
};

function callback (err, response, body){
  console.log(body);
  assert.ok(body.indexOf("CRON:OK") > -1, "Cron has not run recently");
  assert.ok(body.indexOf("ADMIN:OK") > -1, "Admin reports an issue");
}

$http.get(options,callback);

Um...

  • CasperJS / PhantomJS / Selenium
  • Your own cron
<?php
define("NAGIOS_URL", "https://SITENAME/nagios");
define("UNIQUE_ID", "my_unique_id");
define("EMAIL", "me@example.com");
define("ALERT_SUBJ", "SITENAME needs your attention!");

$url = NAGIOS_URL . "?unique_id=" . UNIQUE_ID;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
curl_close($ch);
$stats = preg_split("/[,|;]/", $response);
$errors = array();
foreach ($stats as $stat) {
    if (preg_match("/CRITICAL/", $stat)) {
        $errors[] = $stat;
    }
}

if (isset($errors[0])) {
    $msg = "The following errors have been received:\n\n";
    foreach ($errors as $error) {
        $msg .= $error . "\n";
    }
    mail(EMAIL, ALERT_SUBJ, $msg);
}

Slack via Email

https://your-slack-address/apps

Chapter 3:

Using D7 as a data store for your modern front-end framework, a.k.a. "Headless Drupal"

  1. RSS
  2. views_datasource
  3. Services
  4. Custom feed

Pros:

  • It's built in to both Drupal Core and Views
  • Plenty of RSS parsers available

Cons:

  • RSS Parsers and validators can be finicky
  • No support for Angular's feed API or Yahoo's alternative anymore (check rss2json.com)

Option 1: RSS

Recommendation:

Only if you're already consuming other RSS feeds with your front-end tech

Pros:

  • Pretty easy to install and configure
  • You basically already know how to use it
  • Has exactly the same flexibility as views in general

Cons:

  • Has exactly the same weaknesses as views in general

Option 2: views_datasource

Recommendation:

Great for a number of interface patterns, especially Small Multiples (e.g. Pinterest cards)

Source: Drupal site

Create a View

  • requires views_datasource
  • enable views_json
  • use "Page" display type and "JSON data document" formatter
  • In the Formatter Settings, leave the top two fields blank (optional)
  • Then just complete your view as normal
[
    {
        "title": "Aptent Elit Eu Exerci",
        "Image": {
            "src": "http:\/\/drupal7dev-ldpm.c9users.io:80\/sites\/default\/files\/field\/image\/imagefield_oktsx6.png",
            "alt": "Gilvus nisl plaga refoveo verto."
        },
        "Nid": "1",
        "Path": "\/node\/1"
    },
    {
        "title": "Aliquip Jugis Minim",
        "Image": {
            "src": "http:\/\/drupal7dev-ldpm.c9users.io:80\/sites\/default\/files\/field\/image\/imagefield_YHvfJS.gif",
            "alt": "Caecus consectetuer euismod ibidem illum vel vicis virtus."
        },
        "Nid": "28",
        "Path": "\/node\/28"
    },
    {
        "title": "Abbas Comis Conventio Loquor Lucidus",
        "Image": {
            "src": "http:\/\/drupal7dev-ldpm.c9users.io:80\/sites\/default\/files\/field\/image\/imagefield_mr78UD.png",
            "alt": "Augue enim ratis zelus."
        },
        "Nid": "29",
        "Path": "\/node\/29"
    },
    {
        "title": "Cui Iustum",
        "Image": {
            "src": "http:\/\/drupal7dev-ldpm.c9users.io:80\/sites\/default\/files\/field\/image\/imagefield_4STyap.gif",
            "alt": "Distineo feugiat genitus pneum utrum."
        },
        "Nid": "30",
        "Path": "\/node\/30"
    },
    {
        "title": "Enim Gilvus Interdico Jumentum Saepius",
        "Image": {
            "src": "http:\/\/drupal7dev-ldpm.c9users.io:80\/sites\/default\/files\/field\/image\/imagefield_9XCTua.gif",
            "alt": "Distineo meus plaga quidne si."
        },
        "Nid": "32",
        "Path": "\/node\/32"
    },
    {
        "title": "Abico Aptent Hos Mos",
        "Image": {
            "src": "http:\/\/drupal7dev-ldpm.c9users.io:80\/sites\/default\/files\/field\/image\/imagefield_eNBlTV.jpg",
            "alt": "Antehabeo dolor jugis magna quia tum."
        },
        "Nid": "33",
        "Path": "\/node\/33"
    },
    {
        "title": "Defui Paratus",
        "Image": {
            "src": "http:\/\/drupal7dev-ldpm.c9users.io:80\/sites\/default\/files\/field\/image\/imagefield_Psnez5.png",
            "alt": "Acsi brevitas jus lucidus patria praemitto qui saepius tation."
        },
        "Nid": "35",
        "Path": "\/node\/35"
    },
    {
        "title": "Ludus Neque Patria Praesent Ullamcorper Vulputate",
        "Image": {
            "src": "http:\/\/drupal7dev-ldpm.c9users.io:80\/sites\/default\/files\/field\/image\/imagefield_TLgFoH.jpg",
            "alt": "Amet eu fere nimis nutus qui saepius venio vero."
        },
        "Nid": "36",
        "Path": "\/node\/36"
    },
    {
        "title": "Uxor",
        "Image": {
            "src": "http:\/\/drupal7dev-ldpm.c9users.io:80\/sites\/default\/files\/field\/image\/imagefield_xnlZGQ.gif",
            "alt": "Exerci luptatum sit sudo."
        },
        "Nid": "39",
        "Path": "\/node\/39"
    },
    {
        "title": "Abico Aliquip Conventio Distineo Enim Gilvus",
        "Image": {
            "src": "http:\/\/drupal7dev-ldpm.c9users.io:80\/sites\/default\/files\/field\/image\/imagefield_l81aC7.png",
            "alt": "Caecus commodo diam erat incassum mos secundum sino utrum."
        },
        "Nid": "43",
        "Path": "\/node\/43"
    }
]

Sample JSON Output

Target: HTML + AngularJS

Target: HTML + AngularJS

The cards below "Hello, world!" are generated from the JSON API; everything else is just HTML and CSS

Target: JS file

angular.module('demo', [])
    .controller('Photos', function($scope, $http) {
        $scope.remote = "https://drupal7dev-ldpm.c9users.io";
        $http.get($scope.remote.concat('/articles.json')).
        success(function(data) {
            $scope.photos = data;
            console.log(data);
        }).
        error(function (data) {
    $scope.data = "Request failed";
    console.log("fail");
  })
});

js/photos.js

Target: HTML

<!DOCTYPE html>
<html lang="en">

<head>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
  <script src="js/photos.js"></script>
  ...
</head>

<body>
...
<div ng-app="demo" ng-controller="Photos" class="books">
  <div ng-repeat="x in photos" class="mdc-card book">
    <a class="..." href="{{ remote }}{{ x.Path }}">
	<div class="...">
          <img src="{{ x.Image.src }}" alt="{{ x.Image.alt }}">
        </div>
	<div class="...">
	  <h2 class="...">{{ x.title }}</h2>
	</div>
      ...
  </div>
</div>
...
</body>

Pros:

  • Easy
  • Powerful
  • Supports all CRUD operations, not just Read

Cons:

  • Various forms of authentication can be complex to configure

Option 3: Services

Recommendation:

Perfect for exposing every single node on your site as JSON

Adding a new Service

(/api_demo/node/49.json)

Pros:

  • Does whatever you need it to do

Cons:

  • You gotta build it yourself

Option 4: Custom Feed

Recommendation:

If your use case doesn't fit neatly into Views or Services, Roll your own. That's why we're in Drupal.

<?php

function my_feed_menu() {
    $items = array();
    $items['feeds/my_feed/%'] = array(
        'type' => MENU_NORMAL_ITEM,
        'title' => 'My Custom Feed',
        'description' => t('Custom JSON feed'),
        'page callback' => 'my_feed_callback',
        'page arguments' => array(1),
        'access arguments' => array('access content'),
    );
    return $items;    
}

function my_feed_callback($year = NULL) {
    $query = new EntityFieldQuery();
    $query->entityCondition('entity_type', 'node')
    ->entityCondition('bundle', 'article')
    ->propertyCondition('status', NODE_NOT_PUBLISHED)
    ->propertyOrderBy('created', 'DESC');
    
    if (is_int($year)) {
        $first_minute = mktime(0, 0, 0, 1, 1, $year);
        $last_minute = mktime(23, 59, 59, 12, 31, $year);
        $query->propertyCondition('created', array($first_minute, $last_minute), 'BETWEEN');
    }
    
    $result = $query->execute();
    if (isset($result['node'])) {
        $news_items_nids = array_keys($result['node']);
        $news_items = entity_load('node', $news_items_nids);
        return drupal_json_output($news_items);
    }
}

CORS

https://www.drupal.org/project/cors

  • Cross-Origin Resource Sharing
  • ​Only matters for front-end frameworks like AngularJS or REACT; your server-side consumers won't care

 

Thank You

http://slides.com/lawrencemiller/halp/

https://www.drupalcampatlanta.com/2018/sessions/halp-im-stuck-drupal-7

Lawrence Miller

@ldpm (twitter and drupal.org)

Halp!

By Lawrence Miller

Halp!

Tips for keeping ancient Drupal 7 sites running with less effort.

  • 1,711