Skip to content

Commit

Permalink
Add differential updates
Browse files Browse the repository at this point in the history
  • Loading branch information
Ravan Scafi committed Dec 10, 2019
1 parent 5bde18f commit 2d46136
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 1 deletion.
51 changes: 50 additions & 1 deletion src/Mongolid/DataMapper/DataMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,11 @@ public function update($entity, array $options = []): bool
}

$data = $this->parseToDocument($entity);
$updateData = $this->getUpdateData($entity, $data);

$queryResult = $this->getCollection()->updateOne(
['_id' => $data['_id']],
['$set' => $data],
$updateData,
$this->mergeOptions($options)
);

Expand Down Expand Up @@ -570,4 +571,52 @@ public function setSchema(Schema $schema)
{
$this->schema = $schema;
}

private function getUpdateData($model, array $data): array
{
$changes = [];
$oldData = [];

if ($model instanceof AttributesAccessInterface) {
$oldData = $model->originalAttributes();
}

$data = array_filter($data, function ($value) {
return !is_null($value);
});

$this->calculateChanges($changes, $data, $oldData);

return $changes;
}

/**
* Based on the work of "bjori/mongo-php-transistor".
* Calculate `$set` and `$unset` arrays for update operation and store them on $changes.
*
* @see https://github.com/bjori/mongo-php-transistor/blob/70f5af00795d67f4d5a8c397e831435814df9937/src/Transistor.php#L108
*/
private function calculateChanges(array &$changes, array $newData, array $oldData, string $keyfix = '')
{
foreach ($newData as $k => $v) {
if (!isset($oldData[$k])) { // new field
$changes['$set']["{$keyfix}{$k}"] = $v;
} elseif ($oldData[$k] != $v) { // changed value
if (is_array($v) && is_array($oldData[$k]) && $v) { // check array recursively for changes
$this->calculateChanges($changes, $v, $oldData[$k], "{$keyfix}{$k}.");
} else {
// overwrite normal changes in keys
// this applies to previously empty arrays/documents too
$changes['$set']["{$keyfix}{$k}"] = $v;
}
}
}

foreach ($oldData as $k => $v) { // data that used to exist, but now doesn't
if (!isset($newData[$k])) { // removed field
$changes['$unset']["{$keyfix}{$k}"] = '';
continue;
}
}
}
}
8 changes: 8 additions & 0 deletions src/Mongolid/Model/Attributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ public function setAttribute(string $key, $value)
$this->attributes[$key] = $value;
}

/**
* Get original attributes.
*/
public function originalAttributes()
{
return $this->original;
}

/**
* Stores original attributes from actual data from attributes
* to be used in future comparisons about changes.
Expand Down
64 changes: 64 additions & 0 deletions tests/Mongolid/DataMapper/DataMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ public function testShouldUpdate($entity, $writeConcern, $shouldFireEventAfter,
->andReturn(1);

if ($entity instanceof AttributesAccessInterface) {
$entity->shouldReceive('originalAttributes')
->once()
->with()
->andReturn([]);

$entity->shouldReceive('syncOriginalAttributes')
->once()
->with();
Expand All @@ -276,6 +281,65 @@ public function testShouldUpdate($entity, $writeConcern, $shouldFireEventAfter,
$this->assertEquals($expected, $mapper->update($entity, $options));
}

public function testDifferentialUpdateShouldWork()
{
// Arrange
$entity = m::mock(AttributesAccessInterface::class);
$connPool = m::mock(Pool::class);
$mapper = m::mock(DataMapper::class.'[parseToDocument,getCollection]', [$connPool]);

$collection = m::mock(Collection::class);
$parsedObject = ['_id' => 123, 'name' => 'Original Name', 'age' => 32, 'address' => null, 'other' => null];
$originalAttributes = ['_id' => 123, 'name' => 'Original Name', 'address' => '1 Blue street', 'gender' => 'm'];
$operationResult = m::mock();
$options = ['writeConcern' => new WriteConcern(1)];

$entity->_id = 123;
$updateData = ['$set' => ['age' => 32], '$unset' => ['address' => '', 'gender' => '']];

// Act
$mapper->shouldAllowMockingProtectedMethods();

$mapper->shouldReceive('parseToDocument')
->once()
->with($entity)
->andReturn($parsedObject);

$mapper->shouldReceive('getCollection')
->once()
->andReturn($collection);

$collection->shouldReceive('updateOne')
->once()
->with(
['_id' => 123],
$updateData,
$options
)->andReturn($operationResult);

$operationResult->shouldReceive('isAcknowledged')
->once()
->andReturn(true);

$operationResult->shouldReceive('getModifiedCount')
->andReturn(1);

$entity->shouldReceive('originalAttributes')
->once()
->with()
->andReturn($originalAttributes);

$entity->shouldReceive('syncOriginalAttributes')
->once()
->with();

$this->expectEventToBeFired('updating', $entity, true);
$this->expectEventToBeFired('updated', $entity, false);

// Assert
$this->assertTrue($mapper->update($entity, $options));
}

/**
* @dataProvider getWriteConcernVariations
*/
Expand Down

0 comments on commit 2d46136

Please sign in to comment.