diff --git a/_config.php b/_config.php
index 25f2259a2..67ca3acb4 100644
--- a/_config.php
+++ b/_config.php
@@ -17,15 +17,6 @@
* @subpackage core
*/
-/**
- * PHP 5.2 introduced a conflict with the Datetime field type, which was renamed to SSDatetime. This was later renamed
- * to SS_Datetime to be consistent with other namespaced classes.
- *
- * Overload both of these to support legacy code.
- */
-Object::useCustomClass('SSDatetime', 'SS_Datetime', true);
-Object::useCustomClass('Datetime', 'SS_Datetime', true);
-
/**
* The root directory of TinyMCE
*/
diff --git a/core/Configurable.php b/core/Configurable.php
new file mode 100644
index 000000000..19e02dc5c
--- /dev/null
+++ b/core/Configurable.php
@@ -0,0 +1,55 @@
+get($this->class, .....).
+ * @return \Config_ForClass
+ */
+ public static function config() {
+ return Config::inst()->forClass(get_called_class());
+ }
+
+ /**
+ * Gets the first set value for the given config option
+ *
+ * @param string $name
+ * @return mixed
+ */
+ public function stat($name) {
+ return Config::inst()->get(get_class($this), $name, Config::FIRST_SET);
+ }
+
+ /**
+ * Update the config value for a given property
+ *
+ * @param string $name
+ * @param mixed $value
+ */
+ public function set_stat($name, $value) {
+ Config::inst()->update(get_class($this), $name, $value);
+ }
+
+ /**
+ * Gets the uninherited value for the given config option
+ *
+ * @param string $name
+ * @return mixed
+ */
+ public function uninherited($name) {
+ return Config::inst()->get(get_class($this), $name, Config::UNINHERITED);
+ }
+}
diff --git a/core/Core.php b/core/Core.php
index aecfe4cd3..db03bed74 100644
--- a/core/Core.php
+++ b/core/Core.php
@@ -62,6 +62,10 @@ gc_enable();
// Include the files needed the initial manifest building, as well as any files
// that are needed for the boostrap process on every request.
require_once 'cache/Cache.php';
+require_once 'core/CustomMethods.php';
+require_once 'core/Extensible.php';
+require_once 'core/Injectable.php';
+require_once 'core/Configurable.php';
require_once 'core/Object.php';
require_once 'core/ClassInfo.php';
require_once 'core/DAG.php';
diff --git a/core/CustomMethods.php b/core/CustomMethods.php
new file mode 100644
index 000000000..c6c7df3f4
--- /dev/null
+++ b/core/CustomMethods.php
@@ -0,0 +1,241 @@
+defineMethods();
+ }
+
+ // Validate method being invked
+ $method = strtolower($method);
+ if(!isset(self::$extra_methods[$class][$method])) {
+ // Please do not change the exception code number below.
+ $class = get_class($this);
+ throw new BadMethodCallException("Object->__call(): the method '$method' does not exist on '$class'", 2175);
+ }
+
+ $config = self::$extra_methods[$class][$method];
+
+ switch(true) {
+ case isset($config['property']) :
+ $obj = $config['index'] !== null ?
+ $this->{$config['property']}[$config['index']] :
+ $this->{$config['property']};
+
+ if($obj) {
+ if(!empty($config['callSetOwnerFirst'])) $obj->setOwner($this);
+ $retVal = call_user_func_array(array($obj, $method), $arguments);
+ if(!empty($config['callSetOwnerFirst'])) $obj->clearOwner();
+ return $retVal;
+ }
+
+ if(!empty($this->destroyed)) {
+ throw new BadMethodCallException(
+ "Object->__call(): attempt to call $method on a destroyed $class object"
+ );
+ } else {
+ throw new BadMethodCallException(
+ "Object->__call(): $class cannot pass control to $config[property]($config[index])."
+ . ' Perhaps this object was mistakenly destroyed?'
+ );
+ }
+
+ case isset($config['wrap']) :
+ array_unshift($arguments, $config['method']);
+ return call_user_func_array(array($this, $config['wrap']), $arguments);
+
+ case isset($config['function']) :
+ return $config['function']($this, $arguments);
+
+ default :
+ throw new BadMethodCallException(
+ "Object->__call(): extra method $method is invalid on $class:"
+ . var_export($config, true)
+ );
+ }
+ }
+
+ /**
+ * 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 addMethodsFrom()
+ */
+ protected function defineMethods() {
+ $class = get_class($this);
+
+ // Define from all registered callbacks
+ foreach($this->extra_method_registers as $callback) {
+ call_user_func($callback);
+ }
+ }
+
+ /**
+ * Register an callback to invoke that defines extra methods
+ *
+ * @param string $name
+ * @param callable $callback
+ */
+ protected function registerExtraMethodCallback($name, $callback) {
+ if(!isset($this->extra_method_registers[$name])) {
+ $this->extra_method_registers[$name] = $callback;
+ }
+ }
+
+ // --------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Return TRUE if a method exists on this object
+ *
+ * This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
+ * extensions
+ *
+ * @param string $method
+ * @return bool
+ */
+ public function hasMethod($method) {
+ $class = get_class($this);
+ return method_exists($this, $method) || isset(self::$extra_methods[$class][strtolower($method)]);
+ }
+
+ /**
+ * Return the names of all the methods available on this object
+ *
+ * @param bool $custom include methods added dynamically at runtime
+ * @return array
+ */
+ public function allMethodNames($custom = false) {
+ $class = get_class($this);
+ if(!isset(self::$built_in_methods[$class])) {
+ self::$built_in_methods[$class] = array_map('strtolower', get_class_methods($this));
+ }
+
+ if($custom && isset(self::$extra_methods[$class])) {
+ return array_merge(self::$built_in_methods[$class], array_keys(self::$extra_methods[$class]));
+ } else {
+ return self::$built_in_methods[$class];
+ }
+ }
+
+
+
+ /**
+ * Add all the methods from an object property (which is an {@link Extension}) to this object.
+ *
+ * @param string $property the property name
+ * @param string|int $index an index to use if the property is an array
+ * @throws InvalidArgumentException
+ */
+ protected function addMethodsFrom($property, $index = null) {
+ $class = get_class($this);
+ $extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
+
+ if(!$extension) {
+ throw new InvalidArgumentException (
+ "Object->addMethodsFrom(): could not add methods from {$class}->{$property}[$index]"
+ );
+ }
+
+ if(method_exists($extension, 'allMethodNames')) {
+ $methods = $extension->allMethodNames(true);
+
+ } else {
+ if(!isset(self::$built_in_methods[$extension->class])) {
+ self::$built_in_methods[$extension->class] = array_map('strtolower', get_class_methods($extension));
+ }
+ $methods = self::$built_in_methods[$extension->class];
+ }
+
+ if($methods) {
+ $methodInfo = array(
+ 'property' => $property,
+ 'index' => $index,
+ 'callSetOwnerFirst' => $extension instanceof \Extension,
+ );
+
+ $newMethods = array_fill_keys($methods, $methodInfo);
+
+ if(isset(self::$extra_methods[$class])) {
+ self::$extra_methods[$class] =
+ array_merge(self::$extra_methods[$class], $newMethods);
+ } else {
+ self::$extra_methods[$class] = $newMethods;
+ }
+ }
+ }
+
+ /**
+ * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
+ * can be wrapped to generateThumbnail(x)
+ *
+ * @param string $method the method name to wrap
+ * @param string $wrap the method name to wrap to
+ */
+ protected function addWrapperMethod($method, $wrap) {
+ $class = get_class($this);
+ self::$extra_methods[$class][strtolower($method)] = array (
+ 'wrap' => $wrap,
+ 'method' => $method
+ );
+ }
+
+ /**
+ * Add an extra method using raw PHP code passed as a string
+ *
+ * @param string $method the method name
+ * @param string $code the PHP code - arguments will be in an array called $args, while you can access this object
+ * by using $obj. Note that you cannot call protected methods, as the method is actually an external
+ * function
+ */
+ protected function createMethod($method, $code) {
+ $class = get_class($this);
+ self::$extra_methods[$class][strtolower($method)] = array (
+ 'function' => create_function('$obj, $args', $code)
+ );
+ }
+}
diff --git a/core/Extensible.php b/core/Extensible.php
new file mode 100644
index 000000000..4e942b54b
--- /dev/null
+++ b/core/Extensible.php
@@ -0,0 +1,465 @@
+constructExtensions() in your class constructor.
+ *
+ * Requires CustomMethods trait
+ */
+trait Extensible {
+ use CustomMethods;
+
+ /**
+ * An array of extension names and parameters to be applied to this object upon construction.
+ *
+ * Example:
+ *
+ * private static $extensions = array (
+ * 'Hierarchy',
+ * "Version('Stage', 'Live')"
+ * );
+ *
+ *
+ * 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 = null;
+
+ private static $classes_constructed = array();
+
+ /**
+ * Classes that cannot be extended
+ *
+ * @var array
+ */
+ private static $unextendable_classes = array('Object', 'ViewableData', 'RequestHandler');
+
+ /**
+ * @var array all current extension instances.
+ */
+ protected $extension_instances = array();
+
+ /**
+ * List of callbacks to call prior to extensions having extend called on them,
+ * each grouped by methodName.
+ *
+ * @var array[callable]
+ */
+ protected $beforeExtendCallbacks = array();
+
+ /**
+ * List of callbacks to call after extensions having extend called on them,
+ * each grouped by methodName.
+ *
+ * @var array[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;
+ }
+
+ protected function constructExtensions() {
+ $class = get_class($this);
+
+ // Register this trait as a method source
+ $this->registerExtraMethodCallback('defineExtensionMethods', function() {
+ $this->defineExtensionMethods();
+ });
+
+ // Setup all extension instances for this instance
+ foreach(ClassInfo::ancestry($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) {
+ $instance = \Object::create_from_string($extension);
+ $instance->setOwner(null, $class);
+ $this->extension_instances[$instance->class] = $instance;
+ }
+ }
+
+ if(!isset(self::$classes_constructed[$class])) {
+ $this->defineMethods();
+ self::$classes_constructed[$class] = true;
+ }
+ }
+
+ /**
+ * 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 addMethodsFrom()
+ */
+ protected function defineExtensionMethods() {
+ if(!empty($this->extension_instances)) {
+ foreach (array_keys($this->extension_instances) as $key) {
+ $this->addMethodsFrom('extension_instances', $key);
+ }
+ }
+ }
+
+
+ /**
+ * 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(func_num_args() > 1) {
+ $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, '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::$classes_constructed[$subclass]);
+ unset(self::$extra_methods[$subclass]);
+ }
+
+ Config::inst()->update($class, 'extensions', array($extension));
+ Config::inst()->extraConfigSourcesChanged($class);
+
+ Injector::inst()->unregisterNamedObject($class);
+
+ // load statics now for DataObject classes
+ if(is_subclass_of($class, 'DataObject')) {
+ if(!is_subclass_of($extensionClass, 'DataExtension')) {
+ user_error("$extensionClass cannot be applied to $class without being a DataExtension", E_USER_ERROR);
+ }
+ }
+ return true;
+ }
+
+
+ /**
+ * Remove an extension from a class.
+ *
+ * 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();
+
+ Config::inst()->remove($class, 'extensions', Config::anything(), $extension);
+
+ // remove any instances of the extension with parameters
+ $config = Config::inst()->get($class, 'extensions');
+
+ if($config) {
+ foreach($config as $k => $v) {
+ // extensions with parameters will be stored in config as
+ // ExtensionName("Param").
+ if(preg_match(sprintf("/^(%s)\(/", preg_quote($extension, '/')), $v)) {
+ Config::inst()->remove($class, 'extensions', Config::anything(), $v);
+ }
+ }
+ }
+
+ Config::inst()->extraConfigSourcesChanged($class);
+
+ // unset singletons to avoid side-effects
+ Injector::inst()->unregisterAllObjects();
+
+ // unset some caches
+ $subclasses = ClassInfo::subclassesFor($class);
+ $subclasses[] = $class;
+ if($subclasses) foreach($subclasses as $subclass) {
+ unset(self::$classes_constructed[$subclass]);
+ unset(self::$extra_methods[$subclass]);
+ }
+ }
+
+ /**
+ * @param string $class
+ * @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, $includeArgumentString = false) {
+ $extensions = Config::inst()->get($class, 'extensions');
+ 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;
+ }
+ }
+
+
+ public static function get_extra_config_sources($class = null) {
+ if($class === null) $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::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES);
+
+ if(!$extensions) {
+ return null;
+ }
+
+ // Build a list of all sources;
+ $sources = array();
+
+ foreach($extensions as $extension) {
+ list($extensionClass, $extensionArgs) = \Object::parse_class_spec($extension);
+ $sources[] = $extensionClass;
+
+ 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 if 1 argument supplied, the class name of the extension to
+ * check for; if 2 supplied, the class name to test
+ * @param string $requiredExtension used only if 2 arguments supplied
+ * @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) {
+ //BC support
+ if(func_num_args() > 1){
+ $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;
+ }
+ }
+
+ 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 $argument a single argument to pass
+ * @return mixed
+ * @todo integrate inheritance rules
+ */
+ public function invokeWithExtensions($method, $argument = null) {
+ $result = method_exists($this, $method) ? array($this->$method($argument)) : array();
+ $extras = $this->extend($method, $argument);
+
+ 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,... up to 7 arguments to be passed to the method
+ * @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();
+ }
+
+ if($this->extension_instances) foreach($this->extension_instances as $instance) {
+ if(method_exists($instance, $method)) {
+ $instance->setOwner($this);
+ $value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
+ if($value !== null) $values[] = $value;
+ $instance->clearOwner();
+ }
+ }
+
+ 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.
+ *
+ * @uses hasExtension()
+ *
+ * @param string $extension
+ * @return Extension
+ */
+ public function getExtensionInstance($extension) {
+ if($this->hasExtension($extension)) return $this->extension_instances[$extension];
+ }
+
+ /**
+ * 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()->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 isset($this->extension_instances[$extension]);
+ }
+
+ /**
+ * 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).
+ *
+ * @return array Map of {@link DataExtension} instances, keyed by classname.
+ */
+ public function getExtensionInstances() {
+ return $this->extension_instances;
+ }
+
+}
\ No newline at end of file
diff --git a/core/Injectable.php b/core/Injectable.php
new file mode 100644
index 000000000..2a280bcf0
--- /dev/null
+++ b/core/Injectable.php
@@ -0,0 +1,65 @@
+createWithArgs($class, $args);
+ }
+
+ /**
+ * Creates a class instance by the "singleton" design pattern.
+ * It will always return the same instance for this class,
+ * which can be used for performance reasons and as a simple
+ * way to access instance methods which don't rely on instance
+ * data (e.g. the custom SilverStripe static handling).
+ *
+ * @param string $className Optional classname (if called on Object directly)
+ * @return static The singleton instance
+ */
+ public static function singleton() {
+ $args = func_get_args();
+
+ // Singleton to create should be the calling class if not Object,
+ // otherwise the first parameter
+ $class = get_called_class();
+ if($class === 'Object') {
+ $class = array_shift($args);
+ }
+
+ return Injector::inst()->get($class);
+ }
+}
diff --git a/core/Object.php b/core/Object.php
index 5d332d5e8..df0649b1e 100755
--- a/core/Object.php
+++ b/core/Object.php
@@ -1,4 +1,9 @@
- * private static $extensions = array (
- * 'Hierarchy',
- * "Version('Stage', 'Live')"
- * );
- *
- *
- * Use {@link Object::add_extension()} to add extensions without access to the class code,
- * e.g. to extend core classes.
- *
- * Extensions are instanciated together with the object and stored in {@link $extension_instances}.
- *
- * @var array $extensions
- * @config
- */
- private static $extensions = null;
-
- private static
- $classes_constructed = array(),
- $extra_methods = array(),
- $built_in_methods = array();
-
- private static
- $custom_classes = array(),
- $strong_classes = array();
-
- /**#@-*/
+ use Extensible;
+ use Injectable;
+ use Configurable;
/**
* @var string the class name
*/
public $class;
- /**
- * Get a configuration accessor for this class. Short hand for Config::inst()->get($this->class, .....).
- * @return Config_ForClass|null
- */
- static public function config() {
- return Config::inst()->forClass(get_called_class());
- }
-
- /**
- * @var array all current extension instances.
- */
- protected $extension_instances = array();
-
- /**
- * List of callbacks to call prior to extensions having extend called on them,
- * each grouped by methodName.
- *
- * @var array[callable]
- */
- protected $beforeExtendCallbacks = 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;
- }
-
- /**
- * List of callbacks to call after extensions having extend called on them,
- * each grouped by methodName.
- *
- * @var array[callable]
- */
- protected $afterExtendCallbacks = array();
-
- /**
- * 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;
- }
-
- /**
- * An implementation of the factory method, allows you to create an instance of a class
- *
- * This method first for strong class overloads (singletons & DB interaction), then custom class overloads. If an
- * overload is found, an instance of this is returned rather than the original class. To overload a class, use
- * {@link Object::useCustomClass()}
- *
- * This can be called in one of two ways - either calling via the class directly,
- * or calling on Object and passing the class name as the first parameter. The following
- * are equivalent:
- * $list = DataList::create('SiteTree');
- * $list = SiteTree::get();
- *
- * @param string $class the class name
- * @param mixed $arguments,... arguments to pass to the constructor
- * @return static
- */
- public static function create() {
- $args = func_get_args();
-
- // Class to create should be the calling class if not Object,
- // otherwise the first parameter
- $class = get_called_class();
- if($class == 'Object') $class = array_shift($args);
-
- $class = self::getCustomClass($class);
-
- return Injector::inst()->createWithArgs($class, $args);
- }
-
- /**
- * Creates a class instance by the "singleton" design pattern.
- * It will always return the same instance for this class,
- * which can be used for performance reasons and as a simple
- * way to access instance methods which don't rely on instance
- * data (e.g. the custom SilverStripe static handling).
- *
- * @param string $className Optional classname (if called on Object directly)
- * @return static The singleton instance
- */
- public static function singleton() {
- $args = func_get_args();
-
- // Singleton to create should be the calling class if not Object,
- // otherwise the first parameter
- $class = get_called_class();
- if($class === 'Object') $class = array_shift($args);
-
- return Injector::inst()->get($class);
- }
-
private static $_cache_inst_args = array();
/**
@@ -175,30 +41,39 @@ abstract class Object {
* `Object::create_from_string("Versioned('Stage','Live')")` will return the result of
* `Versioned::create('Stage', 'Live);`
*
- * It is designed for simple, clonable objects. The first time this method is called for a given
+ * It is designed for simple, cloneable objects. The first time this method is called for a given
* string it is cached, and clones of that object are returned.
*
* If you pass the $firstArg argument, this will be prepended to the constructor arguments. It's
* impossible to pass null as the firstArg argument.
*
* `Object::create_from_string("Varchar(50)", "MyField")` will return the result of
- * `Vachar::create('MyField', '50');`
+ * `Varchar::create('MyField', '50');`
*
* Arguments are always strings, although this is a quirk of the current implementation rather
* than something that can be relied upon.
+ *
+ * @param string $classSpec
+ * @param mixed $firstArg
+ * @return object
*/
public static function create_from_string($classSpec, $firstArg = null) {
if(!isset(self::$_cache_inst_args[$classSpec.$firstArg])) {
// an $extension value can contain parameters as a string,
// e.g. "Versioned('Stage','Live')"
- if(strpos($classSpec,'(') === false) {
- if($firstArg === null) self::$_cache_inst_args[$classSpec.$firstArg] = Object::create($classSpec);
- else self::$_cache_inst_args[$classSpec.$firstArg] = Object::create($classSpec, $firstArg);
+ if(strpos($classSpec, '(') === false) {
+ if($firstArg === null) {
+ self::$_cache_inst_args[$classSpec.$firstArg] = Object::create($classSpec);
+ } else {
+ self::$_cache_inst_args[$classSpec.$firstArg] = Object::create($classSpec, $firstArg);
+ }
} else {
list($class, $args) = self::parse_class_spec($classSpec);
- if($firstArg !== null) array_unshift($args, $firstArg);
+ if($firstArg !== null) {
+ array_unshift($args, $firstArg);
+ }
array_unshift($args, $class);
self::$_cache_inst_args[$classSpec.$firstArg] = call_user_func_array(array('Object','create'), $args);
@@ -210,7 +85,11 @@ abstract class Object {
/**
* Parses a class-spec, such as "Versioned('Stage','Live')", as passed to create_from_string().
- * Returns a 2-elemnent array, with classname and arguments
+ * Returns a 2-element array, with classname and arguments
+ *
+ * @param string $classSpec
+ * @return array
+ * @throws Exception
*/
public static function parse_class_spec($classSpec) {
$tokens = token_get_all("createWithArgs($class, $args);
- }
-
- /**
- * This class allows you to overload classes with other classes when they are constructed using the factory method
- * {@link Object::create()}
- *
- * @param string $oldClass the class to replace
- * @param string $newClass the class to replace it with
- * @param bool $strong allows you to enforce a certain class replacement under all circumstances. This is used in
- * singletons and DB interaction classes
- */
- public static function useCustomClass($oldClass, $newClass, $strong = false) {
- if($strong) {
- self::$strong_classes[$oldClass] = $newClass;
- } else {
- self::$custom_classes[$oldClass] = $newClass;
- }
- }
-
- /**
- * If a class has been overloaded, get the class name it has been overloaded with - otherwise return the class name
- *
- * @param string $class the class to check
- * @return string the class that would be created if you called {@link Object::create()} with the class
- */
- public static function getCustomClass($class) {
- if(isset(self::$strong_classes[$class]) && ClassInfo::exists(self::$strong_classes[$class])) {
- return self::$strong_classes[$class];
- } elseif(isset(self::$custom_classes[$class]) && ClassInfo::exists(self::$custom_classes[$class])) {
- return self::$custom_classes[$class];
- }
-
- return $class;
- }
-
/**
* Get the value of a static property of a class, even in that property is declared protected (but not private),
* without any inheritance, merging or parent lookup if it doesn't exist on the given class.
@@ -393,458 +220,9 @@ abstract class Object {
return $default;
}
- /**
- * 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 if 1 argument supplied, the class name of the extension to
- * check for; if 2 supplied, the class name to test
- * @param string $requiredExtension used only if 2 arguments supplied
- * @param boolean $strict if the extension has to match the required extension and not be a subclass
- */
- public static function has_extension($classOrExtension, $requiredExtension = null, $strict = false) {
- //BC support
- if(func_num_args() > 1){
- $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;
- }
- }
-
- return false;
- }
-
- /**
- * 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')"
- */
- public static function add_extension($classOrExtension, $extension = null) {
- if(func_num_args() > 1) {
- $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, '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::$classes_constructed[$subclass]);
- unset(self::$extra_methods[$subclass]);
- }
-
- Config::inst()->update($class, 'extensions', array($extension));
- Config::inst()->extraConfigSourcesChanged($class);
-
- Injector::inst()->unregisterNamedObject($class);
-
- // load statics now for DataObject classes
- if(is_subclass_of($class, 'DataObject')) {
- if(!is_subclass_of($extensionClass, 'DataExtension')) {
- user_error("$extensionClass cannot be applied to $class without being a DataExtension", E_USER_ERROR);
- }
- }
- }
-
-
- /**
- * Remove an extension from a class.
- *
- * 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 Classname of an {@link Extension} subclass, without parameters
- */
- public static function remove_extension($extension) {
- $class = get_called_class();
-
- Config::inst()->remove($class, 'extensions', Config::anything(), $extension);
-
- // remove any instances of the extension with parameters
- $config = Config::inst()->get($class, 'extensions');
-
- if($config) {
- foreach($config as $k => $v) {
- // extensions with parameters will be stored in config as
- // ExtensionName("Param").
- if(preg_match(sprintf("/^(%s)\(/", preg_quote($extension, '/')), $v)) {
- Config::inst()->remove($class, 'extensions', Config::anything(), $v);
- }
- }
- }
-
- Config::inst()->extraConfigSourcesChanged($class);
-
- // unset singletons to avoid side-effects
- Injector::inst()->unregisterAllObjects();
-
- // unset some caches
- $subclasses = ClassInfo::subclassesFor($class);
- $subclasses[] = $class;
- if($subclasses) foreach($subclasses as $subclass) {
- unset(self::$classes_constructed[$subclass]);
- unset(self::$extra_methods[$subclass]);
- }
- }
-
- /**
- * @param string $class
- * @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} classnames,
- * or eval'ed classname strings with constructor arguments.
- */
- public static function get_extensions($class, $includeArgumentString = false) {
- $extensions = Config::inst()->get($class, 'extensions');
- 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;
- }
- }
-
- // --------------------------------------------------------------------------------------------------------------
-
- private static $unextendable_classes = array('Object', 'ViewableData', 'RequestHandler');
-
- static public function get_extra_config_sources($class = null) {
- if($class === null) $class = get_called_class();
-
- // If this class is unextendable, NOP
- if(in_array($class, self::$unextendable_classes)) return;
-
- // Variable to hold sources in
- $sources = null;
-
- // Get a list of extensions
- $extensions = Config::inst()->get($class, 'extensions', Config::UNINHERITED | Config::EXCLUDE_EXTRA_SOURCES);
-
- if($extensions) {
- // Build a list of all sources;
- $sources = array();
-
- foreach($extensions as $extension) {
- list($extensionClass, $extensionArgs) = self::parse_class_spec($extension);
- $sources[] = $extensionClass;
-
- if(!ClassInfo::has_method_from($extensionClass, 'add_to_class', 'Extension')) {
- Deprecation::notice('4.0',
- "add_to_class deprecated on $extensionClass. Use get_extra_config instead");
- }
-
- 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;
- }
-
public function __construct() {
$this->class = get_class($this);
-
- foreach(ClassInfo::ancestry(get_called_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) {
- $instance = self::create_from_string($extension);
- $instance->setOwner(null, $class);
- $this->extension_instances[$instance->class] = $instance;
- }
- }
-
- if(!isset(self::$classes_constructed[$this->class])) {
- $this->defineMethods();
- self::$classes_constructed[$this->class] = true;
- }
- }
-
- /**
- * Attemps to locate and call a method dynamically added to a class at runtime if a default cannot be located
- *
- * You can add extra methods to a class using {@link Extensions}, {@link Object::createMethod()} or
- * {@link Object::addWrapperMethod()}
- *
- * @param string $method
- * @param array $arguments
- * @return mixed
- */
- public function __call($method, $arguments) {
- // If the method cache was cleared by an an Object::add_extension() / Object::remove_extension()
- // call, then we should rebuild it.
- if(empty(self::$extra_methods[get_class($this)])) {
- $this->defineMethods();
- }
-
- $method = strtolower($method);
-
- if(isset(self::$extra_methods[$this->class][$method])) {
- $config = self::$extra_methods[$this->class][$method];
-
- switch(true) {
- case isset($config['property']) :
- $obj = $config['index'] !== null ?
- $this->{$config['property']}[$config['index']] :
- $this->{$config['property']};
-
- if($obj) {
- if(!empty($config['callSetOwnerFirst'])) $obj->setOwner($this);
- $retVal = call_user_func_array(array($obj, $method), $arguments);
- if(!empty($config['callSetOwnerFirst'])) $obj->clearOwner();
- return $retVal;
- }
-
- if($this->destroyed) {
- throw new Exception (
- "Object->__call(): attempt to call $method on a destroyed $this->class object"
- );
- } else {
- throw new Exception (
- "Object->__call(): $this->class cannot pass control to $config[property]($config[index])."
- . ' Perhaps this object was mistakenly destroyed?'
- );
- }
-
- case isset($config['wrap']) :
- array_unshift($arguments, $config['method']);
- return call_user_func_array(array($this, $config['wrap']), $arguments);
-
- case isset($config['function']) :
- return $config['function']($this, $arguments);
-
- default :
- throw new Exception (
- "Object->__call(): extra method $method is invalid on $this->class:"
- . var_export($config, true)
- );
- }
- } else {
- // Please do not change the exception code number below.
- $class = get_class($this);
- throw new Exception("Object->__call(): the method '$method' does not exist on '$class'", 2175);
- }
- }
-
- // --------------------------------------------------------------------------------------------------------------
-
- /**
- * Return TRUE if a method exists on this object
- *
- * This should be used rather than PHP's inbuild method_exists() as it takes into account methods added via
- * extensions
- *
- * @param string $method
- * @return bool
- */
- public function hasMethod($method) {
- return method_exists($this, $method) || isset(self::$extra_methods[$this->class][strtolower($method)]);
- }
-
- /**
- * Return the names of all the methods available on this object
- *
- * @param bool $custom include methods added dynamically at runtime
- * @return array
- */
- public function allMethodNames($custom = false) {
- if(!isset(self::$built_in_methods[$this->class])) {
- self::$built_in_methods[$this->class] = array_map('strtolower', get_class_methods($this));
- }
-
- if($custom && isset(self::$extra_methods[$this->class])) {
- return array_merge(self::$built_in_methods[$this->class], array_keys(self::$extra_methods[$this->class]));
- } else {
- return self::$built_in_methods[$this->class];
- }
- }
-
- /**
- * 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 addMethodsFrom()
- */
- protected function defineMethods() {
- if($this->extension_instances) foreach(array_keys($this->extension_instances) as $key) {
- $this->addMethodsFrom('extension_instances', $key);
- }
-
- if(isset($_REQUEST['debugmethods']) && isset(self::$built_in_methods[$this->class])) {
- Debug::require_developer_login();
-
- echo 'Methods defined on ' . $this->class . '
';
- foreach(self::$built_in_methods[$this->class] as $method) {
- echo "- $method
";
- }
- echo '
';
- }
- }
-
- /**
- * Add all the methods from an object property (which is an {@link Extension}) to this object.
- *
- * @param string $property the property name
- * @param string|int $index an index to use if the property is an array
- */
- protected function addMethodsFrom($property, $index = null) {
- $extension = ($index !== null) ? $this->{$property}[$index] : $this->$property;
-
- if(!$extension) {
- throw new InvalidArgumentException (
- "Object->addMethodsFrom(): could not add methods from {$this->class}->{$property}[$index]"
- );
- }
-
- if(method_exists($extension, 'allMethodNames')) {
- $methods = $extension->allMethodNames(true);
-
- } else {
- if(!isset(self::$built_in_methods[$extension->class])) {
- self::$built_in_methods[$extension->class] = array_map('strtolower', get_class_methods($extension));
- }
- $methods = self::$built_in_methods[$extension->class];
- }
-
- if($methods) {
- $methodInfo = array(
- 'property' => $property,
- 'index' => $index,
- 'callSetOwnerFirst' => $extension instanceof Extension,
- );
-
- $newMethods = array_fill_keys($methods, $methodInfo);
-
- if(isset(self::$extra_methods[$this->class])) {
- self::$extra_methods[$this->class] =
- array_merge(self::$extra_methods[$this->class], $newMethods);
- } else {
- self::$extra_methods[$this->class] = $newMethods;
- }
- }
- }
-
- /**
- * Add a wrapper method - a method which points to another method with a different name. For example, Thumbnail(x)
- * can be wrapped to generateThumbnail(x)
- *
- * @param string $method the method name to wrap
- * @param string $wrap the method name to wrap to
- */
- protected function addWrapperMethod($method, $wrap) {
- self::$extra_methods[$this->class][strtolower($method)] = array (
- 'wrap' => $wrap,
- 'method' => $method
- );
- }
-
- /**
- * Add an extra method using raw PHP code passed as a string
- *
- * @param string $method the method name
- * @param string $code the PHP code - arguments will be in an array called $args, while you can access this object
- * by using $obj. Note that you cannot call protected methods, as the method is actually an external
- * function
- */
- protected function createMethod($method, $code) {
- self::$extra_methods[$this->class][strtolower($method)] = array (
- 'function' => create_function('$obj, $args', $code)
- );
- }
-
- // --------------------------------------------------------------------------------------------------------------
-
- /**
- * @see Object::get_static()
- */
- public function stat($name, $uncached = false) {
- return Config::inst()->get(($this->class ? $this->class : get_class($this)), $name, Config::FIRST_SET);
- }
-
- /**
- * @see Object::set_static()
- */
- public function set_stat($name, $value) {
- Config::inst()->update(($this->class ? $this->class : get_class($this)), $name, $value);
- }
-
- /**
- * @see Object::uninherited_static()
- */
- public function uninherited($name) {
- return Config::inst()->get(($this->class ? $this->class : get_class($this)), $name, Config::UNINHERITED);
+ $this->constructExtensions();
}
// --------------------------------------------------------------------------------------------------------------
@@ -885,203 +263,4 @@ abstract class Object {
return $this->class;
}
- // --------------------------------------------------------------------------------------------------------------
-
- /**
- * 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 $argument a single argument to pass
- * @return mixed
- * @todo integrate inheritance rules
- */
- public function invokeWithExtensions($method, $argument = null) {
- $result = method_exists($this, $method) ? array($this->$method($argument)) : array();
- $extras = $this->extend($method, $argument);
-
- 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,... up to 7 arguments to be passed to the method
- * @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();
- }
-
- if($this->extension_instances) foreach($this->extension_instances as $instance) {
- if(method_exists($instance, $method)) {
- $instance->setOwner($this);
- $value = $instance->$method($a1, $a2, $a3, $a4, $a5, $a6, $a7);
- if($value !== null) $values[] = $value;
- $instance->clearOwner();
- }
- }
-
- 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.
- *
- * @uses hasExtension()
- *
- * @param string $extension
- * @return Extension
- */
- public function getExtensionInstance($extension) {
- if($this->hasExtension($extension)) return $this->extension_instances[$extension];
- }
-
- /**
- * 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()->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 isset($this->extension_instances[$extension]);
- }
-
- /**
- * 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).
- *
- * @return array Map of {@link DataExtension} instances, keyed by classname.
- */
- public function getExtensionInstances() {
- return $this->extension_instances;
- }
-
- // --------------------------------------------------------------------------------------------------------------
-
- /**
- * Cache the results of an instance method in this object to a file, or if it is already cache return the cached
- * results
- *
- * @param string $method the method name to cache
- * @param int $lifetime the cache lifetime in seconds
- * @param string $ID custom cache ID to use
- * @param array $arguments an optional array of arguments
- * @return mixed the cached data
- */
- public function cacheToFile($method, $lifetime = 3600, $ID = false, $arguments = array()) {
- Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
-
- if(!$this->hasMethod($method)) {
- throw new InvalidArgumentException("Object->cacheToFile(): the method $method does not exist to cache");
- }
-
- $cacheName = $this->class . '_' . $method;
-
- if(!is_array($arguments)) $arguments = array($arguments);
-
- if($ID) $cacheName .= '_' . $ID;
- if(count($arguments)) $cacheName .= '_' . md5(serialize($arguments));
-
- $data = $this->loadCache($cacheName, $lifetime);
-
- if($data !== false) {
- return $data;
- }
-
- $data = call_user_func_array(array($this, $method), $arguments);
- $this->saveCache($cacheName, $data);
-
- return $data;
- }
-
- /**
- * Clears the cache for the given cacheToFile call
- */
- public function clearCache($method, $ID = false, $arguments = array()) {
- Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
-
- $cacheName = $this->class . '_' . $method;
- if(!is_array($arguments)) $arguments = array($arguments);
- if($ID) $cacheName .= '_' . $ID;
- if(count($arguments)) $cacheName .= '_' . md5(serialize($arguments));
-
- $file = TEMP_FOLDER . '/' . $this->sanitiseCachename($cacheName);
- if(file_exists($file)) unlink($file);
- }
-
- /**
- * Loads a cache from the filesystem if a valid on is present and within the specified lifetime
- *
- * @param string $cache the cache name
- * @param int $lifetime the lifetime (in seconds) of the cache before it is invalid
- * @return mixed
- */
- protected function loadCache($cache, $lifetime = 3600) {
- Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
-
- $path = TEMP_FOLDER . '/' . $this->sanitiseCachename($cache);
-
- if(!isset($_REQUEST['flush']) && file_exists($path) && (filemtime($path) + $lifetime) > time()) {
- return unserialize(file_get_contents($path));
- }
-
- return false;
- }
-
- /**
- * Save a piece of cached data to the file system
- *
- * @param string $cache the cache name
- * @param mixed $data data to save (must be serializable)
- */
- protected function saveCache($cache, $data) {
- Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
- file_put_contents(TEMP_FOLDER . '/' . $this->sanitiseCachename($cache), serialize($data));
- }
-
- /**
- * Strip a file name of special characters so it is suitable for use as a cache file name
- *
- * @param string $name
- * @return string the name with all special cahracters replaced with underscores
- */
- protected function sanitiseCachename($name) {
- Deprecation::notice('4.0', 'Caching methods on Object have been deprecated. Use the SS_Cache API instead.');
- return str_replace(array('~', '.', '/', '!', ' ', "\n", "\r", "\t", '\\', ':', '"', '\'', ';'), '_', $name);
- }
-
}
diff --git a/docs/en/02_Developer_Guides/07_Debugging/02_URL_Variable_Tools.md b/docs/en/02_Developer_Guides/07_Debugging/02_URL_Variable_Tools.md
index 3e9bb0386..087d00172 100644
--- a/docs/en/02_Developer_Guides/07_Debugging/02_URL_Variable_Tools.md
+++ b/docs/en/02_Developer_Guides/07_Debugging/02_URL_Variable_Tools.md
@@ -33,7 +33,6 @@ Append the option and corresponding value to your URL in your browser's address
| URL Variable | | Values | | Description |
| ------------ | | ------ | | ----------- |
- | debugmethods | | 1 | | Shows all methods available when an object is constructed (useful when extending classes or using object extensions) |
| debugfailover | | 1 | | Shows failover methods from classes extended |
## Database
diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md
index e34d03e75..cab647353 100644
--- a/docs/en/04_Changelogs/4.0.0.md
+++ b/docs/en/04_Changelogs/4.0.0.md
@@ -23,6 +23,7 @@
* `Upload::load` now stores assets directly without saving into a `File` dataobject.
* Protected file storage is now a core Framework API. See [/developer_guides/files/file_security] for
more information.
+ * `Object::useCustomClass` has been removed. You should use the config API with Injector instead.
## New API
@@ -38,6 +39,10 @@
* `AssetControlExtension` is applied by default to all DataObjects, in order to support the management
of linked assets and file protection.
* `ProtectedFileController` class is used to serve up protected assets.
+ * `Object` has been broken up into various traits, each of which can be added to other objects independently:
+ * `Configurable` Provides Config API helper methods
+ * `Injectable` Provides Injector API helper methods
+ * `Extensible` Allows extensions to be applied
### Front-end build tooling for CMS interface
@@ -63,6 +68,10 @@ or developing your own website. These improvements are mainly geared at CMS core
## Deprecated classes/methods
+### Core
+
+ * `debugmethods` querystring argument has been removed from debugging.
+
### ORM
* `DataList::getRelation` is removed, as it was mutable. Use `DataList::applyRelation` instead, which is immutable.
diff --git a/tests/core/ObjectTest.php b/tests/core/ObjectTest.php
index 487affb97..d9b518282 100644
--- a/tests/core/ObjectTest.php
+++ b/tests/core/ObjectTest.php
@@ -118,9 +118,6 @@ class ObjectTest extends SapphireTest {
public function testCreateWithArgs() {
$createdObj = ObjectTest_CreateTest::create('arg1', 'arg2', array(), null, 'arg5');
$this->assertEquals($createdObj->constructArguments, array('arg1', 'arg2', array(), null, 'arg5'));
-
- $strongObj = Object::strong_create('ObjectTest_CreateTest', 'arg1', 'arg2', array(), null, 'arg5');
- $this->assertEquals($strongObj->constructArguments, array('arg1', 'arg2', array(), null, 'arg5'));
}
public function testCreateLateStaticBinding() {
@@ -128,28 +125,6 @@ class ObjectTest extends SapphireTest {
$this->assertEquals($createdObj->constructArguments, array('arg1', 'arg2', array(), null, 'arg5'));
}
- /**
- * Tests that {@link Object::useCustomClass()} correnctly replaces normal and strong objects
- */
- public function testUseCustomClass() {
- $obj1 = ObjectTest_CreateTest::create();
- $this->assertTrue($obj1 instanceof ObjectTest_CreateTest);
-
- Object::useCustomClass('ObjectTest_CreateTest', 'ObjectTest_CreateTest2');
- $obj2 = ObjectTest_CreateTest::create();
- $this->assertTrue($obj2 instanceof ObjectTest_CreateTest2);
-
- $obj2_2 = Object::strong_create('ObjectTest_CreateTest');
- $this->assertTrue($obj2_2 instanceof ObjectTest_CreateTest);
-
- Object::useCustomClass('ObjectTest_CreateTest', 'ObjectTest_CreateTest3', true);
- $obj3 = ObjectTest_CreateTest::create();
- $this->assertTrue($obj3 instanceof ObjectTest_CreateTest3);
-
- $obj3_2 = Object::strong_create('ObjectTest_CreateTest');
- $this->assertTrue($obj3_2 instanceof ObjectTest_CreateTest3);
- }
-
/**
* Tests {@link Object::singleton()}
*/
diff --git a/tests/model/DataListTest.php b/tests/model/DataListTest.php
index 889c61fdc..68144adc2 100755
--- a/tests/model/DataListTest.php
+++ b/tests/model/DataListTest.php
@@ -10,6 +10,7 @@ class DataListTest extends SapphireTest {
protected static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array(
+ // From DataObjectTest
'DataObjectTest_Team',
'DataObjectTest_Fixture',
'DataObjectTest_SubTeam',
@@ -19,12 +20,18 @@ class DataListTest extends SapphireTest {
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
- 'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
+ 'DataObjectTest\RelationClass',
+ 'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_Company',
+ 'DataObjectTest_Staff',
+ 'DataObjectTest_CEO',
'DataObjectTest_Fan',
+ 'DataObjectTest_Play',
+ 'DataObjectTest_Ploy',
+ 'DataObjectTest_Bogey',
);
public function testFilterDataObjectByCreatedDate() {
diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php
index 95e6bb02d..77c9252c6 100644
--- a/tests/model/DataObjectTest.php
+++ b/tests/model/DataObjectTest.php
@@ -1792,7 +1792,7 @@ class DataObjectTest_Fixture extends DataObject implements TestOnly {
// Field types
'DateField' => 'Date',
- 'DatetimeField' => 'Datetime',
+ 'DatetimeField' => 'SS_Datetime',
'MyFieldWithDefault' => 'Varchar',
'MyFieldWithAltDefault' => 'Varchar'
diff --git a/tests/model/HasManyListTest.php b/tests/model/HasManyListTest.php
index 100b861f2..35bc8cd63 100644
--- a/tests/model/HasManyListTest.php
+++ b/tests/model/HasManyListTest.php
@@ -6,10 +6,28 @@ class HasManyListTest extends SapphireTest {
protected static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array(
+ // From DataObjectTest
'DataObjectTest_Team',
+ 'DataObjectTest_Fixture',
'DataObjectTest_SubTeam',
+ 'OtherSubclassWithSameField',
+ 'DataObjectTest_FieldlessTable',
+ 'DataObjectTest_FieldlessSubTable',
+ 'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
'DataObjectTest_TeamComment',
+ 'DataObjectTest_EquipmentCompany',
+ 'DataObjectTest_SubEquipmentCompany',
+ 'DataObjectTest\NamespacedClass',
+ 'DataObjectTest\RelationClass',
+ 'DataObjectTest_ExtendedTeamComment',
+ 'DataObjectTest_Company',
+ 'DataObjectTest_Staff',
+ 'DataObjectTest_CEO',
+ 'DataObjectTest_Fan',
+ 'DataObjectTest_Play',
+ 'DataObjectTest_Ploy',
+ 'DataObjectTest_Bogey',
);
public function testRelationshipEmptyOnNewRecords() {
diff --git a/tests/model/ManyManyListTest.php b/tests/model/ManyManyListTest.php
index 8d2fcb0cc..1ff7de4bc 100644
--- a/tests/model/ManyManyListTest.php
+++ b/tests/model/ManyManyListTest.php
@@ -9,6 +9,7 @@ class ManyManyListTest extends SapphireTest {
protected static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array(
+ // From DataObjectTest
'DataObjectTest_Team',
'DataObjectTest_Fixture',
'DataObjectTest_SubTeam',
@@ -27,6 +28,10 @@ class ManyManyListTest extends SapphireTest {
'DataObjectTest_Staff',
'DataObjectTest_CEO',
'DataObjectTest_Fan',
+ 'DataObjectTest_Play',
+ 'DataObjectTest_Ploy',
+ 'DataObjectTest_Bogey',
+ // From ManyManyListTest
'ManyManyListTest_ExtraFields'
);
diff --git a/tests/model/MapTest.php b/tests/model/MapTest.php
index 43ca7cd19..7590450f0 100755
--- a/tests/model/MapTest.php
+++ b/tests/model/MapTest.php
@@ -10,6 +10,7 @@ class SS_MapTest extends SapphireTest {
protected static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array(
+ // From DataObjectTest
'DataObjectTest_Team',
'DataObjectTest_Fixture',
'DataObjectTest_SubTeam',
@@ -18,7 +19,19 @@ class SS_MapTest extends SapphireTest {
'DataObjectTest_FieldlessSubTable',
'DataObjectTest_ValidatedObject',
'DataObjectTest_Player',
- 'DataObjectTest_TeamComment'
+ 'DataObjectTest_TeamComment',
+ 'DataObjectTest_EquipmentCompany',
+ 'DataObjectTest_SubEquipmentCompany',
+ 'DataObjectTest\NamespacedClass',
+ 'DataObjectTest\RelationClass',
+ 'DataObjectTest_ExtendedTeamComment',
+ 'DataObjectTest_Company',
+ 'DataObjectTest_Staff',
+ 'DataObjectTest_CEO',
+ 'DataObjectTest_Fan',
+ 'DataObjectTest_Play',
+ 'DataObjectTest_Ploy',
+ 'DataObjectTest_Bogey',
);
diff --git a/tests/model/PaginatedListTest.php b/tests/model/PaginatedListTest.php
index 419079ee7..eb9ac4d41 100644
--- a/tests/model/PaginatedListTest.php
+++ b/tests/model/PaginatedListTest.php
@@ -10,9 +10,28 @@ class PaginatedListTest extends SapphireTest {
protected static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array(
+ // From DataObjectTest
'DataObjectTest_Team',
+ 'DataObjectTest_Fixture',
'DataObjectTest_SubTeam',
- 'DataObjectTest_Player'
+ 'OtherSubclassWithSameField',
+ 'DataObjectTest_FieldlessTable',
+ 'DataObjectTest_FieldlessSubTable',
+ 'DataObjectTest_ValidatedObject',
+ 'DataObjectTest_Player',
+ 'DataObjectTest_TeamComment',
+ 'DataObjectTest_EquipmentCompany',
+ 'DataObjectTest_SubEquipmentCompany',
+ 'DataObjectTest\NamespacedClass',
+ 'DataObjectTest\RelationClass',
+ 'DataObjectTest_ExtendedTeamComment',
+ 'DataObjectTest_Company',
+ 'DataObjectTest_Staff',
+ 'DataObjectTest_CEO',
+ 'DataObjectTest_Fan',
+ 'DataObjectTest_Play',
+ 'DataObjectTest_Ploy',
+ 'DataObjectTest_Bogey',
);
public function testPageStart() {
diff --git a/tests/model/PolymorphicHasManyListTest.php b/tests/model/PolymorphicHasManyListTest.php
index 81cb32dd6..f31316c3b 100644
--- a/tests/model/PolymorphicHasManyListTest.php
+++ b/tests/model/PolymorphicHasManyListTest.php
@@ -16,6 +16,7 @@ class PolymorphicHasManyListTest extends SapphireTest {
protected static $fixture_file = 'DataObjectTest.yml';
protected $extraDataObjects = array(
+ // From DataObjectTest
'DataObjectTest_Team',
'DataObjectTest_Fixture',
'DataObjectTest_SubTeam',
@@ -34,6 +35,9 @@ class PolymorphicHasManyListTest extends SapphireTest {
'DataObjectTest_Staff',
'DataObjectTest_CEO',
'DataObjectTest_Fan',
+ 'DataObjectTest_Play',
+ 'DataObjectTest_Ploy',
+ 'DataObjectTest_Bogey',
);
public function testRelationshipEmptyOnNewRecords() {