Problem/Motivation

In #3301370: Model core's standard install profile as recipes @nedjo wrote

It also does something that could potentially be modelled as a config action - sets the site email address as a recipient of the feedback contact form. To get a sense of how recipes might work, a useful exercise would be to look at different models of how the standard install profile might be divided up into recipes.

This brings up an interesting thought - how can we ask the user for information that the recipe can use while be applied. Things like site email address are one possible example but also say you are configuring a payment gateway - there are API keys and the like that need to be provided (thanks @mdencev).

Proposed resolution

Allow recipes to define inputs, and then use those inputs as poor-man's-token replacements in the config actions.

Recipes should be able to define inputs like this, providing different UIs with all the information they need to properly collect a value:

# Each item in `input` is an "input definition". This must be an associative array of arrays.
input:
    # The name of the input. In config actions, the token `${recipient}` will be replaced by the inputted value.
    recipient:
        # Required: a primitive data type, known to Typed Data.
        data_type: email
        # Required: a human-readable description, which can be used in different ways by various UIs.
        description: 'The email address of the person who will receive submissions from the contact form.'
        # Optional validation constraints, in exactly the same way as they're done in config schema.
        constraints:
            Email: []
        # `prompt` is optional, and things in it are only used when collecting input at the
        prompt:
            # The name of a method of Symfony Console's StyleInterface which can return a value.
            method: ask
            arguments:
                # Arguments to pass to the method, keyed by parameter name.
                question: 'Tell me who should get website feedback.'
        # Required: a default value, if the input cannot be collected from the user.
        default:
            # Required: can be `config` or `value`.
            source: config
            # The `config` member is a two-element indexed array, with a config name and a property path, used when `source` is `config`.
            config: ['system.site', 'mail']
            # Used if `source` is `value`, can be anything.
            value: Anything

The recipe can then use the input values in config actions, like so:

config:
  actions:
    contact.form.feedback:
      setRecipients:
        - ${recipient}

I propose one hard limitation up front: inputs cannot be used for dynamic IDs (like "enter the name of your content editor role"). Why? Because that makes recipes unpredictable, which would in turn make them less composable, which would violate a foundational design goal. Recipes are automatons, not functions. Inputs can only be used in the parameters passed to config actions.

(Having just written all that, I could see a way to maybe pull off dynamic IDs without breaking composability, but it would need further thought and hashing out, and we could always add it later. It's definitely out of this issue's scope.)

To help recipe consumers, we'll also add a new recipe:info console command, which prints the recipe's description, and lists every input defined by the recipe and its dependencies, along with their descriptions, in a nice table.

Remaining tasks

Implement it with test coverage.

API changes

Recipes will have some new syntax, but this does not introduce any changes to our data model or PHP API because the recipe system is experimental.

Issue fork drupal-3303126

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

alexpott created an issue. See original summary.

aangel’s picture

I would find this useful for my recipe. I'm setting up a demo module and it needs a unique email address and password that the user must provide or the demo won't be completely set up (some tests will work but those involving emails won't).
https://github.com/Performant-Labs/atk_demo/blob/main/recipe.yml

aangel’s picture

How about this?

1. Ask for input before work begins so the recipe can be cancelled with no side effects.
2. Running recipe with -y skips asking questions.
3. Control-C cancels.
4. Define questions with:

input:
  email_address:
    - text: Enter email address
    - variable: variable_name
    - config:
      - default: email@null.com
      - sanitize: no

Which provides:
Enter email address ([email@null.com]):

The colon after the input text is provided by Recipes.

5. Input is stored as a variable in the form ${{ input.variable_name }}
6. Blank is allowed in which case the default is used.
7. Run variable_name through checks unless sanitize:no is specified.
8. Straight substitution occurs in actions with ${{ input.variable_name }}.
9. Provide input values on the command line with --input variable_name:.
10. Also provide input values via a file with --input-file .

Plus an additional section that provides tokens that the input above would override:

arguments:
  email_address:
    - variable: variable_name
    - config:
      - default: email@null.com

Maybe they can be merged.

aangel’s picture

We could eliminate:

- variable: variable_name

And make the variable name the name of the section after input:.

Is there any reason to have the extra flexibility?

wim leers’s picture

The examples here so far are about unique identifiers: e-mail addresses, API keys, URLs, etc.

But there's a different pattern that also requires input: what if there's a recipe that requires some media type/node type/text editor/…, and you may want to apply it to 1 or more such config entity types? For example: adding a field, adding a view mode, adding a filter that references a field, adding a filter that references a view mode, et cetera.

phenaproxima’s picture

Issue summary: View changes

We ran into this limitation in #3417835-13: Convert the Standard install profile into a set of recipes.

In that case, we needed a recipe that creates a contact form to do one of two things:

  • Ask the user for an email address
  • Copy the system.site:mail config value into another piece of config

Both of these approaches are ways of collecting input, so I think they fall under the scope of this issue. I'm updating the issue summary to also mention this particular use case.

phenaproxima’s picture

I was thinking about this and I had this idea for how we could define this stuff in recipes.

I propose we add a new section, input, to recipe.yml. In this example, the recipe is creating a contact form. It asks the user for a recipient email address, with a default value coming from config:

name: 'Website feedback contact form'
description: 'Provides a website feedback contact form.'
type: 'Contact form'
install:
  - contact
input:
  default_recipient:
    from: config
    config: 'system.site:mail'
  recipient:
    from: prompt
    question: 'Enter an e-mail address which should receive form submissions.'
    default: '%default_recipient'
config:
  import:
    contact:
      - contact.settings
  actions:
    contact.form.feedback:
      addRecipient: '%recipient'

(Imagine for a sec that contact forms have a addRecipient action.)

Some of my thoughts here:

  • Every item in input defines a named variable, which can then be reused in the config:actions section.
  • Each named variable can come from either config, or a prompt.
  • If a variable can come from a prompt, then it can have a default value. The default value may be a named variable previously defined by the same recipe.
  • It's not possible to skip an input prompt conditionally. If you define an input prompt in your recipe, it WILL be presented to the user.
  • Input can be provided non-interactively at the command line: --input recipient=sisko@ds9.space

Thoughts on this idea?

wim leers’s picture

Issue tags: +blocker

Per #6, this blocks #3417835: Convert the Standard install profile into a set of recipes.

#7: I like your proposal! Thoughts:

  1. For usability, it's important that all prompts can be collected for a recipe stack (aka a recipe that depends on other recipes, etc.), so that they can all be presented to the user upfront.
  2. … which also means that some of these may need to be asked in a particular sequence? Or does that not matter? 🤔
  3. For automation, it's important that all prompts can be collected for a recipe stack so that they can be listed, to allow somebody who's automating spinning up a new Drupal site using a recipe to specify all inputs as command line arguments.
  4. We must think about collisions on the config key/property path. I think --input-recipient is guaranteed to run into collisions. I think --input--website-feedback-contact-form--recipient=sisko@ds9.space is more verbose but also more reliable. Verbosity is less important in this context, predictability matters more.
phenaproxima’s picture

Category: Task » Feature request
Priority: Normal » Major
Issue tags: -blocker

This doesn't block #3417835: Convert the Standard install profile into a set of recipes; untagging. Sorry if my comment wasn't clear. We ran into the problem, but can work around it as described in #3417835-14: Convert the Standard install profile into a set of recipes.

That said, I'd guess this does block the recipe system being "finished" and fully usable by authors. So, escalating it.

phenaproxima’s picture

Responding to the points in #8:

For usability, it's important that all prompts can be collected for a recipe stack (aka a recipe that depends on other recipes, etc.), so that they can all be presented to the user upfront.

Agreed.

… which also means that some of these may need to be asked in a particular sequence? Or does that not matter? 🤔

I think it might make sense for input definitions to have a weight field so that ordering can be tweaked if needed. Otherwise they should probably just be presented in the order that they are defined (inputs from dependencies come first, in their defined order; followed by the inputs from the recipe the user has asked to apply, in their defined order). All inputs in the "stack" would ordered by weight.

For automation, it's important that all prompts can be collected for a recipe stack so that they can be listed, to allow somebody who's automating spinning up a new Drupal site using a recipe to specify all inputs as command line arguments.

That's a good point.

We must think about collisions on the config key/property path. I think --input-recipient is guaranteed to run into collisions. I think --input--website-feedback-contact-form--recipient=sisko@ds9.space is more verbose but also more reliable. Verbosity is less important in this context, predictability matters more.

Yeah, I concur. Collisions are inevitable. Maybe the syntax could be something like --input RECIPE_NAME.VARIABLE=foo.

ltrain’s picture

I notice that Drush GenerateCommands uses the Symfony Console component to get input from the user for use in the commands. Maybe it could be used here?

njim’s picture

I foresee some challenges with the current state of recipes that this sort of feature may address. In particular, there may be a need to map site-specific values to the configuration in the recipe. A few examples:

1. Avoid bloat when installing multiple recipes: For example, imagine a small business that is creating a site and wants to include a blog recipe, an event calendar recipe, an image gallery recipe, and a survey recipe. Individually, each one of these recipes may install functionally redundant configuration. A 'blog manager' role, an 'events calendar admin' role, an 'image gallery creator' role, a 'survey creator' role, etc. While it is valid for a user to have multiple roles, this can grow unnecessarily large. I can imagine a command line (or mapping file) that would prompt for things like the name of your content creator role.

2. Overcoming barriers with installing recipes on established sites: For this example, I'm a large, enterprise website that's been using Drupal for many years. I want to use a recipe that turns event content into different calendar feeds (rss, ical, and maybe some JSON standard.) I already have an event content type with named fields, so I don't want to create anything new in my data architecture. I just want to tell the recipe how to map the module's configuration to my content type. Typically we may create a layer of abstraction such as a module settings form or custom plugin. But in this case, I'm just interested in passing a few values such as the name of my content type to the script that is installing the recipe.

Just documenting as food for thought. There are multiple approaches to these issues.

wim leers’s picture

#12

  1. That is mostly a problem of making granular enough recipes. You're right though that this implies a recipe should ASK for input rather than creating a role specifically for its purpose. Which means that this issue is IMHO even more important than Major. To avoid "Drupal sites created by Recipes" to become incredibly complicated in its config (because every independent concern installs its own roles, node types, etc.), asking for input is critical.
    In the case of user roles, for example, it's probably necessary to allow the site builder using a recipe to choose to EITHER use an existing role OR create a new one.
  2. Agreed. But isn't that what this issue is about already? There is IMHO at least one missing piece to make that feasible though — see below.

There are multiple approaches to these issues.

Interesting … can you elaborate? Because AFAICT we need to support all of these use cases to make Recipes practically usable? 😄


I think that we need a way to allow a user to make a choice once, and to have multiple recipes follow that instruction. Lots of recipes would need to know the "content creator role", the "content moderator role", the "administrator role", et cetera. Hence to prevent spamming the user, we need a reliable mechanism to ask this question once, and for dependent recipes to respect that.

I think the clearest path forward (and I suspect the most reliable) is to use the one piece of information we already have and that is strongly related: recipe dependencies.

Example

Say a recipe BAR depends on a recipe FOO. Recipe FOO has an input for "content creator role". Because BAR depends on FOO, it knows that FOO already needs to know the ID of the content creator role. So FOO should still specify

  content_creator_role_id:
    from: prompt
    question: 'Pick the role that represents content creators'

but then BAR should NOT specify the same; instead it should specify:

  content_creator_role_id:
    from: dependency
    dependency: FOO.content_creator_role_id
phenaproxima’s picture

#13: I like the idea of being able to explicitly use input values from a recipe you depend on.

thejimbirch’s picture

Version: 10.0.x-dev » 11.x-dev
thejimbirch’s picture

Issue tags: +Starshot blocker
phenaproxima’s picture

Assigned: Unassigned » phenaproxima

Self-assigning to start implementing this.

phenaproxima’s picture

Title: Getting user input during / before applying a recipe » Getting user input before applying a recipe

phenaproxima’s picture

Assigned: phenaproxima » Unassigned
Status: Active » Needs review

Got an initial implementation up with some basic test coverage.

This only allows getting input from an existing config value (we can add prompting and such in another issue). It supports reusing inputs from recipes you depend on.

narendraR made their first commit to this issue’s fork.

thejimbirch’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new110.11 KB
new248.15 KB
new124.6 KB

I tested this successfully.

I had previously applied the standard recipe which included the feedback_contact_form recipe.

Screenshot of feedback form with default value

The Contact form needs to be deleted to re-apply.

I then changed the site email as this is the value that is being used in the recipe to set the contact form.

Changing the email

I then re-apply the feedback_contact_form and see that the changes email address is not the recipient.

changed email

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

I added some review comments to the MR. I agree with doing this on fork because I think until we have the ability to actually get user input we should not solidify the API by adding to core.

The issue title here is wrong because we're not getting user input here at all.

phenaproxima’s picture

Title: Getting user input before applying a recipe » Allow recipes to reuse existing config values
phenaproxima’s picture

Status: Needs work » Needs review
phenaproxima’s picture

Assigned: Unassigned » phenaproxima
Status: Needs review » Needs work
phenaproxima’s picture

Assigned: phenaproxima » Unassigned
Status: Needs work » Needs review
phenaproxima’s picture

Issue summary: View changes
phenaproxima’s picture

Issue summary: View changes
phenaproxima’s picture

Issue summary: View changes
phenaproxima’s picture

Issue summary: View changes
akhil babu’s picture

Status: Needs review » Needs work

I tested this in my local and it works perfectly. However no errors or warnigs were shown for invalid configurations like

input:
  recipient:
    source: config
    config: ['system.invalid_config', 'mail']

Should we add a check to verify if the config exists or not?

phenaproxima’s picture

phenaproxima’s picture

Status: Needs work » Needs review
b_sharpe’s picture

Everything looks great and resolved from previous review. Added one comment above about invalid sources. Leaving as NR to let others comment, but I feel this could be an easy fix/win. Current fatal is as such for reference:

In ServiceLocator.php line 137:
                                                                                                                                                    
  Service "json" not found: the container inside "Drupal\Core\Recipe\InputCollector" is a smaller service locator that only knows about the "config" service.                                                                                                                                                        
phenaproxima’s picture

I like that idea, @b_sharpe. Fixed! Also made some changes to accommodate what's coming in #3466374: Getting user input before applying a recipe.

b_sharpe’s picture

Status: Needs review » Reviewed & tested by the community

All looks great, tests pass and expected results tested locally, RTBC

longwave’s picture

Status: Reviewed & tested by the community » Needs review

Added some review comments/feedback, happy for all of it to be pushed back on but just some thoughts that came to mind while reviewing.

phenaproxima’s picture

Title: Allow recipes to reuse existing config values » Make it possible for recipes to prompt for input values
Issue tags: +Needs issue summary update
phenaproxima’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update
phenaproxima’s picture

phenaproxima’s picture

Project: Recipes Initiative » Drupal core
Component: Code » recipe system

@alexpott and I agreed that this can probably be committed directly to core, since its scope has been condensed to this one issue. Moving the issue, and will open a new MR.

phenaproxima’s picture

Issue summary: View changes
phenaproxima’s picture

Issue summary: View changes
phenaproxima’s picture

StatusFileSize
new242.55 KB

Gave this a quick manual test and it worked exactly the way you'd expect it to in interactive mode. Screenshot:

A screenshot of the terminal output from manually testing this feature.

thejimbirch’s picture

Issue tags: +Recipes initiative
alexpott’s picture

Status: Needs review » Needs work
Issue tags: +Needs change record

I've reviewed this work quite a few times now and I think it is in a great place and it ready. It puts us in a great position to support getting input via forms for recipe install via the UI and we've got tests for using the CLI. Great work @phenaproxima.

I think we need to document this capability in the recipe docs however we've still not landed the docs we wrote for recipes for the original core patch so it is hard to know what to do. I think the minimum we should be doing here is adding a CR based on the issue summary (while making sure that it is up-to-date). Once the CR has been created I think this is ready for RTBC.

phenaproxima’s picture

Status: Needs work » Needs review
Issue tags: -Needs change record +Needs issue summary update

CR written: https://www.drupal.org/node/3470507. Tagging for a final issue summary update.

phenaproxima’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

The issue summary is fully up to date!

thejimbirch’s picture

Status: Needs review » Reviewed & tested by the community

Marking as rtbc

moshe weitzman’s picture

Would be helpful if someone tried this MR with Drush recipe command - https://www.drush.org/13.x/commands/recipe/. Drush just includes the symfony command from Drupal as is so hopefully it works fine. Testing would be to make sure recipe options appear in command help and can be used successfully.

alexpott’s picture

Updating contribution credit. Crediting all those who have had input on the issue.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 1384a2e and pushed to 11.x. Thanks!

  • alexpott committed 1384a2e1 on 11.x
    Issue #3303126 by phenaproxima, narendrar, thejimbirch, alexpott, aangel...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

gábor hojtsy’s picture

Adding to highlights

zaporylie’s picture

Re:

I propose one hard limitation up front: inputs cannot be used for dynamic IDs (like "enter the name of your content editor role"). Why? Because that makes recipes unpredictable, which would in turn make them less composable, which would violate a foundational design goal. Recipes are automatons, not functions. Inputs can only be used in the parameters passed to config actions.

(Having just written all that, I could see a way to maybe pull off dynamic IDs without breaking composability, but it would need further thought and hashing out, and we could always add it later. It's definitely out of this issue's scope.)

Is there an issue where dynamic IDs as being actively discussed? For all Commerce projects, we'd need to require the currency input from users applying the recipe. Currencies are config entities and the currency code is featured in the config ID. My understanding is that creating a Currency entity programmatically based on the user input would not be possible with the current state of things until support for dynamic IDs is added which is effectively blocking Commerce-based recipes.

zaporylie’s picture

It seems like my understanding was wrong :) We are still able to define a custom config action plugin (currencyImporter) that utilizes a currency importer service for dynamically defining currency config entity. The only non-clean part is the signature of the ConfigActionPluginInterface::apply(string $configName, mixed $value): void; that requires $configName which in my case would be something like commerce_price.commerce_currency.XYZ so it doesn't do much and feels hacky. Otherwise, it seems like I can achieve what I am aiming for.