Skip to content

Commit

Permalink
Merge 936fd5f into 4d8de74
Browse files Browse the repository at this point in the history
  • Loading branch information
ravanscafi committed Dec 11, 2019
2 parents 4d8de74 + 936fd5f commit 34cf982
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 17 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
},
"extra": {
"branch-alias": {
"dev-master": "v2.1.x-dev"
"dev-master": "v2.1.x-dev",
"dev-differential-update": "v2.2.x-dev"
}
}
}
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
3 changes: 2 additions & 1 deletion tests/Mongolid/Cursor/CursorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ public function testShouldGetCurrentUsingActiveRecordClasses()
{
// Arrange
$collection = m::mock(Collection::class);
$entity = m::mock(ActiveRecord::class.'[]');
$entity = new class() extends ActiveRecord {
};
$entity->name = 'John Doe';
$driverCursor = new ArrayIterator([$entity]);
$cursor = $this->getCursor(null, $collection, 'find', [[]], $driverCursor);
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, 'hobbies' => ['bike', 'skate'], 'address' => null, 'other' => null];
$originalAttributes = ['_id' => 123, 'name' => 'Original Name', 'hobbies' => ['bike', 'motorcycle', 'gardening'], 'address' => '1 Blue street', 'gender' => 'm', 'nullField' => null];
$operationResult = m::mock();
$options = ['writeConcern' => new WriteConcern(1)];

$entity->_id = 123;
$updateData = ['$set' => ['age' => 32, 'hobbies.1' => 'skate'], '$unset' => ['hobbies.2' => '', 'address' => '', 'gender' => '', 'nullField' => '']];

// 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
4 changes: 2 additions & 2 deletions tests/Mongolid/DynamicSchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public function tearDown()
public function testShouldExtendSchema()
{
// Arrange
$schema = m::mock(DynamicSchema::class.'[]');
$schema = new DynamicSchema();

// Assert
$this->assertInstanceOf(Schema::class, $schema);
Expand All @@ -27,7 +27,7 @@ public function testShouldExtendSchema()
public function testShouldBeDynamic()
{
// Arrange
$schema = m::mock(DynamicSchema::class.'[]');
$schema = new DynamicSchema();

// Assert
$this->assertAttributeEquals(true, 'dynamic', $schema);
Expand Down
12 changes: 8 additions & 4 deletions tests/Mongolid/Model/RelationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public function testShouldReferenceOne($entity, $field, $fieldValue, $useCache,
{
// Set
$expectedQuery = $expectedQuery['referencesOne'];
$model = m::mock(ActiveRecord::class.'[]');
$model = new class() extends ActiveRecord {
};
$dataMapper = m::mock(DataMapper::class)->makePartial();
$result = m::mock();

Expand Down Expand Up @@ -61,7 +62,8 @@ public function testShouldReferenceMany($entity, $field, $fieldValue, $useCache,
{
// Set
$expectedQuery = $expectedQuery['referencesMany'];
$model = m::mock(ActiveRecord::class.'[]');
$model = new class() extends ActiveRecord {
};
$dataMapper = m::mock(DataMapper::class)->makePartial();
$result = m::mock(Cursor::class);

Expand Down Expand Up @@ -93,7 +95,8 @@ public function testShouldReferenceMany($entity, $field, $fieldValue, $useCache,
public function testShouldEmbedsOne($entity, $field, $fieldValue, $expectedItems)
{
// Set
$model = m::mock(ActiveRecord::class.'[]');
$model = new class() extends ActiveRecord {
};
$cursorFactory = m::mock(CursorFactory::class);
$cursor = m::mock(EmbeddedCursor::class);
$document = $fieldValue;
Expand Down Expand Up @@ -124,7 +127,8 @@ public function testShouldEmbedsOne($entity, $field, $fieldValue, $expectedItems
public function testShouldEmbedsMany($entity, $field, $fieldValue, $expectedItems)
{
// Set
$model = m::mock(ActiveRecord::class.'[]');
$model = new class() extends ActiveRecord {
};
$cursorFactory = m::mock(CursorFactory::class);
$cursor = m::mock(EmbeddedCursor::class);
$document = $fieldValue;
Expand Down
24 changes: 16 additions & 8 deletions tests/Mongolid/SchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public function testMustHaveAnEntityClass()
public function testShouldCastNullIntoObjectId()
{
// Arrange
$schema = m::mock(Schema::class.'[]');
$schema = new class extends Schema {
};
$value = null;

// Assert
Expand All @@ -52,7 +53,8 @@ public function testShouldCastNullIntoObjectId()
public function testShouldNotCastRandomStringIntoObjectId()
{
// Arrange
$schema = m::mock(Schema::class.'[]');
$schema = new class extends Schema {
};
$value = 'A random string';

// Assert
Expand All @@ -65,7 +67,8 @@ public function testShouldNotCastRandomStringIntoObjectId()
public function testShouldCastObjectIdStringIntoObjectId()
{
// Arrange
$schema = m::mock(Schema::class.'[]');
$schema = new class extends Schema {
};
$value = '507f1f77bcf86cd799439011';

// Assert
Expand All @@ -83,7 +86,8 @@ public function testShouldCastObjectIdStringIntoObjectId()
public function testShouldCastNullIntoAutoIncrementSequence()
{
// Arrange
$schema = m::mock(Schema::class.'[]');
$schema = new class extends Schema {
};
$sequenceService = m::mock(SequenceService::class);
$value = null;

Expand All @@ -103,7 +107,8 @@ public function testShouldCastNullIntoAutoIncrementSequence()

public function testShouldNotAutoIncrementSequenceIfValueIsNotNull()
{
$schema = m::mock(Schema::class.'[]');
$schema = new class extends Schema {
};
$sequenceService = m::mock(SequenceService::class);
$value = 3;

Expand All @@ -124,7 +129,8 @@ public function testShouldNotAutoIncrementSequenceIfValueIsNotNull()
public function testShouldCastDocumentTimestamps()
{
// Arrange
$schema = m::mock(Schema::class.'[]');
$schema = new class extends Schema {
};
$value = null;

// Assertion
Expand All @@ -137,7 +143,8 @@ public function testShouldCastDocumentTimestamps()
public function testShouldRefreshUpdatedAtTimestamps()
{
// Arrange
$schema = m::mock(Schema::class.'[]');
$schema = new class extends Schema {
};
$value = (new UTCDateTime(25));

// Assertion
Expand All @@ -155,7 +162,8 @@ public function testShouldNotRefreshCreatedAtTimestamps(
$compareTimestamp = true
) {
// Arrange
$schema = m::mock(Schema::class.'[]');
$schema = new class extends Schema {
};

// Assertion
$result = $schema->createdAtTimestamp($value);
Expand Down

0 comments on commit 34cf982

Please sign in to comment.