component_schema-1.0.x-dev/src/ComponentProcessor.php
src/ComponentProcessor.php
<?php
namespace Drupal\component_schema;
use Drupal\component_schema\Component\Schema\TypedComponentVariableInterface;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Discovery\YamlDiscovery;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Provides the available components based on yml files.
*
* To define components you can use a $name.component_schema.yml file. This
* file defines machine names, human-readable names, and optionally
* descriptions for each component type. The machine names are the canonical
* way to refer to components.
*/
class ComponentProcessor {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The string translation service.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $stringTranslation;
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* The YAML discovery classes to find all .component_schema.yml files.
*
* @var \Drupal\Core\Discovery\YamlDiscovery[]
*/
protected $yamlDiscovery = [];
/**
* Constructs a new ComponentProcessor.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation.
*/
public function __construct(ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, TranslationInterface $string_translation) {
$this->moduleHandler = $module_handler;
$this->themeHandler = $theme_handler;
$this->stringTranslation = $string_translation;
}
/**
* Gets the YAML discovery.
*
* @param string $provider_type
* The type of extension.
*
* @return \Drupal\Core\Discovery\YamlDiscovery
* The YAML discovery.
*/
protected function getYamlDiscovery($provider_type) {
if (!isset($this->yamlDiscovery[$provider_type])) {
switch ($provider_type) {
case 'module':
$directories = $this->moduleHandler->getModuleDirectories();
break;
case 'theme':
$directories = $this->themeHandler->getThemeDirectories();
break;
}
$this->yamlDiscovery[$provider_type] = new YamlDiscovery('component_schema', $directories);
}
return $this->yamlDiscovery[$provider_type];
}
/**
* {@inheritdoc}
*/
public function getComponents() {
$components = $this->buildComponentsYaml();
uasort($components, function ($a, $b) {
return strnatcasecmp($a['label'], $b['label']);
});
return $components;
}
/**
* Builds all components provided by .components.yml files.
*
* @return array[]
* Each return component is an array with the following keys:
* - label: The label (title) of the component.
* - description: The description of the component, defaults to NULL.
* - group: The group of the component.
* - provider: The name of the module or theme that provides the
* component.
* - provider_type: The type (module or theme) of the extension that
* provides the component.
* - mapping: An array of variables.
* - settings: An array of settings.
*/
protected function buildComponentsYaml() {
$all_components = [];
foreach (['module', 'theme'] as $provider_type) {
foreach ($this->getYamlDiscovery($provider_type)->findAll() as $provider => $data) {
$components = $data['components'];
switch ($provider_type) {
case 'module':
$extension = $this->moduleHandler->getModule($provider);
break;
case 'theme':
$extension = $this->themeHandler->getTheme($provider);
break;
}
foreach ($components as &$component) {
assert(
isset($component['label']) &&
isset($component['description']) &&
isset($component['group']) &&
isset($component['variables_yml'])
, 'Component has required keys: label, description, group, and variables_yml.');
$component['type'] = 'type_mapping';
$component['provider'] = $provider;
$component['provider_type'] = $provider_type;
// Derive an AllowedValues constraint from the options.
if (!empty($component['options'])) {
assert(is_array($variable['options']), 'If not empty, the options key is set to an array.');
$component['constraints']['AllowedValues'] = array_keys($component['options']);
}
$file = $extension->getPath() . '/' . $component['variables_yml'];
$variables = Yaml::decode(file_get_contents($file)) ?: [];
$this->processVariables($variables);
$component['mapping'] = $variables;
// Merge global settings with any specific to the component.
$component['settings'] = NestedArray::mergeDeep($data['settings'], $component['settings'] ?? []);
}
$all_components += $components;
}
}
foreach ($all_components as $key => &$component) {
if (!empty($component['mapping'])) {
$this->processComponentTypeVariables($component['mapping'], $all_components);
}
}
return $all_components;
}
/**
* Processes variables.
*
* Recurses through mappings and sequences.
*
* @param array &$variables
* An array of variables.
*/
protected function processVariables(&$variables) {
foreach ($variables as $name => &$variable) {
$this->processVariable($variable, $name);
}
}
/**
* Processes a variable.
*
* Translates the variable's label property and recurses through mappings and
* sequences.
*
* @param array &$variable
* A single variable.
* @param string $name
* The machine name of the variable.
*/
protected function processVariable(&$variable, $name = NULL) {
// The component_type key is processed in a subsequent pass.
if (!empty($variable['component_type'])) {
return;
}
// Validate
assert(isset($variable['label']) && isset($variable['type']), 'Variable has required keys: label, type.');
if (!empty($variable['provides_attribute'])) {
assert(isset($variable['provided_name']), 'A value is set for provided_name when provides_attribute is true');
}
switch ($variable['type']) {
// Validate.
case 'boolean_attribute_provider':
if (!empty($variable['provides_class'])) {
assert(isset($variable['value']), 'A value is set for boolean_attribute_provider variables when provides_class is true');
}
if (!empty($variable['provides_attribute'])) {
assert(isset($variable['value']), 'A value is set for boolean_attribute_provider variables when provides_attribute is true');
}
break;
// Recurse through any child variables.
case 'mapping':
assert(isset($variable['mapping']) && is_array($variable['mapping']), 'Value for mapping is an array.');
$this->processVariables($variable['mapping']);
break;
case 'sequence':
assert(isset($variable['sequence']), 'Variable of type sequence has a sequence key');
assert(is_array($variable['sequence']), 'Value for sequence is an array.');
$this->processVariable($variable['sequence']);
break;
}
}
/**
* Processes component type variables.
*
* @param array &$variables
* An array of variables.
* @param array $components
* An array of components.
*/
protected function processComponentTypeVariables(&$variables, $components) {
foreach ($variables as $name => &$variable) {
$this->processComponentTypeVariable($variable, $components);
}
}
/**
* Processes a component type variable.
*
* @param array &$variable
* A single variable.
* @param array $components
* An array of components.
*/
protected function processComponentTypeVariable(&$variable, $components) {
if (!empty($variable['component_type'])) {
// If the component type provides variables, embed them.
if (!empty($components[$variable['component_type']]['mapping'])) {
// Allow overriding of the component.
$variable = NestedArray::mergeDeep($components[$variable['component_type']], $variable);
}
return;
}
// Recurse through any child variables.
switch ($variable['type']) {
case 'mapping':
$this->processComponentTypeVariables($variable['mapping'], $components);
break;
case 'sequence':
$this->processComponentTypeVariable($variable['sequence'], $components);
break;
}
}
}
