The challenge of building premium Drupal themes with zero dependencies

When we started building Dripyard themes, we made the decision early on to not have any dependencies outside of Drupal core. We wanted to avoid depending on contributed modules, npm build processes, and external libraries so that our premium Drupal themes could adapt to any development workflow. Additionally, we wanted to prevent having our own companion module required for Dripyard themes so we can offer a complete package you can download, drop in, and enable. 

In this article, we explore the hurdles we faced along the way and how we dealt with them.

Drupal’s autoloader implementation for themes is limited

For Dripyard themes, using Composer is an option, not a requirement. However, we wanted to make use of modern PHP and PHP autoloading to avoid a "spaghetti mess" of hooks and if statements in large .theme files. Drupal core has limited support for PSR-4 autoloading in theme directories, and classes aren’t always loaded like you’d expect, especially when just dropping folders in a ./themes directory. To solve this, we implemented a custom class loader to help with autoloading and to make our DripyardBase namespace consistently available. We did this by creating a dripyard-classloader.php that invokes spl_autoload_register when necessary. There we define our namespaces and perform additional class discovery for other Dripyard themes and sub-themes. We also have an internal discovery class that resolves the inheritance tree of themes that reference ours as a base class

Even after  handling these edge cases, it should be noted that our classes are still only available when the theme is in context. For example, these classes aren’t loaded in the admin theme—unless you use Dripyard as your admin theme, which we ensure does work if needed.

Shipping custom layouts via the Drupal Layout API

We plan to offer first-class support for Drupal Canvas (formerly Experience Builder) once it has an official release. In the meantime, we’ve standardized on Layout Builder for all of our recipes and demo content, since it’s already part of Drupal core and provides a solid page-building experience.

During development of our layout classes, we realized that using our theme namespace \Drupal\dripyard_base  would sometimes cause errors. For example when Layout Builder was making AJAX requests for the user interface. By ensuring our classes are registered with a custom class loader, we were able to reliably bundle custom Layouts classes in the theme. Thanks to these customizations, we’re able to ship the Dripyard Dynamic Layout with our base theme—a flexible Layout Builder section that lets you define columns, rows, and a wide range of spacing options.

Batch processing and autoloading

Our themes include the ability to install Dripyard custom Drupal recipes from within the theme settings page. To our knowledge, this capability is a Dripyard first and deserves its own blog post (sign up to be notified). To make this work, we utilize the Drupal Batch API, but subsequent batch requests don’t invoke classes defined in a theme since it's not in context for general Drupal requests. To resolve this, we found a creative way to ensure additional requests invoke our batch process class every time. Using a combination of our class loader and the Batch API’s setFile() method, we’re able to maintain complete control over the batch pipeline to install the recipes.

  public static function queueInstall($recipe_key, $theme) {
   try {
     $recipe_path = static::resolveRecipePath($recipe_key, $theme);
     $recipe = Recipe::createFromDirectory($recipe_path);
     static::$recipeBatch = static::$recipeBatch ?? new BatchBuilder();

     // Ensure this file is loaded on every operation since themes
     // do not have automatic class loading.
     static::$recipeBatch->setFile(\Drupal::service('extension.list.theme')->getPath('dripyard_base') . '/src/Recipes/RecipeBatchProcessor.php');

Special thanks to Adam, who gave us the initial inspiration for the batch recipe installer.

Extended classes for recipes

With the fixes for class loading and object-oriented inheritance, we can establish patterns in our themes—like our RecipeInstaller, which handles the recipe batch processes explained above and allows sub-themes like neonbyte to extend or override select parts of the class. In the neonbyte example, the base class handles all batch processing and the logic of locating and installing items, while the extended neonbyte class defines the recipes that are available and their relationships to other recipes. Additionally, if you use neonbyte as a base theme of your custom theme, the recipes from neonbyte are still discovered and available to be installed form your custom theme settings page. 

class RecipeInstaller extends RecipeInstallerBase {
/**
 * {@inheritdoc}
 */
protected function getAvailableRecipes() {
  return [
    'dripyard_neonbyte_blocks' => [
      'machine_name' => 'dripyard_neonbyte_blocks',
      'title' => t('Neonbyte Blocks'),
      'description' => t('This recipe provides a set of block types based on the single directory components of this theme. These work well with layout builder, but can be used with other page layout modules.'),
      'extended_by' => ['dripyard_neonbyte_demo_content', 'dripyard_neonbyte_landing_pages'],
    ],
....

A cleaner .theme file in our themes

Since Drupal theme files cannot be used to register Symfony events or services, we established a simple wrapper for theme hooks with a set of “pseudo-plugin” style classes to handle logic like form alters and pre-processors. Our theme invokes a simple Drupal hook like theme_preprocess_block and then performs auto-discovery to determine which classes in our themes and sub-themes apply and which ones should be invoked. This abstraction allows us to have better control of hooks across many levels of Dripyard themes and ensures our theme PHP files are clean and organized.


/**
* Implements hook_form_FORM_ID_alter().
*/
function dripyard_base_form_system_theme_settings_alter(&$form, FormStateInterface $form_state) {
 $theme = $form_state->getBuildInfo()['args'][0];
 $theme_settings_classes = ClassDiscovery::getAvailableClasses($theme, 'ThemeSettings');
 foreach ($theme_settings_classes as $class_name) {
   $class = ClassDiscovery::loadClass($theme, 'ThemeSettings', $class_name);
   if ($class !== NULL) {
     $settingsClass = new $class();
     $settingsClass->setTheme($theme);
     $settingsClass->themeSettingsFormAlter($form, $form_state);
   }
 }
}

Inheritable and default theme settings

Using this class-inheritance structure, we’re able to provide base theme settings, forms, and default configurations that all Dripyard themes depend on. From these, we define a common color picker for the site color scheme, global spacing options, and more. Each Dripyard theme can extend the base classes and provide—or override—customizations that are specific to the styled theme, and everything is stored in a consistent manner.

See Dripyard’s core-only approach in action

Interested in more? Join our webinar where we'll be launching our themes for sale and you can see the power of Dripyard and Drupal together. Our premium Drupal themes deliver modern PHP, clean architecture, and Layout Builder support—all while depending only on Drupal core!

Angy Giles in a collared shirt smiling in front of a wooded background

About the author

Andy is a co-founder and a back-end specialist at Dripyard. He's been active in the Drupal community since 2012 and is known for his contributions to Drupal Commerce.

In this post