Create your own customized distribution to speed up your Drupal development process.
Do you find yourself repeating the same steps whenever you start a new Drupal project? Do you always download and enable the same modules, and make the same configuration changes every time? As we start doing more and more Drupal projects at Nomensa, I noticed that we were doing exactly this, so I started to look into ways to streamline our initial project setup process. My solution was to create my own custom installation profile that provides me with a template to start each project, and this article outlines the steps I took to create it. The code outlined in this article has been committed into GitHub at https://github.com/opdavies/linuxjournal_demo, and it's available for you to download and re-use as needed.
Installation profiles are a combination of modules and themes and predefined configuration. A great example of an installation profile that I use regularly is called Commerce Kickstart (drupal.org/project/commerce_kickstart). It provides a version of Drupal 7 along with the Drupal Commerce suite of modules that have been preconfigured to have the correct content types, rules, views and so on. Once an installation profile has been uploaded onto drupal.org as a project, it gets bundled with Drupal core, is available as a packaged download, and is known as a distribution. See drupal.org/project/distributions for a list of existing distributions.
There are two different ways to download an existing Drupal installation profile. The first (and easiest) method is to download it as a distribution from drupal.org. To do this, go to the project page for the installation profile (for example, drupal.org/project/commerce_kickstart), scroll to the bottom of the page, and download a release in the same way that you would download a module or theme. The resulting file will be named something like commerce_kickstart-7.x-1.10-core.tar.gz, and this file will contain both Drupal core and the Commerce Kickstart installation profile. The other option is to download a fresh copy of Drupal core, and then download the installation profile seperately, either using a drush command like drush dl commerce_kickstart, or clone it directly from its Git repository, and then place it within the Drupal's profiles directory.
Now, when you go to install Drupal, there is an additional option to use the Commerce Kickstart installation profile. Select the appropriate profile, click the Save and continue button, and continue through the installation process as normal.
Outside your Drupal directory, create a new directory to hold the files for your installation profile. Mine is called linuxjournal_profile, although the name of the profile is going to be simply linuxjournal. I've appended _profile to the end of the directory name. Later, there will be several different directories with similar names, so this helps provide some clarity. Be sure to give some thought to the name beforehand and ensure that it doesn't conflict with any potential modules or themes.
The first file I need to create is the linuxjournal.info file that, identical to .info files for modules and themes, defines the name and description of the profile, as well as which version of Drupal core that it is compatible with. I'm also going to make it dependent on the core dblog and block modules so that these are enabled automatically when the profile is installed:
name = Linux Journal description = A demonstration installation profile for my LJ article. core = 7.x dependencies[] = dblog dependencies[] = block
The only other mandatory file needed for this profile to be visible to Drupal is the .profile file—in this case, linuxjournal.profile. Within this file, I can put any custom PHP functions or implementations of Drupal hooks that will take effect during the installation process. For now, I'm just going to add an opening PHP tag and leave the rest of the file blank. To confirm that everything works okay so far, I can download a fresh copy of Drupal core, copy linuxjournal_profile into the profiles directory, rename it to linuxjournal so that it is the same as the name of the profile, and load that site in a Web browser.
So far, so good. On the installation page, as well as seeing the default Standard and Minimal profiles, I also can see my Linux Journal profile. I can select this and continue with the installation process as normal to confirm that it's working, although because I've not entered anything into linuxjournal.profile yet, nothing special is going to happen.
Identical to writing a module, I can create a .install file that contains functions to run when the profile is installed, updated or uninstalled. I'm going to utilize the hook_install() function from the minimal installation profile as part of my profile instead of re-declaring its contents myself and duplicating code. Within my own implementation of hook_install(), I can include the .install file from the minimal profile and then run its minimal_install() function:
<?php /** * @file * Install, update and uninstall functions * for the Linux Journal installation profile. */ /** * Implements hook_install(). * * Run the hook_install() function from the minimal * profile as part of this profile. */ function linuxjournal_install() { // Utilize the hook_install() implementation from // the minimal profile. include_once DRUPAL_ROOT . '/profiles/minimal/minimal.install'; minimal_install(); }
I'm now going to create another file called linuxjournal.make. This file contains a listing of all the projects (modules and themes) and libraries that are used within the profile. My company specializes in building accessible Web sites, and I use a number of contributed modules on every site to help me do this. There are also some essential contributed modules, such as Administration Menu (drupal.org/project/admin_menu) and Pathauto (drupal.org/project/pathauto), as well as some custom modules and features that I use on every site. All of these will be listed within the linuxjournal.make file.
At the very top of linuxjournal.make, I need to add the following two lines to define the API version as well as the version of Drupal core that I'm using:
api = 2 core = 7.x
Here is the syntax for adding a project—in this case, the Administration Menu module—into a profile. Personally, I like to download projects from their Git repositories, although they could just be downloaded from drupal.org using wget. It's worth noting that the download URL doesn't have to be a URL at drupal.org. You can download projects from other sources, such as GitHub or BitBucket, or any other source, including from local file directories:
projects[admin_menu][type] = module projects[admin_menu][subdir] = contrib projects[admin_menu][version] = 3.0-rc3 projects[admin_menu][download][type] = git projects[admin_menu][download][url] = ↪http://git.drupal.org/project/admin_menu.git projects[admin_menu][download][branch] = 7.x-3.x
All modules specified will be located in the profiles/linuxjournal/modules directory by default; however, I prefer to store contributed modules in a subdirectory called contrib, hence the subdir value. I've also specified which version of the module to use and which branch within the Git repository to use. If I didn't specify a version number, the latest commit to the specified branch would be used.
To make the Administration Menu module enabled by default after installing the profile, I can declare it as a dependency by adding the following line into linuxjournal.info in the same way that I did for the dblog and block modules earlier:
dependencies[] = admin_menu
If I update my version of Drupal with these changes and try following the installation process, I will get an error because I've made the profile dependent on the Administration Menu module, but this module hasn't been downloaded yet for this instance of Drupal. The next step is to create a distribution that contains the linuxjournal installation profile, as well as all of the projects that are defined in linuxjournal.make.
To create a distribution, the first thing I need to do is make a new file called distro.make that will be used to compile an instance of Drupal that contains the linuxjournal profile. As with linuxjournal.make, I need to start with declaring the API version and the version of Drupal core. I also need to declare Drupal core as a project as well as include the linuxjournal profile:
api = 2 core = 7.x projects[drupal][type] = core projects[drupal][version] = "7" ; Add the Linux Journal profile to the full distribution build. projects[linuxjournal][type] = profile projects[linuxjournal][download][type] = git projects[linuxjournal][download][url] = ↪https://github.com/opdavies/linuxjournal_demo.git
Again, I'm using Git to download the installation profile and providing the distribution with the URL to the repository at GitHub. With distro.make saved, I now can compile Drupal using the following Drush command:
drush make distro.make directory
The last parameter is the name of the directory that you want Drupal to be compiled into. If one isn't specified, the directory that you're currently in will be used, which I wouldn't recommend. I want the resulting directory to be placed in the same level as my linuxjournal_profile directory, so I need to move up one level before specifying the name of the directory, which is going to be linuxjournal_demo:
drush make distro.make ../linuxjournal_demo
This command will download the latest version of Drupal 7 core, as well as all projects defined within linuxjournal.profile, into the linuxjournal_demo directory. Once that's compiled everything, within the profiles/linuxjournal directory, there is a new directory called modules/contrib that contains the admin_menu module. With all of the dependencies downloaded, I now can go to install.php again and go through the installation process without any errors.
I can add a theme into my profile by declaring it within linuxjournal.make in the same way that I can for modules. As I use Omega as a base theme for most of my themes, I'll include that within my profile so that it's downloaded automatically:
; Themes ========================================================= projects[omega][type] = theme projects[omega][version] = 3.1 projects[omega][download][type] = git projects[omega][download][url] = ↪http://git.drupal.org/project/omega.git projects[omega][download][branch] = 7.x-3.x
For now, I'll be creating the sub-theme manually, so I won't change the default theme here.
When going through the installation process, I usually make several changes to the site configuration form—namely setting a default site name and adding the default country. I can automate these changes by adding an implementation of hook_form_FORM_ID_alter() into linuxjournal.profile, as any functions defined in this file will take effect during the installation process. I know that the ID of the form is install_configure_form, and the name of the function that I'm going to create will be linuxjournal_form_install_configure_form_alter():
/** * Implements hook_form_alter(). * * Allows the profile to alter the site configuration form. */ function linuxjournal_form_install_configure_form_alter(&$form, ↪$form_state) { // Set a default site name. $form['site_information']['site_name']['#default_value'] = ↪t('Linux Journal Demo'); }
As the form object is passed into the function by reference, I can add or overwrite information within the object by using this function. Here I'm setting a default value for the site's name field that will be pre-populated on the site configuration form the next time I follow the installation process. I can add and override any other values on the form by adding them into this function:
/** * Implements hook_form_alter(). * * Allows the profile to alter the site configuration form. */ function linuxjournal_form_install_configure_form_alter(&$form, ↪$form_state) { // Set a default site name and email address. $form['site_information']['site_name']['#default_value'] ↪= t('Linux Journal Demo'); $form['site_information']['site_mail']['#default_value'] ↪= 'linuxjournal@oliverdavies.co.uk'; // Set a default username and email address. $form['admin_account']['account']['name']['#default_value'] ↪= 'Oliver Davies'; $form['admin_account']['account']['mail']['#default_value'] ↪= 'linuxjournal@oliverdavies.co.uk'; // Set a default country and timezone. $form['server_settings']['site_default_country']['#default_value'] ↪= 'GB'; $form['server_settings']['date_default_timezone']['#default_value'] ↪= 'Europe/London'; // Disable the 'receive email notifications' check box. $form['update_notifications']['update_status_module'] ↪['#default_value'][1] = 0; }
Now, as well as adding a default site name, I've also added a default site e-mail address, a default user name and e-mail address for the first user account, added a default time zone and country, and disabled the option to receive e-mail alerts when new updates are available. For security reasons, I don't want to define my password in this file and will continue to enter this onto the form directly. I'm using #default_value and not #value so I can edit these predefined values on the form if I need to for this site. If I used #value, I would not be able to do so.
Basically, what I now have is a copy of the minimal installation profile with some additional modules and themes. The first thing I want to do next is use the Seven theme for the administration pages of the site, as it would be if I'd used the standard installation profile. To do this, I first need to enable Seven and then set some variables to set it as the administration theme. I can reference the .install file from the standard installation profile to find out how to do this. To do so, I add the following code into the linuxjournal_install() function in linuxjournal.install:
// Enable the administration theme. $admin_theme = 'seven'; db_update('system') ->fields(array('status' => 1)) ->condition('type', 'theme') ->condition('name', $admin_theme) ->execute(); variable_set('admin_theme', $admin_theme); variable_set('node_admin_theme', '1');
The standard installation profile also creates two default content types: basic pages and articles. I'm going to re-use most of this code to create the basic page content type for my profile:
// Add a 'Basic page' content type. $types = array( array( 'type' => 'page', 'name' => st('Basic page'), 'base' => 'node_content', 'description' => st("Use <em>basic pages</em> for ↪your static content, such as an 'About us' page."), 'custom' => 1, 'modified' => 1, 'locked' => 0, ); ); foreach ($types as $type) { $type = node_type_set_defaults($type); node_type_save($type); node_add_body_field($type); } // Default 'Basic page' to not be promoted and don't // display author information. variable_set('node_options_page', array('status')); variable_set('node_submitted_page', FALSE);
To create additional content types, I can keep adding new items into the $types array, and they will be processed within the foreach() loop.
When you use the standard installation profile, several different text formats are created. Currently, I have only plain text and PHP code (because the PHP module is now also a dependency for my profile). I also want the additional text formats, so I'll copy that section of code from the standard.install file and paste in into the linuxjournal_install() function:
// Add text formats. $text_formats['filtered_html'] = array( 'format' => 'filtered_html', 'name' => 'Filtered HTML', 'weight' => 0, 'filters' => array( // URL filter. 'filter_url' => array( 'weight' => 0, 'status' => 1, ), // HTML filter. 'filter_html' => array( 'weight' => 1, 'status' => 1, ), // Line break filter. 'filter_autop' => array( 'weight' => 2, 'status' => 1, ), // HTML corrector filter. 'filter_htmlcorrector' => array( 'weight' => 10, 'status' => 1, ), ), ); $text_formats['full_html'] = array( 'format' => 'full_html', 'name' => 'Full HTML', 'weight' => 1, 'filters' => array( // URL filter. 'filter_url' => array( 'weight' => 0, 'status' => 1, ), // Line break filter. 'filter_autop' => array( 'weight' => 1, 'status' => 1, ), // HTML corrector filter. 'filter_htmlcorrector' => array( 'weight' => 10, 'status' => 1, ), ), ); $text_formats['raw_html'] = array( 'format' => 'raw_html', 'name' => 'Raw HTML', 'weight' => 2, ); foreach ($text_formats as $text_format) { $text_format = (object) $text_format; filter_format_save($text_format); }
I've amended the format slightly by creating an array called $text_formats, and then using a foreach loop to save each one as opposed to doing them separately as it is done in standard.install. I also usually create a Raw HTML format that allows all HTML tags and doesn't have any filters applied to it, so I've added it here as an additional format to be created automatically.
I also download and install the WYSIWYG module and the TinyMCE editor for each site. Rather than having to download and enable the WYSIWYG module, download and extract the TinyMCE library and configure the editor, I can add it into my profile to have it done automatically. First, I need to add the WYSIWYG (drupal.org/project/wysiwyg) and Libraries API (drupal.org/project/libraries) modules and add them into dependencies to have them enabled by default.
In linuxjournal.make:
projects[wysiwyg][type] = module projects[wysiwyg][subdir] = contrib projects[wysiwyg][version] = 2.1 projects[wysiwyg][download][type] = git projects[wysiwyg][download][url] = ↪http://git.drupal.org/project/wysiwyg.git projects[wysiwyg][download][branch] = 7.x-2.x projects[libraries][type] = module projects[libraries][subdir] = contrib projects[libraries][version] = 2.0 projects[libraries][download][type] = git projects[libraries][download][url] = ↪http://git.drupal.org/project/libraries.git projects[libraries][download][branch] = 7.x-2.x libraries[tinymce][type] = library libraries[tinymce][download][type] = get libraries[tinymce][download][url] = ↪http://github.com/downloads/tinymce/tinymce/tinymce_3.5.6.zip
In linuxjournal.info:
dependencies[] = libraries dependencies[] = wysiwyg
TinyMCE will be downloaded and extracted into the profiles/linuxjournal/profiles directory and will be accessible by the WYSIWYG module once Libraries API is enabled. However, now that it has been downloaded, I need to assign it to one of my text formats before it can be used. The format I'm going to assign it to is Filtered HTML, and I can do that by adding a record into the wysiwyg table in the database. I've also added an array of default settings that I've used on another site, and I've added this into the drupal_write_record() function:
// Add the TinyMCE editor to the Filtered HTML text format. $tinymce_settings = array( 'default' => 1, 'user_choose' => 0, 'show_toggle' => 0, 'theme' => 'advanced', 'language' => 'en', 'buttons' => array( 'default' => array( 'bold' => 1, 'italic' => 1, 'strikethrough' => 1, 'justifyleft' => 1, 'justifycenter' => 1, 'justifyright' => 1, 'justifyfull' => 1, 'bulllist' => 1, 'numlist' => 1, 'link' => 1, 'unlink' => 1, 'anchor' => 1, 'image' => 1, 'formatselect' => 1, 'sup' => 1, 'sub' => 1, 'blockquote' => 1, 'code' => 1, 'hr' => 1, 'removeformat' => 1, 'charmap' => 1, ), ), 'toolbar_loc' => 'top', 'toolbar_align' => 'left', 'path_loc' => 'bottom', 'resizing' => 1, 'verify_html' => 1, 'preformatted' => 0, 'convert_fonts_to_spans' => 1, 'remove_linebreaks' => 1, 'apply_source_formatting' => 0, 'paste_auto_cleanup_on_paste' => 1, 'block_formats' => 'p,address,pre,h2,h3,h4,h5,h6,div', 'css_setting' => 'theme', 'css_path' => '', 'css_classes' => '', ); // Create the record. $record = array( 'format' => 'filtered_html', 'editor' => 'tinymce', 'settings' => $tinymce_settings, ); // Save the record to the database. drupal_write_record('wysiwyg', $record);
It's worth noting here that the $tinymce_settings variable needs to be a standard PHP array as it will be serialized automatically when it is written into the database table. When I first tried this, I tried importing a pre-serialized array into the database, which subsequently generated errors when trying to view the WYSIWYG admin settings form.
All of the Drupal sites that we build at my company have a moderation workflow in place provided by the Workbench Moderation module (drupal.org/project/workbench_moderation). This requires creating some additional roles for users who can edit and publish content. I also usually create a Developer role for use by any of the other Developers in the team that has permissions to all of the modules on the site, as well as an Administrator role for users who need more administrative access than Editors and Publishers, but who don't need full Developer-level access. Again, I can reference a section of code from standard.install and use that as a template:
// Create new user roles for Developers, Administrators, // Editors and Publishers. $roles = array('Developer', 'Administrator', 'Editor', 'Publisher'); foreach ($roles as $weight => $name) { $role = new stdClass; $role->name = $name; $role->weight = $weight + 2, // New roles must have at // least a weight of 2. // Save the new role. user_role_save($role); if ($name == 'Developer') { // Give the Developer role all permissions. user_role_grant_permissions($role->rid, ↪array_keys(module_invoke_all('permission'))); // Set this as the administrator role. variable_set('user_admin_role', $role->rid); // Assign user 1 the Developer role. db_insert('users_roles') ->fields(array('uid' => 1, 'rid' => $role->rid)) ->execute(); }
I've created an array containing the names of the new roles I want to create, and then a foreach() loop including the key of each item that will be used to define the weight for each role. The first section applies to all new roles where the role is created, and then there is an additional section that applies only to the Developer role. This section assigns all permissions to the Developer role as well as assigning the role to user 1.
With the new roles created, I now can set some default permissions:
// Assign some default permissions. $filtered_html_permission = ↪filter_permission_name($filtered_html_format); $raw_html_permission = filter_permission_name($raw_html_format); user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, ↪array('access content', $filtered_html_permission)); user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, ↪array('access content', 'access administration menu', ↪'access devel information', $filtered_html_permission, ↪$raw_html_permission));
To start, I find out the name of the permissions for the Filtered HTML and Raw HTML text formats that I created earlier. Anonymous users are going to have very restricted permissions and are only going to be able to access content and use the Filtered HTML permission. The authenticated users also will be able to access the Administration Menu and information from the Devel module, as well as the Filtered HTML and Raw HTML text formats.
These are just a few examples of what can be done using installation profiles and distributions, and I've shown some of the ways I automate the Drupal installation and configuration processes. The complete installation profile I'm currently using at Nomensa also includes custom themes with theme template overrides that then can be used on custom public-facing and administration themes and additional Drupal configuration. This has saved me hours of time in the initial development stages as opposed to having to redo the same steps each time I start developing a new site.