component_schema-1.0.x-dev/modules/component_schema_ui_patterns/src/Plugin/Deriver/ComponentSchemaDeriver.php
modules/component_schema_ui_patterns/src/Plugin/Deriver/ComponentSchemaDeriver.php
<?php
namespace Drupal\component_schema_ui_patterns\Plugin\Deriver;
use Drupal\component_schema\Component\TypedComponentManager;
use Drupal\component_schema\Component\Schema\ComponentAttribute;
use Drupal\component_schema\Component\Schema\ComponentMapping;
use Drupal\component_schema\Component\Schema\ComponentVariableAttributeProviderDataDefinition;
use Drupal\component_schema\Component\Schema\ComponentVariableDataDefinitionInterface;
use Drupal\Core\Config\Schema\Mapping;
use Drupal\Core\Config\Schema\Sequence;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\TypedData\Plugin\DataType\BooleanData;
use Drupal\Core\TypedData\Plugin\DataType\StringData;
use Drupal\Core\TypedData\TypedDataManager;
use Drupal\ui_patterns\Plugin\Deriver\AbstractPatternsDeriver;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class ComponentSchemaDeriver.
*/
class ComponentSchemaDeriver extends AbstractPatternsDeriver {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The typed component manager.
*
* @var \Drupal\component_schema\Component\TypedComponentManager
*/
protected $typedComponentManager;
/**
* The component schema wrapper object for the current component object.
*
* @var \Drupal\Core\Config\Schema\Element
*/
protected $schema;
/**
* ComponentSchemaDeriver constructor.
*
* @param string $base_plugin_id
* The base plugin ID.
* @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager
* Typed data manager service.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* Messenger service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\component_schema\Component\TypedComponentManager $typed_component_manager
* The typed component manager.
*/
public function __construct($base_plugin_id, TypedDataManager $typed_data_manager, MessengerInterface $messenger, ModuleHandlerInterface $module_handler, TypedComponentManager $typed_component_manager) {
parent::__construct($base_plugin_id, $typed_data_manager, $messenger);
$this->moduleHandler = $module_handler;
$this->typedComponentManager = $typed_component_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$base_plugin_id,
$container->get('typed_data_manager'),
$container->get('messenger'),
$container->get('module_handler'),
$container->get('component_schema.typed')
);
}
/**
* {@inheritdoc}
*/
public function getPatterns() {
$patterns = [];
$component_type_definitions = $this
->typedComponentManager
->getComponentDefinitions();
foreach (array_keys($component_type_definitions) as $component_type) {
if ($pattern_array = $this->getPatternDefinitionArray($component_type)) {
if ($pattern = $this->getPatternDefinition($pattern_array)) {
$patterns[] = $pattern;
}
}
}
return $patterns;
}
/**
* Returns a pattern definition array based on the given component definition.
*
* @param string $component_type
* The component type machine name.
*
* @return array|FALSE
* A pattern definition array or FALSE if the component type doesn't support
* an admin UI.
*/
protected function getPatternDefinitionArray($component_type) {
$this->schema = $this->typedComponentManager
->getComponentTypeSchema($component_type);
$component_definition = $this->schema->getDataDefinition();
if ($component_definition->takesUi()) {
$pattern_definition = [
// Use the component type machine name as the pattern key.
'id' => $component_type,
];
// base_path and file_name are required properties of a pattern definition.
// In our case they're not particularly relevant. The following call uses`
// the template.
// @todo If continuing to use template file here, parse the relative path.
// @todo Consider switching to using the variables_yml file.
$template_path = $this->schema
->getTypedDataManager()
->getTwigEnvironment()
->load($component_definition->getComponentTemplate())
->getSourceContext()
->getPath();
$pattern_definition['base path'] = dirname($template_path);
$pattern_definition['file name'] = basename($template_path);
// Copy over equivalent properties.
// Keys are source (component schema) methods, values are destination
// (pattern) keys.
$mapping = [
'getLabel' => 'label',
'getDescription' => 'description',
'getLibraries' => 'libraries',
'getProvider' => 'provider',
'getComponentTemplate' => 'use',
];
foreach ($mapping as $source_method => $destination_key) {
if ($source_value = $component_definition->{$source_method}()) {
$pattern_definition[$destination_key] = $source_value;
}
}
// Process variables, which will be parsed as pattern fields or settings.
$elements = $this->schema->getElements();
foreach (array_keys($elements) as $key) {
$this->processVariableItem($pattern_definition, $key);
}
return $pattern_definition;
}
return FALSE;
}
/**
* Parses fields and settings from component variables.
*
* In this method we derive a set of fields and/or settings from the
* component's variables. To do so, we need to convert our nested structure
* into flat arrays.
*
* @param array[] &$pattern_definition
* The pattern definition array.
* @param string $key
* The component variable key.
* @param string[] $ancestor_keys
* The keys of ancestor elements. Used internally.
*/
protected function processVariableItem(array &$pattern_definition, $key, $ancestor_keys = []) {
$element = $this->schema
->get($key);
// @todo Support Sequence elements.
if ($element instanceof Sequence) {
return;
}
$definition = $element->getDataDefinition();
// Iterate through a mapping.
if ($element instanceof Mapping) {
$ancestor_keys[] = $key;
$child_elements = $element->getElements();
foreach (array_keys($child_elements) as $child_key) {
$this->processVariableItem($pattern_definition, $key . '.' . $child_key, $ancestor_keys);
}
return;
}
// Return early if the element doesn't take a UI.
// @todo: move this test before the handling of sequences and mappings.
if (!$definition->takesUi()) {
return;
}
// Construct a key using replacements.
$element_key = $this->getPropertyKey($key);
$item = [];
// Copy over equivalent properties that are used by both fields and
// settings.
// Keys are source (component schema) methods, values are destination
// (field or setting) keys.
$mapping = [
'getLabel' => 'label',
'getDescription' => 'description',
'getPreview' => 'preview',
];
foreach ($mapping as $source_method => $destination_key) {
if ($source_value = $definition->{$source_method}()) {
$item[$destination_key] = $source_value;
}
}
$collection = NULL;
$root = $element->getRoot();
switch ($definition->getUiType()) {
case ComponentVariableDataDefinitionInterface::UI_TYPE_FIELD:
$collection = 'fields';
$item['type'] = 'string';
foreach ($ancestor_keys as $ancestor_key) {
// Prepend child variable labels with the parent label.
$item['label'] = $root->get($ancestor_key)->getDataDefinition()->getLabel() . ' » ' . $item['label'];
}
break;
case ComponentVariableDataDefinitionInterface::UI_TYPE_SETTING:
$collection = 'settings';
if ($element instanceof BooleanData) {
$item['type'] = 'boolean';
}
elseif ($definition instanceof ComponentVariableAttributeProviderDataDefinition) {
if ($options = $definition->getOptions()) {
if ((strpos($key, 'color') !== FALSE) && ($ui_classes = $definition->getUiClasses()) && $this->moduleHandler->moduleExists('colorwidget')) {
$item['type'] = 'colorwidget';
foreach ($options as $option_key => $option_value) {
if (isset($ui_classes[$option_key])) {
$item['options'][$option_key] = $option_value . '/' . $ui_classes[$option_key] . '/css_class';
}
}
}
else {
$item['type'] = 'select';
$item['options'] = $options;
}
}
else {
$item['type'] = 'textfield';
}
}
elseif ($element instanceof ComponentAttribute) {
$item['type'] = 'attributes';
}
// Add wrapping groups for inherited settings. We add them here rather
// than when we're handling mapping or sequence elements because at that
// earlier stage we don't yet know if there will be one or more
// contained settings.
$component_parent = NULL;
if ($component_ancestors = $definition->getInheritedFrom()) {
foreach (array_reverse($component_ancestors) as $component_ancestor) {
$ancestor_type = $this
->typedComponentManager
->getComponentTypeSchema($component_ancestor);
// If not already present, add a group for each ancestor.
$ancestor_key = $component_ancestor . '_component';
if (!isset($pattern_definition[$collection][$ancestor_key])) {
$ancestor_definition = $ancestor_type->getDataDefinition();
$pattern_definition[$collection][$ancestor_key] = [
'type' => 'group',
'label' => $ancestor_definition->getLabel(),
'description' => $ancestor_definition->getDescription(),
'group_type' => 'details',
];
if (!is_null($component_parent)) {
$pattern_definition[$collection][$ancestor_key]['group'] = $component_parent;
}
}
$component_parent = $ancestor_key;
}
// Add the last and therefore innermost group.
$item['group'] = $component_parent;
unset($component_parent);
}
// Nest in groups if there are ancestors.
elseif (!empty($ancestor_keys)) {
$element_grandparent_key = NULL;
foreach ($ancestor_keys as $ancestor_key) {
$element_ancestor_key = $this->getPropertyKey($ancestor_key);
if (!isset($pattern_definition[$collection][$element_ancestor_key])) {
$pattern_definition[$collection][$element_ancestor_key] = [
'type' => 'group',
'label' => $root->get($ancestor_key)->getDataDefinition()->getLabel(),
'group_type' => 'details',
];
// If there was a grandparent element, assign this group to its
// group.
if (!is_null($element_grandparent_key)) {
$pattern_definition[$collection][$element_ancestor_key]['group'] = $element_grandparent_key;
}
}
$element_grandparent_key = $element_ancestor_key;
}
$item['group'] = $element_ancestor_key;
}
break;
}
if ($collection) {
$pattern_definition[$collection][$element_key] = $item;
}
}
/**
* Gets a
*
* @param string $key
* A key potentially containing period characters.
*
* @return string
* A key suitable to be used in a render array.
*/
protected function getPropertyKey($key) {
return str_replace('.', '__', $key);
}
}
