diff --git a/docs/en/02_Developer_Guides/14_Files/02_Images.md b/docs/en/02_Developer_Guides/14_Files/02_Images.md
index f7473ed86..7696a953b 100644
--- a/docs/en/02_Developer_Guides/14_Files/02_Images.md
+++ b/docs/en/02_Developer_Guides/14_Files/02_Images.md
@@ -165,21 +165,22 @@ around 2000px on the longest edge.
#### Forced resampling
-Since the 'master' images in your asset store may have a large file size, by
-default SilverStripe will always apply compression to your images to save
-bandwidth - even if no other manipulation (such as a crop or resize) is taking
-place. If you expect the images in your asset store to already have
-compression applied and want to serve up the original when no resampling is
-necessary, you can add this to your mysite/config/config.yml file:
+Since the 'master' images in your asset store may have a large file size, SilverStripe
+can apply compression to your images to save bandwidth - even if no other manipulation
+(such as a crop or resize) is taking place. In many cases this can result in a smaller
+overall file size, which may be appropriate for streaming to web users.
+Please note that turning this feature on can increase the server memory requirements,
+and is off by default to conserve resources.
+
+You can turn this on with the below config:
```yml
-# Configure resampling for File dataobject
+---
+Name: resamplefiles
+---
SilverStripe\Assets\File:
force_resample: false
-# DBFile can be configured independently
-SilverStripe\Assets\Storage\DBFile:
- force_resample: false
```
#### Resampled image quality
diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md
index 12f414bfd..8c83a1f52 100644
--- a/docs/en/04_Changelogs/4.0.0.md
+++ b/docs/en/04_Changelogs/4.0.0.md
@@ -1350,18 +1350,26 @@ These methods are deprecated:
### New Ownership API {#ownership}
In order to support the recursive publishing of dataobjects, a new API has been developed to allow
-developers to declare dependencies between objects. This is done to ensure that the published state
-of linked components are consistent with their "owner." Without the concept of ownership, these linked
-components could be implicitly exposed on the frontend, which may not align with the intent of the
-content author.
+developers to declare dependencies between versioned objects.
-For instance, on a products page which has a list of products, the products should not be published unless the products page is, too. The ownership API solves this by allowing you to declare
- a two-way relationship between objects, typically, but not necessarily, linked by a database relationship
- (`has_many`, `many_many`, etc.).
+```php
+private static $owns = [
+ 'Banners', // This page depends on banners, declared as a separate 'has_many'
+ 'FooterImage', // This page depends on the footer image, declared as a separate 'has_one'
+];
+```
+
+This is done to ensure that the published state of linked components are consistent with their "owner."
+Without the concept of ownership, these linked components could be implicitly exposed on the frontend,
+which may not align with the intent of the content author.
+
+For instance, on a products page which has a list of products, the products should not be published unless
+the products page is, too. The ownership API solves this by allowing you to declare a two-way relationship
+between objects, typically, but not necessarily, linked by a database relationship
+(`has_many`, `many_many`, etc.).
```php
use SilverStripe\Versioned\Versioned;
-use Page;
use SilverStripe\ORM\DataObject;
class ProductPage extends Page
@@ -1386,6 +1394,7 @@ class Product extends DataObject
];
}
```
+
If your objects are linked by something other than a database relationship, for instance, a custom
getter that is computed at runtime, the same rules can be applied, as long as you provide an `$owned_by`
setting on the child object.
diff --git a/src/ORM/Connect/PDOConnector.php b/src/ORM/Connect/PDOConnector.php
index 753957488..7b3902251 100644
--- a/src/ORM/Connect/PDOConnector.php
+++ b/src/ORM/Connect/PDOConnector.php
@@ -163,7 +163,7 @@ class PDOConnector extends DBConnector
$connCollation = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_collation');
// Set charset if given and not null. Can explicitly set to empty string to omit
- if ($parameters['driver'] !== 'sqlsrv') {
+ if (!in_array($parameters['driver'], ['sqlsrv', 'pgsql'])) {
$charset = isset($parameters['charset'])
? $parameters['charset']
: $connCharset;
diff --git a/src/ORM/FieldType/DBDate.php b/src/ORM/FieldType/DBDate.php
index 9f358c9e9..d0cc2cd36 100644
--- a/src/ORM/FieldType/DBDate.php
+++ b/src/ORM/FieldType/DBDate.php
@@ -67,6 +67,9 @@ class DBDate extends DBField
} else {
// Convert US date -> iso, fix y2k, etc
$value = $this->fixInputDate($value);
+ if (is_null($value)) {
+ return null;
+ }
$source = strtotime($value); // convert string to timestamp
}
if ($value === false) {
@@ -529,6 +532,9 @@ class DBDate extends DBField
// split
list($year, $month, $day, $time) = $this->explodeDateString($value);
+ if ((int)$year === 0 && (int)$month === 0 && (int)$day === 0) {
+ return null;
+ }
// Validate date
if (!checkdate($month, $day, $year)) {
throw new InvalidArgumentException(
@@ -568,7 +574,7 @@ class DBDate extends DBField
if ($parts[0] < 1000 && $parts[2] > 1000) {
$parts = array_reverse($parts);
}
- if ($parts[0] < 1000) {
+ if ($parts[0] < 1000 && (int)$parts[0] !== 0) {
throw new InvalidArgumentException(
"Invalid date: '$value'. Use " . self::ISO_DATE . " to prevent this error."
);
diff --git a/src/ORM/FieldType/DBEnum.php b/src/ORM/FieldType/DBEnum.php
index fc7802de7..e2c9d542c 100644
--- a/src/ORM/FieldType/DBEnum.php
+++ b/src/ORM/FieldType/DBEnum.php
@@ -32,45 +32,46 @@ class DBEnum extends DBString
private static $default_search_filter_class = 'ExactMatchFilter';
/**
- * Create a new Enum field.
+ * Create a new Enum field, which is a value within a defined set, with an optional default.
*
- * Example usage in {@link DataObject::$db} with comma-separated string
- * notation ('Val1' is default)
+ * Example field specification strings:
*
*
- * "MyField" => "Enum('Val1, Val2, Val3', 'Val1')"
- *
- *
- * Example usage in in {@link DataObject::$db} with array notation
- * ('Val1' is default)
- *
- *
- * "MyField" => "Enum(array('Val1', 'Val2', 'Val3'), 'Val1')"
+ * "MyField" => "Enum('Val1, Val2, Val3')" // First item 'Val1' is default implicitly
+ * "MyField" => "Enum('Val1, Val2, Val3', 'Val2')" // 'Val2' is default explicitly
+ * "MyField" => "Enum('Val1, Val2, Val3', null)" // Force empty (no) default
+ * "MyField" => "Enum(array('Val1', 'Val2', 'Val3'), 'Val1')" // Supports array notation as well
*
*
* @param string $name
* @param string|array $enum A string containing a comma separated list of options or an array of Vals.
- * @param string $default The default option, which is either NULL or one of the items in the enumeration.
+ * @param string|int|null $default The default option, which is either NULL or one of the items in the enumeration.
+ * If passing in an integer (non-string) it will default to the index of that item in the list.
+ * Set to null or empty string to allow empty values
* @param array $options Optional parameters for this DB field
*/
- public function __construct($name = null, $enum = null, $default = null, $options = [])
+ public function __construct($name = null, $enum = null, $default = 0, $options = [])
{
if ($enum) {
$this->setEnum($enum);
+ $enum = $this->getEnum();
- // If there's a default, then
- if ($default) {
- if (in_array($default, $this->getEnum())) {
+ // If there's a default, then use this
+ if ($default && !is_int($default)) {
+ if (in_array($default, $enum)) {
$this->setDefault($default);
} else {
- user_error("Enum::__construct() The default value '$default' does not match any item in the"
- . " enumeration", E_USER_ERROR);
+ user_error(
+ "Enum::__construct() The default value '$default' does not match any item in the enumeration",
+ E_USER_ERROR
+ );
}
-
- // By default, set the default value to the first item
+ } elseif (is_int($default) && $default < count($enum)) {
+ // Set to specified index if given
+ $this->setDefault($enum[$default]);
} else {
- $enum = $this->getEnum();
- $this->setDefault(reset($enum));
+ // Set to null if specified
+ $this->setDefault(null);
}
}
@@ -187,7 +188,7 @@ class DBEnum extends DBString
ltrim(preg_replace('/,\s*\n\s*$/', '', $enum))
);
}
- $this->enum = $enum;
+ $this->enum = array_values($enum);
return $this;
}
@@ -210,6 +211,7 @@ class DBEnum extends DBString
public function setDefault($default)
{
$this->default = $default;
+ $this->setDefaultValue($default);
return $this;
}
}
diff --git a/src/ORM/FieldType/DBField.php b/src/ORM/FieldType/DBField.php
index e8d04b650..70aa13cb4 100644
--- a/src/ORM/FieldType/DBField.php
+++ b/src/ORM/FieldType/DBField.php
@@ -2,8 +2,9 @@
namespace SilverStripe\ORM\FieldType;
-use SilverStripe\Core\Injector\Injector;
+use InvalidArgumentException;
use SilverStripe\Core\Convert;
+use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\FormField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\DataObject;
@@ -139,7 +140,7 @@ abstract class DBField extends ViewableData implements DBIndexable
if ($options) {
if (!is_array($options)) {
- throw new \InvalidArgumentException("Invalid options $options");
+ throw new InvalidArgumentException("Invalid options $options");
}
$this->setOptions($options);
}
@@ -152,16 +153,26 @@ abstract class DBField extends ViewableData implements DBIndexable
*
* Useful for accessing the classes behaviour for other parts of your code.
*
- * @param string $className class of field to construct
+ * @param string $spec Class specification to construct. May include both service name and additional
+ * constructor arguments in the same format as DataObject.db config.
* @param mixed $value value of field
* @param string $name Name of field
- * @param mixed $object Additional parameter to pass to field constructor
+ * @param mixed $args Additional arguments to pass to constructor if not using args in service $spec
+ * Note: Will raise a warning if using both
* @return static
*/
- public static function create_field($className, $value, $name = null, $object = null)
+ public static function create_field($spec, $value, $name = null, ...$args)
{
+ // Raise warning if inconsistent with DataObject::dbObject() behaviour
+ // This will cause spec args to be shifted down by the number of provided $args
+ if ($args && strpos($spec, '(') !== false) {
+ trigger_error('Additional args provided in both $spec and $args', E_USER_WARNING);
+ }
+ // Ensure name is always first argument
+ array_unshift($args, $name);
+
/** @var DBField $dbField */
- $dbField = Injector::inst()->create($className, $name, $object);
+ $dbField = Injector::inst()->createWithArgs($spec, $args);
$dbField->setValue($value, null, false);
return $dbField;
}
@@ -633,11 +644,13 @@ DBG;
public function getIndexSpecs()
{
- if ($type = $this->getIndexType()) {
+ $type = $this->getIndexType();
+ if ($type) {
return [
'type' => $type,
'columns' => [$this->getName()],
];
}
+ return null;
}
}
diff --git a/tests/php/ORM/DBDateTest.php b/tests/php/ORM/DBDateTest.php
index 65cbc7bb9..6e00803dc 100644
--- a/tests/php/ORM/DBDateTest.php
+++ b/tests/php/ORM/DBDateTest.php
@@ -218,6 +218,12 @@ class DBDateTest extends SapphireTest
$date = DBField::create_field('Date', 0);
$this->assertEquals('1970-01-01', $date->getValue(), 'Zero is UNIX epoch date');
+
+ $date = DBField::create_field('Date', '0000-00-00 00:00:00');
+ $this->assertNull($date->getValue(), '0000-00-00 00:00:00 is set as NULL');
+
+ $date = DBField::create_field('Date', '00/00/0000');
+ $this->assertNull($date->getValue(), '00/00/0000 is set as NULL');
}
public function testDayOfMonth()
diff --git a/tests/php/ORM/DBEnumTest.php b/tests/php/ORM/DBEnumTest.php
new file mode 100644
index 000000000..3ed633f15
--- /dev/null
+++ b/tests/php/ORM/DBEnumTest.php
@@ -0,0 +1,31 @@
+assertEquals('A', $enum1->getDefaultValue());
+ $this->assertEquals('A', $enum1->getDefault());
+ $this->assertEquals(null, $enum2->getDefaultValue());
+ $this->assertEquals(null, $enum2->getDefault());
+ $this->assertEquals(null, $enum3->getDefaultValue());
+ $this->assertEquals(null, $enum3->getDefault());
+ $this->assertEquals('B', $enum4->getDefaultValue());
+ $this->assertEquals('B', $enum4->getDefault());
+ }
+}
diff --git a/tests/php/ORM/PDODatabaseTest.php b/tests/php/ORM/PDODatabaseTest.php
index d7025cbf9..d90e85382 100644
--- a/tests/php/ORM/PDODatabaseTest.php
+++ b/tests/php/ORM/PDODatabaseTest.php
@@ -104,17 +104,17 @@ class PDODatabaseTest extends SapphireTest
$this->markTestSkipped('This test requires the current DB connector is PDO');
}
- $query = new SQLUpdate('MySQLDatabaseTest_Data');
- $query->setAssignments(array('Title' => 'New Title'));
+ $query = new SQLUpdate('"MySQLDatabaseTest_Data"');
+ $query->setAssignments(array('"Title"' => 'New Title'));
// Test update which affects no rows
- $query->setWhere(array('Title' => 'Bob'));
+ $query->setWhere(array('"Title"' => 'Bob'));
$result = $query->execute();
$this->assertInstanceOf(PDOQuery::class, $result);
$this->assertEquals(0, DB::affected_rows());
// Test update which affects some rows
- $query->setWhere(array('Title' => 'First Item'));
+ $query->setWhere(array('"Title"' => 'First Item'));
$result = $query->execute();
$this->assertInstanceOf(PDOQuery::class, $result);
$this->assertEquals(1, DB::affected_rows());