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

DriverPluginManagerBase::getMatchedDefinitions()   C

Complexity

Conditions 7
Paths 19

Size

Total Lines 47
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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