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!