FEATURE: Allow Text/Varchar fields to be configured to differentiate between NULL and empty string. (#4178, petebd)

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@90036 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Andrew O'Neil 2009-10-23 01:29:55 +00:00
parent 813760108c
commit 8679fd9883
8 changed files with 511 additions and 27 deletions

View File

@ -3419,6 +3419,21 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
return $entities;
}
/**
* Returns true if the given method/parameter has a value
* (Uses the DBField::hasValue if the parameter is a database field)
* @param string $funcName The function name.
* @param array $args The arguments.
* @return boolean
*/
function hasValue($funcName, $args = null) {
$field = $this->dbObject($funcName);
if ( $field ) {
return $field->hasValue();
} else {
return parent::hasValue($funcName, $args);
}
}
}
?>

View File

@ -0,0 +1,79 @@
<?php
/**
* An abstract base class for the string field types (i.e. Varchar and Text)
* @package silverstripe
* @subpackage model
* @author Pete Bacon Darwin
*
*/
abstract class StringField extends DBField {
protected $nullifyEmpty = true;
/**
* Construct a string type field with a set of optional parameters
* @param $name string The name of the field
* @param $options array An array of options e.g. array('nullifyEmpty'=>false). See {@link StringField::setOptions()} for information on the available options
* @return unknown_type
*/
function __construct($name = null, $options = array()) {
// Workaround: The singleton pattern calls this constructor with true/1 as the second parameter, so we must ignore it
if(is_array($options)){
$this->setOptions($options);
}
parent::__construct($name);
}
/**
* Update the optional parameters for this field.
* @param $options array of options
* The options allowed are:
* <ul><li>"nullifyEmpty"
* This is a boolean flag.
* True (the default) means that empty strings are automatically converted to nulls to be stored in the database.
* Set it to false to ensure that nulls and empty strings are kept intact in the database.
* </li></ul>
* @return unknown_type
*/
function setOptions(array $options = array()) {
if(array_key_exists("nullifyEmpty", $options)) {
$this->nullifyEmpty = $options["nullifyEmpty"] ? true : false;
}
}
/**
* Set whether this field stores empty strings rather than converting them to null
* @param $value boolean True if empty strings are to be converted to null
* @return
*/
function setNullifyEmpty($value) {
$this->nullifyEmpty == $value ? true : false;
}
/**
* Get whether this field stores empty strings rather than converting them to null
* @return bool True if empty strings are to be converted to null
*/
function getNullifyEmpty() {
return $this->nullifyEmpty;
}
/**
* (non-PHPdoc)
* @see core/model/fieldtypes/DBField#hasValue()
*/
function hasValue() {
return ($this->value || $this->value == '0') || ( !$this->nullifyEmpty && $this->value === '');
}
/**
* (non-PHPdoc)
* @see core/model/fieldtypes/DBField#prepValueForDB($value)
*/
function prepValueForDB($value) {
if ( !$this->nullifyEmpty && $value === '' ) {
return "'" . Convert::raw2sql($value) . "'";
} else {
return parent::prepValueForDB($value);
}
}
}

View File

@ -4,21 +4,21 @@
* @package sapphire
* @subpackage model
*/
class Text extends DBField {
class Text extends StringField {
static $casting = array(
"AbsoluteLinks" => "HTMLText",
);
/**
* (non-PHPdoc)
* @see DBField::requireField()
*/
function requireField() {
$parts=Array('datatype'=>'mediumtext', 'character set'=>'utf8', 'collate'=>'utf8_general_ci', 'arrayValue'=>$this->arrayValue);
$values=Array('type'=>'text', 'parts'=>$parts);
DB::requireField($this->tableName, $this->name, $values, $this->default);
}
function hasValue() {
return ($this->value || $this->value == '0');
}
/**
* Limit this field's content by a number of words.
* CAUTION: This is not XML safe. Please use
@ -42,13 +42,25 @@ class Text extends DBField {
return $ret;
}
/**
* Return the value of the field stripped of html tags
* @return string
*/
function NoHTML() {
return strip_tags($this->value);
}
/**
* Return the value of the field with XML tags escaped.
* @return string
*/
function EscapeXML() {
return str_replace(array('&','<','>','"'), array('&amp;','&lt;','&gt;','&quot;'), $this->value);
}
/**
* Return the value of the field with relative links converted to absolute urls.
* @return string
*/
function AbsoluteLinks() {
return HTTP::absoluteURLs($this->value);
}
@ -292,13 +304,26 @@ class Text extends DBField {
}
}
/**
* (non-PHPdoc)
* @see DBField::scaffoldFormField()
*/
public function scaffoldFormField($title = null, $params = null) {
return new TextareaField($this->name, $title);
if($this->nullifyEmpty) {
// We can have an empty field so we need to let the user specifically set null value in the field.
return new NullableField(new TextareaField($this->name, $title));
} else {
return new TextareaField($this->name, $title);
}
}
/**
* (non-PHPdoc)
* @see DBField::scaffoldSearchField()
*/
public function scaffoldSearchField($title = null, $params = null) {
return new TextField($this->name, $title);
}
}
?>
?>

View File

@ -4,30 +4,48 @@
* @package sapphire
* @subpackage model
*/
class Varchar extends DBField {
class Varchar extends StringField {
protected $size;
function __construct($name, $size = 50) {
/**
* Construct a new short text field
* @param $name string The name of the field
* @param $size int The maximum size of the field, in terms of characters
* @param $options array Optional parameters, e.g. array("nullifyEmpty"=>false). See {@link StringField::setOptions()} for information on the available options
* @return unknown_type
*/
function __construct($name, $size = 50, $options = array()) {
$this->size = $size ? $size : 50;
parent::__construct($name);
parent::__construct($name, $options);
}
/**
* (non-PHPdoc)
* @see DBField::requireField()
*/
function requireField() {
$parts=Array('datatype'=>'varchar', 'precision'=>$this->size, 'character set'=>'utf8', 'collate'=>'utf8_general_ci', 'arrayValue'=>$this->arrayValue);
$values=Array('type'=>'varchar', 'parts'=>$parts);
$parts = array(
'datatype'=>'varchar',
'precision'=>$this->size,
'character set'=>'utf8',
'collate'=>'utf8_general_ci',
'arrayValue'=>$this->arrayValue
);
$values = array(
'type' => 'varchar',
'parts' => $parts
);
DB::requireField($this->tableName, $this->name, $values);
}
function hasValue() {
return ($this->value || $this->value == '0');
}
/**
* Return the first letter of the string followed by a .
*/
function Initial() {
if($this->value) return $this->value[0] . '.';
if($this->hasValue()) return $this->value[0] . '.';
}
/**
@ -37,15 +55,38 @@ class Varchar extends DBField {
if(ereg('^[a-zA-Z]+://', $this->value)) return $this->value;
else return "http://" . $this->value;
}
/**
* Return the value of the field in rich text format
* @return string
*/
function RTF() {
return str_replace("\n", '\par ', $this->value);
}
/**
* Returns the value of the string, limited to the specified number of characters
* @param $limit int Character limit
* @param $add string Extra string to add to the end of the limited string
* @return string
*/
function LimitCharacters($limit = 20, $add = "...") {
$value = trim($this->value);
return (strlen($value) > $limit) ? substr($value, 0, $limit) . $add : $value;
}
/**
* (non-PHPdoc)
* @see DBField::scaffoldFormField()
*/
public function scaffoldFormField($title = null, $params = null) {
if ( !$this->nullifyEmpty ) {
// We can have an empty field so we need to let the user specifically set null value in the field.
return new NullableField(new TextField($this->name, $title));
} else {
return parent::scaffoldFormField($title);
}
}
}
?>
?>

118
forms/NullableField.php Normal file
View File

@ -0,0 +1,118 @@
<?php
/**
* NullableField is a field that wraps other fields when you want to allow the user to specify whether the value of the field is null or not.
*
* The classic case is to wrap a TextField so that the user can distinguish between an empty string and a null string.
* $a = new NullableField(new TextField("Field1", "Field 1", "abc"));
*
* It displays the field that is wrapped followed by a checkbox that is used to specify if the value is null or not.
* It uses the Title of the wrapped field for its title.
* When a form is submitted the field tests the value of the "is null" checkbox and sets its value accordingly.
* You can retrieve the value of the wrapped field from the NullableField as follows:
* $field->Value() or $field->dataValue()
*
* You can specify the label to use for the "is null" checkbox. If you want to use I8N for this label then specify it like this:
* $field->setIsNullLabel(_T(SOME_MODULE_ISNULL_LABEL, "Is Null");
*
* @author Pete Bacon Darwin
*
*/
class NullableField extends FormField {
/**
* The field that holds the value of this field
* @var FormField
*/
protected $valueField;
/**
* The label to show next to the is null check box.
* @var string
*/
protected $isNullLabel;
/**
* Create a new nullable field
* @param $valueField
* @return NullableField
*/
function __construct(FormField $valueField, $isNullLabel = null) {
$this->valueField = $valueField;
$this->isNullLabel = $isNullLabel;
if ( is_null($this->isNullLabel) ) {
// Set a default label if one is not provided.
$this->isNullLabel = _t('NullableField.IsNullLabel', 'Is Null', PR_HIGH);
}
parent::__construct($valueField->Name(), $valueField->Title(), $valueField->Value(), $valueField->getForm(), $valueField->RightTitle());
$this->readonly = $valueField->isReadonly();
}
/**
* Get the label used for the Is Null checkbox.
* @return string
*/
function getIsNullLabel() {
return $this->isNullLabel;
}
/**
* Set the label used for the Is Null checkbox.
* @param $isNulLabel string
* @return
*/
function setIsNullLabel(string $isNulLabel){
$this->isNullLabel = $isNulLabel;
}
/**
* Get the id used for the Is Null check box.
* @return string
*/
function getIsNullId() {
return $this->Name() . "_IsNull";
}
/**
* (non-PHPdoc)
* @see sapphire/forms/FormField#Field()
*/
function Field() {
if ( $this->isReadonly()) {
$nullableCheckbox = new CheckboxField_Readonly($this->getIsNullId());
} else {
$nullableCheckbox = new CheckboxField($this->getIsNullId());
}
$nullableCheckbox->setValue(is_null($this->dataValue()));
return $this->valueField->Field() . ' ' . $nullableCheckbox->Field() . '&nbsp;<span>' . $this->getIsNullLabel().'</span>';
}
/**
* Value is sometimes an array, and sometimes a single value, so we need to handle both cases
*/
function setValue($value, $data = null) {
if ( is_array($data) && array_key_exists($this->getIsNullId(), $data) && $data[$this->getIsNullId()] ) {
$value = null;
}
$this->valueField->setValue($value);
parent::setValue($value);
}
/**
* (non-PHPdoc)
* @see forms/FormField#setName($name)
*/
function setName($name) {
// We need to pass through the name change to the underlying value field.
$this->valueField->setName($name);
parent::setName($name);
}
/**
* (non-PHPdoc)
* @see sapphire/forms/FormField#debug()
*/
function debug() {
$result = "$this->class ($this->name: $this->title : <font style='color:red;'>$this->message</font>) = ";
$result .= (is_null($this->value)) ? "<<null>>" : $this->value;
return result;
}
}

View File

@ -1,5 +1,12 @@
<?php
/**
*
* Tests for DBField objects.
* @package sapphire
* @subpackage tests
*
*/
class DBFieldTest extends SapphireTest {
/**
@ -57,6 +64,22 @@ class DBFieldTest extends SapphireTest {
$this->assertEquals("'test'", singleton('Varchar')->prepValueForDB('test'));
$this->assertEquals("'123'", singleton('Varchar')->prepValueForDB(123));
/* AllowEmpty Varchar behaviour */
$varcharField = new Varchar("testfield", 50, array("nullifyEmpty"=>false));
$this->assertSame("'0'", $varcharField->prepValueForDB(0));
$this->assertSame("null", $varcharField->prepValueForDB(null));
$this->assertSame("null", $varcharField->prepValueForDB(false));
$this->assertSame("''", $varcharField->prepValueForDB(''));
$this->assertSame("'0'", $varcharField->prepValueForDB('0'));
$this->assertSame("'1'", $varcharField->prepValueForDB(1));
$this->assertSame("'1'", $varcharField->prepValueForDB(true));
$this->assertSame("'1'", $varcharField->prepValueForDB('1'));
$this->assertSame("'00000'", $varcharField->prepValueForDB('00000'));
$this->assertSame("'0'", $varcharField->prepValueForDB(0000));
$this->assertSame("'test'", $varcharField->prepValueForDB('test'));
$this->assertSame("'123'", $varcharField->prepValueForDB(123));
unset($varcharField);
/* Text behaviour */
$this->assertEquals("'0'", singleton('Text')->prepValueForDB(0));
$this->assertEquals("null", singleton('Text')->prepValueForDB(null));
@ -70,6 +93,22 @@ class DBFieldTest extends SapphireTest {
$this->assertEquals("'0'", singleton('Text')->prepValueForDB(0000));
$this->assertEquals("'test'", singleton('Text')->prepValueForDB('test'));
$this->assertEquals("'123'", singleton('Text')->prepValueForDB(123));
/* AllowEmpty Text behaviour */
$textField = new Text("testfield", array("nullifyEmpty"=>false));
$this->assertSame("'0'", $textField->prepValueForDB(0));
$this->assertSame("null", $textField->prepValueForDB(null));
$this->assertSame("null", $textField->prepValueForDB(false));
$this->assertSame("''", $textField->prepValueForDB(''));
$this->assertSame("'0'", $textField->prepValueForDB('0'));
$this->assertSame("'1'", $textField->prepValueForDB(1));
$this->assertSame("'1'", $textField->prepValueForDB(true));
$this->assertSame("'1'", $textField->prepValueForDB('1'));
$this->assertSame("'00000'", $textField->prepValueForDB('00000'));
$this->assertSame("'0'", $textField->prepValueForDB(0000));
$this->assertSame("'test'", $textField->prepValueForDB('test'));
$this->assertSame("'123'", $textField->prepValueForDB(123));
unset($textField);
/* Time behaviour */
$time = singleton('Time');
@ -91,9 +130,45 @@ class DBFieldTest extends SapphireTest {
$this->assertEquals("00:00:00", $time->getValue());
$time->setValue('00:00:00');
$this->assertEquals("00:00:00", $time->getValue());
}
function testHasValue() {
$varcharField = new Varchar("testfield");
$this->assertTrue($varcharField->getNullifyEmpty());
$varcharField->setValue('abc');
$this->assertTrue($varcharField->hasValue());
$varcharField->setValue('');
$this->assertFalse($varcharField->hasValue());
$varcharField->setValue(null);
$this->assertFalse($varcharField->hasValue());
$varcharField = new Varchar("testfield", 50, array('nullifyEmpty'=>false));
$this->assertFalse($varcharField->getNullifyEmpty());
$varcharField->setValue('abc');
$this->assertTrue($varcharField->hasValue());
$varcharField->setValue('');
$this->assertTrue($varcharField->hasValue());
$varcharField->setValue(null);
$this->assertFalse($varcharField->hasValue());
$textField = new Text("testfield");
$this->assertTrue($textField->getNullifyEmpty());
$textField->setValue('abc');
$this->assertTrue($textField->hasValue());
$textField->setValue('');
$this->assertFalse($textField->hasValue());
$textField->setValue(null);
$this->assertFalse($textField->hasValue());
$textField = new Text("testfield", array('nullifyEmpty'=>false));
$this->assertFalse($textField->getNullifyEmpty());
$textField->setValue('abc');
$this->assertTrue($textField->hasValue());
$textField->setValue('');
$this->assertTrue($textField->hasValue());
$textField->setValue(null);
$this->assertFalse($textField->hasValue());
}
}
?>
?>

View File

@ -34,8 +34,11 @@ class FormFieldTest extends SapphireTest {
if($constructor->getNumberOfRequiredParameters() > 1) continue;
if($fieldClass == 'CompositeField' || is_subclass_of($fieldClass, 'CompositeField')) continue;
$instance = new $fieldClass("{$fieldClass}_instance");
if ( $fieldClass = 'NullableField' ) {
$instance = new $fieldClass(new TextField("{$fieldClass}_instance"));
} else {
$instance = new $fieldClass("{$fieldClass}_instance");
}
$isReadonlyBefore = $instance->isReadonly();
$readonlyInstance = $instance->performReadonlyTransformation();
$this->assertEquals(
@ -64,8 +67,12 @@ class FormFieldTest extends SapphireTest {
if($constructor->getNumberOfRequiredParameters() > 1) continue;
if($fieldClass == 'CompositeField' || is_subclass_of($fieldClass, 'CompositeField')) continue;
$instance = new $fieldClass("{$fieldClass}_instance");
if ( $fieldClass = 'NullableField' ) {
$instance = new $fieldClass(new TextField("{$fieldClass}_instance"));
} else {
$instance = new $fieldClass("{$fieldClass}_instance");
}
$isDisabledBefore = $instance->isDisabled();
$disabledInstance = $instance->performDisabledTransformation();
$this->assertEquals(

View File

@ -0,0 +1,124 @@
<?php
/**
* Tests the NullableField form field class.
* @package sapphire
* @subpackage tests
* @author Pete Bacon Darwin
*
*/
class NullableFieldTests extends SapphireTest {
/**
* Test that the NullableField works when it wraps a TextField containing actual content
*/
function testWithContent() {
$a = new NullableField(new TextField("Field1", "Field 1", "abc"));
$this->assertEquals("Field1", $a->Name());
$this->assertEquals("Field 1", $a->Title());
$this->assertSame("abc", $a->Value());
$this->assertSame("abc", $a->dataValue());
$field = $a->Field();
$this->assertTag(array(
'tag'=>'input',
'id'=>'Field1',
'attributes'=>array('type'=>'text', 'name'=>'Field1', 'value'=>'abc'),
), $field);
$this->assertTag(array(
'tag'=>'input',
'id'=>'Field1_IsNull',
'attributes'=>array('type'=>'checkbox', 'name'=>'Field1_IsNull', 'value'=>'1'),
), $field);
}
/**
* Test that the NullableField works when it wraps a TextField containing an empty string
*/
function testWithEmpty() {
$a = new NullableField(new TextField("Field1", "Field 1", ""));
$this->assertEquals("Field1", $a->Name());
$this->assertEquals("Field 1", $a->Title());
$this->assertSame("", $a->Value());
$this->assertSame("", $a->dataValue());
$field = $a->Field();
$this->assertTag(array(
'tag'=>'input',
'id'=>'Field1',
'attributes'=>array('type'=>'text', 'name'=>'Field1', 'value'=>''),
), $field);
$this->assertTag(array(
'tag'=>'input',
'id'=>'Field1_IsNull',
'attributes'=>array('type'=>'checkbox', 'name'=>'Field1_IsNull', 'value'=>'1'),
), $field);
}
/**
* Test that the NullableField works when it wraps a TextField containing a null string
*/
function testWithNull() {
$a = new NullableField(new TextField("Field1", "Field 1", null));
$this->assertEquals("Field1", $a->Name());
$this->assertEquals("Field 1", $a->Title());
$this->assertSame(null, $a->Value());
$this->assertSame(null, $a->dataValue());
$field = $a->Field();
$this->assertTag(array(
'tag'=>'input',
'id'=>'Field1',
'attributes'=>array('type'=>'text', 'name'=>'Field1', 'value'=>''),
), $field);
$this->assertTag(array(
'tag'=>'input',
'id'=>'Field1_IsNull',
'attributes'=>array('type'=>'checkbox', 'name'=>'Field1_IsNull', 'value'=>'1', 'checked'=>'checked'),
), $field);
unset($a);
}
/**
* Test NullableField::setValue works when passed simple values
*/
function testSetValueSimple() {
$a = new NullableField(new TextField("Field1", "Field 1"));
$a->setValue("abc");
$this->assertSame("abc", $a->dataValue());
$a = new NullableField(new TextField("Field1", "Field 1"));
$a->setValue(null);
$this->assertSame(null, $a->dataValue());
$a = new NullableField(new TextField("Field1", "Field 1", "abc"));
$a->setValue(null);
$this->assertSame(null, $a->dataValue());
$a = new NullableField(new TextField("Field1", "Field 1", "abc"));
$a->setValue("xyz");
$this->assertSame("xyz", $a->dataValue());
$a = new NullableField(new TextField("Field1", "Field 1"));
$a->setValue("");
$this->assertSame("", $a->dataValue());
$a = new NullableField(new TextField("Field1", "Field 1", "abc"));
$a->setValue("");
$this->assertSame("", $a->dataValue());
}
/**
* Test NullableField::setValue works when passed an array values,
* which happens when the form submits.
*/
function testSetValueArray() {
$a = new NullableField(new TextField("Field1", "Field 1"));
$a->setValue("abc", array("Field1_IsNull"=>false));
$this->assertSame("abc", $a->dataValue());
$a = new NullableField(new TextField("Field1", "Field 1"));
$a->setValue("", array("Field1_IsNull"=>false));
$this->assertSame("", $a->dataValue());
$a = new NullableField(new TextField("Field1", "Field 1"));
$a->setValue("", array("Field1_IsNull"=>true));
$this->assertSame(null, $a->dataValue());
}
}