What is a Drupal recipe?¶
Drupal recipes allow the automation of Drupal module install and configuration via the user interface and via the Drupal recipe runner.
A Drupal recipe is a tool for Site Builders and Developers to add functionality to a Drupal site. It is like a recipe that provides a series of steps to add functionality. These steps could be taken manually to arrive at the same point. For example, I might want to provide a blog recipe that configures modules to provide a great blogging experience and creates a few sample posts. Or it can include a number of other Drupal recipes and modules to build a full site experience. Ideally Drupal recipes would not configure an entire site themselves but would break out their functionality into smaller Drupal recipes that can be reused by other Drupal recipes.
Drupal recipes are:
- Applied to Drupal sites, they are not installed.
- Easy to share
- Do not lock sites in
- Composable from other Drupal recipes
Example Use cases¶
Recipe content type¶
Provides the content type, fields, form and view displays, and views necessary for recipe functionality on a site. Here's an example of how the recipe might break down:
- Composer require - nothing to do - all modules are supplied by core
- Module install - ensure that node, views, field dependencies are installed
- Configuration - create node type, fields, entity displays and views that are required.
- Configuration - update author and editor with permissions if they exist.
- Content - create demo recipe content if selected by the user.
Umami site¶
Provides the full Umami experience.
- Applies the Recipe content type recipe
- Installs other modules used by Umami that are not already installed
- Configures the modules
- Installs the frontend and admin themes \ Notes: once a Drupal recipe is setting theme related configuration it becomes less useful for many sites. Therefore, we should discourage this in Drupal recipes that aspire to widely used and provide functional building blocks for generic use-cases.
- Configures the installation to look as expected.
SEO best practices¶
Provides a recipe to set up SEO features on your site.
- Composer require - requires the metatag and metatag_async_widget project
- Module install - installs the metatag, metatag_open_graph, metatag_twitter_cards and metatag_async_widget modules
- Configuration - adds meta tag field to existing node types, form and view displays, creates metatag_defaults config for existing node types
- Content - do nothing
What can a Drupal recipe do?¶
- Provide a list of other Drupal recipes to apply. Drupal recipe inheritance is not possible. Drupal recipes can be composed.
- Provide a list of projects to composer install
- Provide a list of modules to install
- This step should only install the simple configuration provided by the modules. It should ignore all the configuration entities. This means that Drupal recipes do not need a way to remove module provided configuration entities.
- Provide a list of themes to install
- Provide configuration
- Choose config from the modules it has listed. A single wildcard will indicate that all of module's configuration should be created.
- Supply new configuration
- Update existing configuration \ The ability to script configuration updates to existing configuration is a key new ability for Drupal recipes. They will be able to use this to add permissions to existing roles and fields to entity displays and views. The Config Actions module offers some prior art in contrib. \ Proposal: Config actions are implemented as PHP attributes on methods of the Configuration entity.
- Note is it important that configuration change is only updates and additions. Removals will make it very hard for recipes to work alongside each other.
- Provide content
- Add new content.
- Provided suggested other Drupal recipes you might want to apply afterwards
- Provide tests
- At a minimum core should provide a test that can be run against any Drupal recipe that proves it is installable and that configuration provided by the Drupal recipe matches the configuration schema. There should be a gitlab ci template that makes running this test against a Drupal recipe easy.
- Have the following metadata:
- Name. A short name for the Drupal recipe.
- Description. A longer description of the Drupal recipe.
- Type. Groups related Drupal recipes together. The type 'Site' is a special case that allows such Drupal recipes to possibly be listed on the installer.
What can't a Drupal recipe do?¶
- Provide functionality itself, for example, implement hooks and services. If a Drupal recipe requires special functionality that needs code this should be provided in a regular Drupal module.
- Provide an upgrade path. Once a Drupal recipe has been applied responsibility for updating configuration and content falls to modules and core.
- Be part of a deployment. If you want to deploy a Drupal recipe you run it
against a site and then deploy as you would right now.
- Deploying the content created by a Drupal recipe is out-of-scope.
An example Drupal recipe¶
recipe.yml¶
# A human-readable name of the Recipe for listing in UIs.
name: 'Example recipe'
description: "An example Drupal recipe description"
# The type key is similar to the package key in module.info.yml. It
# can be used by the UI to group Drupal recipes. Additionally,
# the type 'Site' means that the Drupal recipe will be listed in
# the installer.
type: 'Content type'
recipes:
# An array of recipe's to apply prior to applying this recipe. Contributed or
# custom recipes can apply Drupal core recipes by specifying the root-relative
# path to the recipe. If any of these recipes fail to be applied, this
# recipe will not be applied either.
- editorial_ui_for_publishers
- another_recipe
- core/recipes/article_comment
install:
# An array of modules or themes to install, if they are not already.
# The system will detect if it is a theme or a module. During the
# install only simple configuration from the new modules is created.
# This allows the Drupal recipe control over the configuration.
- easy_breadcrumb
- node
- text
input:
recipient:
# REQUIRED: This has to be the name of a data type known to Drupal's Typed
# Data system. It can only be a "primitive" data type, like `string`,
# `integer`, `float`, `boolean`, and a few others.
# It can't be anything "complex" like an array. When in doubt, `string` is
# usually a safe choice.
data_type: email
# REQUIRED: A brief description of this input, and what it is used for.
description: 'The email address that should receive submissions from the feedback form.'
# OPTIONAL: Validation constraints against which the user's input will be
# checked. These constraints are used in exactly the same way that they're
# used by config schema to validate config. This is an array of arrays,
# where the keys are the names of constraints, and the values are arrays of
# options to pass to that constraint. If the constraint takes no options,
# the array can be empty.
constraints:
NotBlank: []
# OPTIONAL: Anything under the form key will be used to build the form
# element for this input. If form is not present, the input will never be
# shown in a form at all. If it is present, it must be an array containing
# only [form element properties](https://www.drupal.org/docs/drupal-apis/form-api/form-render-elements#s-list-of-form-and-render-elements)
# (i.e., every key must begin with #). It cannot contain any child elements.
form:
'#type': email
'#title': 'Feedback form email address'
# OPTIONAL: How to prompt the user at the command line. You can omit this
# entire section if you don't want to prompt the user for this input at all.
prompt:
# REQUIRED: Has to be the name of a method in
# \Symfony\Component\Console\Style\StyleInterface that accept user input.
# There are currently only `ask`, `askHidden`, `choice`, and `confirm`.
method: ask
# OPTIONAL-ISH: Arguments to pass to the method, keyed by argument name.
# Any required arguments have to be in here.
arguments:
# StyleInterface::ask() has a required $question argument, the question
# to ask the user at the command line.
question: 'What email address should receive website feedback?'
# REQUIRED: What should the default value be if the user could not be asked
# for input? For example, if the `drupal recipe` command is running in
# non-interactive mode.
default:
# REQUIRED: Where the default should come from: can be `config` or `value`.
source: config
# REQUIRED if `source` === `config`:
# a two-element indexed array with the name of a config object, and a
# property of that object, in that order. The config object must exist, or
# this will throw an exception.
config: ['system.site', 'mail']
# REQUIRED if `source` === `value`: a value to use as the default. Can be
# anything, as long as it conforms to the data type and validation
# constraints.
value: 'mail@example.com'
# This 'input' token can now be used later in the `config:actions` section.
# config:
# actions:
# contact.form.feedback:
# setRecipients:
# - ${recipient}
extra:
# The key should be a valid project name. We don't validate that it exists, only that it
# *could* exist (i.e., doesn't contain "-", or "*", etc.)
project_browser:
# Some arbitrary stuff here
another_module:
# More other arbitrary stuff here
config:
# A Drupal recipe can have a config directory. All configuration
# is this directory will be imported after the modules have been
# installed.
# Additionally, the Drupal recipe can install configuration entities
# provided by the modules it configures. This allows them to not have
# to maintain or copy this configuration. Note the examples below are
# fictitious.
import:
easy_breadcrumb:
- views.view.easy_breadcrumbs
node:
- node.type.article
# Import all configuration that is provided by the text module and any
# optional configuration that depends on the text module that is provided by
# modules already installed.
text: *
# The recipe system compares configuration from your recipe and the modules
# you bring in against your site's active configuration. By default it
# requires that they be identical in every detail, or it throws an error.
# strict: true
# You can set this to false to have the recipe runner skip the config import
# if it finds anything different.
# strict: false
# A recipe can choose to opt only certain config into the strict checking. If
# you specify individual config files to be treated strictly, then all others
# will be treated lenient.
# strict:
# - field.storage.field_oysters
# - field.storage.field_quahogs
strict: false
# Configuration actions may be defined. The structure here should be
# configuration entity ID, action, and then action arguments. Below the user
# role entity type with an ID of editor is being created if it does not exist
# and then has permissions added. The permissions key will be mapped to the
# \Drupal\user\Entity\Role::grantPermission() method.
actions:
user.role.editor:
createIfNotExists:
label: 'Editor'
grantPermissions:
- 'delete any article content'
- 'edit any article content'
content:
# A Drupal recipe can have a content directory. All content in this directory
# will be created after the configuration is installed.
composer.json¶
{
"name": "drupal_recipe/example_recipe",
"description": "An example Drupal recipe description",
"type": "drupal-recipe",
"require": {
"drupal/core": "^9.3",
"drupal/easy_breadcrumb": "^2.0.1",
"drupal-recipe/editorial_ui_for_publishers": "^1.0"
},
"suggest": {
"drupal-recipe/moderated_content_for_publishers": "Allows more advanced moderation of content"
},
"license": "GPL-2.0-or-later"
}
The suggest
key can be used to suggest other recipes a user might want to
apply.
How a recipe is named¶
Recipe's have a human-readable name in the recipe.yml. This name should be used
when listing recipes in the user interface. A folder can contain a single
recipe. Recipe machine names are determined by the folder name that contains the
recipe. This means that the machine name is consistent with the composer porject
name. For example, if the recipe's composer project name is
drupal_recipe/example_recipe
this will be placed in a folder called
example_recipe
by composer and the recipe's machine name is example_recipe
.
Note that this is different from other Drupal extension machine names which are
determined by the info.yml filename.
Drupal recipe creation¶
Initial implementation will be by hand. One of the goals of the initiative will be to build a CLI command to create Drupal recipes from an already configured site.
Other ideas include:
- Is there UI tooling that we can create? For example, perhaps the site builder could click "Start recording", then perform a bunch of UI actions in Drupal, then click "Stop recording", and then Drupal exports the required YAML files?
- Can we leverage the Dependency Calculation module and use it to build Drupal recipes for specific configuration or content entities?
- Features module has a pretty good UI for selecting configuration and calculating dependencies.
Drupal recipe maintenance¶
One of the aims is to make Drupal recipes as easy to maintain as possible. The biggest issue that Drupal recipes will have to deal with is what happens when a module that provides the code for a configuration entity or content entity makes a change that would result in an update to the Drupal recipe. When the Drupal recipe is applied to a new site composer will get the latest versions of the dependencies and therefore any updates that this modules ship will not be run against configuration or content provided by the Drupal recipe.
- Can we do something similar to Symfony's recipe update functionality? See https://symfony.com/blog/fast-smart-flex-recipe-upgrades-with-recipes-update or maybe we can do something similar to https://www.drupal.org/project/drupal/issues/3206226 ?
- How to deal with versioning? Should a Drupal recipe store the versions of modules used to create it? Can we leverage this information to provide automatically generated updates to Drupal recipes or some CLI command to make it easy?
Implementation tasks¶
See the roadmap for a list of tasks by implementation phase and links to issues on Drupal.org.
- A Drupal recipe is a new project type on Drupal.org
- A recipe consists of:
- recipe.yml
- composer.json (optional)
- logo.png (optional)
- /config directory (optional)
- /content directory (optional)
- Recipes can be discovered in:
- /core/recipes - this is where core supplied recipes will be.
- /recipes - this is where custom and contrib recipes will be.
- A composer plugin detects composer project types of
drupal-recipe
. When installing a recipe it will: - Copy the recipe to DRUPAL_ROOT/recipes. If there is an existing recipe this will cause an error.
- unpack the dependencies into the root composer.json.
- This has the following effects:
- Using composer means that dependency resolution and compatibilities are composer's responsibility.
- Unpacking the recipe's composer.json into the root composer.json ensures that recipes can be removed without removing their dependencies.
- This is inspired by Symfony Flex. We are still determining how much Symfony code & practice we can leverage.
- Prior to installing the modules, the recipe runner checks that they exist and
their requirements are met.
- Proposal: use Package Manager's API from the automated update initiative.
- The recipe runner leverages the module install to install the list of
modules from the Drupal recipe
- During install we only install the simple configuration the module provides. We do not create any configuration entities or optional configuration by default. Only config entities explicitly declared in the "config:" section of the recipe file are installed.
- Prior to creating or modifying configuration, the recipe runner checks that
the configuration is valid (its dependencies can be met).
- This needs to happen after module install because config validation can only occur when modules that provide the schema and plugins are installed.
- Configuration in the recipe's /config directory is always installed by the
recipe runner.
- Should we support the ability to choose specific config? For example, it might be nice to support tours by allowing a Drupal recipe to ship with tours and then if the tour module is not installed by the Drupal recipe and is not already installed somehow ask the user if they'd like to install tour as part of applying the Drupal recipe.
- The Drupal recipe is able to override configuration provided by the modules. It is also able to pick out specific configurations from the modules it requires. It does not need to provide all the configuration in its config folder.
- The recipe runner can create content provided by the Drupal recipe. The
recipe runner asks if you want to also install the provided content.
- Prior art: Default content module, Open Social custom code , Umami CSV content creator
- If content is created when a Drupal recipe is installed there needs to be a way to remove it. As part of applying a Drupal recipe, we need to record the content created in state in order to offer the ability to remove it.
- Any optional functionality has to be provided in a module
- New screen that lists available Drupal recipes
- Local Drupal recipes should be listed
- Once Drupal recipes are supported by the project browser, then the project browser should be used on this screen.
- Convert core install profiles (minimal, standard, and Umami) to Drupal recipes.
- Work out what to do with the testing profile - seems like that should be a Drupal recipe too.
- Determine how Drupal recipes listed by the project browser will be curated.
- Licensing requirements to be listed?
- Can curation encourage collaboration over competition?
- What about Drupal recipes that configure functionality to connect with 3rd parties, for example a subscription component?
Things to discuss:¶
- Do Drupal recipes have an installation status? The current opinion is that we should avoid using the word install with respect to a Drupal recipe. A Drupal recipe is something that can be applied against a site.
- Should we have a log of what Drupal recipes have been applied to a site?
Potential uses include:
- Being able to use the applied Drupal recipe suggestions to recommend further steps to take.
- Being able to list Drupal recipes that can be reverted.
- How is this different from an installation status?
- Do we need to borrow the idea of the symfony.lock file from Symfony Flex?
- What happens if a module from a Drupal recipe is already installed on the Drupal site?
- What happens if configuration with the same name already exists.
- How are recipes obtained? Via packagist/drupal.org if composer, a custom recipe server for Flex.
- Can Drupal recipes be sold on markets outside of Drupal.org (similar to the WP
ecosystem)?
- Proposal: Not a question for this initiative.
- Is the composer project template idea just a special form of Drupal recipe?
- How can a Drupal recipe be reverted? I.e. I should be able to apply a Drupal recipe, decide I don't like it and want to try another one, and revert to my previous state (config if not composer) to try the next one. At least before I've created content or configuration was changed. Potential idea: use configuration snapshots. Take a snapshot of configuration prior to applying a Drupal recipe and a store a hash or another snapshot of the configuration after applying. If the current configuration state matches the stored hash then a Drupal recipe revert will involve removing any content created and then importing the previous config snapshot.
- What happens when you re-apply a Drupal recipe that has been applied before?
- What happens when a Drupal recipe fails halfway through application?
- Should we have a post-install message feature? (Borrowing an idea from Symfony flex) How to make them look good in UI and CLI?
- Namespacing? How do we avoid a plethora of Drupal recipes all with very similar names. We need to avoid drupal-recipe/moderated-article, drupal-recipe/better-moderated-article, drupal-recipe/moderated-article-tng
- Where are local Drupal recipes stored? How do we keep them out of the
extension discovery complexities? Ideally this would live outside of settings
and services as these are per site.
- use $ENV_VAR/recipes and core/recipes. Block access to these directories using .htaccess by default. Local discovery works using directory traversal and only looks for Drupal recipe yaml files one directory down as Drupal recipes can not contain other Drupal recipes. The default if $ENV_VAR is not defined is the root directory of the project.
- Can Drupal recipes reference themes/modules outside of Drupal.org?
- As long as the module or theme is present on the file system by the time we come to module install it will work. The composer step will be able to require modules from anywhere composer can. Note this hints at the possibility to extend the composer section with the ability to add repositories.
- The project browser contents meet certain criteria, what criteria will
determine if a Drupal recipe will be available?
- If we're not using composer, how can we determine simply that a Drupal recipe can be applied to a site? Or to put this another way around do we really want to re-invent composer when it comes to getting a list of applicable recipes based on core version, php version and other dependency versions?
- What best practices and instructions should be created to assist people
creating and maintaining Site Recipes?
- See the draft Recipe author guide