Completed
Pull Request — master (#157)
by
unknown
01:57
created

DriverPluginManagerBase::getDriverPluginType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
c 0
b 0
f 0
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace Drupal\Driver\Plugin;
4
5
use Drupal\Core\Plugin\DefaultPluginManager;
6
use Drupal\Core\Cache\CacheBackendInterface;
7
use Drupal\Core\Extension\ModuleHandlerInterface;
8
9
/**
10
 * Provides a base class for the Driver's plugin managers.
11
 */
12
abstract class DriverPluginManagerBase extends DefaultPluginManager implements DriverPluginManagerInterface {
13
14
  /**
15
   * The name of the plugin type this is the manager for.
16
   *
17
   * @var string
18
   */
19
  protected $driverPluginType;
20
21
  /**
22
   * Discovered plugin definitions that match targets.
23
   *
24
   * An array, keyed by target. Each array value is a sub-array of sorted
25
   * plugin definitions that match that target.
26
   *
27
   * @var array
28
   */
29
  protected $matchedDefinitions;
30
31
  /**
32
   * An array of target characteristics that plugins should be filtered by.
33
   *
34
   * @var array
35
   */
36
  protected $filters;
37
38
  /**
39
   * An multi-dimensional array of sets of target characteristics.
40
   *
41
   * The order indicates the specificity of the match between the plugin
42
   * definition and the target; earlier arrays are a more precise match.
43
   *
44
   * @var array
45
   */
46
  protected $specificityCriteria;
47
48
  /**
49
   * The Drupal version being driven.
50
   *
51
   * @var int
52
   */
53
  protected $version;
54
55
  /**
56
   * Constructor for DriverPluginManagerBase objects.
57
   *
58
   * @param \Traversable $namespaces
59
   *   An object that implements \Traversable which contains the root paths
60
   *   keyed by the corresponding namespace to look for plugin implementations.
61
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
62
   *   Cache backend instance to use.
63
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
64
   *   The module handler to invoke the alter hook with.
65
   * @param int $version
66
   *   Drupal major version number.
67
   * @param string $projectPluginRoot
68
   *   The directory to search for additional project-specific driver plugins.
69
   */
70
  public function __construct(
71
        \Traversable $namespaces,
72
        CacheBackendInterface $cache_backend,
73
        ModuleHandlerInterface $module_handler,
74
        $version,
75
        $projectPluginRoot = NULL
76
    ) {
77
78
    $this->version = $version;
79
80
    // Add the driver to the namespaces searched for plugins.
81
    $reflection = new \ReflectionClass($this);
82
    $driverPath = dirname(dirname($reflection->getFileName()));
83
    $namespaces = $namespaces->getArrayCopy();
84
    $supplementedNamespaces = new \ArrayObject();
85
    foreach ($namespaces as $name => $class) {
86
      $supplementedNamespaces[$name] = $class;
87
    }
88
    $supplementedNamespaces['Drupal\Driver'] = $driverPath;
89
90
    if (!is_null($projectPluginRoot)) {
91
      // Need some way to load project-specific plugins.
92
      // $supplementedNamespaces['Drupal\Driver'] = $projectPluginRoot;.
93
    }
94
95
    parent::__construct(
96
        'Plugin/' . $this->getDriverPluginType(),
97
        $supplementedNamespaces,
98
        $module_handler,
99
        'Drupal\Driver\Plugin\\' . $this->getDriverPluginType() . 'PluginInterface',
100
        'Drupal\Driver\Annotation\\' . $this->getDriverPluginType()
101
    );
102
103
    if (!is_null($cache_backend)) {
104
      $this->setCacheBackend($cache_backend, $this->getDriverPluginType() . '_plugins');
105
    }
106
  }
107
108
  /**
109
   * {@inheritdoc}
110
   */
111
  public function getMatchedDefinitions($rawTarget) {
112
    // Make sure the target is in a filterable format.
113
    $target = $this->getFilterableTarget($rawTarget);
114
    foreach ($this->getFilters() as $filter) {
115
      if (!isset($target[$filter])) {
116
        throw new \Exception("Plugin target is missing required filter property '" . $filter . "'.");
117
      }
118
    }
119
120
    // Get stored plugins if available.
121
    $targetKey = serialize($target);
122
    if (isset($this->matchedDefinitions[$targetKey])) {
123
      return $this->matchedDefinitions[$targetKey];
124
    }
125
126
    // Discover plugins & discard those that don't match the target.
127
    $definitions = $this->getDefinitions();
128
    $definitions = $this->filterDefinitionsByTarget($target, $definitions);
129
130
    // Group the plugins according to weight.
131
    $weighted_definitions = [];
132
    foreach ($definitions as $definition) {
133
      $weight = $definition['weight'];
134
      $weighted_definitions[$weight][] = $definition;
135
    }
136
137
    // Group by specificity within each weight group.
138
    $groupedDefinitions = [];
139
    foreach ($weighted_definitions as $weight => $weightGroup) {
140
      $groupedDefinitions[$weight] = $this->sortDefinitionsBySpecificity($weightGroup);
141
    }
142
143
    // Sort the weight groups high to low.
144
    krsort($groupedDefinitions);
145
146
    // Flatten the weight and specificity groups, while preserving sort order.
147
    if (count($groupedDefinitions) === 0) {
148
      $flattenedDefinitions = [];
149
    }
150
    else {
151
      $flattenedDefinitions = call_user_func_array('array_merge', $groupedDefinitions);
152
      $flattenedDefinitions = call_user_func_array('array_merge', $flattenedDefinitions);
153
    }
154
155
    $this->setMatchedDefinitions($targetKey, $flattenedDefinitions);
156
    return $this->matchedDefinitions[$targetKey];
157
  }
158
159
  /**
160
   * Convert a target object into a filterable target.
161
   *
162
   * @param array|object $rawTarget
163
   *   An array or object that is the target to match definitions against.
164
   *
165
   * @return array
166
   *   An array with a key for each filter used by this plugin manager.
167
   */
168
  protected function getFilterableTarget($rawTarget) {
169
    return $rawTarget;
170
  }
171
172
  /**
173
   * Sort an array of definitions by their specificity.
174
   *
175
   * @param array $definitions
176
   *   An array of definitions.
177
   *
178
   * @return array
179
   *   An array of definitions sorted by the specificity criteria.
180
   */
181
  protected function sortDefinitionsBySpecificity(array $definitions) {
182
    // Group definitions by which criteria they match.
183
    $groupedDefinitions = [];
184
    foreach ($definitions as $definition) {
185
      $group = $this->findSpecificityGroup($definition);
186
      $groupedDefinitions[$group][] = $definition;
187
    }
188
189
    // Sort alphabetically by id within groups.
190
    $sortedDefinitions = [];
191
    foreach ($groupedDefinitions as $groupName => $groupDefinitions) {
192
      usort($groupDefinitions, function ($a, $b) {
193
        return strcmp($a['id'], $b['id']);
194
      });
195
      $sortedDefinitions[$groupName] = $groupDefinitions;
196
    }
197
198
    // Sort groups by the order of the specificity criteria.
199
    ksort($sortedDefinitions);
200
    return $sortedDefinitions;
201
  }
202
203
  /**
204
   * Find the specificity group a plugin definition belongs to.
205
   *
206
   * @param array $definition
207
   *   A plugin definition with keys for the specificity criteria.
208
   *
209
   * @return int
210
   *   An integer for which of the specificity criteria the definition fits.
211
   */
212
  protected function findSpecificityGroup(array $definition) {
213
    // Work  though specificity criteria until a match is found.
214
    foreach ($this->getSpecificityCriteria() as $key => $criteria) {
215
      foreach ($criteria as $criterion) {
216
        if (!isset($definition[$criterion])) {
217
          continue(2);
218
        }
219
      }
220
      return $key;
221
    }
222
223
    // If it matched no criteria, it must be a catch-all plugin.
224
    return count($this->getSpecificityCriteria());
225
  }
226
227
  /**
228
   * Remove plugin definitions that don't fit a target according to filters.
229
   *
230
   * @param array $target
231
   *   An array with keys for each filter that plugins may or may not match.
232
   * @param array $definitions
233
   *   An array of plugin definitions to match against the target.
234
   *
235
   * @return array
236
   *   An array of plugin definitions, only those which match the target.
237
   */
238
  protected function filterDefinitionsByTarget(array $target, array $definitions) {
239
    $filters = $this->getFilters();
240
    $filteredDefinitions = [];
241
    foreach ($definitions as $definition) {
242
      // Drop plugins for other Drupal versions if version specified.
243
      if (isset($definition['version']) && $definition['version'] !== $this->getVersion()) {
244
        continue;
245
      }
246
      reset($filters);
247
      foreach ($filters as $filter) {
248
        // If a definition doesn't contain the value specified by the target,
249
        // for this filter, then skip this definition and don't store it.
250
        $isCompatibleArray = isset($definition[$filter]) &&
251
                is_array($definition[$filter]) && (count($definition[$filter]) > 0);
252
        if ($isCompatibleArray) {
253
          // Use case insensitive comparison.
254
          $definitionFilters = array_map('mb_strtolower', $definition[$filter]);
255
          if (!in_array(mb_strtolower($target[$filter]), $definitionFilters, TRUE)) {
256
            continue(2);
257
          }
258
        }
259
      }
260
      $filteredDefinitions[] = $definition;
261
    }
262
    return $filteredDefinitions;
263
  }
264
265
  /**
266
   * Finds plugin definitions.
267
   *
268
   * Overwrites the parent method to retain discovered plugins with the provider
269
   * 'driver'. The parent implementation is not aware of this Drupal Driver.
270
   *
271
   * @return array
272
   *   List of discovered plugin definitions.
273
   */
274
  protected function findDefinitions() {
275
    $definitions = $this->getDiscovery()->getDefinitions();
276
    foreach ($definitions as $plugin_id => &$definition) {
277
      $this->processDefinition($definition, $plugin_id);
278
    }
279
    $this->alterDefinitions($definitions);
280
    // If this plugin was provided by a module that does not exist, remove the
281
    // plugin definition.
282
    foreach ($definitions as $plugin_id => $plugin_definition) {
283
      $provider = $this->extractProviderFromDefinition($plugin_definition);
284
      if ($provider && !in_array($provider, ['driver', 'core', 'component']) && !$this->providerExists($provider)) {
285
        unset($definitions[$plugin_id]);
286
      }
287
    }
288
    return $definitions;
289
  }
290
291
  /**
292
   * Get the name of the type of driver plugin this is the manager of.
293
   *
294
   * @return string
295
   *   The name of the type of driver plugin being managed.
296
   */
297
  protected function getDriverPluginType() {
298
    return $this->driverPluginType;
299
  }
300
301
  /**
302
   * Get the specificity criteria for this driver plugin type.
303
   *
304
   * @return array
305
   *   An multi-dimensional array of sets of target characteristics. The order
306
   *   indicates the specificity of the match between the plugin definition and
307
   *   the target; earlier arrays are a more precise match.
308
   */
309
  protected function getSpecificityCriteria() {
310
    return $this->specificityCriteria;
311
  }
312
313
  /**
314
   * Get the filters for this driver plugin type.
315
   *
316
   * @return array
317
   *   An array of target characteristics that plugins should be filtered by.
318
   */
319
  protected function getFilters() {
320
    return $this->filters;
321
  }
322
323
  /**
324
   * Get the Drupal version being driven.
325
   *
326
   * @return int
327
   *   The Drupal major version number.
328
   */
329
  protected function getVersion() {
330
    return $this->version;
331
  }
332
333
  /**
334
   * Sets the matched plugin definitions.
335
   *
336
   * @param string $targetKey
337
   *   A serialized representation of a filterable target.
338
   * @param array $definitions
339
   *   An array of plugin definitions matched & sorted against the target key.
340
   */
341
  protected function setMatchedDefinitions($targetKey, array $definitions) {
342
    $this->matchedDefinitions[$targetKey] = $definitions;
343
  }
344
345
}
346