Skip to content

Commit 1f86a99

Browse files
[9.x] Opt-in Model::preventAccessingMissingAttributes() option (#44283)
* preventAccessingMissingAttributes concept * Reset missing attribute flag * StyleCI * Always revert Model::preventAccessingMissingAttributes * Only throw on retrieved models * Add model name to missing attribute exception message * formatting * fix oversight * add should be strict method Co-authored-by: Taylor Otwell <[email protected]>
1 parent dfb215e commit 1f86a99

File tree

4 files changed

+120
-2
lines changed

4 files changed

+120
-2
lines changed

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Illuminate\Database\Eloquent\Casts\Attribute;
1818
use Illuminate\Database\Eloquent\InvalidCastException;
1919
use Illuminate\Database\Eloquent\JsonEncodingException;
20+
use Illuminate\Database\Eloquent\MissingAttributeException;
2021
use Illuminate\Database\Eloquent\Relations\Relation;
2122
use Illuminate\Database\LazyLoadingViolationException;
2223
use Illuminate\Support\Arr;
@@ -445,10 +446,31 @@ public function getAttribute($key)
445446
// since we don't want to treat any of those methods as relationships because
446447
// they are all intended as helper methods and none of these are relations.
447448
if (method_exists(self::class, $key)) {
448-
return;
449+
return $this->throwMissingAttributeExceptionIfApplicable($key);
450+
}
451+
452+
return $this->isRelation($key) || $this->relationLoaded($key)
453+
? $this->getRelationValue($key)
454+
: $this->throwMissingAttributeExceptionIfApplicable($key);
455+
}
456+
457+
/**
458+
* Either throw a missing attribute exception or return null depending on Eloquent's configuration.
459+
*
460+
* @param string $key
461+
* @return null
462+
*
463+
* @throws \Illuminate\Database\Eloquent\MissingAttributeException
464+
*/
465+
protected function throwMissingAttributeExceptionIfApplicable($key)
466+
{
467+
if ($this->exists &&
468+
! $this->wasRecentlyCreated &&
469+
static::preventsAccessingMissingAttributes()) {
470+
throw new MissingAttributeException($this, $key);
449471
}
450472

451-
return $this->getRelationValue($key);
473+
return null;
452474
}
453475

454476
/**
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent;
4+
5+
use OutOfBoundsException;
6+
7+
class MissingAttributeException extends OutOfBoundsException
8+
{
9+
/**
10+
* Create a new missing attribute exception instance.
11+
*
12+
* @param \Illuminate\Database\Eloquent\Model $model
13+
* @param string $key
14+
* @return void
15+
*/
16+
public function __construct($model, $key)
17+
{
18+
parent::__construct(sprintf(
19+
'The attribute [%s] either does not exist or was not retrieved for model [%s].',
20+
$key, get_class($model)
21+
));
22+
}
23+
}

src/Illuminate/Database/Eloquent/Model.php

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

184+
/**
185+
* Indicates if an exception should be thrown when trying to access a missing attribute on a retrieved model.
186+
*
187+
* @var bool
188+
*/
189+
protected static $modelsShouldPreventAccessingMissingAttributes = false;
190+
184191
/**
185192
* Indicates if broadcasting is currently enabled.
186193
*
@@ -377,6 +384,23 @@ public static function isIgnoringTouch($class = null)
377384
return false;
378385
}
379386

387+
/**
388+
* Indicate that models should prevent lazy loading, silently discarding attributes, and accessing missing attributes.
389+
*
390+
* @param bool $shouldBeStrict
391+
* @return void
392+
*/
393+
public static function shouldBeStrict(bool $shouldBeStrict = true)
394+
{
395+
if (! $shouldBeStrict) {
396+
return;
397+
}
398+
399+
static::preventLazyLoading();
400+
static::preventSilentlyDiscardingAttributes();
401+
static::preventsAccessingMissingAttributes();
402+
}
403+
380404
/**
381405
* Prevent model relationships from being lazy loaded.
382406
*
@@ -410,6 +434,17 @@ public static function preventSilentlyDiscardingAttributes($value = true)
410434
static::$modelsShouldPreventSilentlyDiscardingAttributes = $value;
411435
}
412436

437+
/**
438+
* Prevent accessing missing attributes on retrieved models.
439+
*
440+
* @param bool $value
441+
* @return void
442+
*/
443+
public static function preventAccessingMissingAttributes($value = true)
444+
{
445+
static::$modelsShouldPreventAccessingMissingAttributes = $value;
446+
}
447+
413448
/**
414449
* Execute a callback without broadcasting any model events for all model types.
415450
*
@@ -2100,6 +2135,16 @@ public static function preventsSilentlyDiscardingAttributes()
21002135
return static::$modelsShouldPreventSilentlyDiscardingAttributes;
21012136
}
21022137

2138+
/**
2139+
* Determine if accessing missing attributes is disabled.
2140+
*
2141+
* @return bool
2142+
*/
2143+
public static function preventsAccessingMissingAttributes()
2144+
{
2145+
return static::$modelsShouldPreventAccessingMissingAttributes;
2146+
}
2147+
21032148
/**
21042149
* Get the broadcast channel route definition that is associated with the given entity.
21052150
*

tests/Database/DatabaseEloquentModelTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Illuminate\Database\Eloquent\Collection;
2424
use Illuminate\Database\Eloquent\JsonEncodingException;
2525
use Illuminate\Database\Eloquent\MassAssignmentException;
26+
use Illuminate\Database\Eloquent\MissingAttributeException;
2627
use Illuminate\Database\Eloquent\Model;
2728
use Illuminate\Database\Eloquent\Relations\BelongsTo;
2829
use Illuminate\Database\Eloquent\Relations\Relation;
@@ -2319,6 +2320,33 @@ public function testWithoutTouchingOnCallback()
23192320
$this->assertTrue($called);
23202321
}
23212322

2323+
public function testAccessingMissingAttributes()
2324+
{
2325+
try {
2326+
Model::preventAccessingMissingAttributes(false);
2327+
2328+
$model = new EloquentModelStub(['id' => 1]);
2329+
$model->exists = true;
2330+
2331+
// Default behavior
2332+
$this->assertEquals(1, $model->id);
2333+
$this->assertNull($model->this_attribute_does_not_exist);
2334+
2335+
Model::preventAccessingMissingAttributes(true);
2336+
2337+
// "preventAccessingMissingAttributes" behavior
2338+
$this->expectException(MissingAttributeException::class);
2339+
$model->this_attribute_does_not_exist;
2340+
2341+
// Ensure that unsaved models do not trigger the exception
2342+
$newModel = new EloquentModelStub(['id' => 2]);
2343+
$this->assertEquals(2, $newModel->id);
2344+
$this->assertNull($newModel->this_attribute_does_not_exist);
2345+
} finally {
2346+
Model::preventAccessingMissingAttributes(false);
2347+
}
2348+
}
2349+
23222350
protected function addMockConnection($model)
23232351
{
23242352
$model->setConnectionResolver($resolver = m::mock(ConnectionResolverInterface::class));

0 commit comments

Comments
 (0)