silverstripe-framework/src/Core/Extensible.php

572 lines
20 KiB
PHP
Raw Normal View History

<?php
namespace SilverStripe\Core;
use InvalidArgumentException;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Deprecation;
use SilverStripe\View\ViewableData;
/**
* Allows an object to have extensions applied to it.
*/
2016-11-29 00:31:16 +01:00
trait Extensible
{
use CustomMethods {
defineMethods as defineMethodsCustom;
}
2016-11-29 00:31:16 +01:00
/**
* An array of extension names and parameters to be applied to this object upon construction.
*
* Example:
* <code>
* private static $extensions = array (
* 'Hierarchy',
* "Version('Stage', 'Live')"
* );
* </code>
*
* Use {@link Object::add_extension()} to add extensions without access to the class code,
* e.g. to extend core classes.
*
* Extensions are instantiated together with the object and stored in {@link $extension_instances}.
*
* @var array $extensions
* @config
*/
private static $extensions = [];
2016-11-29 00:31:16 +01:00
/**
* Classes that cannot be extended
*
* @var array
*/
private static $unextendable_classes = array(
ViewableData::class,
2016-11-29 00:31:16 +01:00
);
/**
* @var Extension[] all current extension instances, or null if not declared yet.
2016-11-29 00:31:16 +01:00
*/
protected $extension_instances = null;
2016-11-29 00:31:16 +01:00
/**
* List of callbacks to call prior to extensions having extend called on them,
* each grouped by methodName.
*
* Top level array is method names, each of which is an array of callbacks for that name.
*
* @var callable[][]
*/
protected $beforeExtendCallbacks = array();
/**
* List of callbacks to call after extensions having extend called on them,
* each grouped by methodName.
*
* Top level array is method names, each of which is an array of callbacks for that name.
*
* @var callable[][]
*/
protected $afterExtendCallbacks = array();
/**
* Allows user code to hook into Object::extend prior to control
* being delegated to extensions. Each callback will be reset
* once called.
*
* @param string $method The name of the method to hook into
* @param callable $callback The callback to execute
*/
protected function beforeExtending($method, $callback)
{
if (empty($this->beforeExtendCallbacks[$method])) {
$this->beforeExtendCallbacks[$method] = array();
}
$this->beforeExtendCallbacks[$method][] = $callback;
}
/**
* Allows user code to hook into Object::extend after control
* being delegated to extensions. Each callback will be reset
* once called.
*
* @param string $method The name of the method to hook into
* @param callable $callback The callback to execute
*/
protected function afterExtending($method, $callback)
{
if (empty($this->afterExtendCallbacks[$method])) {
$this->afterExtendCallbacks[$method] = array();
}
$this->afterExtendCallbacks[$method][] = $callback;
}
/**
* @deprecated 4.0.0:5.0.0 Extensions and methods are now lazy-loaded
*/
2016-11-29 00:31:16 +01:00
protected function constructExtensions()
{
Deprecation::notice('5.0', 'constructExtensions does not need to be invoked and will be removed in 5.0');
}
protected function defineMethods()
{
$this->defineMethodsCustom();
// Define extension methods
$this->defineExtensionMethods();
2016-11-29 00:31:16 +01:00
}
/**
* Adds any methods from {@link Extension} instances attached to this object.
* All these methods can then be called directly on the instance (transparently
* mapped through {@link __call()}), or called explicitly through {@link extend()}.
*
* @uses addCallbackMethod()
2016-11-29 00:31:16 +01:00
*/
protected function defineExtensionMethods()
{
$extensions = $this->getExtensionInstances();
foreach ($extensions as $extensionClass => $extensionInstance) {
foreach ($this->findMethodsFromExtension($extensionInstance) as $method) {
$this->addCallbackMethod($method, function ($inst, $args) use ($method, $extensionClass) {
/** @var Extensible $inst */
$extension = $inst->getExtensionInstance($extensionClass);
if (!$extension) {
return null;
}
try {
$extension->setOwner($inst);
return call_user_func_array([$extension, $method], $args);
} finally {
$extension->clearOwner();
}
});
}
2016-11-29 00:31:16 +01:00
}
}
/**
* Add an extension to a specific class.
*
* The preferred method for adding extensions is through YAML config,
* since it avoids autoloading the class, and is easier to override in
* more specific configurations.
*
* As an alternative, extensions can be added to a specific class
* directly in the {@link Object::$extensions} array.
* See {@link SiteTree::$extensions} for examples.
* Keep in mind that the extension will only be applied to new
* instances, not existing ones (including all instances created through {@link singleton()}).
*
* @see http://doc.silverstripe.org/framework/en/trunk/reference/dataextension
* @param string $classOrExtension Class that should be extended - has to be a subclass of {@link Object}
* @param string $extension Subclass of {@link Extension} with optional parameters
* as a string, e.g. "Versioned" or "Translatable('Param')"
* @return bool Flag if the extension was added
*/
public static function add_extension($classOrExtension, $extension = null)
{
if ($extension) {
2016-11-29 00:31:16 +01:00
$class = $classOrExtension;
} else {
$class = get_called_class();
$extension = $classOrExtension;
}
if (!preg_match('/^([^(]*)/', $extension, $matches)) {
return false;
}
$extensionClass = $matches[1];
if (!class_exists($extensionClass)) {
user_error(
sprintf('Object::add_extension() - Can\'t find extension class for "%s"', $extensionClass),
E_USER_ERROR
);
}
if (!is_subclass_of($extensionClass, 'SilverStripe\\Core\\Extension')) {
user_error(
sprintf('Object::add_extension() - Extension "%s" is not a subclass of Extension', $extensionClass),
E_USER_ERROR
);
}
// unset some caches
$subclasses = ClassInfo::subclassesFor($class);
$subclasses[] = $class;
if ($subclasses) {
foreach ($subclasses as $subclass) {
unset(self::$extra_methods[$subclass]);
}
}
Config::modify()
->merge($class, 'extensions', array(
$extension
));
2016-11-29 00:31:16 +01:00
Injector::inst()->unregisterNamedObject($class);
return true;
}
/**
* Remove an extension from a class.
* Note: This will not remove extensions from parent classes, and must be called
* directly on the class assigned the extension.
2016-11-29 00:31:16 +01:00
*
* Keep in mind that this won't revert any datamodel additions
* of the extension at runtime, unless its used before the
* schema building kicks in (in your _config.php).
* Doesn't remove the extension from any {@link Object}
* instances which are already created, but will have an
* effect on new extensions.
* Clears any previously created singletons through {@link singleton()}
* to avoid side-effects from stale extension information.
*
* @todo Add support for removing extensions with parameters
*
* @param string $extension class name of an {@link Extension} subclass, without parameters
*/
public static function remove_extension($extension)
{
$class = get_called_class();
// Build filtered extension list
$found = false;
$config = Config::inst()->get($class, 'extensions', Config::EXCLUDE_EXTRA_SOURCES | Config::UNINHERITED) ?: [];
foreach ($config as $key => $candidate) {
// extensions with parameters will be stored in config as ExtensionName("Param").
if (strcasecmp($candidate, $extension) === 0 ||
2018-01-16 19:39:30 +01:00
stripos($candidate, $extension . '(') === 0
) {
$found = true;
unset($config[$key]);
2016-11-29 00:31:16 +01:00
}
}
// Don't dirty cache if no changes
if (!$found) {
return;
}
Config::modify()->set($class, 'extensions', $config);
2016-11-29 00:31:16 +01:00
API Refactor bootstrap, request handling See https://github.com/silverstripe/silverstripe-framework/pull/7037 and https://github.com/silverstripe/silverstripe-framework/issues/6681 Squashed commit of the following: commit 8f65e5653211240650eaa4fa65bb83b45aae6d58 Author: Ingo Schommer <me@chillu.com> Date: Thu Jun 22 22:25:50 2017 +1200 Fixed upgrade guide spelling commit 76f95944fa89b0b540704b8d744329f690f9698c Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 16:38:34 2017 +1200 BUG Fix non-test class manifest including sapphiretest / functionaltest commit 9379834cb4b2e5177a2600049feec05bf111c16b Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 15:50:47 2017 +1200 BUG Fix nesting bug in Kernel commit 188ce35d82599360c40f0f2de29579c56fb90761 Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 15:14:51 2017 +1200 BUG fix db bootstrapping issues commit 7ed4660e7a63915e8e974deeaba9807bc4d38b0d Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 14:49:07 2017 +1200 BUG Fix issue in DetailedErrorFormatter commit 738f50c497166f81ccbe3f40fbcff895ce71f82f Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 11:49:19 2017 +1200 Upgrading notes on mysite/_config.php commit 6279d28e5e455916f902a2f963c014d8899f7fc7 Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 11:43:28 2017 +1200 Update developer documentation commit 5c90d53a84ef0139c729396949a7857fae60436f Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 10:48:44 2017 +1200 Update installer to not use global databaseConfig commit f9b2ba4755371f08bd95f6908ac612fcbb7ca205 Author: Damian Mooyman <damian@silverstripe.com> Date: Wed Jun 21 21:04:39 2017 +1200 Fix behat issues commit 5b59a912b60282b4dad4ef10ed3b97c5d0a761ac Author: Damian Mooyman <damian@silverstripe.com> Date: Wed Jun 21 17:07:11 2017 +1200 Move HTTPApplication to SilverStripe\Control namespace commit e2c4a18f637bdd3d276619554de60ee8b4d95ced Author: Damian Mooyman <damian@silverstripe.com> Date: Wed Jun 21 16:29:03 2017 +1200 More documentation Fix up remaining tests Refactor temp DB into TempDatabase class so it’s available outside of unit tests. commit 5d235e64f341d6251bfe9f4833f15cc8593c5034 Author: Damian Mooyman <damian@silverstripe.com> Date: Wed Jun 21 12:13:15 2017 +1200 API HTTPRequestBuilder::createFromEnvironment() now cleans up live globals BUG Fix issue with SSViewer Fix Security / View tests commit d88d4ed4e48291cb65407f222f190064b1f1deeb Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 20 16:39:43 2017 +1200 API Refactor AppKernel into CoreKernel commit f7946aec3391139ae1b4029c353c327a36552b36 Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 20 16:00:40 2017 +1200 Docs and minor cleanup commit 12bd31f9366327650b5c0c0f96cd0327d44faf0a Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 20 15:34:34 2017 +1200 API Remove OutputMiddleware API Move environment / global / ini management into Environment class API Move getTempFolder into TempFolder class API Implement HTTPRequestBuilder / CLIRequestBuilder BUG Restore SS_ALLOWED_HOSTS check in original location API CoreKernel now requires $basePath to be passed in API Refactor installer.php to use application to bootstrap API move memstring conversion globals to Convert BUG Fix error in CoreKernel nesting not un-nesting itself properly. commit bba979114624247cf463cf2a8c9e4be9a7c3a772 Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 19 18:07:53 2017 +1200 API Create HTTPMiddleware and standardise middleware for request handling commit 2a10c2397bdc53001013f607b5d38087ce6c0730 Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 19 17:42:42 2017 +1200 Fixed ORM tests commit d75a8d1d93398af4bd0432df9e4bc6295c15a3fe Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 19 17:15:07 2017 +1200 FIx i18n tests commit 06364af3c379c931889c4cc34dd920fee3db204a Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 19 16:59:34 2017 +1200 Fix controller namespace Move states to sub namespace commit 2a278e2953d2dbb19f78d91c919048e1fc935436 Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 19 12:49:45 2017 +1200 Fix forms namespace commit b65c21241bee019730027071d815dbf7571197a4 Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 15 18:56:48 2017 +1200 Update API usages commit d1d4375c95a264a6b63cbaefc2c1d12f808bfd82 Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 15 18:41:44 2017 +1200 API Refactor $flush into HTPPApplication API Enforce health check in Controller::pushCurrent() API Better global backup / restore Updated Director::test() to use new API commit b220534f06732db4fa940d8724c2a85c0ba2495a Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 22:05:57 2017 +1200 Move app nesting to a test state helper commit 603704165c08d0c1c81fd5e6bb9506326eeee17b Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 21:46:04 2017 +1200 Restore kernel stack to fix multi-level nesting commit 2f6336a15bf79dc8c2edd44cec1931da2dd51c28 Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 17:23:21 2017 +1200 API Implement kernel nesting commit fc7188da7d6ad6785354bab61f08700454c81d91 Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 15:43:13 2017 +1200 Fix core tests commit a0ae7235148fffd71f2f02d1fe7fe45bf3aa39eb Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 15:23:52 2017 +1200 Fix manifest tests commit ca033952513633e182040d3d13e1caa9000ca184 Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 15:00:00 2017 +1200 API Move extension management into test state commit c66d4339777663a8a04661fea32a0cf35b95d20f Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 14:10:59 2017 +1200 API Refactor SapphireTest state management into SapphireTestState API Remove Injector::unregisterAllObjects() API Remove FakeController commit f26ae75c6ecaafa0dec1093264e0187191e6764d Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 12 18:04:34 2017 +1200 Implement basic CLI application object commit 001d5596621404892de0a5413392379eff990641 Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 12 17:39:38 2017 +1200 Remove references to SapphireTest::is_running_test() Upgrade various code commit de079c041dacd96bc4f4b66421fa2b2cc4c320f8 Author: Damian Mooyman <damian@silverstripe.com> Date: Wed Jun 7 18:07:33 2017 +1200 API Implement APP object API Refactor of Session
2017-06-22 12:50:45 +02:00
// Unset singletons
Injector::inst()->unregisterObjects($class);
2016-11-29 00:31:16 +01:00
// unset some caches
$subclasses = ClassInfo::subclassesFor($class);
$subclasses[] = $class;
if ($subclasses) {
foreach ($subclasses as $subclass) {
unset(self::$extra_methods[$subclass]);
}
}
}
/**
* @param string $class If omitted, will get extensions for the current class
2016-11-29 00:31:16 +01:00
* @param bool $includeArgumentString Include the argument string in the return array,
* FALSE would return array("Versioned"), TRUE returns array("Versioned('Stage','Live')").
* @return array Numeric array of either {@link DataExtension} class names,
* or eval'ed class name strings with constructor arguments.
*/
public static function get_extensions($class = null, $includeArgumentString = false)
2016-11-29 00:31:16 +01:00
{
if (!$class) {
$class = get_called_class();
}
$extensions = Config::forClass($class)->get('extensions', Config::EXCLUDE_EXTRA_SOURCES);
2016-11-29 00:31:16 +01:00
if (empty($extensions)) {
return array();
}
// Clean nullified named extensions
$extensions = array_filter(array_values($extensions));
if ($includeArgumentString) {
return $extensions;
} else {
$extensionClassnames = array();
if ($extensions) {
foreach ($extensions as $extension) {
$extensionClassnames[] = Extension::get_classname_without_arguments($extension);
}
}
return $extensionClassnames;
}
}
/**
* Get extra config sources for this class
*
* @param string $class Name of class. If left null will return for the current class
* @return array|null
*/
2016-11-29 00:31:16 +01:00
public static function get_extra_config_sources($class = null)
{
if (!$class) {
2016-11-29 00:31:16 +01:00
$class = get_called_class();
}
// If this class is unextendable, NOP
if (in_array($class, self::$unextendable_classes)) {
return null;
}
// Variable to hold sources in
$sources = null;
// Get a list of extensions
$extensions = Config::inst()->get($class, 'extensions', Config::EXCLUDE_EXTRA_SOURCES | Config::UNINHERITED);
2016-11-29 00:31:16 +01:00
if (!$extensions) {
return null;
}
// Build a list of all sources;
$sources = array();
foreach ($extensions as $extension) {
list($extensionClass, $extensionArgs) = ClassInfo::parse_class_spec($extension);
// Strip service name specifier
$extensionClass = strtok($extensionClass, '.');
2016-11-29 00:31:16 +01:00
$sources[] = $extensionClass;
if (!class_exists($extensionClass)) {
throw new InvalidArgumentException("$class references nonexistent $extensionClass in \$extensions");
}
call_user_func(array($extensionClass, 'add_to_class'), $class, $extensionClass, $extensionArgs);
foreach (array_reverse(ClassInfo::ancestry($extensionClass)) as $extensionClassParent) {
if (ClassInfo::has_method_from($extensionClassParent, 'get_extra_config', $extensionClassParent)) {
$extras = $extensionClassParent::get_extra_config($class, $extensionClass, $extensionArgs);
if ($extras) {
$sources[] = $extras;
}
}
}
}
return $sources;
}
/**
* Return TRUE if a class has a specified extension.
* This supports backwards-compatible format (static Object::has_extension($requiredExtension))
* and new format ($object->has_extension($class, $requiredExtension))
* @param string $classOrExtension Class to check extension for, or the extension name to check
* if the second argument is null.
* @param string $requiredExtension If the first argument is the parent class, this is the extension to check.
* If left null, the first parameter will be treated as the extension.
2016-11-29 00:31:16 +01:00
* @param boolean $strict if the extension has to match the required extension and not be a subclass
* @return bool Flag if the extension exists
*/
public static function has_extension($classOrExtension, $requiredExtension = null, $strict = false)
{
if ($requiredExtension) {
2016-11-29 00:31:16 +01:00
$class = $classOrExtension;
} else {
$class = get_called_class();
$requiredExtension = $classOrExtension;
}
$requiredExtension = Extension::get_classname_without_arguments($requiredExtension);
$extensions = self::get_extensions($class);
foreach ($extensions as $extension) {
if (strcasecmp($extension, $requiredExtension) === 0) {
return true;
}
if (!$strict && is_subclass_of($extension, $requiredExtension)) {
return true;
}
if (Injector::inst()->get($extension) instanceof $requiredExtension) {
return true;
}
2016-11-29 00:31:16 +01:00
}
return false;
}
/**
* Calls a method if available on both this object and all applied {@link Extensions}, and then attempts to merge
* all results into an array
*
* @param string $method the method name to call
* @param mixed $a1
* @param mixed $a2
* @param mixed $a3
* @param mixed $a4
* @param mixed $a5
* @param mixed $a6
* @param mixed $a7
* @return array List of results with nulls filtered out
*/
public function invokeWithExtensions($method, &$a1 = null, &$a2 = null, &$a3 = null, &$a4 = null, &$a5 = null, &$a6 = null, &$a7 = null)
{
$result = array();
if (method_exists($this, $method)) {
$thisResult = $this->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
if ($thisResult !== null) {
$result[] = $thisResult;
}
}
$extras = $this->extend($method, $a1, $a2, $a3, $a4, $a5, $a6, $a7);
return $extras ? array_merge($result, $extras) : $result;
}
/**
* Run the given function on all of this object's extensions. Note that this method originally returned void, so if
* you wanted to return results, you're hosed
*
* Currently returns an array, with an index resulting every time the function is called. Only adds returns if
* they're not NULL, to avoid bogus results from methods just defined on the parent extension. This is important for
* permission-checks through extend, as they use min() to determine if any of the returns is FALSE. As min() doesn't
* do type checking, an included NULL return would fail the permission checks.
*
* The extension methods are defined during {@link __construct()} in {@link defineMethods()}.
*
* @param string $method the name of the method to call on each extension
* @param mixed $a1
* @param mixed $a2
* @param mixed $a3
* @param mixed $a4
* @param mixed $a5
* @param mixed $a6
* @param mixed $a7
* @return array
*/
public function extend($method, &$a1 = null, &$a2 = null, &$a3 = null, &$a4 = null, &$a5 = null, &$a6 = null, &$a7 = null)
{
$values = array();
if (!empty($this->beforeExtendCallbacks[$method])) {
foreach (array_reverse($this->beforeExtendCallbacks[$method]) as $callback) {
$value = call_user_func_array($callback, array(&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7));
if ($value !== null) {
$values[] = $value;
}
}
$this->beforeExtendCallbacks[$method] = array();
}
foreach ($this->getExtensionInstances() as $instance) {
if (method_exists($instance, $method)) {
try {
$instance->setOwner($this);
$value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
} finally {
$instance->clearOwner();
}
if ($value !== null) {
$values[] = $value;
2016-11-29 00:31:16 +01:00
}
}
}
if (!empty($this->afterExtendCallbacks[$method])) {
foreach (array_reverse($this->afterExtendCallbacks[$method]) as $callback) {
$value = call_user_func_array($callback, array(&$a1, &$a2, &$a3, &$a4, &$a5, &$a6, &$a7));
if ($value !== null) {
$values[] = $value;
}
}
$this->afterExtendCallbacks[$method] = array();
}
return $values;
}
/**
* Get an extension instance attached to this object by name.
*
* @param string $extension
* @return Extension|null
2016-11-29 00:31:16 +01:00
*/
public function getExtensionInstance($extension)
{
$instances = $this->getExtensionInstances();
if (array_key_exists($extension, $instances)) {
return $instances[$extension];
}
// in case Injector has been used to replace an extension
foreach ($instances as $instance) {
if (is_a($instance, $extension)) {
return $instance;
}
}
return null;
2016-11-29 00:31:16 +01:00
}
/**
* Returns TRUE if this object instance has a specific extension applied
* in {@link $extension_instances}. Extension instances are initialized
* at constructor time, meaning if you use {@link add_extension()}
* afterwards, the added extension will just be added to new instances
* of the extended class. Use the static method {@link has_extension()}
* to check if a class (not an instance) has a specific extension.
* Caution: Don't use singleton(<class>)->hasExtension() as it will
* give you inconsistent results based on when the singleton was first
* accessed.
*
* @param string $extension Classname of an {@link Extension} subclass without parameters
* @return bool
*/
public function hasExtension($extension)
{
return (bool) $this->getExtensionInstance($extension);
2016-11-29 00:31:16 +01:00
}
/**
* Get all extension instances for this specific object instance.
* See {@link get_extensions()} to get all applied extension classes
* for this class (not the instance).
*
* This method also provides lazy-population of the extension_instances property.
*
* @return Extension[] Map of {@link DataExtension} instances, keyed by classname.
2016-11-29 00:31:16 +01:00
*/
public function getExtensionInstances()
{
if (isset($this->extension_instances)) {
return $this->extension_instances;
}
// Setup all extension instances for this instance
$this->extension_instances = [];
foreach (ClassInfo::ancestry(static::class) as $class) {
if (in_array($class, self::$unextendable_classes)) {
continue;
}
$extensions = Config::inst()->get($class, 'extensions', Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES);
if ($extensions) {
foreach ($extensions as $extension) {
$name = $extension;
// Allow service names of the form "%$ServiceName"
if (substr($name, 0, 2) == '%$') {
$name = substr($name, 2);
}
$name = trim(strtok($name, '('));
if (class_exists($name)) {
$name = ClassInfo::class_name($name);
}
$this->extension_instances[$name] = Injector::inst()->get($extension);
}
}
}
2016-11-29 00:31:16 +01:00
return $this->extension_instances;
}
}