Skip to content

Commit 0432012

Browse files
[9.x] Add Model::withoutTimestamps(...) (#44138)
* add withoutTimestamps * pass through $this * ensure timestamps are restored * refactor to static * fix test * formatting Co-authored-by: Taylor Otwell <[email protected]>
1 parent ba37a05 commit 0432012

File tree

4 files changed

+231
-2
lines changed

4 files changed

+231
-2
lines changed

src/Illuminate/Database/Eloquent/Concerns/HasTimestamps.php

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ trait HasTimestamps
1313
*/
1414
public $timestamps = true;
1515

16+
/**
17+
* The list of models classes that have timestamps temporarily disabled.
18+
*
19+
* @var array
20+
*/
21+
protected static $ignoreTimestampsOn = [];
22+
1623
/**
1724
* Update the model's update timestamp.
1825
*
@@ -113,7 +120,7 @@ public function freshTimestampString()
113120
*/
114121
public function usesTimestamps()
115122
{
116-
return $this->timestamps;
123+
return $this->timestamps && ! static::isIgnoringTimestamps($this::class);
117124
}
118125

119126
/**
@@ -155,4 +162,52 @@ public function getQualifiedUpdatedAtColumn()
155162
{
156163
return $this->qualifyColumn($this->getUpdatedAtColumn());
157164
}
165+
166+
/**
167+
* Disable timestamps for the current class during the given callback scope.
168+
*
169+
* @param callable $callback
170+
* @return void
171+
*/
172+
public static function withoutTimestamps(callable $callback)
173+
{
174+
static::withoutTimestampsOn([static::class], $callback);
175+
}
176+
177+
/**
178+
* Disable timestamps for the given model classes during the given callback scope.
179+
*
180+
* @param array $models
181+
* @param callable $callback
182+
* @return mixed
183+
*/
184+
public static function withoutTimestampsOn($models, $callback)
185+
{
186+
static::$ignoreTimestampsOn = array_values(array_merge(static::$ignoreTimestampsOn, $models));
187+
188+
try {
189+
return $callback();
190+
} finally {
191+
static::$ignoreTimestampsOn = array_values(array_diff(static::$ignoreTimestampsOn, $models));
192+
}
193+
}
194+
195+
/**
196+
* Determine if the given model is ignoring timestamps / touches.
197+
*
198+
* @param string|null $class
199+
* @return bool
200+
*/
201+
public static function isIgnoringTimestamps($class = null)
202+
{
203+
$class ??= static::class;
204+
205+
foreach (static::$ignoreTimestampsOn as $ignoredClass) {
206+
if ($class === $ignoredClass || is_subclass_of($class, $ignoredClass)) {
207+
return true;
208+
}
209+
}
210+
211+
return false;
212+
}
158213
}

src/Illuminate/Database/Eloquent/SoftDeletes.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ protected function runSoftDelete()
8787

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

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

9393
$columns[$this->getUpdatedAtColumn()] = $this->fromDateTime($time);

tests/Database/DatabaseEloquentTimestampsTest.php

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Illuminate\Database\Eloquent\Model as Eloquent;
77
use Illuminate\Support\Carbon;
88
use PHPUnit\Framework\TestCase;
9+
use RuntimeException;
910

1011
class DatabaseEloquentTimestampsTest extends TestCase
1112
{
@@ -102,6 +103,178 @@ public function testUserWithUpdatedAt()
102103
$this->assertEquals($now->toDateTimeString(), $user->updated_at->toDateTimeString());
103104
}
104105

106+
public function testWithoutTimestamp()
107+
{
108+
Carbon::setTestNow($now = Carbon::now()->setYear(1995)->startOfYear());
109+
$user = UserWithCreatedAndUpdated::create(['email' => '[email protected]']);
110+
Carbon::setTestNow(Carbon::now()->addHour());
111+
112+
$this->assertTrue($user->usesTimestamps());
113+
114+
$user->withoutTimestamps(function () use ($user) {
115+
$this->assertFalse($user->usesTimestamps());
116+
$user->update([
117+
'email' => '[email protected]',
118+
]);
119+
});
120+
121+
$this->assertTrue($user->usesTimestamps());
122+
$this->assertTrue($now->equalTo($user->updated_at));
123+
$this->assertSame('[email protected]', $user->email);
124+
}
125+
126+
public function testWithoutTimestampWhenAlreadyIgnoringTimestamps()
127+
{
128+
Carbon::setTestNow($now = Carbon::now()->setYear(1995)->startOfYear());
129+
$user = UserWithCreatedAndUpdated::create(['email' => '[email protected]']);
130+
Carbon::setTestNow(Carbon::now()->addHour());
131+
132+
$user->timestamps = false;
133+
134+
$this->assertFalse($user->usesTimestamps());
135+
136+
$user->withoutTimestamps(function () use ($user) {
137+
$this->assertFalse($user->usesTimestamps());
138+
$user->update([
139+
'email' => '[email protected]',
140+
]);
141+
});
142+
143+
$this->assertFalse($user->usesTimestamps());
144+
$this->assertTrue($now->equalTo($user->updated_at));
145+
$this->assertSame('[email protected]', $user->email);
146+
}
147+
148+
public function testWithoutTimestampRestoresWhenClosureThrowsException()
149+
{
150+
$user = UserWithCreatedAndUpdated::create(['email' => '[email protected]']);
151+
152+
$user->timestamps = true;
153+
154+
try {
155+
$user->withoutTimestamps(function () use ($user) {
156+
$this->assertFalse($user->usesTimestamps());
157+
throw new RuntimeException();
158+
});
159+
$this->fail();
160+
} catch (RuntimeException) {
161+
//
162+
}
163+
164+
$this->assertTrue($user->timestamps);
165+
}
166+
167+
public function testWithoutTimestampsRespectsClasses()
168+
{
169+
$a = new UserWithCreatedAndUpdated();
170+
$b = new UserWithCreatedAndUpdated();
171+
$z = new UserWithUpdated();
172+
173+
$this->assertTrue($a->usesTimestamps());
174+
$this->assertTrue($b->usesTimestamps());
175+
$this->assertTrue($z->usesTimestamps());
176+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
177+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
178+
179+
Eloquent::withoutTimestamps(function () use ($a, $b, $z) {
180+
$this->assertFalse($a->usesTimestamps());
181+
$this->assertFalse($b->usesTimestamps());
182+
$this->assertFalse($z->usesTimestamps());
183+
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
184+
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
185+
});
186+
187+
$this->assertTrue($a->usesTimestamps());
188+
$this->assertTrue($b->usesTimestamps());
189+
$this->assertTrue($z->usesTimestamps());
190+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
191+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
192+
193+
UserWithCreatedAndUpdated::withoutTimestamps(function () use ($a, $b, $z) {
194+
$this->assertFalse($a->usesTimestamps());
195+
$this->assertFalse($b->usesTimestamps());
196+
$this->assertTrue($z->usesTimestamps());
197+
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
198+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
199+
});
200+
201+
$this->assertTrue($a->usesTimestamps());
202+
$this->assertTrue($b->usesTimestamps());
203+
$this->assertTrue($z->usesTimestamps());
204+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
205+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
206+
207+
UserWithUpdated::withoutTimestamps(function () use ($a, $b, $z) {
208+
$this->assertTrue($a->usesTimestamps());
209+
$this->assertTrue($b->usesTimestamps());
210+
$this->assertFalse($z->usesTimestamps());
211+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
212+
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
213+
});
214+
215+
$this->assertTrue($a->usesTimestamps());
216+
$this->assertTrue($b->usesTimestamps());
217+
$this->assertTrue($z->usesTimestamps());
218+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
219+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
220+
221+
Eloquent::withoutTimestampsOn([], function () use ($a, $b, $z) {
222+
$this->assertTrue($a->usesTimestamps());
223+
$this->assertTrue($b->usesTimestamps());
224+
$this->assertTrue($z->usesTimestamps());
225+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
226+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
227+
});
228+
229+
$this->assertTrue($a->usesTimestamps());
230+
$this->assertTrue($b->usesTimestamps());
231+
$this->assertTrue($z->usesTimestamps());
232+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
233+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
234+
235+
Eloquent::withoutTimestampsOn([UserWithCreatedAndUpdated::class], function () use ($a, $b, $z) {
236+
$this->assertFalse($a->usesTimestamps());
237+
$this->assertFalse($b->usesTimestamps());
238+
$this->assertTrue($z->usesTimestamps());
239+
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
240+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
241+
});
242+
243+
$this->assertTrue($a->usesTimestamps());
244+
$this->assertTrue($b->usesTimestamps());
245+
$this->assertTrue($z->usesTimestamps());
246+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
247+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
248+
249+
Eloquent::withoutTimestampsOn([UserWithUpdated::class], function () use ($a, $b, $z) {
250+
$this->assertTrue($a->usesTimestamps());
251+
$this->assertTrue($b->usesTimestamps());
252+
$this->assertFalse($z->usesTimestamps());
253+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
254+
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
255+
});
256+
257+
$this->assertTrue($a->usesTimestamps());
258+
$this->assertTrue($b->usesTimestamps());
259+
$this->assertTrue($z->usesTimestamps());
260+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
261+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
262+
263+
Eloquent::withoutTimestampsOn([UserWithCreatedAndUpdated::class, UserWithUpdated::class], function () use ($a, $b, $z) {
264+
$this->assertFalse($a->usesTimestamps());
265+
$this->assertFalse($b->usesTimestamps());
266+
$this->assertFalse($z->usesTimestamps());
267+
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
268+
$this->assertTrue(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
269+
});
270+
271+
$this->assertTrue($a->usesTimestamps());
272+
$this->assertTrue($b->usesTimestamps());
273+
$this->assertTrue($z->usesTimestamps());
274+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithCreatedAndUpdated::class));
275+
$this->assertFalse(Eloquent::isIgnoringTimestamps(UserWithUpdated::class));
276+
}
277+
105278
/**
106279
* Get a database connection instance.
107280
*

tests/Database/DatabaseSoftDeletingTraitTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function testDeleteSetsSoftDeletedColumn()
2929
'deleted_at',
3030
'updated_at',
3131
]);
32+
$model->shouldReceive('usesTimestamps')->once()->andReturn(true);
3233
$model->delete();
3334

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

0 commit comments

Comments
 (0)