function Recipe::parse

Same name and namespace in other branches
  1. 10 core/lib/Drupal/Core/Recipe/Recipe.php \Drupal\Core\Recipe\Recipe::parse()

Parses and validates a recipe.yml file.

Parameters

string $file: The path of a recipe.yml file.

Return value

mixed[] The parsed and validated data from the file.

Throws

\Drupal\Core\Recipe\RecipeFileException Thrown if the recipe.yml file is unreadable, invalid, or cannot be validated.

1 call to Recipe::parse()
Recipe::createFromDirectory in core/lib/Drupal/Core/Recipe/Recipe.php
Creates a recipe object from the provided path.

File

core/lib/Drupal/Core/Recipe/Recipe.php, line 113

Class

Recipe
@internal This API is experimental.

Namespace

Drupal\Core\Recipe

Code

private static function parse(string $file) : array {
  if (!file_exists($file)) {
    throw new RecipeFileException($file, "There is no {$file} file");
  }
  $recipe_contents = file_get_contents($file);
  if (!$recipe_contents) {
    throw new RecipeFileException($file, "{$file} does not exist or could not be read.");
  }
  // Certain parts of our validation need to be able to scan for other
  // recipes.
  // @see ::validateRecipeExists()
  // @see ::validateConfigActions()
  $include_path = dirname($file, 2);
  $constraints = new Collection([
    'name' => new Required([
      new Type('string'),
      new NotBlank(),
      // Matching `type: label` in core.data_types.schema.yml.
new RegexConstraint(pattern: '/([^\\PC])/u', message: 'Recipe names cannot span multiple lines or contain control characters.', match: FALSE),
    ]),
    'description' => new Optional([
      new NotBlank(),
      // Matching `type: text` in core.data_types.schema.yml.
new RegexConstraint(pattern: '/([^\\PC\\x09\\x0a\\x0d])/u', message: 'The recipe description cannot contain control characters, only visible characters.', match: FALSE),
    ]),
    'type' => new Optional([
      new Type('string'),
      new NotBlank(),
      // Matching `type: label` in core.data_types.schema.yml.
new RegexConstraint(pattern: '/([^\\PC])/u', message: 'Recipe type cannot span multiple lines or contain control characters.', match: FALSE),
    ]),
    'recipes' => new Optional([
      new All([
        new Type('string'),
        new NotBlank(),
        // If recipe depends on itself, ::validateRecipeExists() will set off
        // an infinite loop. We can avoid that by skipping that validation if
        // the recipe depends on itself, which is what Sequentially does.
new Sequentially([
          new NotIdenticalTo(value: basename(dirname($file)), message: 'The {{ compared_value }} recipe cannot depend on itself.'),
          new Callback(callback: self::validateRecipeExists(...), payload: $include_path),
        ]),
      ]),
    ]),
    // @todo https://www.drupal.org/i/3424603 Validate the corresponding
    //   import.
'install' => new Optional([
      new All([
        new Type('string'),
        new Sequentially([
          new NotBlank(),
          new Callback(self::validateExtensionIsAvailable(...)),
        ]),
      ]),
    ]),
    'input' => new Optional([
      new Type('associative_array'),
      new All([
        new Collection(fields: [
          // Every input definition must have a description.
'description' => [
            new Type('string'),
            new NotBlank(),
          ],
          // There can be an optional set of constraints, which is an
          // associative array of arrays, as in config schema.
'constraints' => new Optional([
            new Type('associative_array'),
          ]),
          'data_type' => [
            // The data type must be known to the typed data system.
\Drupal::service('validation.constraint')->createInstance('PluginExists', [
              'manager' => 'typed_data_manager',
              // Only primitives are supported because it's not always clear
              // how to collect, validate, and cast complex structures.
'interface' => PrimitiveInterface::class,
            ]),
          ],
          // The `prompt` and `form` elements, though optional, have their
          // own sets of constraints,
'prompt' => new Optional([
            new Collection([
              'method' => [
                new Choice([
                  'ask',
                  'askHidden',
                  'confirm',
                  'choice',
                ]),
              ],
              'arguments' => new Optional([
                new Type('associative_array'),
              ]),
            ]),
          ]),
          'form' => new Optional([
            new Sequentially([
              new Type('associative_array'),
              // Every element in the `form` array has to be a form API
              // property, prefixed with `#`. Because recipe inputs can only
              // be primitive data types, child elements aren't allowed.
new Callback(function (array $element, ExecutionContextInterface $context) : void {
                if (Element::children($element)) {
                  $context->addViolation('Form elements for recipe inputs cannot have child elements.');
                }
              }),
            ]),
          ]),
          // Every input must define a default value.
'default' => new Required([
            new Collection([
              'source' => new Required([
                new Choice([
                  'value',
                  'config',
                  'env',
                ]),
              ]),
              'value' => new Optional(),
              'config' => new Optional([
                new Sequentially([
                  new Type('list'),
                  new Count(2),
                  new All([
                    new Type('string'),
                    new NotBlank(),
                  ]),
                ]),
              ]),
              'env' => new Optional([
                new Type('string'),
                new NotBlank(),
              ]),
            ]),
            new Callback(self::validateDefaultValueDefinition(...)),
          ]),
        ]),
      ]),
    ]),
    'config' => new Optional([
      new Collection([
        // Each entry in the `import` list can either be `*` (import all of
        // the extension's config), or a list of config names to import from
        // the extension.
        // @todo https://www.drupal.org/i/3439716 Validate config file name,
        //   if given.
'import' => new Optional([
          new All([
            new AtLeastOneOf([
              new IdenticalTo('*'),
              new All([
                new Type('string'),
                new NotBlank(),
                new Regex('/^.+\\./'),
              ]),
            ]),
          ]),
        ]),
        'strict' => new Optional([
          new AtLeastOneOf([
            new Type('boolean'),
            new All([
              new Type('string'),
              new NotBlank(),
              new Regex('/^.+\\./'),
            ]),
          ], message: 'This value must be a boolean, or a list of config names.', includeInternalMessages: FALSE),
        ]),
        'actions' => new Optional([
          new All([
            new Type('array'),
            new NotBlank(),
            new Callback(callback: self::validateConfigActions(...), payload: $include_path),
          ]),
        ]),
      ]),
    ]),
    'content' => new Optional([
      new Type('array'),
    ]),
    'extra' => new Optional([
      new Sequentially([
        new Type('associative_array'),
        new Callback(self::validateKeysAreValidExtensionNames(...)),
      ]),
    ]),
  ]);
  $recipe_data = Yaml::decode($recipe_contents);
  /** @var \Symfony\Component\Validator\ConstraintViolationList $violations */
  $violations = Validation::createValidator()->validate($recipe_data, $constraints);
  if (count($violations) > 0) {
    throw RecipeFileException::fromViolationList($file, $violations);
  }
  $recipe_data += [
    'description' => '',
    'type' => '',
    'recipes' => [],
    'install' => [],
    'config' => [],
    'content' => [],
  ];
  return $recipe_data;
}

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.