diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php index 576cf8055..53f175ba9 100644 --- a/src/ORM/DataObject.php +++ b/src/ORM/DataObject.php @@ -1477,11 +1477,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if (!isset($tableManipulation['fields'])) { continue; } - foreach ($tableManipulation['fields'] as $fieldValue) { + foreach ($tableManipulation['fields'] as $fieldName => $fieldValue) { if (is_array($fieldValue)) { - throw new InvalidArgumentException( - 'DataObject::writeManipulation: parameterised field assignments are disallowed' - ); + $dbObject = $this->dbObject($fieldName); + // If the field allows non-scalar values we'll let it do dynamic assignments + if ($dbObject && $dbObject->scalarValueOnly()) { + throw new InvalidArgumentException( + 'DataObject::writeManipulation: parameterised field assignments are disallowed' + ); + } } } } diff --git a/src/ORM/ManyManyList.php b/src/ORM/ManyManyList.php index 8e07f10e4..865e42a7a 100644 --- a/src/ORM/ManyManyList.php +++ b/src/ORM/ManyManyList.php @@ -283,6 +283,8 @@ class ManyManyList extends RelationList ); } + /** @var DBField[] $fieldObjects */ + $fieldObjects = []; if ($extraFields && $this->extraFields) { // Write extra field to manipluation in the same way // that DataObject::prepareManipulationTable writes fields @@ -292,6 +294,7 @@ class ManyManyList extends RelationList $fieldObject = Injector::inst()->create($fieldSpec, $fieldName); $fieldObject->setValue($extraFields[$fieldName]); $fieldObject->writeToManipulation($manipulation[$this->joinTable]); + $fieldObjects[$fieldName] = $fieldObject; } } } @@ -304,11 +307,14 @@ class ManyManyList extends RelationList if (!isset($tableManipulation['fields'])) { continue; } - foreach ($tableManipulation['fields'] as $fieldValue) { + foreach ($tableManipulation['fields'] as $fieldName => $fieldValue) { if (is_array($fieldValue)) { - throw new InvalidArgumentException( - 'ManyManyList::add: parameterised field assignments are disallowed' - ); + // If the field allows non-scalar values we'll let it do dynamic assignments + if (isset($fieldObjects[$fieldName]) && $fieldObjects[$fieldName]->scalarValueOnly()) { + throw new InvalidArgumentException( + 'ManyManyList::add: parameterised field assignments are disallowed' + ); + } } } } diff --git a/tests/php/ORM/DataObjectTest.php b/tests/php/ORM/DataObjectTest.php index 60d198243..f51fdf52e 100644 --- a/tests/php/ORM/DataObjectTest.php +++ b/tests/php/ORM/DataObjectTest.php @@ -58,6 +58,7 @@ class DataObjectTest extends SapphireTest DataObjectTest\RelationParent::class, DataObjectTest\RelationChildFirst::class, DataObjectTest\RelationChildSecond::class, + DataObjectTest\MockDynamicAssignmentDataObject::class ); public static function getExtraDataObjects() @@ -2239,4 +2240,34 @@ class DataObjectTest extends SapphireTest $do->SalaryCap = array('Amount' => 123456, 'Currency' => 'CAD'); $this->assertNotEmpty($do->SalaryCap); } + + public function testWriteManipulationWithNonScalarValuesAllowed() + { + $do = DataObjectTest\MockDynamicAssignmentDataObject::create(); + $do->write(); + + $do->StaticScalarOnlyField = true; + $do->DynamicScalarOnlyField = false; + $do->DynamicField = true; + + $do->write(); + + $this->assertTrue($do->StaticScalarOnlyField); + $this->assertFalse($do->DynamicScalarOnlyField); + $this->assertTrue($do->DynamicField); + } + + public function testWriteManipulationWithNonScalarValuesDisallowed() + { + $this->expectException(InvalidArgumentException::class); + + $do = DataObjectTest\MockDynamicAssignmentDataObject::create(); + $do->write(); + + $do->StaticScalarOnlyField = false; + $do->DynamicScalarOnlyField = true; + $do->DynamicField = false; + + $do->write(); + } } diff --git a/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDBField.php b/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDBField.php new file mode 100644 index 000000000..25828f9dd --- /dev/null +++ b/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDBField.php @@ -0,0 +1,55 @@ +scalarOnly = $scalarOnly; + $this->dynamicAssignment = $dynamicAssignment; + parent::__construct($name); + } + + /** + * If the field value and $dynamicAssignment are true, we'll try to do a dynamic assignment. + * @param $value + * @return array|int + */ + public function prepValueForDB($value) + { + if ($value) { + return $this->dynamicAssignment + ? ['ABS(?)' => [1]] + : 1; + } + + return 0; + } + + public function scalarValueOnly() + { + return $this->scalarOnly; + } +} diff --git a/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDataObject.php b/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDataObject.php new file mode 100644 index 000000000..1617734cf --- /dev/null +++ b/tests/php/ORM/DataObjectTest/MockDynamicAssignmentDataObject.php @@ -0,0 +1,52 @@ + MockDynamicAssignmentDBField::class . '(1,0)', + + // This field tries to emit dynamic assignment but will fail because of scalar only + 'DynamicScalarOnlyField' => MockDynamicAssignmentDBField::class . '(1,1)', + + // This field does dynamic assignement and will pass + 'DynamicField' => MockDynamicAssignmentDBField::class . '(0,1)', + ]; + + private static $many_many = [ + 'MockManyMany' => self::class, + ]; + + private static $belongs_many_many = [ + 'MockBelongsManyMany' => self::class, + ]; + + private static $many_many_extraFields = [ + 'MockManyMany' => [ + // This field only emits scalar value and will save + 'ManyManyStaticScalarOnlyField' => MockDynamicAssignmentDBField::class . '(1,0)', + + // This field tries to emit dynamic assignment but will fail because of scalar only + 'ManyManyDynamicScalarOnlyField' => MockDynamicAssignmentDBField::class . '(1,1)', + + // This field does dynamic assignement and will pass + 'ManyManyDynamicField' => MockDynamicAssignmentDBField::class . '(0,1)', + ] + ]; +} diff --git a/tests/php/ORM/ManyManyListTest.php b/tests/php/ORM/ManyManyListTest.php index aa6218474..008ec8189 100644 --- a/tests/php/ORM/ManyManyListTest.php +++ b/tests/php/ORM/ManyManyListTest.php @@ -10,6 +10,7 @@ use SilverStripe\ORM\Tests\DataObjectTest\Player; use SilverStripe\ORM\Tests\DataObjectTest\Team; use SilverStripe\ORM\Tests\ManyManyListTest\ExtraFieldsObject; use SilverStripe\ORM\Tests\ManyManyListTest\Product; +use InvalidArgumentException; class ManyManyListTest extends SapphireTest { @@ -20,6 +21,7 @@ class ManyManyListTest extends SapphireTest ManyManyListTest\Category::class, ManyManyListTest\ExtraFieldsObject::class, ManyManyListTest\Product::class, + DataObjectTest\MockDynamicAssignmentDataObject::class ]; public static function getExtraDataObjects() @@ -378,4 +380,40 @@ class ManyManyListTest extends SapphireTest $productsRelatedToProductB = $category->Products()->filter('RelatedProducts.Title', 'Product A'); $this->assertEquals(1, $productsRelatedToProductB->count()); } + + public function testWriteManipulationWithNonScalarValuesAllowed() + { + $left = DataObjectTest\MockDynamicAssignmentDataObject::create(); + $left->write(); + $right = DataObjectTest\MockDynamicAssignmentDataObject::create(); + $right->write(); + + $left->MockManyMany()->add($right, [ + 'ManyManyStaticScalarOnlyField' => true, + 'ManyManyDynamicScalarOnlyField' => false, + 'ManyManyDynamicField' => true, + ]); + + $pivot = $left->MockManyMany()->first(); + + $this->assertNotFalse($pivot->ManyManyStaticScalarOnlyField); + $this->assertNotTrue($pivot->ManyManyDynamicScalarOnlyField); + $this->assertNotFalse($pivot->ManyManyDynamicField); + } + + public function testWriteManipulationWithNonScalarValuesDisallowed() + { + $this->expectException(InvalidArgumentException::class); + + $left = DataObjectTest\MockDynamicAssignmentDataObject::create(); + $left->write(); + $right = DataObjectTest\MockDynamicAssignmentDataObject::create(); + $right->write(); + + $left->MockManyMany()->add($right, [ + 'ManyManyStaticScalarOnlyField' => false, + 'ManyManyDynamicScalarOnlyField' => true, + 'ManyManyDynamicField' => false, + ]); + } }