Skip to content

Commit 82c7dac

Browse files
[9.x] Adds handlers for silently discarded and missing attribute violations (#44664)
* Add the ability to register a callback to handle missing attribute violations. * Add the ability to register a callback to handle silently discarded attributes. * Style
1 parent a45a894 commit 82c7dac

File tree

3 files changed

+129
-26
lines changed

3 files changed

+129
-26
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,10 @@ protected function throwMissingAttributeExceptionIfApplicable($key)
467467
if ($this->exists &&
468468
! $this->wasRecentlyCreated &&
469469
static::preventsAccessingMissingAttributes()) {
470+
if (isset(static::$missingAttributeViolationCallback)) {
471+
return call_user_func(static::$missingAttributeViolationCallback, $this, $key);
472+
}
473+
470474
throw new MissingAttributeException($this, $key);
471475
}
472476

src/Illuminate/Database/Eloquent/Model.php

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,27 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt
181181
*/
182182
protected static $modelsShouldPreventSilentlyDiscardingAttributes = false;
183183

184+
/**
185+
* The callback that is responsible for handling discarded attribute violations.
186+
*
187+
* @var callable|null
188+
*/
189+
protected static $discardedAttributeViolationCallback;
190+
184191
/**
185192
* Indicates if an exception should be thrown when trying to access a missing attribute on a retrieved model.
186193
*
187194
* @var bool
188195
*/
189196
protected static $modelsShouldPreventAccessingMissingAttributes = false;
190197

198+
/**
199+
* The callback that is responsible for handling missing attribute violations.
200+
*
201+
* @var callable|null
202+
*/
203+
protected static $missingAttributeViolationCallback;
204+
191205
/**
192206
* Indicates if broadcasting is currently enabled.
193207
*
@@ -430,6 +444,17 @@ public static function preventSilentlyDiscardingAttributes($value = true)
430444
static::$modelsShouldPreventSilentlyDiscardingAttributes = $value;
431445
}
432446

447+
/**
448+
* Register a callback that is responsible for handling discarded attribute violations.
449+
*
450+
* @param callable|null $callback
451+
* @return void
452+
*/
453+
public static function handleDiscardedAttributeViolationUsing(?callable $callback)
454+
{
455+
static::$discardedAttributeViolationCallback = $callback;
456+
}
457+
433458
/**
434459
* Prevent accessing missing attributes on retrieved models.
435460
*
@@ -441,6 +466,17 @@ public static function preventAccessingMissingAttributes($value = true)
441466
static::$modelsShouldPreventAccessingMissingAttributes = $value;
442467
}
443468

469+
/**
470+
* Register a callback that is responsible for handling lazy loading violations.
471+
*
472+
* @param callable|null $callback
473+
* @return void
474+
*/
475+
public static function handleMissingAttributeViolationUsing(?callable $callback)
476+
{
477+
static::$missingAttributeViolationCallback = $callback;
478+
}
479+
444480
/**
445481
* Execute a callback without broadcasting any model events for all model types.
446482
*
@@ -481,20 +517,30 @@ public function fill(array $attributes)
481517
if ($this->isFillable($key)) {
482518
$this->setAttribute($key, $value);
483519
} elseif ($totallyGuarded || static::preventsSilentlyDiscardingAttributes()) {
484-
throw new MassAssignmentException(sprintf(
485-
'Add [%s] to fillable property to allow mass assignment on [%s].',
486-
$key, get_class($this)
487-
));
520+
if (isset(static::$discardedAttributeViolationCallback)) {
521+
call_user_func(static::$discardedAttributeViolationCallback, $this, [$key]);
522+
} else {
523+
throw new MassAssignmentException(sprintf(
524+
'Add [%s] to fillable property to allow mass assignment on [%s].',
525+
$key, get_class($this)
526+
));
527+
}
488528
}
489529
}
490530

491531
if (count($attributes) !== count($fillable) &&
492532
static::preventsSilentlyDiscardingAttributes()) {
493-
throw new MassAssignmentException(sprintf(
494-
'Add fillable property [%s] to allow mass assignment on [%s].',
495-
implode(', ', array_diff(array_keys($attributes), array_keys($fillable))),
496-
get_class($this)
497-
));
533+
$keys = array_diff(array_keys($attributes), array_keys($fillable));
534+
535+
if (isset(static::$discardedAttributeViolationCallback)) {
536+
call_user_func(static::$discardedAttributeViolationCallback, $this, $keys);
537+
} else {
538+
throw new MassAssignmentException(sprintf(
539+
'Add fillable property [%s] to allow mass assignment on [%s].',
540+
implode(', ', $keys),
541+
get_class($this)
542+
));
543+
}
498544
}
499545

500546
return $this;
@@ -1025,7 +1071,7 @@ public function push()
10251071
// us to recurse into all of these nested relations for the model instance.
10261072
foreach ($this->relations as $models) {
10271073
$models = $models instanceof Collection
1028-
? $models->all() : [$models];
1074+
? $models->all() : [$models];
10291075

10301076
foreach (array_filter($models) as $model) {
10311077
if (! $model->push()) {
@@ -1072,7 +1118,7 @@ public function save(array $options = [])
10721118
// clause to only update this model. Otherwise, we'll just insert them.
10731119
if ($this->exists) {
10741120
$saved = $this->isDirty() ?
1075-
$this->performUpdate($query) : true;
1121+
$this->performUpdate($query) : true;
10761122
}
10771123

10781124
// If the model is brand new, we'll insert it into our database and set the
@@ -1474,8 +1520,8 @@ public function registerGlobalScopes($builder)
14741520
public function newQueryWithoutScopes()
14751521
{
14761522
return $this->newModelQuery()
1477-
->with($this->with)
1478-
->withCount($this->withCount);
1523+
->with($this->with)
1524+
->withCount($this->withCount);
14791525
}
14801526

14811527
/**
@@ -1498,8 +1544,8 @@ public function newQueryWithoutScope($scope)
14981544
public function newQueryForRestoration($ids)
14991545
{
15001546
return is_array($ids)
1501-
? $this->newQueryWithoutScopes()->whereIn($this->getQualifiedKeyName(), $ids)
1502-
: $this->newQueryWithoutScopes()->whereKey($ids);
1547+
? $this->newQueryWithoutScopes()->whereIn($this->getQualifiedKeyName(), $ids)
1548+
: $this->newQueryWithoutScopes()->whereKey($ids);
15031549
}
15041550

15051551
/**
@@ -1547,7 +1593,7 @@ public function newCollection(array $models = [])
15471593
public function newPivot(self $parent, array $attributes, $table, $exists, $using = null)
15481594
{
15491595
return $using ? $using::fromRawAttributes($parent, $attributes, $table, $exists)
1550-
: Pivot::fromAttributes($parent, $attributes, $table, $exists);
1596+
: Pivot::fromAttributes($parent, $attributes, $table, $exists);
15511597
}
15521598

15531599
/**
@@ -1625,9 +1671,9 @@ public function fresh($with = [])
16251671
}
16261672

16271673
return $this->setKeysForSelectQuery($this->newQueryWithoutScopes())
1628-
->useWritePdo()
1629-
->with(is_string($with) ? func_get_args() : $with)
1630-
->first();
1674+
->useWritePdo()
1675+
->with(is_string($with) ? func_get_args() : $with)
1676+
->first();
16311677
}
16321678

16331679
/**
@@ -1705,9 +1751,9 @@ public function replicateQuietly(array $except = null)
17051751
public function is($model)
17061752
{
17071753
return ! is_null($model) &&
1708-
$this->getKey() === $model->getKey() &&
1709-
$this->getTable() === $model->getTable() &&
1710-
$this->getConnectionName() === $model->getConnectionName();
1754+
$this->getKey() === $model->getKey() &&
1755+
$this->getTable() === $model->getTable() &&
1756+
$this->getConnectionName() === $model->getConnectionName();
17111757
}
17121758

17131759
/**
@@ -2050,8 +2096,8 @@ protected function resolveChildRouteBindingQuery($childType, $value, $field)
20502096
}
20512097

20522098
return $relationship instanceof Model
2053-
? $relationship->resolveRouteBindingQuery($relationship, $value, $field)
2054-
: $relationship->getRelated()->resolveRouteBindingQuery($relationship, $value, $field);
2099+
? $relationship->resolveRouteBindingQuery($relationship, $value, $field)
2100+
: $relationship->getRelated()->resolveRouteBindingQuery($relationship, $value, $field);
20552101
}
20562102

20572103
/**
@@ -2295,8 +2341,8 @@ public static function __callStatic($method, $parameters)
22952341
public function __toString()
22962342
{
22972343
return $this->escapeWhenCastingToString
2298-
? e($this->toJson())
2299-
: $this->toJson();
2344+
? e($this->toJson())
2345+
: $this->toJson();
23002346
}
23012347

23022348
/**

tests/Database/DatabaseEloquentModelTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,6 +1296,32 @@ public function testGuarded()
12961296
Model::preventSilentlyDiscardingAttributes(false);
12971297
}
12981298

1299+
public function testUsesOverriddenHandlerWhenDiscardingAttributes()
1300+
{
1301+
EloquentModelStub::setConnectionResolver($resolver = m::mock(Resolver::class));
1302+
$resolver->shouldReceive('connection')->andReturn($connection = m::mock(stdClass::class));
1303+
$connection->shouldReceive('getSchemaBuilder->getColumnListing')->andReturn(['name', 'age', 'foo']);
1304+
1305+
Model::preventSilentlyDiscardingAttributes();
1306+
1307+
$callbackModel = null;
1308+
$callbackKeys = null;
1309+
Model::handleDiscardedAttributeViolationUsing(function ($model, $keys) use (&$callbackModel, &$callbackKeys) {
1310+
$callbackModel = $model;
1311+
$callbackKeys = $keys;
1312+
});
1313+
1314+
$model = new EloquentModelStub;
1315+
$model->guard(['name', 'age']);
1316+
$model->fill(['Foo' => 'bar']);
1317+
1318+
$this->assertInstanceOf(EloquentModelStub::class, $callbackModel);
1319+
$this->assertEquals(['Foo'], $callbackKeys);
1320+
1321+
Model::preventSilentlyDiscardingAttributes(false);
1322+
Model::handleDiscardedAttributeViolationUsing(null);
1323+
}
1324+
12991325
public function testFillableOverridesGuarded()
13001326
{
13011327
Model::preventSilentlyDiscardingAttributes(false);
@@ -2338,6 +2364,33 @@ public function testThrowsWhenAccessingMissingAttributes()
23382364
}
23392365
}
23402366

2367+
public function testUsesOverriddenHandlerWhenAccessingMissingAttributes()
2368+
{
2369+
$originalMode = Model::preventsAccessingMissingAttributes();
2370+
Model::preventAccessingMissingAttributes();
2371+
2372+
$callbackModel = null;
2373+
$callbackKey = null;
2374+
2375+
Model::handleMissingAttributeViolationUsing(function ($model, $key) use (&$callbackModel, &$callbackKey) {
2376+
$callbackModel = $model;
2377+
$callbackKey = $key;
2378+
});
2379+
2380+
$model = new EloquentModelStub(['id' => 1]);
2381+
$model->exists = true;
2382+
2383+
$this->assertEquals(1, $model->id);
2384+
2385+
$model->this_attribute_does_not_exist;
2386+
2387+
$this->assertInstanceOf(EloquentModelStub::class, $callbackModel);
2388+
$this->assertEquals('this_attribute_does_not_exist', $callbackKey);
2389+
2390+
Model::preventAccessingMissingAttributes($originalMode);
2391+
Model::handleMissingAttributeViolationUsing(null);
2392+
}
2393+
23412394
public function testDoesntThrowWhenAccessingMissingAttributesOnModelThatIsNotSaved()
23422395
{
23432396
$originalMode = Model::preventsAccessingMissingAttributes();

0 commit comments

Comments
 (0)