Apr 03 2018
Apr 03

When you provide a module-defined menu link in Drupal 8, there is some great documentation on how to add a menu link on Drupal.org. This gets into how to provide a menu link with YAML. In a lot of cases, you might want to nest this menu link under another item. This is especially the case if you were providing a menu link for something in the main menu of your site.

The 'parent' indicator in the my_module.links.menu.yml file is how you nest items. However, you need to find the menu link item's "id", which is actually terribly confusing because the id is usually found by hovering over your menu edit link on the menu structure page and pulling out a number like "22", or whatever your menu item id is in your browser. The menu will show your item on the top level of whatever menu you are targeting if it can't find that parent id. And it won't, because it's not obvious how to find it...

Hold on, though! Here's where your Drupal brain is going to explode, because there is some sort of internal menu link id that is used in the database. One is not able to find this id unless they look in the menu_tree table in Drupal. One way to find it in this table is if you search for a sibling menu item in the "route_param_key" column by its node id (example: node=250074) you should be able to find the "parent" column of that one. There will be other ways to search this table, but it's not obvious. Your "parent" that you put in YAML will look something like this for menu content items: menu_link_content:d37de038-32a1-4bb5-8d79-9fcbbcbeaf8d

Here's hoping the user experience of finding this menu link id becomes easier at some point!

Jun 20 2017
Jun 20

I'm working on a large, complex migration from Drupal 7 to Drupal 8 right now. One thing I noticed is that the migrate modules pollute the database with an unreal number of tables which allow migrations to be re-run, etc. Well if you don't need that, here's how to remove these tables. Currently the migrate modules don't clean up after themselves. Put this in a custom module or PHP script that has bootstrapped Drupal. Note this code only works in Drupal 8. Shown is a .install file for a custom module. If you uninstall the custom module, it will run the cleanup:

/**
 * Implements hook_uninstall().
 *
 * Removes stale migration configs during uninstall.
 */
function MY_MODULE_uninstall() {

  // Clean up database pollution
  $tables_to_cleanup = [
    'migrate_message_',
    'migrate_map_',
  ];

  $query = db_query('show tables;');
  $schema = $query->fetchAll();
  
  foreach ($schema as $table_name) {
    $table_name = (Array) $table_name;
    $table_name = reset($table_name);
    foreach($tables_to_cleanup as $table_to_cleanup) {
      if(strpos($table_name, $table_to_cleanup) !== false) {
        db_drop_table($table_name);
      }
    }
  }

  // Clean up old migrate configuration
  $query = db_select('config', 'c')
    ->fields('c', array('name'))
    ->condition('name', db_like('migrate_plus.') . '%', 'LIKE')
    ->execute();

  $config_names = $query->fetchAll();

  // Delete each config using configFactory.
  foreach ($config_names as $config_name) {
    \Drupal::configFactory()->getEditable($config_name->name)->delete();
  }
}
Apr 04 2017
Apr 04

Every time I install Drupal I find myself fumbling for a nice hash salt string. Instead of finding a random solution, I decided to put together a quick page that generates a random hash salt string and get a shiny domain for it.

Check out DrupalHashSalt.com (easy to remember!) next time you need a random hash salt. I also posted the code used to generate the string.

If you have ideas for this site feel free to contact me.

Mar 02 2017
Mar 02

The default ordering of all comments in Drupal 8's core comment module is ascending, meaning the oldest comments are displayed first. Sometimes you don't want this.

Assuming your comment field's machine name is field_comments, put this code into a module to get descending comments. I was having trouble with threaded comments, but this works since it alters the query by the c.thread database column instead of just 'torder':

/**
 * Implements hook_query_TAG_alter() for comment_filter tag.
 *
 * @see CommentStorage::loadThread().
 */
function MYMODULE_query_comment_filter_alter(Drupal\Core\Database\Query\AlterableInterface $query) {
  
  // Change comment order to DESC for 'comment' field.
  if ($query->getMetaData('field_name') == 'field_comments') {

    $order_by = &$query->getOrderBy();
    $expressions = &$query->getExpressions();
    // Sorting for threaded comments.
    if (isset($order_by['torder']) && $order_by['torder'] == 'ASC') {
      // Get rid of the expressions that prepare the threads for ASC ordering.
      unset($expressions['torder']);
      unset($order_by['torder']);
      // Simply order by the thread field.
      $order_by['c.thread'] = 'DESC';
    }

  }

}
Feb 10 2017
Feb 10

Sometimes you just want a cleaner comment entry box. Here's a quick Gist module that will remove your comment tip link beneath comment form body entries in Drupal 8. This uses a form alter to remove the filter help on the comment form.

If you wanted an even cleaner look, you could remove the other text below the comment box altogether by overriding filter-guidelines.html.twig and filter-tips.html.twig in your theme!

Jul 16 2016
Jul 16

With Drupal 8's Object Oriented focus, it's just a matter of time before cooler approaches to the theme's giant template.php (now yourtheme.theme) file are found. I've written a nicer way to get block output into the $variables array in page and node preprocess functions. What this allows you to do is use simple variables like {{ my_cool_block_stuff }} in yor Drupal Twig templates.

The source is available on Github. Feel free to take this module and modify it for your project.  This is a great approach if you're designing larger sections and pages and still want granularity on when blocks are rendered, but don't want to overflow the block layout page with a huge number of items. With this you can make a View output a block, but not PLACE the block anywhere. You would then just render the View through this module and the block output would be in a Twig variable for adding to a custom theme template (either a page or node template, see the module for specific code example).

For non views-based blocks, it requires the block to be placed and/or disabled to work.

If you didn't do this you would have a large amount of if statements based on page and node ids to get the same Twig variables. Now, you just add paths or content type conditionals to the switch statements in the module.

May 09 2016
May 09

Doing some hacking at DrupalCon New Orleans and my use case is to add a node-* class to the body HTML element in Drupal 8. With the preprocess hook in D8 it looks like there is access to different variables than previously. To get the node id one has to look at the cache tags. Here's how I did it:

In mytheme.theme:

function mytheme_preprocess_html(&$variables) {
  // Add node id to the body class.
  $node = \Drupal::routeMatch()->getParameter('node');
  if($node) {
    $variables['attributes']['class'][] = 'node-' . $node->id();
  }
}

In your html.html.twig include this:

<body{{ attributes }}>

</body>
Jan 08 2016
Jan 08

Drupal 8 allows you to create multiple search pages which show up as primary tabs on the search page after performing a search. This is cool, but has the limitation of making the user input a search term a second time when they navigate to the other tab.

Suppose you wanted to prepopulate that tab's URL with the current search term. This is possible! So if you tab between the search pages, you can search automatically. In my case I have a search plugin for a "content part" custom entity already made, and a route defined for that search page. I could give more details on how to do that later if requested, but the more interesting bit is being able to change the tab URLs. Here's the code, inside your .theme file:

/**
 * Implements hook_pre_render_HOOK() for menu-local-tasks templates.
 *
 * Changes search tab URLs if you have more than one search page to be able
 * to automatically search the other page when you navigate to it.
 * This snippet assumes two search plugins:
 *  search.plugins:node_search (core)
 *  search.plugins:content_part (custom)
 *
 * This snippet assumes two search routes:
 *  search.view_node_search (core)
 *  search.view_content_part (custom)
 */
function yourtheme_preprocess_menu_local_tasks(&$variables) {
  $keys = \Drupal::request()->query->get('keys');

  if($keys && is_array($variables['primary']['search.plugins:node_search']) && is_array($variables['primary']['search.plugins:content_part'])) {
    // Get the current URL minus query params
    $url  = @( $_SERVER["HTTPS"] != 'on' ) ? 'http://'.$_SERVER["SERVER_NAME"] :  'https://'.$_SERVER["SERVER_NAME"];
    $url .= ( $_SERVER["SERVER_PORT"] !== 80 ) ? ":".$_SERVER["SERVER_PORT"] : "";

    // Make a new URL with query params and assign it to the node search tab link
    $search_node_url = $variables['primary']['search.plugins:node_search']['#link']['url'] =
  \Drupal\Core\Url::fromRoute('search.view_node_search', [], ['query' => ['keys' => $keys]]);
    // Tab links get cached unless we clear this
    $variables['primary']['search.plugins:node_search']['#access']->setCacheMaxAge(1);

    // Make a new URL with query params and assign it to the content part search tab link
    $search_content_part_url = $variables['primary']['search.plugins:content_part']['#link']['url'] =
  \Drupal\Core\Url::fromRoute('search.view_content_part', [], ['query' => ['keys' => $keys]]);
    // Tab links get cached unless we clear this
    $variables['primary']['search.plugins:content_part']['#access']->setCacheMaxAge(1);

  }
  
}

View the Gist on Github.

Dec 28 2015
Dec 28

Both of these drush commands are awesome on their own, but they really need a wrapper script to become workhorses. I built this script to refresh a local Drupal environment with 1 command. You can choose file folder sync, databases, or both. This assumes you have working Drupal installations on both ends already (minus the database). So once you're setup with your settings files and Drupal codebase, you'd run this to populate the site. Click here for the Github repository. Summary:

  • This utilizes drush sql-sync and drush rsync to pull down a production server's file system and database to your local version by executing the shell script.
  • You must have a drush aliases file set up with local and prod aliases for this to work. Sample included at the end of the shell script.
  • Fill out the top variables of the shell script and then run it. It assumes your local environment can log in to the remote environment via SSH already.
  • The script will create a local database for you if it doesn't exist yet.
Dec 16 2015
Dec 16

I've created an interim solution to the lack of the node_clone module for Drupal 8. During a project at work, I created a custom module with very similar functionality so I ended up making it a contrib Sandbox project at Drupal.org. I don't have any community module releases yet, so it's currently up for project review. Click here to check out the project!

Dec 03 2015
Dec 03

Drupal 8 was just released! Awesome! Unfortunately, the state of contrib is a warzone, as usually happens after a big Drupal point release. Even more so now that everything has changed. The reality is that if you're doing something custom, you're not going to be able to take a contrib module like Rules, Relation, etc and use it how it is (yet). Thankfully with the Object Oriented approach in D8, you can extend classes of contrib modules that are unfinished and implement your own custom functionality. Gone are the long, complex module files from hell with little structure. You'll still use module files for some things, but the core MVC of your custom implementation can extend contrib modules without completely forking them.

Are you not technically inclined? This is a good time to learn. If you know the basics of OO programming, Drupal 8 is a good place to build cool things somewhat quickly once you get through PHP's quirks. If you've come from a Rails background, D8 is still a far cry from a lot of that simplicity, but it's getting better. Once you know D8 internals you can do a lot with content and configuration entities, for instance.

While Drupal has bid farewell to the simpler and in some ways better times of Drupal 7, hopefully in a year or so D8 has the level of out-of-box customization that 7 (and even 6) had without custom programming. I am working on a large custom Drupal 8 application for my organization, so it has been cool tracking its progress.

Dec 03 2015
Dec 03

If you're getting a little frustrated with the new Drupal::l function in D8, which is actually going to be changed to Link in Drupal 9, here's how to create a link with class attributes right now. Attributes like link classes used to be defined on the l() function in Drupal 7, but are now passed into the Url() function as extra options.

Here's the ugly truth in how you link to something within module procedural code. You'll want to put the "use Drupal\Core\Url" at the top of your file:

use Drupal\Core\Url;

\Drupal::l(
     t('My Button'), 
      new Url('my.link.route', 
        ['route_parameter' => $custom_variable],
        ['attributes' => ['class' => ['button']]])
    ),

This would output a regular <a> with the "button" class.

Oct 20 2015
Oct 20

If you're sending a request to a custom URL in Drupal 8, you might be tempted to implement a solution using cURL or another library. However, Drupal core comes with Guzzle, a "PHP HTTP client that makes it easy to send HTTP requests and trivial to integrate with web services." As with most things in Drupal, it's not obvious how to use something immediately, so here's a demo to show you how to take care of sending a request to an arbitrary URL inside a custom Drupal module. You might use this example class to display a status code on some page. Here's the actual class, and a Github repo to show module structure:

<?php

namespace Drupal\custom_guzzle_request\Http;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/** 
 * Get a response code from any URL using Guzzle in Drupal 8!
 * 
 * Usage: 
 * In the head of your document:
 * 
 * use Drupal\custom_guzzle_request\Http\CustomGuzzleHttp;
 * 
 * In the area you want to return the result, using any URL for $url:
 *
 * $check = new CustomGuzzleHttp();
 * $response = $check->performRequest($url);
 *  
 **/

class CustomGuzzleHttp {
  use StringTranslationTrait;
  
  public function performRequest($siteUrl) {
    $client = new \GuzzleHttp\Client();
    try {
      $res = $client->get($siteUrl, ['http_errors' => false]);
      return($res->getStatusCode());
    } catch (RequestException $e) {
      return($this->t('Error'));
    }

  }
}
Oct 19 2015
Oct 19

If you need to check for a specific role such as 'administrator' in Drupal 8, the process is changed from past versions. The following is an example of how you'd check for an administrator role when rendering the user edit form. You might do this if you were unsetting form variables based on role:

use Drupal\Core\Form\FormStateInterface;

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 * Demo functionality gives non-admins no permission to update their username/password
 * by unsetting it when they view their profile edit screen.
 */
function check_user_role_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $user = \Drupal::currentUser()->getRoles();
  if(!in_array("administrator", $user) && $form_id == 'user_form') {
        unset($form['account']);
  }
}

Get the full demo module on Github.

Oct 15 2015
Oct 15

For those starting to build on Drupal 8, some things can be in the same place as Drupal 7 while others can be "hidden". While the idea is to move things to more logical places, sometimes it's confusing to site builders since they have to learn the system anew in some regards.

One thing I couldn't find for a bit while working on a new Drupal 8 project was the default search block of all things!  The cool system blocks are no longer clumped at the bottom of the block layout page. Instead, for each region there's a "Place block" button. When you click it, all the old blocks and some new friends are available. Cool! See the image for what that modal looks like.

Placing Drupal 8 blocks

Jan 15 2015
Jan 15

I've had some trouble using Twig's include statements in Drupal 8 theming.  I'm not sure if this is a bug since it's at Beta 4, but it's sort of annoying.  I include my content areas in page.html.twig in a separate include file in Drupal 6 and insert it into the area I need.  For example, if I have a 3 column layout, I'm changing the Bootstrap classes from "col-md-12" to "col-md-9" and "col-md-3" (for a sidebar) if the sidebars have content in them.  Includes are apparently escaping (?), though, and not printing anything other than the include file's name.  As a workaround... which is hopefully temporary, I opted for blocks.

Here's my initial block definition for my first columnar scenario (no sidebars):

        {# Show if there are no sidebars #}
        {% if (not page.left_top and not page.left_mid) and not page.right_top %}
        <div class="col-md-12 main-bar">
          {% block pagecontent %}
            {% if page.above_content %}
            <div class="row above-content">
              <div class="col-md-12">
                {{ page.above_content }}
              </div><!--/.col-md-12-->
            </div><!--/.row-->
            {% endif %}
            {% if not is_front and title != 'Search' %}
              {{ breadcrumb }}
              {% if title %}
                <h2 class="page-title">{{ title }}</h2>
              {% endif %}
            {% endif %}
            {% if messages %}
              {{ messages }}
            {% endif %}
            {% if page.below_title %}
              <div class="belowtitle">
                {{ page.below_title }}
              </div>
            {% endif %}
            <div class="clear-block contentblk">
              {{ page.content }}
            </div>
            {% if page.content_2col_left or page.content_2col_right %}
              <div class="double-columns row">
                <div class="col col-md-6">
                  {{ page.content_2col_left }}
                </div><!--/.col-md-6-->
                <div class="col col-md-6">
                  {{ page.content_2col_right }}
                </div><!--/.col-md-6-->
              </div><!--/.row-->
            {% endif %}
            {% if page.content_3col_left or page.content_3col_mid or page.content_3col_right %}
              <div class="triple-columns row">
                <div class="col col-md-4">
                  {{ page.content_3col_left }}
                </div><!--/.col-md-4-->
                <div class="col col-md-4">
                  {{ page.content_3col_mid }}
                </div><!--/.col-md-4-->
                <div class="col col-md-4">
                  {{ page.content_3col_right }}
                </div><!--/.col-md-4-->
              </div><!--/.row-->
            {% endif %}
            {% if page.below_content %}
              {{ page.below_content }}
            {% endif %}
          {% endblock %}
        </div><!--/.col-md-12-->
        {% endif %}

Cool.  The stuff inside the {% block pagecontent %} and {% endblock %} is what will repeat in our other sidebar scenarios.  Here's another scenario with the block repeated:

        {# Show if there are left sidebars #}
        {% if (page.left_top or page.left_mid) and not page.right_top %}
        <div class="col-md-3 side-bar left-side-bar">
          {{ page.left_top }}
          {{ page.left_mid }}
        </div><!--/col-md-3-->
        <div class="col-md-9 main-bar">
          {{ block('pagecontent') }}
        </div><!--/.col-md-9-->
        {% endif %}

The bit that's interesting is the {{ block('pagecontent') }} function, which just inserts the earlier block again.

Jan 15 2015
Jan 15

I've had some trouble using Twig's include statements in Drupal 8 theming.  I'm not sure if this is a bug since it's at Beta 4, but it's sort of annoying.  I include my content areas in page.html.twig in a separate include file in Drupal 6 and insert it into the area I need.  For example, if I have a 3 column layout, I'm changing the Bootstrap classes from "col-md-12" to "col-md-9" and "col-md-3" (for a sidebar) if the sidebars have content in them.  Includes are apparently escaping (?), though, and not printing anything other than the include file's name.  As a workaround... which is hopefully temporary, I opted for blocks.

Here's my initial block definition for my first columnar scenario (no sidebars):

        {# Show if there are no sidebars #}
        {% if (not page.left_top and not page.left_mid) and not page.right_top %}
        div class="col-md-12 main-bar">
          {% block pagecontent %}
            {% if page.above_content %}
            div class="row above-content">
              div class="col-md-12">
                {{ page.above_content }}
              div>
            div>
            {% endif %}
            {% if not is_front and title != 'Search' %}
              {{ breadcrumb }}
              {% if title %}
                h2 class="page-title">{{ title }}h2>
              {% endif %}
            {% endif %}
            {% if messages %}
              {{ messages }}
            {% endif %}
            {% if page.below_title %}
              div class="belowtitle">
                {{ page.below_title }}
              div>
            {% endif %}
            div class="clear-block contentblk">
              {{ page.content }}
            div>
            {% if page.content_2col_left or page.content_2col_right %}
              div class="double-columns row">
                div class="col col-md-6">
                  {{ page.content_2col_left }}
                div>
                div class="col col-md-6">
                  {{ page.content_2col_right }}
                div>
              div>
            {% endif %}
            {% if page.content_3col_left or page.content_3col_mid or page.content_3col_right %}
              div class="triple-columns row">
                div class="col col-md-4">
                  {{ page.content_3col_left }}
                div>
                div class="col col-md-4">
                  {{ page.content_3col_mid }}
                div>
                div class="col col-md-4">
                  {{ page.content_3col_right }}
                div>
              div>
            {% endif %}
            {% if page.below_content %}
              {{ page.below_content }}
            {% endif %}
          {% endblock %}
        div>
        {% endif %}

Cool.  The stuff inside the {% block pagecontent %} and {% endblock %} is what will repeat in our other sidebar scenarios.  Here's another scenario with the block repeated:

        {# Show if there are left sidebars #}
        {% if (page.left_top or page.left_mid) and not page.right_top %}
        div class="col-md-3 side-bar left-side-bar">
          {{ page.left_top }}
          {{ page.left_mid }}
        div>
        div class="col-md-9 main-bar">
          {{ block('pagecontent') }}
        div>
        {% endif %}

The bit that's interesting is the {{ block('pagecontent') }} function, which just inserts the earlier block again.

Jan 14 2015
Jan 14

I'm updating a Drupal 6 theme to Drupal 8.  One thing I'm doing is making the logo in my Twig template a Twig variable instead of hardcoding the path.  Here's how you do it.  This assumes a theme named 'acton', but you'll change that to your own theme's name.

In 'acton.theme', assuming your logo is 'logo.png' in your theme's root:

function acton_preprocess_page(&$variables) {
  $variables['logopath'] = '/' . drupal_get_path('theme','acton') . '/logo.png';
}

In your Twig template, do something like this:

<img class="img-responsive" src="http://www.lohmeyer.rocks/blog/2015/01/14/00187-print-your-themes-logo-path-drupal-8-twig/{{ logopath }}" />

Done!

Jan 14 2015
Jan 14

I'm updating a Drupal 6 theme to Drupal 8.  One thing I'm doing is making the logo in my Twig template a Twig variable instead of hardcoding the path.  Here's how you do it.  This assumes a theme named 'acton', but you'll change that to your own theme's name.

In 'acton.theme', assuming your logo is 'logo.png' in your theme's root:

function acton_preprocess_page(&$variables) {
  $variables['logopath'] = '/' . drupal_get_path('theme','acton') . '/logo.png';
}

In your Twig template, do something like this:

img class="img-responsive" src="{{ logopath }}" />

Done!

Jan 08 2014
Jan 08

Bootstrap is a great Drupal theme that makes it so your form elements and other Drupal things get output with proper Twitter Bootstrap CSS attributes.  One downside to it is that the popular Webform module has elements that don't get styled by the Bootstrap Drupal theme and then they look like unstyled form fields.

To fix it, go to Bootstrap's theme/process.inc file.  Inside it, add 'webform_email' to the 'types' array in the _bootstrap_process_input() function.  This will style Webform's special email field.  Other fields likely have different types.  The reason it doesn't get styled is because the 'types' array is coded to look for only default types and not the special ones that Webform is using.

If you want to see what the #type is on an element, I recommend installing the Devel module and calling "dpm($element);" inside the theme/alter.inc bootstrap_element_info_alter() function.  This will output all of the elements on your current Webform.

Happy Bootstrapping!

Jan 08 2014
Jan 08

Bootstrap is a great Drupal theme that makes it so your form elements and other Drupal things get output with proper Twitter Bootstrap CSS attributes.  One downside to it is that the popular Webform module has elements that don't get styled by the Bootstrap Drupal theme and then they look like unstyled form fields.

To fix it, go to Bootstrap's theme/process.inc file.  Inside it, add 'webform_email' to the 'types' array in the _bootstrap_process_input() function.  This will style Webform's special email field.  Other fields likely have different types.  The reason it doesn't get styled is because the 'types' array is coded to look for only default types and not the special ones that Webform is using.

If you want to see what the #type is on an element, I recommend installing the Devel module and calling "dpm($element);" inside the theme/alter.inc bootstrap_element_info_alter() function.  This will output all of the elements on your current Webform.

Happy Bootstrapping!

Sep 25 2013
Sep 25

If you're doing any theming with Drupal, you'll undoubtedly want to implement template suggestions for some of your fields at some point.  Usually you'll have some undesirable formatting, especially in Panels panes.  This post at Drupal.org has a method of how to find the appropriate template suggestion for your panel pane by working in your template.php file with the Devel module and dpm().

One gotcha would be with pane subtypes that have colons in them.  This can happen with tokens relatively often.  The answer is to substitute the colon with an underscore in the filename.  For instance, I was theming a token price with the pane type 'token' and pane subtype 'node:price'.  The correct template suggestion is 'panels-pane--token--node_price.tpl.php'.

Note that this technique with the colons only works after applying this patch with the 7.x-3.3 version of Panels!

Sep 25 2013
Sep 25

If you're doing any theming with Drupal, you'll undoubtedly want to implement template suggestions for some of your fields at some point.  Usually you'll have some undesirable formatting, especially in Panels panes.  This post at Drupal.org has a method of how to find the appropriate template suggestion for your panel pane by working in your template.php file with the Devel module and dpm().

One gotcha would be with pane subtypes that have colons in them.  This can happen with tokens relatively often.  The answer is to substitute the colon with an underscore in the filename.  For instance, I was theming a token price with the pane type 'token' and pane subtype 'node:price'.  The correct template suggestion is 'panels-pane--token--node_price.tpl.php'.

Note that this technique with the colons only works after applying this patch with the 7.x-3.3 version of Panels!

Feb 22 2013
Feb 22

At last year's Drupalcon in Denver there was an excellent session called Delivering Drupal.  It had to do with the oftentimes painful process of deploying a website to web servers.  This was a huge deep dive session that went into the vast underbelly of devops and production server deployment.  There were a ton of great nuggets and I recommend watching the session recording for serious web developers.

The most effective takeway for me was the manipulation of the settings files for your Drupal site, which was only briefly covered but not demonstrated.  The seed of this idea that Sam Boyer presented got me wondering about how to streamline my site deployment with Git.  I was using Git for my Drupal sites, but not effectively for easy site deployment.  Here are the details of what I changed with new sites that I build.  This can be applied to Wordpress as well, which I'll demonstrate after Drupal.

Why would I want to do this?

When you push your site to production you won't have to update a database connection string after the first time.  When you develop locally you won't have to update database connections, either.

Streamlining settings files in Drupal

Drupal has the following settings file for your site:

sites/yourdomain.com/settings.php

This becomes a read only file when your site is set up and is difficult to edit.  It's a pain editing it to run a local site for development.  Not to mention if you include it in your git repository, it's flagged as modified when you change it locally.

Instead, let's go ahead and create two new files:

sites/yourdomain.com/settings.local.php
sites/yourdomain.com/settings.production.php

Add the following to your .gitignore file in the site root:

sites/yourdomain.com/settings.local.php

This will put settings.php and settings.production.php under version control, while your local settings.local.php file is not.  With this in place, remove the $databases array from settings.php.  At the bottom of settings.php, insert the following:

$settingsDirectory = dirname(__FILE__) . '/';
if(file_exists($settingsDirectory . 'settings.local.php')){
    require_once($settingsDirectory . 'settings.local.php');
}else{
    require_once($settingsDirectory . 'settings.production.php');
}

This code tells Drupal to include the local settings file if it exists, and if it doesn't it will include the production settings file.  Since settings.local.php is not in Git, when you push your code to production you won't have to mess with the settings file at all.  Your next step is to populate the settings.local.php and settings.production.php files with your database configuration.  Here's my settings.local.php with database credentials obscured.  The production file looks identical but with the production database server defined:

<?php
    $databases['default']['default'] = array(
      'driver' => 'mysql',
      'database' => 'drupal_site_db',
      'username' => 'db_user',
      'password' => 'db_user_password',
      'host' => 'localhost',
      'prefix' => '',
    );

Streamlining settings files in Wordpress

Wordpress has a similar process to Drupal, but the settings files are a bit different.  The config file for Wordpress is the following in site root:

wp-config.php

Go ahead and create two new files:

wp-config.local.php
?wp-config.production.php

Add the following to your .gitinore file in the site root:

wp-config.local.php

This will make it so wp-config.php and wp-config.production.php are under version control when you create your Git repository, but wp-config.local.php is not.  The local config will not be present when you push your site to production.  Next, open the Wordpress wp-config.php and remove the defined DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, DB_CHARSET, and DB_COLLATE variables.  Insert the following in their place:

/** Absolute path to the WordPress directory. */
if ( !defined('ABSPATH') ) {
    define('ABSPATH', dirname(__FILE__) . '/');
}
if(file_exists(ABSPATH  . 'wp-config.local.php')){
    require_once(ABSPATH  . 'wp-config.local.php');
}else{
    require_once(ABSPATH . 'wp-config.production.php');
}

This code tells Wordpress to include the local settings file if it exists, and if it doesn't it will include the production settings file. Your next step is to populate the wp-config.local.php and wp-config.production.php files with your database configuration.  Here's my wp-config.local.php with database credentials obscured.  The production file looks identical but with the production database server defined:

<?php
// ** MySQL settings - You can get this info from your web host ** //

/** The name of the database for WordPress */
define('DB_NAME', 'db_name');

/** MySQL database username */
define('DB_USER', 'db_user');

/** MySQL database password */
define('DB_PASSWORD', 'db_user_password');

/** MySQL hostname */
define('DB_HOST', 'localhost');

/** Database Charset to use in creating database tables. */
define('DB_CHARSET', 'utf8');

/** The Database Collate type. Don't change this if in doubt. */
define('DB_COLLATE', '');

What's next?

Now that you're all set up to deploy easily to production with Git and Wordpress or Drupal, the next step is to actually get your database updated from local to production.  This is a topic for another post, but I've created my own set of Unix shell scripts to simplify this task greatly.  If you're ambitious, go grab my MySQL Loaders scripts that I've put on Github.

Feb 22 2013
Feb 22

Simplifying Wordpress and Drupal configurationAt last year's Drupalcon in Denver there was an excellent session called Delivering Drupal.  It had to do with the oftentimes painful process of deploying a website to web servers.  This was a huge deep dive session that went into the vast underbelly of devops and production server deployment.  There were a ton of great nuggets and I recommend watching the session recording for serious web developers.

The most effective takeway for me was the manipulation of the settings files for your Drupal site, which was only briefly covered but not demonstrated.  The seed of this idea that Sam Boyer presented got me wondering about how to streamline my site deployment with Git.  I was using Git for my Drupal sites, but not effectively for easy site deployment.  Here are the details of what I changed with new sites that I build.  This can be applied to Wordpress as well, which I'll demonstrate after Drupal.

Why would I want to do this?

When you push your site to production you won't have to update a database connection string after the first time.  When you develop locally you won't have to update database connections, either.

Streamlining settings files in Drupal

Drupal has the following settings file for your site:

sites/yourdomain.com/settings.php

This becomes a read only file when your site is set up and is difficult to edit.  It's a pain editing it to run a local site for development.  Not to mention if you include it in your git repository, it's flagged as modified when you change it locally.

Instead, let's go ahead and create two new files:

sites/yourdomain.com/settings.local.php
sites/yourdomain.com/settings.production.php

Add the following to your .gitignore file in the site root:

sites/yourdomain.com/settings.local.php

This will put settings.php and settings.production.php under version control, while your local settings.local.php file is not.  With this in place, remove the $databases array from settings.php.  At the bottom of settings.php, insert the following:

$settingsDirectory = dirname(__FILE__) . '/';
if(file_exists($settingsDirectory . 'settings.local.php')){
    require_once($settingsDirectory . 'settings.local.php');
}else{
    require_once($settingsDirectory . 'settings.production.php');
}

This code tells Drupal to include the local settings file if it exists, and if it doesn't it will include the production settings file.  Since settings.local.php is not in Git, when you push your code to production you won't have to mess with the settings file at all.  Your next step is to populate the settings.local.php and settings.production.php files with your database configuration.  Here's my settings.local.php with database credentials obscured.  The production file looks identical but with the production database server defined:

<?php
    $databases['default']['default'] = array(
      'driver' => 'mysql',
      'database' => 'drupal_site_db',
      'username' => 'db_user',
      'password' => 'db_user_password',
      'host' => 'localhost',
      'prefix' => '',
    );

Streamlining settings files in Wordpress

Wordpress has a similar process to Drupal, but the settings files are a bit different.  The config file for Wordpress is the following in site root:

wp-config.php

Go ahead and create two new files:

wp-config.local.php
?wp-config.production.php

Add the following to your .gitinore file in the site root:

wp-config.local.php

This will make it so wp-config.php and wp-config.production.php are under version control when you create your Git repository, but wp-config.local.php is not.  The local config will not be present when you push your site to production.  Next, open the Wordpress wp-config.php and remove the defined DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, DB_CHARSET, and DB_COLLATE variables.  Insert the following in their place:

/** Absolute path to the WordPress directory. */
if ( !defined('ABSPATH') ) {
    define('ABSPATH', dirname(__FILE__) . '/');
}
if(file_exists(ABSPATH  . 'wp-config.local.php')){
    require_once(ABSPATH  . 'wp-config.local.php');
}else{
    require_once(ABSPATH . 'wp-config.production.php');
}

This code tells Wordpress to include the local settings file if it exists, and if it doesn't it will include the production settings file. Your next step is to populate the wp-config.local.php and wp-config.production.php files with your database configuration.  Here's my wp-config.local.php with database credentials obscured.  The production file looks identical but with the production database server defined:

<?php
// ** MySQL settings - You can get this info from your web host ** //
 
/** The name of the database for WordPress */
define('DB_NAME', 'db_name');
 
/** MySQL database username */
define('DB_USER', 'db_user');
 
/** MySQL database password */
define('DB_PASSWORD', 'db_user_password');
 
/** MySQL hostname */
define('DB_HOST', 'localhost');
 
/** Database Charset to use in creating database tables. */
define('DB_CHARSET', 'utf8');
 
/** The Database Collate type. Don't change this if in doubt. */
define('DB_COLLATE', '');

What's next?

Now that you're all set up to deploy easily to production with Git and Wordpress or Drupal, the next step is to actually get your database updated from local to production.  This is a topic for another post, but I've created my own set of Unix shell scripts to simplify this task greatly.  If you're ambitious, go grab my MySQL Loaders scripts that I've put on Github.

Jul 17 2011
Jul 17

Recently, a user filed a dispute against one of my digital subscription-based websites.  Having never dealt with a PayPal dispute before, I went in not knowing what to expect.  I examined the user's account in Ubercart and they had definitely paid via PayPal and the IPN information was sent back to the site that the order had been successful.  The user had also received the digital subscriber role.  PayPal contacted me noting the user had filed a dispute against me that they did not receive the product.  However, I could see that they very clearly had.

I provided PayPal with screenshots of the order, details on my site, and the nature of my subscriptions.  However, after 15 days PayPal reversed the payment in the buyer's favor.  The only reason given is that I did not meet the qualifications for seller protection.  Checking PayPal's website, you can clearly see that intangible items and digital goods are not covered at all for seller protection.

This is a warning that you should not wait for a dispute on a digital sale to be resolved.  If you wait, the user may be able to freely use the service until the dispute is reversed (and it will always be reversed according to that policy).

PayPal's policy here, in my opinion, is flawed.  Many digital services are time sensitive, in that if a user has access to something for even a short time they will glean benefit that should have cost them money.  This could possibly be addressed in Drupal and Ubercart as well if a dispute is raised on a transaction but I found no mention of this online.

Be diligent and watch what your users are doing.  If you see a claim, cancel their digital access as soon as possible.

There ain't no such thing as a free lunch, buyers.

Jan 25 2011
Jan 25

I noticed this happening while I was getting a site ready with the AddToAny Drupal module.  Pages would randomly hang in my dev environment and it was trying to contact Media6degrees.  After some Googling I found this was a third party tracking cookie being installed with the AddToAny module.  To disable it in Drupal 7, go to the modules page and click "Configure" next to AddToAny.  Under additional options near the bottom, paste this:

a2a_config.no_3p = 1;

Once you have this the module should no longer contact media6degrees.  There's additional information on this in the AddToAny issue queue.  This is a disappointing "feature" of the AddToAny module...

Dec 23 2010
Dec 23

The Webform Drupal module is an amazing thing.  It lets you build all sorts of amazing forms.  Combined with jQuery you can construct dynamic forms that improve user quality of life.  However, Webform's export features are extremely limited.  You can get a run-of-the-mill exported CSV with every component includes with the generic field name.  What if you want or require a different format or a variable number of submissions only?  It seems like the only real solution right now is to write your own app or module that reads Webform's data.

I found a a new module called Webform MySQL Views that helps target data from Webform submissions.  This should help anyone writing something to interact with Webform submission data.

I recently wrote a backend PHP application that reads Webform data and exports it into a completely customized CSV.  It is pretty generic, though it requires manual setting updates to change what you're exporting into CSV (it needs component ID's from the targeted Webform).  The purpose of this customized CSV exporter is to import into The Raiser's Edge, a proprietary CRM that nonprofits use to track constituents.  Why not a module?  I definitely tried to make a module out of this, though having not authored a Drupal module yet my experience is somewhat limited in the Drupal API and it was more efficient to make an external app.  I learned a lot about module development though, so this could be a future endeavor (or maybe the Webform maintainers will expand on their export functionality).  Jumping straight into the Webform code and trying to understand all of it as a relative newcomer to module development isn't something I would recommend for everyone.

Dec 23 2010
Dec 23

The Webform Drupal module is an amazing thing.  It lets you build all sorts of amazing forms.  Combined with jQuery you can construct dynamic forms that improve user quality of life.  However, Webform's export features are extremely limited.  You can get a run-of-the-mill exported CSV with every component includes with the generic field name.  What if you want or require a different format or a variable number of submissions only?  It seems like the only real solution right now is to write your own app or module that reads Webform's data.

I found a a new module called Webform MySQL Views that helps target data from Webform submissions.  This should help anyone writing something to interact with Webform submission data.

I recently wrote a backend PHP application that reads Webform data and exports it into a completely customized CSV.  It is pretty generic, though it requires manual setting updates to change what you're exporting into CSV (it needs component ID's from the targeted Webform).  The purpose of this customized CSV exporter is to import into The Raiser's Edge, a proprietary CRM that nonprofits use to track constituents.  Why not a module?  I definitely tried to make a module out of this, though having not authored a Drupal module yet my experience is somewhat limited in the Drupal API and it was more efficient to make an external app.  I learned a lot about module development though, so this could be a future endeavor (or maybe the Webform maintainers will expand on their export functionality).  Jumping straight into the Webform code and trying to understand all of it as a relative newcomer to module development isn't something I would recommend for everyone.

Dec 16 2010
Dec 16

Here's a handy commandline snippet that will help you upgrade Drupal core (for example, 6.19 to 6.20) without having to replace everything.  It will only update the files that have changed.

  • Navigate to Drupal site root.
  • wget <drupal tar.gz URL>
  • tar -xzvf <drupal tar.gz file>
  • cd <new extracted folder>
  • \cp -Rvupf * ../

The cp command there recursively goes through the new extracted folder and copies it into the folder structure above the folder you're in (into your Drupal site root).  It only updates changed files.  It will also tell you which files are copied over.  So long painful upgrades!  Be sure to visit the admin page after for database updates.

Note that this will most likely not work from one major release to another such as Drupal 6 to Drupal 7.

UPDATE 5/27/11: This technique works on both Drupal 6 and Drupal 7 sites to new point releases.  The above bolded comment still likely stands... though I have not tested it.

UPDATE 8/8/11: Upgrading Drupal on a Mac? Unfortunately the Mac's cp command does not include the update (-u) flag. To do it, follow all of the instructions above and use rsync instead: rsync -ur * ../

Nov 11 2010
Nov 11

If you need a way to redirect webform submissions to a dynamic URL in Drupal 6, using the Webform PHP module can work well with Webform 3.x.  I recommend only using post processing conditionals on select fields if possible with Webform PHP since you need to enable the permission to "use PHP for additional processing" for users who submit webforms (typically anonymous).

Here's the process:

  • Install Webform and Webform PHP
  • Allow the appropriate users to have the "use PHP for additional processing" permission under Admin > User Management > Permissions
  • Create two new pages with the following URL aliases: 'submitted' and 'formredirect'
  • Create a webform with all your components
  • Go to the webform tab, then form settings for the form you created
  • Redirect the form to "custom page" and enter in a new page you create with Drupal, ie http://example.org/formredirect
  • Go to the bottom of form settings, and under additional processing do the following:
<?php
  if ($form_values['submitted_tree']['fieldset']['fieldname_under_fieldset'] == '0' && $form_values['submitted_tree']['fieldset_last_page']['fieldname_under_fieldset'])

   {
    $_SESSION['exredirect']['redirect_url'] = 'http://example.org/submitted?s=n';
  }else{
    $_SESSION['exredirect']['redirect_url'] = 'http://example.org/submitted?s=y';
}
?>
  • In this code, the field on the last page is underneath a fieldset named "fieldset_last_page" (not the label, the actual name).  Both fields we check against are selects.  The "0" in the first is a key value assigned in the select when you configure that component.  Both of these selects are under a fieldset.  If you're not under a fieldset, you can remove the second set of brackets.  What this does is set a session variable with the final page we want to redirect to.
  • Now, go to the formredirect page you made earlier, enable PHP on it under Input Formats, and do the following:
<?php
// get information from our session
if (!empty($_SESSION['exredirect']['redirect_url'])) {
    // redirect to path
    drupal_goto($_SESSION['exredirect']['redirect_url']);
}
else {
    // redirect to frontpage
    drupal_goto('submitted');
}
?>
  • This will do the redirect to submitted with the ?s=n or ?s=y parameter, depending on what your session was set to.  If there's no session it will just redirect to the submitted page with no parameter.  Now, on your submitted page do the following:
<?php $s = $_GET["s"]; 
if($s == 'y'){ ?-->Something here, s is "y"
<?php } ?>
<?php if($s == 'n'){ ?>
Something else here, s is "n"
<?php } ?>
  • This final code grabs the parameter from the URL and shows appropriate content.

There are many variations on this but the method here worked for me without much trouble.

Nov 11 2010
Nov 11

If you need a way to redirect webform submissions to a dynamic URL in Drupal 6, using the Webform PHP module can work well with Webform 3.x.  I recommend only using post processing conditionals on select fields if possible with Webform PHP since you need to enable the permission to "use PHP for additional processing" for users who submit webforms (typically anonymous).

Here's the process:

  • Install Webform and Webform PHP
  • Allow the appropriate users to have the "use PHP for additional processing" permission under Admin > User Management > Permissions
  • Create two new pages with the following URL aliases: 'submitted' and 'formredirect'
  • Create a webform with all your components
  • Go to the webform tab, then form settings for the form you created
  • Redirect the form to "custom page" and enter in a new page you create with Drupal, ie http://example.org/formredirect
  • Go to the bottom of form settings, and under additional processing do the following:
<?php
  if ($form_values['submitted_tree']['fieldset']['fieldname_under_fieldset'] == '0' && $form_values['submitted_tree']['fieldset_last_page']['fieldname_under_fieldset'])
 
   {
    $_SESSION['exredirect']['redirect_url'] = 'http://example.org/submitted?s=n';
  }else{
    $_SESSION['exredirect']['redirect_url'] = 'http://example.org/submitted?s=y';
}
?>
  • In this code, the field on the last page is underneath a fieldset named "fieldset_last_page" (not the label, the actual name).  Both fields we check against are selects.  The "0" in the first is a key value assigned in the select when you configure that component.  Both of these selects are under a fieldset.  If you're not under a fieldset, you can remove the second set of brackets.  What this does is set a session variable with the final page we want to redirect to.
  • Now, go to the formredirect page you made earlier, enable PHP on it under Input Formats, and do the following:
<?php
// get information from our session
if (!empty($_SESSION['exredirect']['redirect_url'])) {
    // redirect to path
    drupal_goto($_SESSION['exredirect']['redirect_url']);
}
else {
    // redirect to frontpage
    drupal_goto('submitted');
}
?>
  • This will do the redirect to submitted with the ?s=n or ?s=y parameter, depending on what your session was set to.  If there's no session it will just redirect to the submitted page with no parameter.  Now, on your submitted page do the following:
<?php $s = $_GET["s"]; 
if($s == 'y'){ ?>Something here, s is "y"
<?php } ?><?php
if($s == 'n'){ ?>
Something else here, s is "n"
<?php } ?>
  • This final code grabs the parameter from the URL and shows appropriate content.

There are many variations on this but the method here worked for me without much trouble.

Oct 08 2010
Oct 08

DrupalSN posted a nice guide to styling exposed View dropdown menus with a JQuery plugin and some stylish CSS.  The guide there worked perfectly except for the step where he hid the submit button.  I changed this:

.views-exposed-form label,
.jquery_dropdown_page .views-exposed-form .form-submit {
  display: none;
}

to this:

.views-exposed-form label,
.views-exposed-widget .form-submit {
  display: none;
}

Also, I wanted a fancy hover effect over the dropdown menu itself, so I added the following style and created a hover version of the background image for the dropdown menu:

 div.jquery_dropdown_header:hover {
  background:url(images/jquery_dropdown-header-hover.png) left top no-repeat;
}

Why the module?  The module turns your select box into an unordered list which is much easier to style.  My only gripe is that it fails to show the "any" option on the exposed dropdown once you navigate away from it (it shows up fine on the initial page load).

Oct 08 2010
Oct 08

DrupalSN posted a nice guide to styling exposed View dropdown menus with a JQuery plugin and some stylish CSS.  The guide there worked perfectly except for the step where he hid the submit button.  I changed this:

.views-exposed-form label,
.jquery_dropdown_page .views-exposed-form .form-submit {
  display: none;
}

to this:

.views-exposed-form label,
.views-exposed-widget .form-submit {
  display: none;
}

Also, I wanted a fancy hover effect over the dropdown menu itself, so I added the following style and created a hover version of the background image for the dropdown menu:

div.jquery_dropdown_header:hover {
  background:url(images/jquery_dropdown-header-hover.png) left top no-repeat;
}

Why the module?  The module turns your select box into an unordered list which is much easier to style.  My only gripe is that it fails to show the "any" option on the exposed dropdown once you navigate away from it (it shows up fine on the initial page load).

Sep 27 2010
Sep 27

I had a bit of an issue with this since I'm used to using arguments on view pages.  To use node id as an argument with a block view you cannot simple use the "Hide view / Page not found (404)" under "Action to take if argument is not present".  Instead, do the following:

  • Select "Provide default argument" under "Action to take if argument is not present"
  • Select "Node ID from URL"
  • Save your view and include your block on the targetted pages

This way you'll be able to pass the nid argument in your URL to the block view.  Whew!

Sep 27 2010
Sep 27

I had a bit of an issue with this since I'm used to using arguments on view pages.  To use node id as an argument with a block view you cannot simple use the "Hide view / Page not found (404)" under "Action to take if argument is not present".  Instead, do the following:

  • Select "Provide default argument" under "Action to take if argument is not present"
  • Select "Node ID from URL"
  • Save your view and include your block on the targetted pages

This way you'll be able to pass the nid argument in your URL to the block view.  Whew!

Sep 20 2010
Sep 20

I recently ran into an issue where international customers could not checkout properly in Ubercart, a fully featured Drupal ecommerce solution.  I was using Authorize.net as the credit card processer.  To allow international customers to be verified with Authorize.net, do the following in your merchant account:

  • Under account, click settings
  • Under settings, click Address Verification Service
  • Uncheck "G", "U", and "S"

According to this post on the Ubercart forums, these three values are the ones that prevent non-US addresses to be processed.

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