Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PHPORM-75 Defer Model::unset($field) to the save() #2578

Merged
merged 3 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file.
- Support `%` and `_` in `like` expression [#17](https://github.com/GromNaN/laravel-mongodb/pull/17) by [@GromNaN](https://github.com/GromNaN).
- Change signature of `Query\Builder::__constructor` to match the parent class [#26](https://github.com/GromNaN/laravel-mongodb-private/pull/26) by [@GromNaN](https://github.com/GromNaN).
- Fix Query on `whereDate`, `whereDay`, `whereMonth`, `whereYear`, `whereTime` to use MongoDB operators [#2570](https://github.com/jenssegers/laravel-mongodb/pull/2376) by [@Davpyu](https://github.com/Davpyu) and [@GromNaN](https://github.com/GromNaN).
- `Model::unset()` does not persist the change. Call `Model::save()` to persist the change [#2578](https://github.com/jenssegers/laravel-mongodb/pull/2578) by [@GromNaN](https://github.com/GromNaN).

## [3.9.2] - 2022-09-01

Expand Down
93 changes: 74 additions & 19 deletions src/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ abstract class Model extends BaseModel
*/
protected $parentRelation;

/**
* List of field names to unset from the document on save.
*
* @var array{string, true}
*/
private array $unset = [];

/**
* Custom accessor for the model's id.
*
Expand Down Expand Up @@ -151,7 +158,12 @@ public function getTable()
public function getAttribute($key)
{
if (! $key) {
return;
return null;
}

// An unset attribute is null or throw an exception.
if (isset($this->unset[$key])) {
return $this->throwMissingAttributeExceptionIfApplicable($key);
}

// Dot notation support.
Expand Down Expand Up @@ -206,6 +218,9 @@ public function setAttribute($key, $value)
return $this;
}

// Setting an attribute cancels the unset operation.
unset($this->unset[$key]);

return parent::setAttribute($key, $value);
}

Expand Down Expand Up @@ -239,6 +254,21 @@ public function getCasts()
return $this->casts;
}

/**
* @inheritdoc
*/
public function getDirty()
{
$dirty = parent::getDirty();

// The specified value in the $unset expression does not impact the operation.
if (! empty($this->unset)) {
$dirty['$unset'] = $this->unset;
}

return $dirty;
}

/**
* @inheritdoc
*/
Expand All @@ -248,6 +278,11 @@ public function originalIsEquivalent($key)
return false;
}

// Calling unset on an attribute marks it as "not equivalent".
if (isset($this->unset[$key])) {
return false;
}

$attribute = Arr::get($this->attributes, $key);
$original = Arr::get($this->original, $key);

Expand Down Expand Up @@ -275,23 +310,56 @@ public function originalIsEquivalent($key)
&& strcmp((string) $attribute, (string) $original) === 0;
}

/**
* @inheritdoc
*/
public function offsetUnset($offset): void
{
parent::offsetUnset($offset);

// Force unsetting even if the attribute is not set.
// End user can optimize DB calls by checking if the attribute is set before unsetting it.
$this->unset[$offset] = true;
}

/**
* @inheritdoc
*/
public function offsetSet($offset, $value): void
{
parent::offsetSet($offset, $value);

// Setting an attribute cancels the unset operation.
unset($this->unset[$offset]);
}

/**
* Remove one or more fields.
*
* @param mixed $columns
* @return int
* @param string|string[] $columns
* @return void
*
* @deprecated Use unset() instead.
*/
public function drop($columns)
{
$this->unset($columns);
}

/**
* Remove one or more fields.
*
* @param string|string[] $columns
* @return void
*/
public function unset($columns)
{
$columns = Arr::wrap($columns);

// Unset attributes
foreach ($columns as $column) {
$this->__unset($column);
}

// Perform unset only on current document
return $this->newQuery()->where($this->getKeyName(), $this->getKey())->unset($columns);
}

/**
Expand Down Expand Up @@ -502,19 +570,6 @@ protected function isGuardableColumn($key)
return true;
}

/**
* @inheritdoc
*/
public function __call($method, $parameters)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's nice to see this gone! :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why the method unset was not declared. It might be to allow a relation named unset.

{
// Unset method
if ($method == 'unset') {
return $this->drop(...$parameters);
}

return parent::__call($method, $parameters);
}

/**
* @inheritdoc
*/
Expand Down
9 changes: 6 additions & 3 deletions src/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -607,9 +607,12 @@ public function insertGetId(array $values, $sequence = null)
*/
public function update(array $values, array $options = [])
{
// Use $set as default operator.
if (! str_starts_with(key($values), '$')) {
$values = ['$set' => $values];
// Use $set as default operator for field names that are not in an operator
foreach ($values as $key => $value) {
if (! str_starts_with($key, '$')) {
$values['$set'][$key] = $value;
unset($values[$key]);
}
}

$options = $this->inheritConnectionOptions($options);
Expand Down
55 changes: 55 additions & 0 deletions tests/ModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,10 @@ public function testUnset(): void

$user1->unset('note1');

$this->assertFalse(isset($user1->note1));

$user1->save();

$this->assertFalse(isset($user1->note1));
$this->assertTrue(isset($user1->note2));
$this->assertTrue(isset($user2->note1));
Expand All @@ -488,9 +492,60 @@ public function testUnset(): void
$this->assertTrue(isset($user2->note2));

$user2->unset(['note1', 'note2']);
$user2->save();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there was a separate issue where the model was not marked as dirty - maybe assert on isDirty as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was not dirty since the change was done directly in the database.
I added the assertions on isDirty for exhautivity.


$this->assertFalse(isset($user2->note1));
$this->assertFalse(isset($user2->note2));

// Re-re-fetch to be sure
$user2 = User::find($user2->_id);

$this->assertFalse(isset($user2->note1));
$this->assertFalse(isset($user2->note2));
}

public function testUnsetAndSet(): void
{
$user = User::create(['name' => 'John Doe', 'note1' => 'ABC', 'note2' => 'DEF']);

$this->assertTrue($user->originalIsEquivalent('note1'));

// Unset the value
$user->unset('note1');
$this->assertFalse(isset($user->note1));
$this->assertNull($user['note1']);
$this->assertFalse($user->originalIsEquivalent('note1'));
$this->assertTrue($user->isDirty());
$this->assertSame(['$unset' => ['note1' => true]], $user->getDirty());

// Reset the previous value
$user->note1 = 'ABC';
$this->assertTrue($user->originalIsEquivalent('note1'));
$this->assertFalse($user->isDirty());
$this->assertSame([], $user->getDirty());

// Change the value
$user->note1 = 'GHI';
$this->assertTrue(isset($user->note1));
$this->assertSame('GHI', $user['note1']);
$this->assertFalse($user->originalIsEquivalent('note1'));
$this->assertTrue($user->isDirty());
$this->assertSame(['note1' => 'GHI'], $user->getDirty());

// Fetch to be sure the changes are not persisted yet
$userCheck = User::find($user->_id);
$this->assertSame('ABC', $userCheck['note1']);

// Persist the changes
$user->save();

// Re-fetch to be sure
$user = User::find($user->_id);

$this->assertTrue(isset($user->note1));
$this->assertSame('GHI', $user->note1);
$this->assertTrue($user->originalIsEquivalent('note1'));
$this->assertFalse($user->isDirty());
}

public function testDates(): void
Expand Down
32 changes: 32 additions & 0 deletions tests/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,38 @@ public function testUpdate()
$this->assertEquals(20, $jane['age']);
}

public function testUpdateOperators()
{
DB::collection('users')->insert([
['name' => 'Jane Doe', 'age' => 20],
['name' => 'John Doe', 'age' => 19],
]);

DB::collection('users')->where('name', 'John Doe')->update(
[
'$unset' => ['age' => 1],
'ageless' => true,
]
);
DB::collection('users')->where('name', 'Jane Doe')->update(
[
'$inc' => ['age' => 1],
'$set' => ['pronoun' => 'she'],
'ageless' => false,
]
);

$john = DB::collection('users')->where('name', 'John Doe')->first();
$jane = DB::collection('users')->where('name', 'Jane Doe')->first();

$this->assertArrayNotHasKey('age', $john);
$this->assertTrue($john['ageless']);

$this->assertEquals(21, $jane['age']);
$this->assertEquals('she', $jane['pronoun']);
$this->assertFalse($jane['ageless']);
}

public function testDelete()
{
DB::collection('users')->insert([
Expand Down