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
* 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;
/**
* 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.
* Use {@link getChangedFields()} and {@link isChanged()} to inspect
@ -339,7 +346,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
* Construct a new DataObject.
*
* @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.
*/
public function __construct($record = [], $creationType = self::CREATE_OBJECT, $queryParams = [])
@ -366,31 +375,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
$this->record = [];
switch ($creationType) {
// Hydrate a record from the database
// Hydrate a record
case self::CREATE_HYDRATED:
if (!is_array($record) || empty($record['ID'])) {
throw new \InvalidArgumentException("Hydrated records must be passed a record array including an ID");
}
$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;
case self::CREATE_MEMORY_HYDRATED:
$this->hydrate($record, $creationType === self::CREATE_HYDRATED);
break;
// 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.
* 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;
/** @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
if ($newClassName != $originalClass) {

View File

@ -1669,6 +1669,31 @@ class DataObjectTest extends SapphireTest
$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()
{
$team = $this->objFromFixture(DataObjectTest\Team::class, 'team1');
@ -2561,8 +2586,22 @@ class DataObjectTest extends SapphireTest
'Salary' => 50,
], DataObject::CREATE_HYDRATED);
$this->assertEquals(null, $staff->EmploymentType);
$this->assertEquals(DataObjectTest\Staff::class, $staff->ClassName);
$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)
// Values are ingored
$staff = new DataObjectTest\Staff([
@ -2572,4 +2611,15 @@ class DataObjectTest extends SapphireTest
$this->assertEquals(null, $staff->Salary);
$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);
}
}