bootstrap_five_layouts-1.0.x-dev/src/BootstrapFiveLayoutsManager.php
src/BootstrapFiveLayoutsManager.php
<?php
namespace Drupal\bootstrap_five_layouts;
use Drupal\bootstrap_five_layouts\Plugin\Layout\BootstrapFiveLayoutsBase;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Drupal\Core\Layout\LayoutPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
/**
* Class BootstrapFiveLayoutsManager.
*/
class BootstrapFiveLayoutsManager extends BootstrapFiveLayoutsPluginManager {
/**
* The layout manager.
*
* @var \Drupal\Core\Layout\LayoutPluginManager
*/
protected LayoutPluginManager $layoutManager;
/**
* The Bootstrap layout update manager.
*
* @var \Drupal\bootstrap_five_layouts\BootstrapFiveLayoutsUpdateManager
*/
protected BootstrapFiveLayoutsUpdateManager $updateManager;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected MessengerInterface $messenger;
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected ContainerInterface $container;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected ConfigFactoryInterface $config_factory;
/**
* Constructs a new \Drupal\bootstrap_five_layouts\BootstrapFiveLayoutsManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme manager used to invoke the alter hook with.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager used to invoke the alter hook with.
* @param \Drupal\Core\Layout\LayoutPluginManager $layout_manager
* The Layout Manager.
* @param \Drupal\bootstrap_five_layouts\BootstrapFiveLayoutsUpdateManager $update_manager
* The Bootstrap Layouts update manager.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(
\Traversable $namespaces,
CacheBackendInterface $cache_backend,
ModuleHandlerInterface $module_handler,
ThemeHandlerInterface $theme_handler,
ThemeManagerInterface $theme_manager,
LayoutPluginManager $layout_manager,
BootstrapFiveLayoutsUpdateManager $update_manager,
MessengerInterface $messenger,
ConfigFactoryInterface $config_factory,
) {
parent::__construct(
$namespaces,
$cache_backend,
$module_handler,
$theme_handler,
$theme_manager
);
$this->layoutManager = $layout_manager;
$this->updateManager = $update_manager;
$this->alterInfo('bootstrap_five_layouts_handler_info');
$this->setCacheBackend($cache_backend, 'bootstrap_five_layouts_handler_info');
$this->messenger = $messenger;
$this->config_factory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('container.namespaces'),
$container->get('cache.discovery'),
$container->get('module_handler'),
$container->get('theme_handler'),
$container->get('theme.manager'),
$container->get('plugin.manager.core.layout'),
$container->get('plugin.manager.bootstrap_five_layouts.update'),
$container->get('messenger'),
$container->get('config.factory')
);
}
/**
* {@inheritdoc}
*/
public function setContainer(ContainerInterface $container): void {
$this->container = $container;
}
/**
* {@inheritdoc}
*/
protected function findDefinitions() {
$definitions = parent::findDefinitions();
// The handler plugin identifiers represent the module or theme that
// implements said layouts. Remove any handler plugins that not installed.
foreach (array_keys($definitions) as $provider) {
if (!$this->providerExists($provider)) {
unset($definitions[$provider]);
}
else {
// Attempt to retrieve the theme human readable label first.
try {
$label = $this->themeHandler->getName($provider);
}
// Otherwise attempt to retrieve the module human readable label.
catch (\Exception $e) {
$label = $this->moduleHandler->getName($provider);
}
$definitions[$provider]['label'] = $label;
}
}
return $definitions;
}
/**
* Retrieves base breakpoint options for Bootstrap layouts as select options.
*
* @return array
* An associative array of breakpoint options to be used in select options.
*/
public function getBreakpointOptions() {
$config = $this->config_factory->get('bootstrap_five_layouts.settings');
return $config->get('responsive_breakpoints') ?: [];
}
/**
* Retrieves primary utility options from configuration.
*
* @return array
* An associative array of primary utility options where each option
* has 'primary' set to true.
*/
public function getPrimaryOptions() {
// Load the utility options from configuration.
$config = $this->config_factory->get('bootstrap_five_layouts.settings');
$utility_options = $this->getUtilityOptions();
// Filter thru and get the primary options.
$primary_options = array_filter($utility_options, function ($option) {
return $option['primary'] === TRUE;
});
foreach ($primary_options as $key => $option) {
// buildOptionsList.
$primary_options[$key]['field']['#options'] = $this->buildOptionsList($key);
$primary_options[$key]['field']['#title'] = $option['field-label'];
$primary_options[$key]['field']['#description'] = $option['field-description'];
}
return $primary_options;
}
/**
* Retrieves utility options from configuration.
*
* @return array
* An associative array of utility options, or an empty array if none are configured.
*/
public function getUtilityOptions() {
// Load the utility options from configuration.
$config = $this->config_factory->get('bootstrap_five_layouts.settings');
return $config->get('utility_options') ?: [];
}
/**
* Groups utility options by their applicable contexts for easier processing.
*
* @return array
* A multi-dimensional array of utilities grouped by context and detail group.
* Each utility includes an '#access' key indicating whether it should be
* accessible based on the LTR setting.
*/
public function getUtilityDivisions() {
// Load the utility options from configuration.
$config = $this->config_factory->get('bootstrap_five_layouts.settings');
$utility_options = $this->getUtilityOptions();
// Group utilities by their applicable contexts for easier processing.
$grouped_utilities = [];
foreach ($utility_options as $key => $utility) {
// Skip items where primary is true (ie: grow/shrink wil be merged in sigle field)
if (isset($utility['primary']) && $utility['primary'] === TRUE) {
continue;
}
$utility['item'] = $key;
$utility['field']['#description'] = $utility['field-description'];
$utility['field']['#description_display'] = 'before';
$utility['field']['#options'] = $this->buildOptionsList($key);
$utility['field']['#type'] = 'select';
$utility['field']['#size'] = 12;
$utility['field']['#multiple'] = TRUE;
$utility['field']['#attributes'] = [
'data-multiselect-enhanced' => 'true',
'data-multiselect-single-group' => 'true',
];
if (isset($utility['applicable']) && is_array($utility['applicable'])) {
foreach ($utility['applicable'] as $context) {
$detail_group = $utility['detail-group'] ?? 'default';
$grouped_utilities[$context][$detail_group][$key] = $utility;
}
}
}
return $grouped_utilities;
}
/**
*
*/
public function getAllKnownClassnames() {
// Build a flat, de-duplicated list of all known class names derived from
// configured utility options (responsive and non-responsive).
$utility_options = $this->getUtilityOptions();
$classnames_map = [];
foreach ($utility_options as $name => $info) {
$options = $this->buildOptionsList($name);
if (empty($options)) {
continue;
}
// $options can be either a flat key=>label map (non-responsive), or a
// grouped array keyed by breakpoint label where each value is a
// key=>label map of classes (responsive). Normalize both cases to a
// flat set of class keys.
foreach ($options as $group_or_class => $values) {
if (is_array($values)) {
foreach ($values as $class => $label) {
$classnames_map[$class] = TRUE;
}
}
else {
// Non-responsive option; key is the class name.
$classnames_map[$group_or_class] = TRUE;
}
}
}
// Return sorted list of unique class names.
$classnames = array_keys($classnames_map);
sort($classnames, SORT_STRING);
return $classnames;
}
/**
* Returns the configured pillbox classes as a normalized unique list.
*
* @return array
* A numerically indexed array of unique class tokens.
*/
public function getPillboxClasses() {
$config = $this->config_factory->get('bootstrap_five_layouts.settings');
$raw = (string) $config->get('pillbox_classes');
if ($raw === '') {
return [];
}
$lines = preg_split('/\r\n|\r|\n/', $raw);
$unique = [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') {
continue;
}
// Support multiple tokens per line defensively.
$tokens = preg_split('/\s+/', $line);
foreach ($tokens as $token) {
$token = trim($token);
if ($token === '') {
continue;
}
$key = mb_strtolower($token);
$unique[$key] = $token;
}
}
return array_values($unique);
}
/**
* Retrieves classes that can be used in Bootstrap layouts as select options.
*
* @return array
* An associative array of grouped classes to be used in select options.
*/
public function getContainerOptions() {
$config = $this->config_factory->get('bootstrap_five_layouts.settings');
$container_options = $config->get('container_options');
// Reminder: 'no-container' is an logic of this module, not an acually class value.
$container_options["no-container"] = $this->t('No Container');
return $container_options;
}
/**
*
*/
public function buildOptionsList($option = '') {
// Generate cache key based on the option and configuration dependencies.
$cache_key = $this->getBuildOptionsListCacheKey($option);
// Check if result is already cached.
$cached = $this->cacheGet($cache_key);
if ($cached !== FALSE) {
return $cached->data;
}
$opts = [];
$utilities = $this->getUtilityOptions();
// Check if utilities has the specified option group.
if (!empty($option) && isset($utilities[$option])) {
$utility = $utilities[$option];
$opts = $this->processUtilityOption($utility);
}
// Cache the result with appropriate tags for invalidation.
$this->cacheSet($cache_key, $opts, CacheBackendInterface::CACHE_PERMANENT, $this->getBuildOptionsListCacheTags());
return $opts;
}
/**
* Processes a utility option and returns formatted options array.
*
* @param array $utility
* The utility option configuration array.
*
* @return array
* The processed options array.
*/
public function processUtilityOption(array $utility) {
$sizes = $this->getBreakpointOptions();
$opts = [];
// Check if this utility option is not responsive.
if (isset($utility['responsive']) && $utility['responsive'] === FALSE) {
// For non-responsive utilities, just return the values array as simple key->value pairs.
foreach ($utility['values'] as $key => $value) {
$class_name = $this->normalizeDashes($utility['class'] . '-' . $key);
// If utility have groupings, then setup 'non-responsive'.
if (isset($utility['grouping']) && is_array($utility['grouping'])) {
foreach ($utility['grouping'] as $optgroup => $grouping) {
foreach ($grouping as $group_item) {
// Handle new structure where each group item can have label and classes array
if (is_array($group_item) && isset($group_item['classes'])) {
$classes = $group_item['classes'];
$label = $group_item['label'];
// Create a combined key from all classes in the array
$combined_key = implode(' ', $classes);
$opts[$optgroup][$combined_key] = $label;
}
// Handle legacy structure where grouping contains direct class strings
elseif (is_string($group_item)) {
if ($group_item === $class_name) {
$opts[$optgroup][$class_name] = $value;
}
}
}
}
}
else {
$opts[$class_name] = $value;
}
}
}
else {
// Use the key field of the 'values' segments instead of hardcoded alignments.
$alignments = array_keys($utility['values']);
foreach ($sizes as $size => $breakpoint) {
foreach ($alignments as $alignment) {
$class = $size !== '' ? "{$utility['class']}-{$size}-{$alignment}" : "{$utility['class']}-{$alignment}";
$class = $this->normalizeDashes($class);
$group = $size === '' ? $this->config_factory->get('bootstrap_five_layouts.settings')->get('xs_label') : $breakpoint['name'];
$opts[$group][$class] = $class;
}
}
}
return $opts;
}
/**
* Generates a cache key for the buildOptionsList method.
*
* @param string $option
* The utility option name.
*
* @return string
* The cache key.
*/
private function getBuildOptionsListCacheKey($option) {
// Include configuration data that affects the result in the cache key.
$config = $this->config_factory->get('bootstrap_five_layouts.settings');
$utility_options = $config->get('utility_options');
$breakpoints = $config->get('responsive_breakpoints');
$xs_label = $config->get('xs_label');
// Create a hash of the relevant configuration to include in cache key.
$config_hash = hash('md5', serialize([
'utility_options' => $utility_options[$option] ?? NULL,
'breakpoints' => $breakpoints,
'xs_label' => $xs_label,
]));
return "bootstrap_five_layouts:build_options_list:{$option}:{$config_hash}";
}
/**
* Gets cache tags for buildOptionsList cache invalidation.
*
* @return array
* Array of cache tags.
*/
private function getBuildOptionsListCacheTags() {
return ['bootstrap_five_layouts_settings'];
}
/**
* Retrieves theme ready classes from 'site theme' (complex issue)
*
* @return array
* An associative array of theme classes to be used in select options.
*/
public function getThemeOptions() {
// Load the configuration for theme_classes.
$config = $this->config_factory->get('bootstrap_five_layouts.settings');
$theme_classes = $config->get('theme_classes');
$theme = [];
$theme[] = $this->t('None/Neutral');
if (!empty($theme_classes)) {
$lines = preg_split('/\r\n|\r|\n/', $theme_classes);
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') {
continue;
}
$parts = explode('|', $line, 2);
if (count($parts) === 2) {
$class = trim($parts[0]);
$label = trim($parts[1]);
$theme[$class] = $label;
}
}
}
return $theme;
}
/**
* Determines if a layout is a Bootstrap layout.
*
* @param string $id
* The layout identifier to test.
*
* @return bool
* TRUE or FALSE
*/
public function isBootstrapLayout($id) {
static $layouts;
if (!isset($layouts)) {
$layouts = [];
foreach (array_keys($this->layoutManager->getDefinitions()) as $layout_id) {
$plugin = $this->layoutManager->createInstance($layout_id);
if ($plugin instanceof BootstrapFiveLayoutsBase) {
$layouts[] = $layout_id;
}
}
}
return in_array($id, $layouts);
}
/**
* Replaces all double dashes with a single dash.
*
* @param string $string
* The input string.
*
* @return string
* The string with double dashes replaced.
*/
protected function normalizeDashes($string) {
$string = str_replace('--', '-', $string);
$string = trim($string, '-');
$string = trim($string);
return $string;
}
/**
* Retrieves all available handler instances.
*
* @return \Drupal\bootstrap_five_layouts\Plugin\BootstrapFiveLayouts\BootstrapFiveLayoutsHandlerInterface[]
*/
public function getHandlers() {
$instances = [];
foreach (array_keys($this->getDefinitions()) as $plugin_id) {
$instances[$plugin_id] = $this->createInstance($plugin_id);
}
return $instances;
}
/**
* Runs update(s) for a specific schema version.
*
* @param int $schema
* The schema version to update.
* @param bool $display_messages
* Flag determining whether a message will be displayed indicating whether
* the layout was processed successfully or not.
*/
public function update($schema, $display_messages = TRUE) {
$handlers = $this->getHandlers();
$data = [];
foreach ($this->updateManager->getUpdates($schema) as $update) {
// See if there's an adjoining YML file with the update plugin.
$r = new \ReflectionClass($update);
$data_paths = [dirname($r->getFileName()), $update->getPath()];
// Merge in any update data.
foreach ($data_paths as $path) {
$file = "$path/bootstrap_five_layouts.update.$schema.yml";
if (file_exists($file) && ($yaml = Yaml::decode(file_get_contents($file)))) {
$data = NestedArray::mergeDeep($data, $yaml);
}
}
// Perform the update.
$update->update($this, $data, $display_messages);
// Process any existing layouts after the update.
foreach ($handlers as $handler_id => $handler) {
foreach ($handler->loadInstances() as $storage_id => $layout) {
$update->processExistingLayout($layout, $data, $display_messages);
// Determine if the layout has changed and then save it.
if ($layout->hasChanged()) {
try {
$handler->saveInstance($storage_id, $layout);
if ($display_messages) {
$message = $this->t('Successfully updated the existing Bootstrap layout found in "@id".', ['@id' => $storage_id]);
$this->messenger->addStatus($message);
}
}
catch (\Exception $exception) {
$message = $this->t('Unable to update the existing Bootstrap layout found in "@id":', ['@id' => $storage_id]);
$this->messenger->addError($message);
}
}
}
}
}
}
}
