Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.x] Opt-in Model::preventAccessingMissingAttributes() option #44283

Merged
merged 11 commits into from
Oct 7, 2022
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