summaryrefslogtreecommitdiffstatshomepage
path: root/core/modules/help/src/HelpTopicPluginManager.php
blob: 12aef2382e339cf9d68cbb0879bcc65e398cdd11 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
<?php

namespace Drupal\help;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;

/**
 * Provides the default help_topic manager.
 *
 * Modules and themes can provide help topics in .html.twig files called
 * provider.name_of_topic.html.twig inside the module or theme sub-directory
 * help_topics. The provider is validated to be the extension that provides the
 * help topic.
 *
 * The Twig file must contain YAML front matter with a key named 'label'. It can
 * also contain keys named 'top_level' and 'related'. For example:
 * @code
 * ---
 * label: 'Configuring error responses, including 403/404 pages'
 *
 * # Related help topics in an array.
 * related:
 *   - core.config_basic
 *   - core.maintenance
 *
 * # If the value is true then the help topic will appear on admin/help.
 * top_level: true
 * ---
 * @endcode
 *
 * In addition, modules wishing to add plugins can define them in a
 * module_name.help_topics.yml file, with the plugin ID as the heading for
 * each entry, and these properties:
 * - id: The plugin ID.
 * - class: The name of your plugin class, implementing
 *   \Drupal\help\HelpTopicPluginInterface.
 * - top_level: TRUE if the topic is top-level.
 * - related: Array of IDs of topics this one is related to.
 * - Additional properties that your plugin class needs, such as 'label'.
 *
 * You can also provide an entry that designates a plugin deriver class in your
 * help_topics.yml file, with a heading giving a prefix ID for your group of
 * derived plugins, and a 'deriver' property giving the name of a class
 * implementing \Drupal\Component\Plugin\Derivative\DeriverInterface. Example:
 * @code
 * my_module_prefix:
 *   deriver: 'Drupal\my_module\Plugin\Deriver\HelpTopicDeriver'
 * @endcode
 *
 * @ingroup help_docs
 *
 * @see \Drupal\help\HelpTopicDiscovery
 * @see \Drupal\help\HelpTopicTwig
 * @see \Drupal\help\HelpTopicTwigLoader
 * @see \Drupal\help\HelpTopicPluginInterface
 * @see \Drupal\help\HelpTopicPluginBase
 * @see hook_help_topics_info_alter()
 * @see plugin_api
 * @see \Drupal\Component\Plugin\Derivative\DeriverInterface
 */
class HelpTopicPluginManager extends DefaultPluginManager implements HelpTopicPluginManagerInterface {

  /**
   * Provides default values for all help topic plugins.
   *
   * @var array
   */
  protected $defaults = [
    // The plugin ID.
    'id' => '',
    // The title of the help topic plugin.
    'label' => '',
    // Whether or not the topic should appear on the help topics list.
    'top_level' => '',
    // List of related topic machine names.
    'related' => [],
    // The class used to instantiate the plugin.
    'class' => '',
  ];

  /**
   * Constructs a new HelpTopicManager object.
   *
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\Extension\ThemeHandlerInterface $themeHandler
   *   The theme handler.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
   *   Cache backend instance to use.
   * @param string $root
   *   The app root.
   */
  public function __construct(ModuleHandlerInterface $module_handler, protected ThemeHandlerInterface $themeHandler, CacheBackendInterface $cache_backend, protected string $root) {
    // Note that the parent construct is not called because this class does not use
    // annotated class discovery.
    $this->moduleHandler = $module_handler;
    $this->alterInfo('help_topics_info');
    $this->setCacheBackend($cache_backend, 'help_topics');
  }

  /**
   * {@inheritdoc}
   */
  protected function getDiscovery() {
    if (!isset($this->discovery)) {
      $module_directories = $this->moduleHandler->getModuleDirectories();
      $all_directories = array_merge(
        ['core' => $this->root . '/core'],
        $module_directories,
        $this->themeHandler->getThemeDirectories()
      );

      // Search for Twig help topics in subdirectory help_topics, under
      // modules/profiles, themes, and the core directory.
      $all_directories = array_map(function ($dir) {
        return [$dir . '/help_topics'];
      }, $all_directories);
      $discovery = new HelpTopicDiscovery($all_directories);

      // Also allow modules/profiles to extend help topic discovery to their
      // own plugins and derivers, in my_module.help_topics.yml files.
      $discovery = new YamlDiscoveryDecorator($discovery, 'help_topics', $module_directories);
      $discovery = new ContainerDerivativeDiscoveryDecorator($discovery);
      $this->discovery = $discovery;
    }
    return $this->discovery;
  }

  /**
   * {@inheritdoc}
   */
  protected function providerExists($provider) {
    return $this->moduleHandler->moduleExists($provider) || $this->themeHandler->themeExists($provider);
  }

  /**
   * {@inheritdoc}
   */
  protected function findDefinitions() {
    $definitions = parent::findDefinitions();

    // At this point the plugin list only contains valid plugins. Ensure all
    // related plugins exist and the relationship is bi-directional. This
    // ensures topics are listed on their related topics.
    foreach ($definitions as $plugin_id => $plugin_definition) {
      foreach ($plugin_definition['related'] as $key => $related_id) {
        // If the related help topic does not exist it might be for a module
        // that is not installed. Remove it.
        // @todo Discuss this more as this could cause silent errors but it
        //   offers useful functionality to relate to a help topic provided by
        //   extensions that are yet to be installed.
        //   https://www.drupal.org/i/3360133
        if (!isset($definitions[$related_id])) {
          unset($definitions[$plugin_id]['related'][$key]);
          continue;
        }
        // Make the related relationship bi-directional.
        if (isset($definitions[$related_id]) && !in_array($plugin_id, $definitions[$related_id]['related'], TRUE)) {
          $definitions[$related_id]['related'][] = $plugin_id;
        }
      }
    }
    return $definitions;
  }

}