Completed
Push — 4.0 ( 3312a6...a15c68 )
by Damian
33s
created

Extensible   F

Complexity

Total Complexity 74

Size/Duplication

Total Lines 558
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 558
rs 2.3809
c 0
b 0
f 0
wmc 74

15 Methods

Rating   Name   Duplication   Size   Complexity  
A invokeWithExtensions() 0 12 4
C remove_extension() 0 31 8
C get_extra_config_sources() 0 47 9
D extend() 0 39 10
A defineExtensionMethods() 0 13 3
C add_extension() 0 51 9
A beforeExtending() 0 6 2
B get_extensions() 0 24 6
B has_extension() 0 21 6
A afterExtending() 0 6 2
A defineMethods() 0 6 1
A constructExtensions() 0 3 1
A getExtensionInstance() 0 13 4
A hasExtension() 0 3 1
C getExtensionInstances() 0 31 8

How to fix   Complexity   

Complex Class

Complex classes like Extensible often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Extensible, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Core;
4
5
use InvalidArgumentException;
6
use SilverStripe\Control\RequestHandler;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\Dev\Deprecation;
10
use SilverStripe\ORM\DataExtension;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\View\ViewableData;
13
14
/**
15
 * Allows an object to have extensions applied to it.
16
 */
17
trait Extensible
18
{
19
    use CustomMethods {
20
        defineMethods as defineMethodsCustom;
21
    }
22
23
    /**
24
     * An array of extension names and parameters to be applied to this object upon construction.
25
     *
26
     * Example:
27
     * <code>
28
     * private static $extensions = array (
29
     *   'Hierarchy',
30
     *   "Version('Stage', 'Live')"
31
     * );
32
     * </code>
33
     *
34
     * Use {@link Object::add_extension()} to add extensions without access to the class code,
35
     * e.g. to extend core classes.
36
     *
37
     * Extensions are instantiated together with the object and stored in {@link $extension_instances}.
38
     *
39
     * @var array $extensions
40
     * @config
41
     */
42
    private static $extensions = [];
43
44
    /**
45
     * Classes that cannot be extended
46
     *
47
     * @var array
48
     */
49
    private static $unextendable_classes = array(
50
        ViewableData::class,
51
        RequestHandler::class,
52
    );
53
54
    /**
55
     * @var Extension[] all current extension instances, or null if not declared yet.
56
     */
57
    protected $extension_instances = null;
58
59
    /**
60
     * List of callbacks to call prior to extensions having extend called on them,
61
     * each grouped by methodName.
62
     *
63
     * Top level array is method names, each of which is an array of callbacks for that name.
64
     *
65
     * @var callable[][]
66
     */
67
    protected $beforeExtendCallbacks = array();
68
69
    /**
70
     * List of callbacks to call after extensions having extend called on them,
71
     * each grouped by methodName.
72
     *
73
     * Top level array is method names, each of which is an array of callbacks for that name.
74
     *
75
     * @var callable[][]
76
     */
77
    protected $afterExtendCallbacks = array();
78
79
    /**
80
     * Allows user code to hook into Object::extend prior to control
81
     * being delegated to extensions. Each callback will be reset
82
     * once called.
83
     *
84
     * @param string $method The name of the method to hook into
85
     * @param callable $callback The callback to execute
86
     */
87
    protected function beforeExtending($method, $callback)
88
    {
89
        if (empty($this->beforeExtendCallbacks[$method])) {
90
            $this->beforeExtendCallbacks[$method] = array();
91
        }
92
        $this->beforeExtendCallbacks[$method][] = $callback;
93
    }
94
95
    /**
96
     * Allows user code to hook into Object::extend after control
97
     * being delegated to extensions. Each callback will be reset
98
     * once called.
99
     *
100
     * @param string $method The name of the method to hook into
101
     * @param callable $callback The callback to execute
102
     */
103
    protected function afterExtending($method, $callback)
104
    {
105
        if (empty($this->afterExtendCallbacks[$method])) {
106
            $this->afterExtendCallbacks[$method] = array();
107
        }
108
        $this->afterExtendCallbacks[$method][] = $callback;
109
    }
110
111
    /**
112
     * @deprecated 4.0..5.0 Extensions and methods are now lazy-loaded
113
     */
114
    protected function constructExtensions()
115
    {
116
        Deprecation::notice('5.0', 'constructExtensions does not need to be invoked and will be removed in 5.0');
117
    }
118
119
    protected function defineMethods()
120
    {
121
        $this->defineMethodsCustom();
0 ignored issues
show
Bug introduced by
The method defineMethodsCustom() does not exist on SilverStripe\Core\Extensible. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

121
        $this->/** @scrutinizer ignore-call */ 
122
               defineMethodsCustom();
Loading history...
122
123
        // Define extension methods
124
        $this->defineExtensionMethods();
125
    }
126
127
    /**
128
     * Adds any methods from {@link Extension} instances attached to this object.
129
     * All these methods can then be called directly on the instance (transparently
130
     * mapped through {@link __call()}), or called explicitly through {@link extend()}.
131
     *
132
     * @uses addCallbackMethod()
133
     */
134
    protected function defineExtensionMethods()
135
    {
136
        $extensions = $this->getExtensionInstances();
137
        foreach ($extensions as $extensionClass => $extensionInstance) {
138
            foreach ($this->findMethodsFromExtension($extensionInstance) as $method) {
139
                $this->addCallbackMethod($method, function ($inst, $args) use ($method, $extensionClass) {
140
                    /** @var Extensible $inst */
141
                    $extension = $inst->getExtensionInstance($extensionClass);
142
                    try {
143
                        $extension->setOwner($inst);
144
                        return call_user_func_array([$extension, $method], $args);
145
                    } finally {
146
                        $extension->clearOwner();
147
                    }
148
                });
149
            }
150
        }
151
    }
152
153
    /**
154
     * Add an extension to a specific class.
155
     *
156
     * The preferred method for adding extensions is through YAML config,
157
     * since it avoids autoloading the class, and is easier to override in
158
     * more specific configurations.
159
     *
160
     * As an alternative, extensions can be added to a specific class
161
     * directly in the {@link Object::$extensions} array.
162
     * See {@link SiteTree::$extensions} for examples.
163
     * Keep in mind that the extension will only be applied to new
164
     * instances, not existing ones (including all instances created through {@link singleton()}).
165
     *
166
     * @see http://doc.silverstripe.org/framework/en/trunk/reference/dataextension
167
     * @param string $classOrExtension Class that should be extended - has to be a subclass of {@link Object}
168
     * @param string $extension Subclass of {@link Extension} with optional parameters
169
     *  as a string, e.g. "Versioned" or "Translatable('Param')"
170
     * @return bool Flag if the extension was added
171
     */
172
    public static function add_extension($classOrExtension, $extension = null)
173
    {
174
        if ($extension) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extension of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
175
            $class = $classOrExtension;
176
        } else {
177
            $class = get_called_class();
178
            $extension = $classOrExtension;
179
        }
180
181
        if (!preg_match('/^([^(]*)/', $extension, $matches)) {
182
            return false;
183
        }
184
        $extensionClass = $matches[1];
185
        if (!class_exists($extensionClass)) {
186
            user_error(
187
                sprintf('Object::add_extension() - Can\'t find extension class for "%s"', $extensionClass),
188
                E_USER_ERROR
189
            );
190
        }
191
192
        if (!is_subclass_of($extensionClass, 'SilverStripe\\Core\\Extension')) {
193
            user_error(
194
                sprintf('Object::add_extension() - Extension "%s" is not a subclass of Extension', $extensionClass),
195
                E_USER_ERROR
196
            );
197
        }
198
199
        // unset some caches
200
        $subclasses = ClassInfo::subclassesFor($class);
201
        $subclasses[] = $class;
202
203
        if ($subclasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subclasses of type array<mixed,mixed|string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
204
            foreach ($subclasses as $subclass) {
205
                unset(self::$extra_methods[$subclass]);
206
            }
207
        }
208
209
        Config::modify()
210
            ->merge($class, 'extensions', array(
211
                $extension
212
            ));
213
214
        Injector::inst()->unregisterNamedObject($class);
215
216
        // load statics now for DataObject classes
217
        if (is_subclass_of($class, DataObject::class)) {
218
            if (!is_subclass_of($extensionClass, DataExtension::class)) {
219
                user_error("$extensionClass cannot be applied to $class without being a DataExtension", E_USER_ERROR);
220
            }
221
        }
222
        return true;
223
    }
224
225
226
    /**
227
     * Remove an extension from a class.
228
     * Note: This will not remove extensions from parent classes, and must be called
229
     * directly on the class assigned the extension.
230
     *
231
     * Keep in mind that this won't revert any datamodel additions
232
     * of the extension at runtime, unless its used before the
233
     * schema building kicks in (in your _config.php).
234
     * Doesn't remove the extension from any {@link Object}
235
     * instances which are already created, but will have an
236
     * effect on new extensions.
237
     * Clears any previously created singletons through {@link singleton()}
238
     * to avoid side-effects from stale extension information.
239
     *
240
     * @todo Add support for removing extensions with parameters
241
     *
242
     * @param string $extension class name of an {@link Extension} subclass, without parameters
243
     */
244
    public static function remove_extension($extension)
245
    {
246
        $class = get_called_class();
247
248
        // Build filtered extension list
249
        $found = false;
250
        $config = Config::inst()->get($class, 'extensions', Config::EXCLUDE_EXTRA_SOURCES | Config::UNINHERITED) ?: [];
251
        foreach ($config as $key => $candidate) {
252
            // extensions with parameters will be stored in config as ExtensionName("Param").
253
            if (strcasecmp($candidate, $extension) === 0 ||
254
                stripos($candidate, $extension.'(') === 0
255
            ) {
256
                $found = true;
257
                unset($config[$key]);
258
            }
259
        }
260
        // Don't dirty cache if no changes
261
        if (!$found) {
262
            return;
263
        }
264
        Config::modify()->set($class, 'extensions', $config);
265
266
        // Unset singletons
267
        Injector::inst()->unregisterObjects($class);
268
269
        // unset some caches
270
        $subclasses = ClassInfo::subclassesFor($class);
271
        $subclasses[] = $class;
272
        if ($subclasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $subclasses of type array<mixed,mixed|string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
273
            foreach ($subclasses as $subclass) {
274
                unset(self::$extra_methods[$subclass]);
275
            }
276
        }
277
    }
278
279
    /**
280
     * @param string $class If omitted, will get extensions for the current class
281
     * @param bool $includeArgumentString Include the argument string in the return array,
282
     *  FALSE would return array("Versioned"), TRUE returns array("Versioned('Stage','Live')").
283
     * @return array Numeric array of either {@link DataExtension} class names,
284
     *  or eval'ed class name strings with constructor arguments.
285
     */
286
    public static function get_extensions($class = null, $includeArgumentString = false)
287
    {
288
        if (!$class) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
289
            $class = get_called_class();
290
        }
291
292
        $extensions = Config::forClass($class)->get('extensions', Config::EXCLUDE_EXTRA_SOURCES);
293
        if (empty($extensions)) {
294
            return array();
295
        }
296
297
        // Clean nullified named extensions
298
        $extensions = array_filter(array_values($extensions));
299
300
        if ($includeArgumentString) {
301
            return $extensions;
302
        } else {
303
            $extensionClassnames = array();
304
            if ($extensions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
305
                foreach ($extensions as $extension) {
306
                    $extensionClassnames[] = Extension::get_classname_without_arguments($extension);
307
                }
308
            }
309
            return $extensionClassnames;
310
        }
311
    }
312
313
314
    /**
315
     * Get extra config sources for this class
316
     *
317
     * @param string $class Name of class. If left null will return for the current class
318
     * @return array|null
319
     */
320
    public static function get_extra_config_sources($class = null)
321
    {
322
        if (!$class) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
323
            $class = get_called_class();
324
        }
325
326
        // If this class is unextendable, NOP
327
        if (in_array($class, self::$unextendable_classes)) {
328
            return null;
329
        }
330
331
        // Variable to hold sources in
332
        $sources = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $sources is dead and can be removed.
Loading history...
333
334
        // Get a list of extensions
335
        $extensions = Config::inst()->get($class, 'extensions', Config::EXCLUDE_EXTRA_SOURCES | Config::UNINHERITED);
336
337
        if (!$extensions) {
338
            return null;
339
        }
340
341
        // Build a list of all sources;
342
        $sources = array();
343
344
        foreach ($extensions as $extension) {
345
            list($extensionClass, $extensionArgs) = ClassInfo::parse_class_spec($extension);
346
            // Strip service name specifier
347
            $extensionClass = strtok($extensionClass, '.');
348
            $sources[] = $extensionClass;
349
350
            if (!class_exists($extensionClass)) {
351
                throw new InvalidArgumentException("$class references nonexistent $extensionClass in \$extensions");
352
            }
353
354
            call_user_func(array($extensionClass, 'add_to_class'), $class, $extensionClass, $extensionArgs);
355
356
            foreach (array_reverse(ClassInfo::ancestry($extensionClass)) as $extensionClassParent) {
357
                if (ClassInfo::has_method_from($extensionClassParent, 'get_extra_config', $extensionClassParent)) {
358
                    $extras = $extensionClassParent::get_extra_config($class, $extensionClass, $extensionArgs);
359
                    if ($extras) {
360
                        $sources[] = $extras;
361
                    }
362
                }
363
            }
364
        }
365
366
        return $sources;
367
    }
368
369
370
    /**
371
     * Return TRUE if a class has a specified extension.
372
     * This supports backwards-compatible format (static Object::has_extension($requiredExtension))
373
     * and new format ($object->has_extension($class, $requiredExtension))
374
     * @param string $classOrExtension Class to check extension for, or the extension name to check
375
     * if the second argument is null.
376
     * @param string $requiredExtension If the first argument is the parent class, this is the extension to check.
377
     * If left null, the first parameter will be treated as the extension.
378
     * @param boolean $strict if the extension has to match the required extension and not be a subclass
379
     * @return bool Flag if the extension exists
380
     */
381
    public static function has_extension($classOrExtension, $requiredExtension = null, $strict = false)
382
    {
383
        if ($requiredExtension) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $requiredExtension of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
384
            $class = $classOrExtension;
385
        } else {
386
            $class = get_called_class();
387
            $requiredExtension = $classOrExtension;
388
        }
389
390
        $requiredExtension = Extension::get_classname_without_arguments($requiredExtension);
391
        $extensions = self::get_extensions($class);
392
        foreach ($extensions as $extension) {
393
            if (strcasecmp($extension, $requiredExtension) === 0) {
394
                return true;
395
            }
396
            if (!$strict && is_subclass_of($extension, $requiredExtension)) {
397
                return true;
398
            }
399
        }
400
401
        return false;
402
    }
403
404
405
    /**
406
     * Calls a method if available on both this object and all applied {@link Extensions}, and then attempts to merge
407
     * all results into an array
408
     *
409
     * @param string $method the method name to call
410
     * @param mixed $a1
411
     * @param mixed $a2
412
     * @param mixed $a3
413
     * @param mixed $a4
414
     * @param mixed $a5
415
     * @param mixed $a6
416
     * @param mixed $a7
417
     * @return array List of results with nulls filtered out
418
     */
419
    public function invokeWithExtensions($method, &$a1 = null, &$a2 = null, &$a3 = null, &$a4 = null, &$a5 = null, &$a6 = null, &$a7 = null)
420
    {
421
        $result = array();
422
        if (method_exists($this, $method)) {
423
            $thisResult = $this->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
424
            if ($thisResult !== null) {
425
                $result[] = $thisResult;
426
            }
427
        }
428
        $extras = $this->extend($method, $a1, $a2, $a3, $a4, $a5, $a6, $a7);
429
430
        return $extras ? array_merge($result, $extras) : $result;
431
    }
432
433
    /**
434
     * Run the given function on all of this object's extensions. Note that this method originally returned void, so if
435
     * you wanted to return results, you're hosed
436
     *
437
     * Currently returns an array, with an index resulting every time the function is called. Only adds returns if
438
     * they're not NULL, to avoid bogus results from methods just defined on the parent extension. This is important for
439
     * permission-checks through extend, as they use min() to determine if any of the returns is FALSE. As min() doesn't
440
     * do type checking, an included NULL return would fail the permission checks.
441
     *
442
     * The extension methods are defined during {@link __construct()} in {@link defineMethods()}.
443
     *
444
     * @param string $method the name of the method to call on each extension
445
     * @param mixed $a1
446
     * @param mixed $a2
447
     * @param mixed $a3
448
     * @param mixed $a4
449
     * @param mixed $a5
450
     * @param mixed $a6
451
     * @param mixed $a7
452
     * @return array
453
     */
454
    public function extend($method, &$a1 = null, &$a2 = null, &$a3 = null, &$a4 = null, &$a5 = null, &$a6 = null, &$a7 = null)
455
    {
456
        $values = array();
457
458
        if (!empty($this->beforeExtendCallbacks[$method])) {
459
            foreach (array_reverse($this->beforeExtendCallbacks[$method]) as $callback) {
460
                $value = call_user_func_array($callback, array(&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7));
461
                if ($value !== null) {
462
                    $values[] = $value;
463
                }
464
            }
465
            $this->beforeExtendCallbacks[$method] = array();
466
        }
467
468
        foreach ($this->getExtensionInstances() as $instance) {
469
            if (method_exists($instance, $method)) {
470
                try {
471
                    $instance->setOwner($this);
472
                    $value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
473
                } finally {
474
                    $instance->clearOwner();
475
                }
476
                if ($value !== null) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.
Loading history...
477
                    $values[] = $value;
478
                }
479
            }
480
        }
481
482
        if (!empty($this->afterExtendCallbacks[$method])) {
483
            foreach (array_reverse($this->afterExtendCallbacks[$method]) as $callback) {
484
                $value = call_user_func_array($callback, array(&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7));
485
                if ($value !== null) {
486
                    $values[] = $value;
487
                }
488
            }
489
            $this->afterExtendCallbacks[$method] = array();
490
        }
491
492
        return $values;
493
    }
494
495
    /**
496
     * Get an extension instance attached to this object by name.
497
     *
498
     * @param string $extension
499
     * @return Extension|null
500
     */
501
    public function getExtensionInstance($extension)
502
    {
503
        $instances = $this->getExtensionInstances();
504
        if (array_key_exists($extension, $instances)) {
505
            return $instances[$extension];
506
        }
507
        // in case Injector has been used to replace an extension
508
        foreach ($instances as $instance) {
509
            if (is_a($instance, $extension)) {
510
                return $instance;
511
            }
512
        }
513
        return null;
514
    }
515
516
    /**
517
     * Returns TRUE if this object instance has a specific extension applied
518
     * in {@link $extension_instances}. Extension instances are initialized
519
     * at constructor time, meaning if you use {@link add_extension()}
520
     * afterwards, the added extension will just be added to new instances
521
     * of the extended class. Use the static method {@link has_extension()}
522
     * to check if a class (not an instance) has a specific extension.
523
     * Caution: Don't use singleton(<class>)->hasExtension() as it will
524
     * give you inconsistent results based on when the singleton was first
525
     * accessed.
526
     *
527
     * @param string $extension Classname of an {@link Extension} subclass without parameters
528
     * @return bool
529
     */
530
    public function hasExtension($extension)
531
    {
532
        return (bool) $this->getExtensionInstance($extension);
533
    }
534
535
    /**
536
     * Get all extension instances for this specific object instance.
537
     * See {@link get_extensions()} to get all applied extension classes
538
     * for this class (not the instance).
539
     *
540
     * This method also provides lazy-population of the extension_instances property.
541
     *
542
     * @return Extension[] Map of {@link DataExtension} instances, keyed by classname.
543
     */
544
    public function getExtensionInstances()
545
    {
546
        if (isset($this->extension_instances)) {
547
            return $this->extension_instances;
548
        }
549
550
        // Setup all extension instances for this instance
551
        $this->extension_instances = [];
552
        foreach (ClassInfo::ancestry(static::class) as $class) {
553
            if (in_array($class, self::$unextendable_classes)) {
554
                continue;
555
            }
556
            $extensions = Config::inst()->get($class, 'extensions', Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES);
557
558
            if ($extensions) {
559
                foreach ($extensions as $extension) {
560
                    $name = $extension;
561
                    // Allow service names of the form "%$ServiceName"
562
                    if (substr($name, 0, 2) == '%$') {
563
                        $name = substr($name, 2);
564
                    }
565
                    $name = trim(strtok($name, '('));
566
                    if (class_exists($name)) {
567
                        $name = ClassInfo::class_name($name);
568
                    }
569
                    $this->extension_instances[$name] = Injector::inst()->get($extension);
570
                }
571
            }
572
        }
573
574
        return $this->extension_instances;
575
    }
576
}
577