Skip to content

Commit

Permalink
[9.x] Opt-in Model::preventAccessingMissingAttributes() option (#44283
Browse files Browse the repository at this point in the history
)

* 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]>
  • Loading branch information
inxilpro and taylorotwell authored Oct 7, 2022
1 parent dfb215e commit 1f86a99
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 2 deletions.
26 changes: 24 additions & 2 deletions src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\JsonEncodingException;
use Illuminate\Database\Eloquent\MissingAttributeException;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\LazyLoadingViolationException;
use Illuminate\Support\Arr;
Expand Down Expand Up @@ -445,10 +446,31 @@ public function getAttribute($key)
// since we don't want to treat any of those methods as relationships because
// they are all intended as helper methods and none of these are relations.
if (method_exists(self::class, $key)) {
return;
return $this->throwMissingAttributeExceptionIfApplicable($key);
}

return $this->isRelation($key) || $this->relationLoaded($key)
? $this->getRelationValue($key)
: $this->throwMissingAttributeExceptionIfApplicable($key);
}

/**
* Either throw a missing attribute exception or return null depending on Eloquent's configuration.
*
* @param string $key
* @return null
*
* @throws \Illuminate\Database\Eloquent\MissingAttributeException
*/
protected function throwMissingAttributeExceptionIfApplicable($key)
{
if ($this->exists &&
! $this->wasRecentlyCreated &&
static::preventsAccessingMissingAttributes()) {
throw new MissingAttributeException($this, $key);
}

return $this->getRelationValue($key);
return null;
}

/**
Expand Down
23 changes: 23 additions & 0 deletions src/Illuminate/Database/Eloquent/MissingAttributeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Illuminate\Database\Eloquent;

use OutOfBoundsException;

class MissingAttributeException extends OutOfBoundsException
{
/**
* Create a new missing attribute exception instance.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @return void
*/
public function __construct($model, $key)
{
parent::__construct(sprintf(
'The attribute [%s] either does not exist or was not retrieved for model [%s].',
$key, get_class($model)
));
}
}
45 changes: 45 additions & 0 deletions src/Illuminate/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,13 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt
*/
protected static $modelsShouldPreventSilentlyDiscardingAttributes = false;

/**
* Indicates if an exception should be thrown when trying to access a missing attribute on a retrieved model.
*
* @var bool
*/
protected static $modelsShouldPreventAccessingMissingAttributes = false;

/**
* Indicates if broadcasting is currently enabled.
*
Expand Down Expand Up @@ -377,6 +384,23 @@ public static function isIgnoringTouch($class = null)
return false;
}

/**
* Indicate that models should prevent lazy loading, silently discarding attributes, and accessing missing attributes.
*
* @param bool $shouldBeStrict
* @return void
*/
public static function shouldBeStrict(bool $shouldBeStrict = true)
{
if (! $shouldBeStrict) {
return;
}

static::preventLazyLoading();
static::preventSilentlyDiscardingAttributes();
static::preventsAccessingMissingAttributes();
}

/**
* Prevent model relationships from being lazy loaded.
*
Expand Down Expand Up @@ -410,6 +434,17 @@ public static function preventSilentlyDiscardingAttributes($value = true)
static::$modelsShouldPreventSilentlyDiscardingAttributes = $value;
}

/**
* Prevent accessing missing attributes on retrieved models.
*
* @param bool $value
* @return void
*/
public static function preventAccessingMissingAttributes($value = true)
{
static::$modelsShouldPreventAccessingMissingAttributes = $value;
}

/**
* Execute a callback without broadcasting any model events for all model types.
*
Expand Down Expand Up @@ -2100,6 +2135,16 @@ public static function preventsSilentlyDiscardingAttributes()
return static::$modelsShouldPreventSilentlyDiscardingAttributes;
}

/**
* Determine if accessing missing attributes is disabled.
*
* @return bool
*/
public static function preventsAccessingMissingAttributes()
{
return static::$modelsShouldPreventAccessingMissingAttributes;
}

/**
* Get the broadcast channel route definition that is associated with the given entity.
*
Expand Down
28 changes: 28 additions & 0 deletions tests/Database/DatabaseEloquentModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\JsonEncodingException;
use Illuminate\Database\Eloquent\MassAssignmentException;
use Illuminate\Database\Eloquent\MissingAttributeException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Relation;
Expand Down Expand Up @@ -2319,6 +2320,33 @@ public function testWithoutTouchingOnCallback()
$this->assertTrue($called);
}

public function testAccessingMissingAttributes()
{
try {
Model::preventAccessingMissingAttributes(false);

$model = new EloquentModelStub(['id' => 1]);
$model->exists = true;

// Default behavior
$this->assertEquals(1, $model->id);
$this->assertNull($model->this_attribute_does_not_exist);

Model::preventAccessingMissingAttributes(true);

// "preventAccessingMissingAttributes" behavior
$this->expectException(MissingAttributeException::class);
$model->this_attribute_does_not_exist;

// Ensure that unsaved models do not trigger the exception
$newModel = new EloquentModelStub(['id' => 2]);
$this->assertEquals(2, $newModel->id);
$this->assertNull($newModel->this_attribute_does_not_exist);
} finally {
Model::preventAccessingMissingAttributes(false);
}
}

protected function addMockConnection($model)
{
$model->setConnectionResolver($resolver = m::mock(ConnectionResolverInterface::class));
Expand Down

0 comments on commit 1f86a99

Please sign in to comment.