mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
API Add a CREATE_MEMORY_HYDRATED option to DataObject constructor (#9767)
This commit is contained in:
parent
0dabdbfa41
commit
9ca33950a2
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user