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());