API Add a CREATE_MEMORY_HYDRATED option to DataObject constructor (#9767)

This commit is contained in:
Maxime Rainville 2021-01-21 14:07:06 +13:00 committed by GitHub
parent 0dabdbfa41
commit 9ca33950a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 102 additions and 27 deletions

View File

@ -199,10 +199,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
/** /**
* Value for 2nd argument to constructor, indicating that a record is being hydrated from the database * Value for 2nd argument to constructor, indicating that a record is being hydrated from the database
* Neither setters and nor default population will be called * Setter methods are not called, and population via private static $defaults will not occur.
*/ */
const CREATE_HYDRATED = 2; const CREATE_HYDRATED = 2;
/**
* Value for 2nd argument to constructor, indicating that a record is being hydrated from memory. This can be used
* to initialised a record that doesn't yet have an ID. Setter methods are not called, and population via private
* static $defaults will not occur.
*/
const CREATE_MEMORY_HYDRATED = 3;
/** /**
* An array indexed by fieldname, true if the field has been changed. * An array indexed by fieldname, true if the field has been changed.
* Use {@link getChangedFields()} and {@link isChanged()} to inspect * Use {@link getChangedFields()} and {@link isChanged()} to inspect
@ -339,7 +346,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Construct a new DataObject. * Construct a new DataObject.
* *
* @param array $record Initial record content, or rehydrated record content, depending on $creationType * @param array $record Initial record content, or rehydrated record content, depending on $creationType
* @param int|boolean $creationType Set to DataObject::CREATE_OBJECT, DataObject::CREATE_HYDRATED, or DataObject::CREATE_SINGLETON. Used by SilverStripe internals as best left as the default by regular users. * @param int|boolean $creationType Set to DataObject::CREATE_OBJECT, DataObject::CREATE_HYDRATED,
* DataObject::CREATE_MEMORY_HYDRATED or DataObject::CREATE_SINGLETON. Used by Silverstripe internals and best
* left as the default by regular users.
* @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects. * @param array $queryParams List of DataQuery params necessary to lazy load, or load related objects.
*/ */
public function __construct($record = [], $creationType = self::CREATE_OBJECT, $queryParams = []) public function __construct($record = [], $creationType = self::CREATE_OBJECT, $queryParams = [])
@ -366,31 +375,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->record = []; $this->record = [];
switch ($creationType) { switch ($creationType) {
// Hydrate a record from the database // Hydrate a record
case self::CREATE_HYDRATED: case self::CREATE_HYDRATED:
if (!is_array($record) || empty($record['ID'])) { case self::CREATE_MEMORY_HYDRATED:
throw new \InvalidArgumentException("Hydrated records must be passed a record array including an ID"); $this->hydrate($record, $creationType === self::CREATE_HYDRATED);
}
$this->record = $record;
// Identify fields that should be lazy loaded, but only on existing records
// Get all field specs scoped to class for later lazy loading
$fields = static::getSchema()->fieldSpecs(
static::class,
DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
);
foreach ($fields as $field => $fieldSpec) {
$fieldClass = strtok($fieldSpec, ".");
if (!array_key_exists($field, $record)) {
$this->record[$field . '_Lazy'] = $fieldClass;
}
}
$this->original = $this->record;
$this->changed = [];
$this->changeForced = false;
break; break;
// Create a new object, using the constructor argument as the initial content // Create a new object, using the constructor argument as the initial content
@ -449,6 +437,43 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
} }
} }
/**
* Constructor hydration logic for CREATE_HYDRATED and CREATE_MEMORY_HYDRATED.
* @param array $record
* @param bool $mustHaveID If true, an exception will be thrown if $record doesn't have an ID.
*/
private function hydrate(array $record, bool $mustHaveID)
{
if ($mustHaveID && empty($record['ID'])) {
// CREATE_HYDRATED requires an ID to be included in the record
throw new \InvalidArgumentException(
"Hydrated records must be passed a record array including an ID."
);
} elseif (empty($record['ID'])) {
// CREATE_MEMORY_HYDRATED implicitely set the record ID to 0 if not provided
$record['ID'] = 0;
}
$this->record = $record;
// Identify fields that should be lazy loaded, but only on existing records
// Get all field specs scoped to class for later lazy loading
$fields = static::getSchema()->fieldSpecs(
static::class,
DataObjectSchema::INCLUDE_CLASS | DataObjectSchema::DB_ONLY
);
foreach ($fields as $field => $fieldSpec) {
$fieldClass = strtok($fieldSpec, ".");
if (!array_key_exists($field, $record)) {
$this->record[$field . '_Lazy'] = $fieldClass;
}
}
$this->original = $this->record;
$this->changed = [];
$this->changeForced = false;
}
/** /**
* Destroy all of this objects dependant objects and local caches. * Destroy all of this objects dependant objects and local caches.
* You'll need to call this to get the memory of an object that has components or extensions freed. * You'll need to call this to get the memory of an object that has components or extensions freed.
@ -750,7 +775,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$originalClass = $this->ClassName; $originalClass = $this->ClassName;
/** @var DataObject $newInstance */ /** @var DataObject $newInstance */
$newInstance = Injector::inst()->create($newClassName, $this->record, self::CREATE_HYDRATED); $newInstance = Injector::inst()->create($newClassName, $this->record, self::CREATE_MEMORY_HYDRATED);
// Modify ClassName // Modify ClassName
if ($newClassName != $originalClass) { if ($newClassName != $originalClass) {

View File

@ -1669,6 +1669,31 @@ class DataObjectTest extends SapphireTest
$dataObject->newClassInstance('Controller'); $dataObject->newClassInstance('Controller');
} }
public function testNewClassInstanceFromUnsavedDataObject()
{
$dataObject = new DataObjectTest\Team([
'Title' => 'Team 1'
]);
$changedDO = $dataObject->newClassInstance(DataObjectTest\SubTeam::class);
$changedFields = $changedDO->getChangedFields();
// Don't write the record, it will reset changed fields
$this->assertInstanceOf(DataObjectTest\SubTeam::class, $changedDO);
$this->assertEquals($changedDO->ClassName, DataObjectTest\SubTeam::class);
$this->assertEquals($changedDO->RecordClassName, DataObjectTest\SubTeam::class);
$this->assertContains('ClassName', array_keys($changedFields));
$this->assertEquals($changedFields['ClassName']['before'], DataObjectTest\Team::class);
$this->assertEquals($changedFields['ClassName']['after'], DataObjectTest\SubTeam::class);
$this->assertEquals($changedFields['RecordClassName']['before'], DataObjectTest\Team::class);
$this->assertEquals($changedFields['RecordClassName']['after'], DataObjectTest\SubTeam::class);
$changedDO->write();
$this->assertInstanceOf(DataObjectTest\SubTeam::class, $changedDO);
$this->assertEquals($changedDO->ClassName, DataObjectTest\SubTeam::class);
$this->assertNotEmpty($changedDO->ID, 'New class instance got an ID generated on write');
}
public function testMultipleManyManyWithSameClass() public function testMultipleManyManyWithSameClass()
{ {
$team = $this->objFromFixture(DataObjectTest\Team::class, 'team1'); $team = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
@ -2561,8 +2586,22 @@ class DataObjectTest extends SapphireTest
'Salary' => 50, 'Salary' => 50,
], DataObject::CREATE_HYDRATED); ], DataObject::CREATE_HYDRATED);
$this->assertEquals(null, $staff->EmploymentType); $this->assertEquals(null, $staff->EmploymentType);
$this->assertEquals(DataObjectTest\Staff::class, $staff->ClassName);
$this->assertEquals([], $staff->getChangedFields()); $this->assertEquals([], $staff->getChangedFields());
// Test hydration (DataObject::CREATE_HYDRATED)
// Defaults are not used, changes are not tracked
$staff = new DataObjectTest\Staff([
'Salary' => 50,
], DataObject::CREATE_MEMORY_HYDRATED);
$this->assertEquals(DataObjectTest\Staff::class, $staff->ClassName);
$this->assertEquals(null, $staff->EmploymentType);
$this->assertEquals([], $staff->getChangedFields());
$this->assertFalse(
$staff->isInDB(),
'DataObject hydrated from memory without an ID are assumed to not be in the Database.'
);
// Test singleton (DataObject::CREATE_SINGLETON) // Test singleton (DataObject::CREATE_SINGLETON)
// Values are ingored // Values are ingored
$staff = new DataObjectTest\Staff([ $staff = new DataObjectTest\Staff([
@ -2572,4 +2611,15 @@ class DataObjectTest extends SapphireTest
$this->assertEquals(null, $staff->Salary); $this->assertEquals(null, $staff->Salary);
$this->assertEquals([], $staff->getChangedFields()); $this->assertEquals([], $staff->getChangedFields());
} }
public function testDataObjectCreationHydrateWithoutID()
{
$this->expectExceptionMessage(
"Hydrated records must be passed a record array including an ID."
);
// Hydrating a record without an ID should throw an exception
$staff = new DataObjectTest\Staff([
'Salary' => 50,
], DataObject::CREATE_HYDRATED);
}
} }