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 . '

'; - } - } - - /** - * 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() {