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

Extensible::extend()   D

Complexity

Conditions 10
Paths 32

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 23
nc 32
nop 8
dl 0
loc 39
rs 4.8196
c 0
b 0
f 0

How to fix   Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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