Skip to content

Commit

Permalink
[9.x] Add Model::withoutTimestamps(...) (#44138)
Browse files Browse the repository at this point in the history
* add withoutTimestamps

* pass through $this

* ensure timestamps are restored

* refactor to static

* fix test

* formatting

Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
timacdonald and taylorotwell committed Sep 15, 2022
1 parent ba37a05 commit 0432012
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 2 deletions.
57 changes: 56 additions & 1 deletion src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ trait HasTimestamps
*/
public $timestamps = true;

/**
* The list of models classes that have timestamps temporarily disabled.
*
* @var array
*/
protected static $ignoreTimestampsOn = [];

/**
* Update the model's update timestamp.
*
Expand Down Expand Up @@ -113,7 +120,7 @@ public function freshTimestampString()
*/
public function usesTimestamps()
{
return $this->timestamps;
return $this->timestamps && ! static::isIgnoringTimestamps($this::class);
}

/**
Expand Down Expand Up @@ -155,4 +162,52 @@ public function getQualifiedUpdatedAtColumn()
{
return $this->qualifyColumn($this->getUpdatedAtColumn());
}

/**
* Disable timestamps for the current class during the given callback scope.
*
* @param callable $callback
* @return void
*/
public static function withoutTimestamps(callable $callback)
{
static::withoutTimestampsOn([static::class], $callback);
}

/**
* Disable timestamps for the given model classes during the given callback scope.
*
* @param array $models
* @param callable $callback
* @return mixed
*/
public static function withoutTimestampsOn($models, $callback)
{
static::$ignoreTimestampsOn = array_values(array_merge(static::$ignoreTimestampsOn, $models));

try {
return $callback();
} finally {
static::$ignoreTimestampsOn = array_values(array_diff(static::$ignoreTimestampsOn, $models));
}
}

/**
* Determine if the given model is ignoring timestamps / touches.
*
* @param string|null $class
* @return bool
*/
public static function isIgnoringTimestamps($class = null)
{
$class ??= static::class;

foreach (static::$ignoreTimestampsOn as $ignoredClass) {
if ($class === $ignoredClass || is_subclass_of($class, $ignoredClass)) {
return true;
}
}

return false;
}
}
2 changes: 1 addition & 1 deletion src/Illuminate/Database/Eloquent/SoftDeletes.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ protected function runSoftDelete()

$this->{$this->getDeletedAtColumn()} = $time;

if ($this->timestamps && ! is_null($this->getUpdatedAtColumn())) {
if ($this->usesTimestamps() && ! is_null($this->getUpdatedAtColumn())) {
$this->{$this->getUpdatedAtColumn()} = $time;

$columns[$this->getUpdatedAtColumn()] = $this->fromDateTime($time);
Expand Down
173 changes: 173 additions & 0 deletions tests/Database/DatabaseEloquentTimestampsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Support\Carbon;
use PHPUnit\Framework\TestCase;
use RuntimeException;

class DatabaseEloquentTimestampsTest extends TestCase
{
Expand Down Expand Up @@ -102,6 +103,178 @@ public function testUserWithUpdatedAt()
$this->assertEquals($now->toDateTimeString(), $user->updated_at->toDateTimeString());
}

public function testWithoutTimestamp()
{
Carbon::setTestNow($now = Carbon::now()->setYear(1995)->startOfYear());
$user = UserWithCreatedAndUpdated::create(['email' => '[email protected]']);
Carbon::setTestNow(Carbon::now()->addHour());

$this->assertTrue($user->usesTimestamps());

$user->withoutTimestamps(function () use ($user) {
$this->assertFalse($user->usesTimestamps());
$user->update([
'email' => '[email protected]',
]);
});

$this->assertTrue($user->usesTimestamps());
$this->assertTrue($now->equalTo($user->updated_at));
$this->assertSame('[email protected]', $user->email);
}

public function testWithoutTimestampWhenAlreadyIgnoringTimestamps()
{
Carbon::setTestNow($now = Carbon::now()->setYear(1995)->startOfYear());
$user = UserWithCreatedAndUpdated::create(['email' => '[email protected]']);
Carbon::setTestNow(Carbon::now()->addHour());

$user->timestamps = false;

$this->assertFalse($user->usesTimestamps());

$user->withoutTimestamps(function () use ($user) {
$this->assertFalse($user->usesTimestamps());
$user->update([
'email' => '[email protected]',
]);
});

$this->assertFalse($user->usesTimestamps());
$this->assertTrue($now->equalTo($user->updated_at));
$this->assertSame('[email protected]', $user->email);
}

public function testWithoutTimestampRestoresWhenClosureThrowsException()
{
$user = UserWithCreatedAndUpdated::create(['email' => '[email protected]']);

$user->timestamps = true;

try {
$user->withoutTimestamps(function () use ($user) {
$this->assertFalse($user->usesTimestamps());
throw new RuntimeException();
});
$this->fail();
} catch (RuntimeException) {
//
}

$this->assertTrue($user->timestamps);
}

public function testWithoutTimestampsRespectsClasses()
{
$a = new UserWithCreatedAndUpdated();
$b = new UserWithCreatedAndUpdated();
$z = new UserWithUpdated();

$this->assertTrue($a->usesTimestamps());
$this->assertTrue($b->usesTimestamps());
$this->assertTrue($z->usesTimestamps());
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));

Eloquent::withoutTimestamps(function () use ($a, $b, $z) {
$this->assertFalse($a->usesTimestamps());
$this->assertFalse($b->usesTimestamps());
$this->assertFalse($z->usesTimestamps());
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
});

$this->assertTrue($a->usesTimestamps());
$this->assertTrue($b->usesTimestamps());
$this->assertTrue($z->usesTimestamps());
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));

UserWithCreatedAndUpdated::withoutTimestamps(function () use ($a, $b, $z) {
$this->assertFalse($a->usesTimestamps());
$this->assertFalse($b->usesTimestamps());
$this->assertTrue($z->usesTimestamps());
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
});

$this->assertTrue($a->usesTimestamps());
$this->assertTrue($b->usesTimestamps());
$this->assertTrue($z->usesTimestamps());
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));

UserWithUpdated::withoutTimestamps(function () use ($a, $b, $z) {
$this->assertTrue($a->usesTimestamps());
$this->assertTrue($b->usesTimestamps());
$this->assertFalse($z->usesTimestamps());
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
});

$this->assertTrue($a->usesTimestamps());
$this->assertTrue($b->usesTimestamps());
$this->assertTrue($z->usesTimestamps());
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));

Eloquent::withoutTimestampsOn([], function () use ($a, $b, $z) {
$this->assertTrue($a->usesTimestamps());
$this->assertTrue($b->usesTimestamps());
$this->assertTrue($z->usesTimestamps());
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
});

$this->assertTrue($a->usesTimestamps());
$this->assertTrue($b->usesTimestamps());
$this->assertTrue($z->usesTimestamps());
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));

Eloquent::withoutTimestampsOn([UserWithCreatedAndUpdated::class], function () use ($a, $b, $z) {
$this->assertFalse($a->usesTimestamps());
$this->assertFalse($b->usesTimestamps());
$this->assertTrue($z->usesTimestamps());
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
});

$this->assertTrue($a->usesTimestamps());
$this->assertTrue($b->usesTimestamps());
$this->assertTrue($z->usesTimestamps());
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));

Eloquent::withoutTimestampsOn([UserWithUpdated::class], function () use ($a, $b, $z) {
$this->assertTrue($a->usesTimestamps());
$this->assertTrue($b->usesTimestamps());
$this->assertFalse($z->usesTimestamps());
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
});

$this->assertTrue($a->usesTimestamps());
$this->assertTrue($b->usesTimestamps());
$this->assertTrue($z->usesTimestamps());
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));

Eloquent::withoutTimestampsOn([UserWithCreatedAndUpdated::class, UserWithUpdated::class], function () use ($a, $b, $z) {
$this->assertFalse($a->usesTimestamps());
$this->assertFalse($b->usesTimestamps());
$this->assertFalse($z->usesTimestamps());
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
});

$this->assertTrue($a->usesTimestamps());
$this->assertTrue($b->usesTimestamps());
$this->assertTrue($z->usesTimestamps());
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
}

/**
* Get a database connection instance.
*
Expand Down
1 change: 1 addition & 0 deletions tests/Database/DatabaseSoftDeletingTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function testDeleteSetsSoftDeletedColumn()
'deleted_at',
'updated_at',
]);
$model->shouldReceive('usesTimestamps')->once()->andReturn(true);
$model->delete();

$this->assertInstanceOf(Carbon::class, $model->deleted_at);
Expand Down

0 comments on commit 0432012

Please sign in to comment.