navigation_extra-1.0.x-dev/src/NavigationExtraPluginBase.php
src/NavigationExtraPluginBase.php
<?php
declare(strict_types=1);
namespace Drupal\navigation_extra;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
/**
* Provides a basic deriver with caching capabilities.
*/
abstract class NavigationExtraPluginBase extends PluginBase implements NavigationExtraPluginInterface, ContainerFactoryPluginInterface {
use StringTranslationTrait;
/**
* The entire list of links.
*
* @var array
*/
protected array $links;
/**
* The collections for this plugin.
*
* @var array
*/
protected array $collections;
/**
* The items this plugin is handling.
*
* @var array
*/
protected array $items;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected LanguageManagerInterface $languageManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected AccountProxyInterface $currentUser;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected RouteProviderInterface $routeProvider;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected ModuleHandlerInterface $moduleHandler;
/**
* The admin toolbar config settings.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected ImmutableConfig $config;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected EntityRepositoryInterface $entityRepository;
/**
* Constructs a \Drupal\Component\Plugin\PluginBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
* The language manager.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
*/
public function __construct(
array $configuration,
string $plugin_id,
mixed $plugin_definition,
LanguageManagerInterface $languageManager,
AccountProxyInterface $current_user,
EntityTypeManagerInterface $entity_type_manager,
RouteProviderInterface $route_provider,
ModuleHandlerInterface $module_handler,
ConfigFactoryInterface $config_factory,
EntityRepositoryInterface $entity_repository,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->languageManager = $languageManager;
$this->currentUser = $current_user;
$this->entityTypeManager = $entity_type_manager;
$this->routeProvider = $route_provider;
$this->moduleHandler = $module_handler;
$this->config = $config_factory->get('navigation_extra.settings');
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('language_manager'),
$container->get('current_user'),
$container->get('entity_type.manager'),
$container->get('router.route_provider'),
$container->get('module_handler'),
$container->get('config.factory'),
$container->get('entity.repository'),
);
}
/**
* {@inheritdoc}
*/
public function buildConfigForm(array &$form, FormStateInterface $form_state): array {
$elements = [];
$elements['enabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable'),
'#description' => $this->getPluginDefinition()['description'] ?? $this->t('Enable the %plugin plugin.', [
'%plugin' => $this->getPluginDefinition()['name'],
]),
'#default_value' => $this->config->get("plugins." . $this->getPluginId() . ".enabled") ?? FALSE,
];
$weight = $this->getPluginDefinition()['weight'] ?? 0;
$elements['weight'] = [
'#type' => 'number',
'#title' => $this->t('Weight'),
'#description' => $this->t('Menu weight of the %plugin plugin. Set to 0 for default behavior.', ['%plugin' => $this->getPluginDefinition()['name']]),
'#default_value' => $this->config->get("plugins." . $this->getPluginId() . ".weight") ?? $weight,
];
return $elements;
}
/**
* Add config fields to a plugin config form when it is using create items.
*
* @param array $form
* The complete config form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* An array of elements for configuring the create items.
*/
protected function buildConfigFormCreateItems(array &$form, FormStateInterface $form_state): array {
$elements = [];
$elements['create_items'] = [
'#type' => 'details',
'#title' => $this->t('Create items'),
];
$elements['create_items']['hide_from_navigation_create'] = [
'#type' => 'checkbox',
'#title' => $this->t("Hide from create new content menu"),
'#description' => $this->t("Hide from create new content menu."),
'#default_value' => $this->config->get("plugins.{$this->getPluginId()}.create_items.hide_from_navigation_create") ?? 0,
];
$elements['create_items']['show_create_new_links'] = [
'#type' => 'checkbox',
'#title' => $this->t("Show create new content items"),
'#description' => $this->t("Show create new content items in content menu."),
'#default_value' => $this->config->get("plugins.{$this->getPluginId()}.create_items.show_create_new_links") ?? 0,
];
$elements['create_items']['navigation_create_collections'] = [
'#type' => 'checkbox',
'#title' => $this->t("Use collections in create new content"),
'#description' => $this->t("Use collections hierarchy in the top level create content menu item."),
'#default_value' => $this->config->get("plugins.{$this->getPluginId()}.create_items.navigation_create_collections") ?? 0,
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function submitConfigForm(array &$form, FormStateInterface $form_state): void {
// Base class does nothing.
}
/**
* {@inheritdoc}
*/
public function preAlterDiscoveredMenuLinks(array &$links): void {
// Base class does nothing.
}
/**
* {@inheritdoc}
*/
public function alterDiscoveredMenuLinks(array &$links): void {
// Base class does nothing.
}
/**
* {@inheritdoc}
*/
public function postAlterDiscoveredMenuLinks(array &$links): void {
// Base class does nothing.
}
/**
* {@inheritdoc}
*/
public function needsMenuLinkRebuild(EntityInterface $entity): bool {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function preprocessMenu(array &$variables): void {
$this->filterMenuItems($variables['items']);
}
/**
* {@inheritdoc}
*/
public function pageAttachments(array &$page): void {
// Base class does nothing.
}
/**
* Ensure a route exists and add the link.
*
* @param string $link_name
* The name of the link being added.
* @param array $link
* The link array, as defined in hook_menu_links_discovered_alter().
* @param array $links
* The existing array of links.
*/
protected function addLink(string $link_name, array $link, array &$links): void {
try {
// Ensure the route exists (there is no separate "exists" method).
if (isset($link['route_name'])) {
$this->routeProvider->getRouteByName($link['route_name']);
}
// By default add everything to the content menu.
$links[$link_name] = $link + [
'menu_name' => 'content',
'provider' => 'navigation_extra',
];
}
catch (RouteNotFoundException) {
// The module isn't installed, or the route (such as provided by a view)
// has been deleted.
}
}
/**
* Remove the top level create content links.
*
* @param string $link_name
* The name of the link to be removed.
* @param array $links
* The array of links being altered.
*/
public function removeLink(string $link_name, array &$links): void {
unset($links[$link_name]);
// Also remove any links that have set admin/content as their parent link.
// They are unsupported by the Navigation module.
foreach ($links as $link_name => $link) {
if (isset($link['parent']) && $link['parent'] === $link_name) {
unset($links[$link_name]);
}
}
}
/**
* Hides links from admin menu, if user doesn't have access rights.
*/
protected function filterMenuItems(array &$items, &$parent = NULL): void {
foreach ($items as $menu_id => &$item) {
if (!empty($item['below'])) {
// Recursively call this function for the child items.
$this->filterMenuItems($item['below'], $item);
}
if ($this->filterMenuItem($item, $parent)) {
unset($items[$menu_id]);
}
}
}
/**
* Determines if a menu item needs to be filtered out or not.
*
* The base class will filter out collection items with no children.
*
* @param array $item
* The item to check for filtering.
* @param array|null $parent
* The parent item of the given $item.
*
* @return bool
* True if the items needs to be filtered out, false if we can leave it in.
*/
protected function filterMenuItem(array &$item, ?array &$parent = NULL): bool {
if (empty($item['below']) && ($this->config->get('common.hide_empty_collections') ?? FALSE)) {
$attributes = $item['url']->getOption('attributes') ?? [];
if (in_array('navigation-extra--collection', $attributes['class'])) {
return TRUE;
}
}
return FALSE;
}
/**
* Helper to determine if a route exists.
*
* @param string $route_name
* The name of the route to check.
*
* @return bool
* True if the route exists or not.
*/
protected function isRouteAvailable(string $route_name): bool {
return (count($this->routeProvider->getRoutesByNames([$route_name])) === 1);
}
/**
* {@inheritdoc}
*/
public function isEnabled(): bool {
// Check if plugin is enabled in config.
$enabled = (bool) $this->config->get("plugins." . $this->getPluginId() . '.enabled') ?? FALSE;
if ($enabled) {
// If the plugin deals with entities, make sure the type it handles
// exists.
$definition = $this->getPluginDefinition();
$entity_type = $definition['entity_type'] ?? FALSE;
if ($entity_type) {
try {
// Try to fetch the storage for the entity type.
$this->entityTypeManager->getStorage($entity_type);
}
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
return FALSE;
}
}
}
return $enabled;
}
/**
* Get the collection an item belongs to.
*
* @param mixed $item
* The item.
* @param array|null $collections
* The collections.
*
* @return array
* The collections for the item.
*/
protected function getItemCollection(mixed $item, ?array $collections = NULL): array {
$found = [];
if (!isset($collections)) {
$collections = $this->getCollections();
}
foreach ($collections as $collection) {
if (in_array($item->id(), $collection['items'] ?? [])) {
$found = $collection;
}
else {
$found = $this->getItemCollection($item, $collection['collections'] ?? []);
}
if (!empty($found)) {
break;
}
}
return $found;
}
/**
* Get the collection structure for this plugin and stores it into the class.
*
* @return array
* A flattened array of collections to process for this plugin.
*/
protected function getCollections(): array {
if (empty($this->collections)) {
// $collections = $this->moduleHandler->invokeAll(
// 'navigation_extra_collections'
// );
$this->collections = $this->moduleHandler->invokeAll('navigation_extra_collections');
foreach ($this->collections as &$collection) {
// Sort collections so grouping will use alphabetic order.
asort($collection);
$this->prepCollection($collection);
}
}
return $this->collections[$this->getPluginId()] ?? [];
}
/**
* Preps the collections array, so it can be used for creating items.
*
* @param array $collections
* The collections.
*/
protected function prepCollection(array &$collections): void {
// Get the starting weight, or 0 if no grouping for collections is set.
$group_collections_weight = [
'bottom' => 100,
'top' => -100,
][$this->config->get('common.group_collections')] ?? 0;
foreach ($collections as $id => &$collection) {
// The id represents the hierarchy of the collections, so add the parent.
$collection['id'] = $id;
// Use group collections weight if group collections is enabled.
if (!empty($group_collections_weight) && empty($collection['weight'])) {
$collection['weight'] = $group_collections_weight;
$group_collections_weight++;
}
if (isset($collection['collections'])) {
asort($collection['collections']);
$this->prepCollection($collection['collections']);
}
}
}
/**
* Get an array of items to create item links for.
*
* @return array
* The items as array.
*/
protected function getItems($entity_type): array {
if (empty($this->items[$entity_type])) {
try {
$this->items[$entity_type] = $this->entityTypeManager
->getStorage($entity_type)
->loadMultiple();
}
catch (InvalidPluginDefinitionException | PluginNotFoundException) {
return [];
}
}
return $this->items[$entity_type];
}
/**
* Creates the collection links menu items tree.
*
* Example:
*
* $links['navigation.content.collection_a']
* $links['navigation.content.collection_b']
*
* @param string $parent
* The parent link ID, used to nest child collections.
* @param callable $link_callback
* Callback providing additional default route configuration
* for the collection links.
* @param array $links
* The links generated so far.
* @param array|null $collections
* The collections to be processed.
*
* @see \Drupal\navigation_extra\NavigationExtraPluginBase::getCollections()
*/
protected function addCollectionLinks(
string $parent,
callable $link_callback,
array &$links,
?array $collections = NULL,
): void {
if (!isset($collections)) {
$collections = $this->getCollections();
}
foreach ($collections as $collection) {
$link_name = "$parent.{$collection['id']}";
$link = $link_callback($collection) + [
'title' => $collection['label'],
'route_parameters' => [
'collection' => $collection['id'],
],
'parent' => $parent,
'weight' => $collection['weight'] ?? 0,
'options' => [
'attributes' => [
'class' => [
'navigation-extra--collection',
'navigation-extra--collection--' . $this->getPluginId(),
'navigation-extra--collection--' . str_replace('.', '--', $collection['id']),
],
],
],
];
$this->addLink($link_name, $link, $links);
if (!empty($collection['collections'])) {
$this->addCollectionLinks($link_name, $link_callback, $links, $collection['collections']);
}
}
}
/**
* Creates the item links for a collection.
*
* @param string $parent
* The parent link ID, used to nest child collections.
* @param string $entity_type
* The kind of entity items we need to process.
* @param callable $link_callback
* Callback providing additional default route configuration
* for the item links.
* @param array $links
* The links generated so far.
*/
protected function addItemLinks(
string $parent,
string $entity_type,
callable $link_callback,
array &$links,
): void {
$items = $this->getItems($entity_type);
foreach ($items as $item) {
$collection = $this->getItemCollection($item);
$item_parent = rtrim("$parent." . ($collection['id'] ?? ''), '.');
$link_name = "$item_parent.{$item->id()}";
$link = $link_callback($item) + [
'class' => '\Drupal\navigation_extra\Plugin\Menu\TranslatedMenuLink',
'title' => $item->label(),
'parent' => $item_parent,
'weight' => $collection['weight'] ?? 0,
'options' => [
'attributes' => [
'class' => [
'navigation-extra--collection--item',
'navigation-extra--collection--item--' . $item->id(),
],
],
],
'metadata' => [
'entity_id' => $item->id(),
'entity_type' => $entity_type,
],
];
$this->addLink($link_name, $link, $links);
}
}
/**
* Creates the add new item links for a collection item.
*
* @param string $parent
* The parent link menu to add the item links.
* @param string $entity_type
* The kind of entity items we need to process.
* @param callable $link_callback
* Callback providing additional default route configuration
* for the item add links.
* @param array $links
* The links generated so far.
*/
protected function addCreateNewItemLinks(
string $parent,
string $entity_type,
callable $link_callback,
array &$links,
): void {
$items = $this->getItems($entity_type);
foreach ($items as $item) {
$collection = $this->getItemCollection($item);
$item_parent = rtrim("$parent." . ($collection['id'] ?? ''), '.');
$link_name = "$item_parent.{$item->id()}.add";
$link = $link_callback($item) + [
'title' => $this->t("Add new"),
'parent' => "$item_parent.{$item->id()}",
'options' => [
'attributes' => [
'class' => [
'navigation-extra--collection--item--add',
'navigation-extra--collection--item--add--' . $item->id(),
],
],
],
];
$this->addLink($link_name, $link, $links);
}
}
/**
* Adds create entity links to the menu structure.
*
* This method adds links for creating new entities to the specified
* collection or parent link in the navigation menu. If the configuration
* specifies that the links should be hidden, it removes them instead.
*
* @param string $parent_link_name
* The name of the parent link where the new links will be added.
* @param string $collection_route_name
* The route name of the collection where the links are added.
* @param string $add_route_name
* The route name for adding new entities.
* @param string $entity_type
* The type of the entity being linked.
* @param array &$links
* The array of links being altered.
*/
protected function addCreateEntityLinks(string $parent_link_name, string $collection_route_name, string $add_route_name, string $entity_type, array &$links): void {
$hide_from_navigation_create = $this->config->get("plugins.{$this->getPluginId()}.create_items.hide_from_navigation_create") ?? 0;
if ($hide_from_navigation_create) {
// Remove the items from navigation.create.
$items = $this->getItems($entity_type);
foreach ($items as $item) {
$this->removeLink("navigation.content.{$entity_type}.{$item->id()}", $links);
}
}
else {
// Use collections in the create new content menu item.
$navigation_create_collections = $this->config->get("plugins.{$this->getPluginId()}.create_items.navigation_create_collections") ?? 0;
if ($navigation_create_collections) {
// Remove the items from navigation.create.
$items = $this->getItems($entity_type);
foreach ($items as $item) {
$this->removeLink("navigation.content.{$entity_type}.{$item->id()}", $links);
}
// Add collections to navigation.create.
$this->addCollectionLinks(
'navigation.create',
fn($collection) => ([
'route_name' => $collection_route_name,
'route_parameters' => [
'collection' => $collection['id'],
],
]),
$links
);
// Re-create items to navigation.create.
$this->addItemLinks(
'navigation.create',
$entity_type,
fn($item) => ([
'route_name' => $add_route_name,
'route_parameters' => [
$entity_type => $item->id(),
],
]),
$links
);
}
}
$show_create_new_links = $this->config->get("plugins.{$this->getPluginId()}.create_items.show_create_new_links") ?? 0;
if ($show_create_new_links) {
$this->addCreateNewItemLinks(
$parent_link_name,
$entity_type,
fn($item) => ([
'route_name' => $add_route_name,
'route_parameters' => [
$entity_type => $item->id(),
],
]),
$links
);
}
}
}
