Skip to content
Snippets Groups Projects
Commit c1a1aa01 authored by Pierre Dureau's avatar Pierre Dureau
Browse files

Issue #3413527 by pdureau: Make our plugin manager ready SDC in Core

parent c01ec2f9
Branches
Tags
No related merge requests found
Showing
with 222 additions and 282 deletions
......@@ -4,8 +4,8 @@ declare(strict_types = 1);
namespace Drupal\ui_patterns_legacy;
use Drupal\Core\Render\Component\Exception\InvalidComponentDataException;
use Drupal\sdc\Component\ComponentValidator;
use Drupal\sdc\Exception\InvalidComponentDataException;
/**
* Component converter.
......@@ -34,6 +34,9 @@ class ComponentConverter {
$target = $this->addProperty('description', $source, 'description', $target);
$target = $this->addProperty('category', $source, 'group', $target);
$target = $this->addProperty('links', $source, 'links', $target);
$target = $this->addProperty('use', $source, '_template', $target);
$target = $this->addProperty('icon_map', $source, 'icon_map', $target);
$target = $this->addProperty('icon_path', $source, 'icon_path', $target);
if (\array_key_exists('variants', $source)) {
$target['variants'] = $this->getVariants($source);
}
......
......@@ -53,7 +53,7 @@ class PatternPreview extends Pattern {
$slots = $story["slots"] ?? [];
$props = $story["props"] ?? [];
$slots = array_merge($element["#slots"], $slots);
$element["#slots"] = $manager::processStoriesSlots($slots);
$element["#slots"] = self::processStoriesSlots($slots);
$element["#props"] = array_merge($element["#props"], $props);
return $element;
}
......
......@@ -50,7 +50,7 @@ class ComponentStory extends ComponentElement {
}
$story = $component["stories"][$story_id];
$slots = array_merge($element["#slots"] ?? [], $story["slots"] ?? []);
$element["#slots"] = $manager::processStoriesSlots($slots);
$element["#slots"] = self::processStoriesSlots($slots);
$element["#props"] = array_merge($element["#props"] ?? [], $story["props"] ?? []);
return $element;
}
......
<?php
declare(strict_types = 1);
namespace Drupal\ui_patterns;
use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\sdc\Component\ComponentValidator;
use Drupal\sdc\Component\SchemaCompatibilityChecker;
use Drupal\sdc\ComponentNegotiator;
use Drupal\sdc\ComponentPluginManager as SdcPluginManager;
use Drupal\ui_patterns\SchemaManager\ReferencesResolver;
/**
* UI Patterns extension of SDC component plugin manager.
*/
class ComponentPluginManager extends SdcPluginManager implements CategorizingPluginManagerInterface {
/**
* Cache key prefix to use in the cache backend.
*/
const CACHE_KEY = 'ui_patterns';
/**
* {@inheritdoc}
*/
public function __construct(
ModuleHandlerInterface $module_handler,
ThemeHandlerInterface $themeHandler,
CacheBackendInterface $cacheBackend,
ConfigFactoryInterface $configFactory,
ThemeManagerInterface $themeManager,
ComponentNegotiator $componentNegotiator,
FileSystemInterface $fileSystem,
SchemaCompatibilityChecker $compatibilityChecker,
ComponentValidator $componentValidator,
string $appRoot,
protected PropTypePluginManager $propTypePluginManager,
protected ReferencesResolver $referencesSolver,
) {
parent::__construct(
$module_handler,
$themeHandler,
$cacheBackend,
$configFactory,
$themeManager,
$componentNegotiator,
$fileSystem,
$compatibilityChecker,
$componentValidator,
$appRoot
);
$this->setCacheBackend($cacheBackend, self::CACHE_KEY);
}
/**
* {@inheritdoc}
*/
protected function alterDefinitions(&$definitions) {
parent::alterDefinitions($definitions);
foreach ($definitions as $component_id => $definition) {
$definition = $this->annotateProps($definition);
$definition = $this->resolveTemplatePath($definition);
$definitions[$component_id] = $definition;
}
}
/**
* Annotate each prop in a component definition.
*
* This is the main purpose of overriding SDC component plugin manager.
* We add a 'ui_patterns' object in each prop schema of the definition.
*/
protected function annotateProps(array $definition): array {
if (!isset($definition['props'])) {
return $definition;
}
if (!isset($definition['props']['properties'])) {
return $definition;
}
foreach ($definition['props']['properties'] as $prop_id => $prop) {
$prop_type = $this->propTypePluginManager->getPropTypePlugin($prop);
if (isset($prop['$ref']) && str_starts_with($prop['$ref'], "ui-patterns://")) {
// Resolve prop schema here, because:
// - Drupal\sdc\Component\ComponentValidator::getClassProps() is
// executed before schema references are resolved, so SDC believe
// a reference is a PHP namespace.
// - It is not possible to propose a patch to SDC because
// SchemaStorage::resolveRefSchema() is not recursively resolving
// the schemas anyway.
$prop = $this->referencesSolver->resolve($prop);
}
$prop['ui_patterns']['type_definition'] = $prop_type;
$prop['ui_patterns']["summary"] = $prop_type->getSummary($prop);
$definition['props']['properties'][$prop_id] = $prop;
}
return $definition;
}
/**
* Resolve template path.
*
* SDC is not allowing to have a custom template property because it is always
* overriding its value. So we use _template instead.
* See: https://www.drupal.org/project/drupal/issues/3390717
*/
protected function resolveTemplatePath(array $definition): array {
if (!isset($definition["_template"])) {
return $definition;
}
$definition["template"] = $definition["_template"];
unset($definition["_template"]);
if (str_starts_with($definition["template"], "@")) {
// @todo resolve namespace.
}
return $definition;
}
/**
* {@inheritdoc}
*/
public function getCategories() {
// Fetch all categories from definitions and remove duplicates.
$categories = array_unique(array_values(array_map(function ($definition) {
return $definition['group'];
}, $this->getDefinitions())));
natcasesort($categories);
return $categories;
}
/**
* {@inheritdoc}
*/
public function getSortedDefinitions(array $definitions = NULL) {
// Sort the plugins first by category, then by label.
$definitions = $definitions ?? $this->getDefinitions();
uasort($definitions, function ($a, $b) use ($label_key) {
if ((string) $a['group'] != (string) $b['group']) {
return strnatcasecmp($a['group'], $b['group']);
}
return strnatcasecmp($a[$label_key], $b[$label_key]);
});
return $definitions;
}
/**
* {@inheritdoc}
*/
public function getGroupedDefinitions(?array $definitions = NULL): array {
$definitions = $definitions ?: $this->getDefinitions();
$groups = [];
foreach ($definitions as $id => $definition) {
$group = $definition["group"] ?? "Other";
$groups[$group][$id] = $definition;
}
return $groups;
}
}
......@@ -5,8 +5,8 @@ declare(strict_types = 1);
namespace Drupal\ui_patterns\Element;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Render\Element\ComponentElement as SdcComponentElement;
use Drupal\Core\Render\RenderableInterface;
use Drupal\sdc\Element\ComponentElement as SdcComponentElement;
/**
* Empty for now. Act as a proxy between SDC element and our additions.
......@@ -79,4 +79,38 @@ class ComponentElement extends SdcComponentElement {
return $slot;
}
/**
* Process stories slots.
*
* Stories slots have no "#" prefix in render arrays. Let's add them.
* A bit like UI Patterns 1.x's PatternPreview::getPreviewMarkup()
* This method belongs here because used by both ui_patterns_library and
* ui_patterns_legacy.
*/
public static function processStoriesSlots(array $slots): array {
foreach ($slots as $slot_id => $slot) {
if (!is_array($slot)) {
continue;
}
if (array_is_list($slot)) {
$slots[$slot_id] = self::processStoriesSlots($slot);
}
$slot_keys = array_keys($slot);
$render_keys = ["theme", "type", "markup", "plain_text"];
if (count(array_intersect($slot_keys, $render_keys)) > 0) {
foreach ($slot as $key => $value) {
if (is_array($value)) {
$value = self::processStoriesSlots($value);
}
if (str_starts_with($key, "#")) {
continue;
}
$slots[$slot_id]["#" . $key] = $value;
unset($slots[$slot_id][$key]);
}
}
}
return $slots;
}
}
<?php
declare(strict_types = 1);
namespace Drupal\ui_patterns\Sdc;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\sdc\Component\ComponentValidator;
use Drupal\sdc\Component\SchemaCompatibilityChecker;
use Drupal\sdc\ComponentNegotiator;
use Drupal\sdc\ComponentPluginManager;
use Drupal\sdc\Plugin\Component;
use Drupal\ui_patterns\PropTypePluginManager;
use Drupal\ui_patterns\SourcePluginManager;
/**
* Plugin Manager for *.ui_patterns.yml configuration files.
*
* Plugin Manager overwrites getDiscovery() to provide a decorated
* Discovery. Decoration of the service seems not possible for me.
* Probably there is more gentle way.
*
* @see plugin_api
*
* @internal
*/
abstract class ComponentPluginManagerDecorator extends ComponentPluginManager {
/**
*
*/
public function __construct(
protected ComponentPluginManager $parentSdcPluginManager,
protected PropTypePluginManager $propTypePluginManager,
protected SourcePluginManager $sourcePluginManager,
ModuleHandlerInterface $module_handler,
ThemeHandlerInterface $themeHandler,
CacheBackendInterface $cacheBackend,
ConfigFactoryInterface $configFactory,
ThemeManagerInterface $themeManager,
ComponentNegotiator $componentNegotiator,
FileSystemInterface $fileSystem,
SchemaCompatibilityChecker $compatibilityChecker,
ComponentValidator $componentValidator,
string $appRoot,
) {
parent::__construct(
$module_handler,
$themeHandler,
$cacheBackend,
$configFactory,
$themeManager,
$componentNegotiator,
$fileSystem,
$compatibilityChecker,
$componentValidator,
$appRoot
);
$this->setCacheBackend($cacheBackend, $this->getCacheKey());
}
/**
*
*/
public function createInstance($plugin_id, array $configuration = []): Component {
if (parent::hasDefinition($plugin_id)) {
return parent::createInstance($plugin_id, $configuration);
}
else {
return $this->parentSdcPluginManager->createInstance($plugin_id, $configuration);
}
}
/**
* Returns the cache key for the decorated service.
*
* @return string
* The cache key.
*/
abstract protected function getCacheKey();
/**
* {@inheritdoc}
*/
public function find(string $component_id): Component {
if (parent::hasDefinition($component_id)) {
return parent::find($component_id);
}
return $this->parentSdcPluginManager->find($component_id);
}
/**
* {@inheritdoc}
*/
public function getAllComponents(): array {
$original_components = $this->parentSdcPluginManager->getAllComponents();
return array_merge($original_components, parent::getAllComponents());
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
$decorated_definitions = parent::getDefinitions();
$original_definitions = $this->parentSdcPluginManager->getDefinitions();
return array_merge($original_definitions, $decorated_definitions);
}
/**
* {@inheritdoc}
*/
public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
$original_definition = parent::getDefinition($plugin_id, FALSE);
return $original_definition ?? $this->parentSdcPluginManager->getDefinition($plugin_id, $exception_on_invalid);
}
}
<?php
declare(strict_types = 1);
namespace Drupal\ui_patterns\Sdc;
/**
* Plugin Manager for....
*
* @see plugin_api
*
* @internal
*/
class UiPatternsSdcPluginManager extends ComponentPluginManagerDecorator {
/**
* {@inheritdoc}
*/
protected function getCacheKey() {
return 'ui_patterns';
}
/**
* {@inheritdoc}
*/
protected function alterDefinitions(&$definitions) {
parent::alterDefinitions($definitions);
// @todo injection.
$resolver = \Drupal::service('ui_patterns.schema_reference_solver');
foreach ($definitions as $component_id => $definition) {
if (!isset($definition['props'])) {
continue;
}
if (!isset($definition['props']['properties'])) {
continue;
}
foreach ($definition['props']['properties'] as $prop_id => $prop) {
$prop = $this->replacePhpNamespace($prop);
$prop_type = $this->propTypePluginManager->getPropTypePlugin($prop);
if (isset($prop['$ref']) && str_starts_with($prop['$ref'], "ui-patterns://")) {
// Resolve prop schema here, because:
// - Drupal\sdc\Component\ComponentValidator::getClassProps() is
// executed before schema references are resolved, so SDC believe
// a reference is a PHP namespace.
// - It is not possible to propose a patch to SDC because
// SchemaStorage::resolveRefSchema() is not recursively resolving
// the schemas anyway.
$prop = $resolver->resolve($prop);
}
$prop['ui_patterns']['type_definition'] = $prop_type;
$prop['ui_patterns']["summary"] = $prop_type->getSummary($prop);
$definition['props']['properties'][$prop_id] = $prop;
}
$definitions[$component_id] = $definition;
}
}
/**
* Replace PHP namespace in schema's type.
*
* SDC allows namespaced PHP classes as prop types. This is not compliant with
* JSON schema, and props using this are ignored by SDC during JSON schema
* validation. So let replace them.
*/
protected function replacePhpNamespace(array $schema): array {
if (!isset($schema['type'])) {
return $schema;
}
if (!is_string($schema['type'])) {
return $schema;
}
$mappings = [
'Drupal\Core\Template\Attribute' => "ui-patterns://attributes",
'\Drupal\Core\Template\Attribute' => "ui-patterns://attributes",
];
foreach ($mappings as $namespace => $ref) {
if ($schema['type'] == $namespace) {
unset($schema['type']);
$schema['$ref'] = $ref;
return $schema;
}
}
return $schema;
}
/**
* {@inheritdoc}
*/
public function getGroupedDefinitions(?array $definitions = NULL): array {
$definitions = $definitions ?: $this->getDefinitions();
$groups = [];
foreach ($definitions as $id => $definition) {
$group = $definition["group"] ?? "Other";
$groups[$group][$id] = $definition;
}
return $groups;
}
/**
* Process stories slots.
*
* Stories slots have no "#" prefix in render arrays. Let's add them.
* A bit like UI Patterns 1.x's PatternPreview::getPreviewMarkup()
* This method belongs here because used by both ui_patterns_library and
* ui_patterns_legacy.
*/
public static function processStoriesSlots(array $slots): array {
foreach ($slots as $slot_id => $slot) {
if (!is_array($slot)) {
continue;
}
if (array_is_list($slot)) {
$slots[$slot_id] = self::processStoriesSlots($slot);
}
$slot_keys = array_keys($slot);
$render_keys = ["theme", "type", "markup", "plain_text"];
if (count(array_intersect($slot_keys, $render_keys)) > 0) {
foreach ($slot as $key => $value) {
if (is_array($value)) {
$value = self::processStoriesSlots($value);
}
if (str_starts_with($key, "#")) {
continue;
}
$slots[$slot_id]["#" . $key] = $value;
unset($slots[$slot_id][$key]);
}
}
}
return $slots;
}
}
......@@ -4,7 +4,7 @@ description: "For actions in forms, dialogs, and more with support for multiple
links:
- "https://getbootstrap.com/docs/5.3/components/buttons/"
group: "Button"
template: pattern-button.html.twig
_template: ../../components/button/pattern-button.html.twig
variants:
default:
title: "Default"
......
......@@ -3,7 +3,7 @@ name: "Progress"
description: "The progress element displays an indicator showing the completion progress of a task, typically in the form of a bar. Progress components are built with two HTML elements, some CSS to set the width, and a few attributes. Bootstrap does not use the HTML5 <progress> element, ensuring you can stack progress bars, animate them, and place text labels over them."
links:
- "https://getbootstrap.com/docs/5.3/components/progress/"
template: path/to/template/progress.twig
_template: path/to/template/progress.twig
variants:
default:
title: "Default"
......
services:
# Plugins managers.
plugin.manager.ui_patterns_prop_type:
class: Drupal\ui_patterns\PropTypePluginManager
arguments:
- "@container.namespaces"
- "@cache.discovery"
- "@module_handler"
- '@Drupal\sdc\Component\SchemaCompatibilityChecker'
plugin.manager.ui_patterns_source:
class: Drupal\ui_patterns\SourcePluginManager
parent: default_plugin_manager
# SDC extension.
ui_patterns.plugin.manager.sdc:
class: Drupal\ui_patterns\Sdc\UiPatternsSdcPluginManager
decorates: plugin.manager.sdc
decoration_priority: 9
public: false
plugin.manager.sdc:
class: Drupal\ui_patterns\ComponentPluginManager
arguments:
- "@ui_patterns.plugin.manager.sdc.inner"
- "@plugin.manager.ui_patterns_prop_type"
- "@plugin.manager.ui_patterns_source"
- "@module_handler"
- "@theme_handler"
- "@cache.discovery"
......@@ -31,6 +13,18 @@ services:
- '@Drupal\sdc\Component\SchemaCompatibilityChecker'
- '@Drupal\sdc\Component\ComponentValidator'
- "%app.root%"
- "@plugin.manager.ui_patterns_prop_type"
- "@ui_patterns.schema_reference_solver"
plugin.manager.ui_patterns_prop_type:
class: Drupal\ui_patterns\PropTypePluginManager
arguments:
- "@container.namespaces"
- "@cache.discovery"
- "@module_handler"
- '@Drupal\sdc\Component\SchemaCompatibilityChecker'
plugin.manager.ui_patterns_source:
class: Drupal\ui_patterns\SourcePluginManager
parent: default_plugin_manager
# Builders
ui_patterns.component_element_builder:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment