API CHANGE: Replaced eval based creation of extension and field objects with Object::create_from_string().

API CHANGE: Introduced new function Object::create_from_string() to instantiate an object from a string like 'Int(50)' (from r101093)

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@111585 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Sam Minnee 2010-10-04 04:46:41 +00:00
parent deb88d1143
commit cc355a4f18
6 changed files with 137 additions and 52 deletions

View File

@ -285,7 +285,7 @@ class RSSFeed_Entry extends ViewableData {
*/ */
function rssField($fieldName, $defaultClass = 'Varchar') { function rssField($fieldName, $defaultClass = 'Varchar') {
if($fieldName) { if($fieldName) {
if($this->failover->castingHelperPair($fieldName)) { if($this->failover->castingHelper($fieldName)) {
$value = $this->failover->$fieldName; $value = $this->failover->$fieldName;
$obj = $this->failover->obj($fieldName); $obj = $this->failover->obj($fieldName);
$obj->setValue($value); $obj->setValue($value);

View File

@ -96,6 +96,100 @@ abstract class Object {
} }
} }
private static $_cache_inst_args = array();
/**
* Create an object from a string representation. It treats it as a PHP constructor without the
* 'new' keyword. It also manages to construct the object without the use of eval().
*
* Construction itself is done with Object::create(), so that Object::useCustomClass() calls
* are respected.
*
* `Object::create_from_string("Versioned('Stage','Live')")` will return the result of
* `Object::create('Versioned', 'Stage', 'Live);`
*
* It is designed for simple, clonable 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
* `Object::create('Vachar', 'MyField', '50');`
*
* Arguments are always strings, although this is a quirk of the current implementation rather
* than something that can be relied upon.
*/
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);
} else {
list($class, $args) = self::parse_class_spec($classSpec);
if($firstArg !== null) array_unshift($args, $firstArg);
array_unshift($args, $class);
self::$_cache_inst_args[$classSpec.$firstArg] = call_user_func_array('Object::create', $args);
}
}
return clone self::$_cache_inst_args[$classSpec.$firstArg];
}
/**
* Parses a class-spec, such as "Versioned('Stage','Live')", as passed to create_from_string().
* Returns a 2-elemnent array, with classname and arguments
*/
static function parse_class_spec($classSpec) {
$tokens = token_get_all("<?php $classSpec");
$class = null;
$args = array();
$passedBracket = false;
foreach($tokens as $token) {
$tName = is_array($token) ? $token[0] : $token;
// Get the class naem
if($class == null && is_array($token) && $token[0] == T_STRING) {
$class = $token[1];
// Get arguments
} else if(is_array($token)) {
switch($token[0]) {
case T_CONSTANT_ENCAPSED_STRING:
$argString = $token[1];
switch($argString[0]) {
case '"': $argString = stripcslashes(substr($argString,1,-1)); break;
case "'": $argString = str_replace(array("\\\\", "\\'"),array("\\", "'"), substr($argString,1,-1)); break;
default: throw new Exception("Bad T_CONSTANT_ENCAPSED_STRING arg $argString");
}
$args[] = $argString;
break;
case T_DNUMBER:
$args[] = (double)$token[1];
break;
case T_LNUMBER:
$args[] = (int)$token[1];
break;
case T_STRING:
switch($token[1]) {
case 'true': $args[] = true; break;
case 'false': $args[] = false; break;
default: throw new Exception("Bad T_STRING arg '{$token[1]}'");
}
}
}
}
return array($class, $args);
}
/** /**
* Similar to {@link Object::create()}, except that classes are only overloaded if you set the $strong parameter to * Similar to {@link Object::create()}, except that classes are only overloaded if you set the $strong parameter to
* TRUE when using {@link Object::useCustomClass()} * TRUE when using {@link Object::useCustomClass()}
@ -535,11 +629,7 @@ abstract class Object {
if($extensions = self::uninherited_static($class, 'extensions')) { if($extensions = self::uninherited_static($class, 'extensions')) {
foreach($extensions as $extension) { foreach($extensions as $extension) {
// an $extension value can contain parameters as a string, $instance = self::create_from_string($extension);
// e.g. "Versioned('Stage','Live')"
if(strpos($extension,'(') === false) $instance = new $extension();
else $instance = eval("return new $extension;");
$instance->setOwner(null, $class); $instance->setOwner(null, $class);
$this->extension_instances[$instance->class] = $instance; $this->extension_instances[$instance->class] = $instance;
} }
@ -552,7 +642,6 @@ abstract class Object {
} }
} }
/** /**
* Attemps to locate and call a method dynamically added to a class at runtime if a default cannot be located * Attemps to locate and call a method dynamically added to a class at runtime if a default cannot be located
* *

View File

@ -74,11 +74,7 @@ class ViewableData extends Object implements IteratorAggregate {
* @return string * @return string
*/ */
public static function castingObjectCreator($fieldSchema) { public static function castingObjectCreator($fieldSchema) {
if(strpos($fieldSchema, '(') === false) { user_error("Deprecated in a breaking way, use Object::create_from_string()", E_USER_WARNING);
return "return Object::create('{$fieldSchema}', \$fieldName);";
} else {
return 'return Object::create(' . preg_replace('/^([^(]+)\(/', '\'$1\', $fieldName, ', $fieldSchema) . ';';
}
} }
/** /**
@ -89,21 +85,7 @@ class ViewableData extends Object implements IteratorAggregate {
* @return array * @return array
*/ */
public static function castingObjectCreatorPair($fieldSchema) { public static function castingObjectCreatorPair($fieldSchema) {
if(strpos($fieldSchema, '(') === false) { user_error("Deprecated in a breaking way, use Object::create_from_string()", E_USER_WARNING);
return array (
'className' => $fieldSchema,
'castingHelper' => self::castingObjectCreator($fieldSchema)
);
}
if(preg_match('/^([^(]+)\(/', $fieldSchema, $parts)) {
return array (
'className' => $parts[1],
'castingHelper' => self::castingObjectCreator($fieldSchema)
);
}
throw new InvalidArgumentException("ViewableData::castingObjectCreatorPair(): bad field schema '$fieldSchema'");
} }
// FIELD GETTERS & SETTERS ----------------------------------------------------------------------------------------- // FIELD GETTERS & SETTERS -----------------------------------------------------------------------------------------
@ -251,15 +233,8 @@ class ViewableData extends Object implements IteratorAggregate {
* @return array * @return array
*/ */
public function castingHelperPair($field) { public function castingHelperPair($field) {
if(!isset(self::$casting_cache[$this->class])) { user_error("castingHelperPair() Deprecated, use castingHelper() instead", E_USER_NOTICE);
if($this->failover) { return $this->castingHelper($field);
$this->failover->buildCastingCache(self::$casting_cache[$this->class]);
}
$this->buildCastingCache(self::$casting_cache[$this->class]);
}
if(isset(self::$casting_cache[$this->class][$field])) return self::$casting_cache[$this->class][$field];
} }
/** /**
@ -270,7 +245,12 @@ class ViewableData extends Object implements IteratorAggregate {
* @return string * @return string
*/ */
public function castingHelper($field) { public function castingHelper($field) {
if($pair = $this->castingHelperPair($field)) return $pair['castingHelper']; if($this instanceof DataObject && ($fieldSpec = $this->db($field))) {
return $fieldSpec;
}
$specs = Object::combined_static(get_class($this), 'casting');
if(isset($specs[$field])) return $specs[$field];
} }
/** /**
@ -280,7 +260,12 @@ class ViewableData extends Object implements IteratorAggregate {
* @return string * @return string
*/ */
public function castingClass($field) { public function castingClass($field) {
if($pair = $this->castingHelperPair($field)) return $pair['className']; $spec = $this->castingHelper($field);
if(!$spec) return null;
$bPos = strpos($spec,'(');
if($bPos === false) return $spec;
else return substr($spec, 0, $bPos-1);
} }
/** /**
@ -386,10 +371,10 @@ class ViewableData extends Object implements IteratorAggregate {
if(!is_object($value) && ($this->castingClass($fieldName) || $forceReturnedObject)) { if(!is_object($value) && ($this->castingClass($fieldName) || $forceReturnedObject)) {
if(!$castConstructor = $this->castingHelper($fieldName)) { if(!$castConstructor = $this->castingHelper($fieldName)) {
$castConstructor = self::castingObjectCreator($this->stat('default_cast')); $castConstructor = $this->stat('default_cast');
} }
$valueObject = eval($castConstructor); $valueObject = Object::create_from_string($castConstructor, $fieldName);
$valueObject->setValue($value, ($this->hasMethod('getAllFields') ? $this->getAllFields() : null)); $valueObject->setValue($value, ($this->hasMethod('getAllFields') ? $this->getAllFields() : null));
$value = $valueObject; $value = $valueObject;

View File

@ -2007,10 +2007,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
// Otherwise, we need to determine if this is a complex field // Otherwise, we need to determine if this is a complex field
if(self::is_composite_field($this->class, $field)) { if(self::is_composite_field($this->class, $field)) {
$helperPair = $this->castingHelperPair($field); $helper = $this->castingHelper($field);
$constructor = $helperPair['castingHelper']; $fieldObj = Object::create_from_string($helper, $field);
$fieldName = $field;
$fieldObj = eval($constructor);
// write value only if either the field value exists, // write value only if either the field value exists,
// or a valid record has been loaded from the database // or a valid record has been loaded from the database
@ -2156,7 +2154,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
$castingHelper = $this->castingHelper($fieldName); $castingHelper = $this->castingHelper($fieldName);
if($castingHelper) { if($castingHelper) {
$fieldObj = eval($castingHelper); $fieldObj = Object::create_from_string($castingHelper, $fieldName);
$fieldObj->setValue($val); $fieldObj->setValue($val);
$fieldObj->saveInto($this); $fieldObj->saveInto($this);
} else { } else {
@ -2409,9 +2407,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return new PrimaryKey($fieldName, $this); return new PrimaryKey($fieldName, $this);
// General casting information for items in $db or $casting // General casting information for items in $db or $casting
} else if($helperPair = $this->castingHelperPair($fieldName)) { } else if($helper = $this->castingHelper($fieldName)) {
$constructor = $helperPair['castingHelper']; $obj = Object::create_from_string($helper, $fieldName);
$obj = eval($constructor);
$obj->setValue($this->$fieldName, $this->record, false); $obj->setValue($this->$fieldName, $this->record, false);
return $obj; return $obj;
@ -2445,8 +2442,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$component = singleton($rel); $component = singleton($rel);
} elseif ($rel = $component->many_many($relation)) { } elseif ($rel = $component->many_many($relation)) {
$component = singleton($rel[1]); $component = singleton($rel[1]);
} elseif($info = $this->castingHelperPair($relation)) { } elseif($className = $this->castingClass($relation)) {
$component = singleton($info['className']); $component = $className;
} }
} }

View File

@ -313,7 +313,7 @@ abstract class SS_Database {
$fieldSpec=substr($fieldSpec, 0, $pos); $fieldSpec=substr($fieldSpec, 0, $pos);
} }
$fieldObj = eval(ViewableData::castingObjectCreator($fieldSpec)); $fieldObj = Object::create_from_string($fieldSpec, $fieldName);
$fieldObj->arrayValue=$arrayValue; $fieldObj->arrayValue=$arrayValue;
$fieldObj->setTable($table); $fieldObj->setTable($table);

View File

@ -336,6 +336,20 @@ class ObjectTest extends SapphireTest {
); );
} }
public function testParseClassSpec() {
$this->assertEquals(
array('Versioned',array('Stage', 'Live')),
Object::parse_class_spec("Versioned('Stage','Live')")
);
$this->assertEquals(
array('Versioned',array('Stage,Live', 'Stage')),
Object::parse_class_spec("Versioned('Stage,Live','Stage')")
);
$this->assertEquals(
array('Versioned',array('Stage\'Stage,Live\'Live', 'Live')),
Object::parse_class_spec("Versioned('Stage\'Stage,Live\'Live','Live')")
);
}
} }
/**#@+ /**#@+