Skip to content

Commit

Permalink
[8.x] Add eloquent strict loading mode (#37363)
Browse files Browse the repository at this point in the history
* add eloquent strict loading mode

* stop throwing exceptions on trying to get the value of non loaded attributes

* refactor

* fix tests

* change to public

* fix tests

* formatting and method naming

Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
themsaid and taylorotwell authored May 19, 2021
1 parent a1ff226 commit 12799b4
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 7 deletions.
7 changes: 6 additions & 1 deletion src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,12 @@ public function hydrate(array $items)
$instance = $this->newModelInstance();

return $instance->newCollection(array_map(function ($item) use ($instance) {
return $instance->newFromBuilder($item);
$model = $instance->newFromBuilder($item);

$model->preventsLazyLoading = Model::preventsLazyLoading();


return $model;
}, $items));
}

Expand Down
27 changes: 23 additions & 4 deletions src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\JsonEncodingException;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\LazyLoadingViolationException;
use Illuminate\Database\StrictLoadingViolationException;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection as BaseCollection;
Expand Down Expand Up @@ -433,13 +435,30 @@ public function getRelationValue($key)
return $this->relations[$key];
}

if (! $this->isRelation($key)) {
return;
}

if ($this->preventsLazyLoading) {
throw new LazyLoadingViolationException($this, $key);
}

// If the "attribute" exists as a method on the model, we will just assume
// it is a relationship and will load and return results from the query
// and hydrate the relationship's value on the "relationships" array.
if (method_exists($this, $key) ||
(static::$relationResolvers[get_class($this)][$key] ?? null)) {
return $this->getRelationshipFromMethod($key);
}
return $this->getRelationshipFromMethod($key);
}

/**
* Determine if the given key is a relationship method on the model.
*
* @param string $key
* @return bool
*/
public function isRelation($key)
{
return method_exists($this, $key) ||
(static::$relationResolvers[get_class($this)][$key] ?? null);
}

/**
Expand Down
35 changes: 35 additions & 0 deletions src/Illuminate/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializab
*/
protected $withCount = [];

/**
* Indicates whether lazy loading will be prevented on this model.
*
* @var bool
*/
public $preventsLazyLoading = false;

/**
* The number of models to return for pagination.
*
Expand Down Expand Up @@ -144,6 +151,13 @@ abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializab
*/
protected static $ignoreOnTouch = [];

/**
* Indicates whether lazy loading should be restricted on all models.
*
* @var bool
*/
protected static $modelsShouldPreventLazyLoading = false;

/**
* The name of the "created at" column.
*
Expand Down Expand Up @@ -333,6 +347,17 @@ public static function isIgnoringTouch($class = null)
return false;
}

/**
* Prevent model relationships from being lazy loaded.
*
* @param bool $value
* @return void
*/
public static function preventLazyLoading($value = true)
{
static::$modelsShouldPreventLazyLoading = $value;
}

/**
* Fill the model with an array of attributes.
*
Expand Down Expand Up @@ -1807,6 +1832,16 @@ public function setPerPage($perPage)
return $this;
}

/**
* Determine if lazy loading is disabled.
*
* @return bool
*/
public static function preventsLazyLoading()
{
return static::$modelsShouldPreventLazyLoading;
}

/**
* Dynamically retrieve attributes on the model.
*
Expand Down
39 changes: 39 additions & 0 deletions src/Illuminate/Database/LazyLoadingViolationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Illuminate\Database;

use RuntimeException;

class LazyLoadingViolationException extends RuntimeException
{
/**
* The name of the affected Eloquent model.
*
* @var string
*/
public $model;

/**
* The name of the relation.
*
* @var string
*/
public $relation;

/**
* Create a new exception instance.
*
* @param object $model
* @param string $relation
* @return static
*/
public function __construct($model, $relation)
{
$class = get_class($model);

parent::__construct("Attempted to lazy load [{$relation}] on model [{$class}] but lazy loading is disabled.");

$this->model = $class;
$this->relation = $relation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ public function testModelsAreProperlyMatchedToParents()
$model1->shouldReceive('getAttribute')->with('foo')->passthru();
$model1->shouldReceive('hasGetMutator')->andReturn(false);
$model1->shouldReceive('getCasts')->andReturn([]);
$model1->shouldReceive('getRelationValue', 'relationLoaded', 'setRelation')->passthru();
$model1->shouldReceive('getRelationValue', 'relationLoaded', 'setRelation', 'isRelation')->passthru();

$model2 = m::mock(Model::class);
$model2->shouldReceive('getAttribute')->with('parent_key')->andReturn(2);
$model2->shouldReceive('getAttribute')->with('foo')->passthru();
$model2->shouldReceive('hasGetMutator')->andReturn(false);
$model2->shouldReceive('getCasts')->andReturn([]);
$model2->shouldReceive('getRelationValue', 'relationLoaded', 'setRelation')->passthru();
$model2->shouldReceive('getRelationValue', 'relationLoaded', 'setRelation', 'isRelation')->passthru();

$result1 = (object) [
'pivot' => (object) [
Expand Down
134 changes: 134 additions & 0 deletions tests/Integration/Database/EloquentStrictLoadingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php

namespace Illuminate\Tests\Integration\Database;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\LazyLoadingViolationException;
use Illuminate\Support\Facades\Schema;

/**
* @group integration
*/
class EloquentStrictLoadingTest extends DatabaseTestCase
{
protected function setUp(): void
{
parent::setUp();

Schema::create('test_model1', function (Blueprint $table) {
$table->increments('id');
$table->integer('number')->default(1);
});

Schema::create('test_model2', function (Blueprint $table) {
$table->increments('id');
$table->foreignId('model_1_id');
});

Schema::create('test_model3', function (Blueprint $table) {
$table->increments('id');
$table->foreignId('model_2_id');
});

Model::preventLazyLoading();
}

public function testStrictModeThrowsAnExceptionOnLazyLoading()
{
$this->expectException(LazyLoadingViolationException::class);
$this->expectExceptionMessage('Attempted to lazy load');

EloquentStrictLoadingTestModel1::create();
EloquentStrictLoadingTestModel1::create();

$models = EloquentStrictLoadingTestModel1::get();

$models[0]->modelTwos;
}

public function testStrictModeDoesntThrowAnExceptionOnAttributes()
{
EloquentStrictLoadingTestModel1::create();

$models = EloquentStrictLoadingTestModel1::get(['id']);

$this->assertNull($models[0]->number);
}

public function testStrictModeDoesntThrowAnExceptionOnEagerLoading()
{
$this->app['config']->set('database.connections.testbench.zxc', false);

EloquentStrictLoadingTestModel1::create();
EloquentStrictLoadingTestModel1::create();

$models = EloquentStrictLoadingTestModel1::with('modelTwos')->get();

$this->assertInstanceOf(Collection::class, $models[0]->modelTwos);
}

public function testStrictModeDoesntThrowAnExceptionOnLazyEagerLoading()
{
EloquentStrictLoadingTestModel1::create();
EloquentStrictLoadingTestModel1::create();

$models = EloquentStrictLoadingTestModel1::get();

$models->load('modelTwos');

$this->assertInstanceOf(Collection::class, $models[0]->modelTwos);
}

public function testStrictModeDoesntThrowAnExceptionOnSingleModelLoading()
{
$model = EloquentStrictLoadingTestModel1::create();

$this->assertInstanceOf(Collection::class, $model->modelTwos);
}

public function testStrictModeThrowsAnExceptionOnLazyLoadingInRelations()
{
$this->expectException(LazyLoadingViolationException::class);
$this->expectExceptionMessage('Attempted to lazy load');

$model1 = EloquentStrictLoadingTestModel1::create();
EloquentStrictLoadingTestModel2::create(['model_1_id' => $model1->id]);

$models = EloquentStrictLoadingTestModel1::with('modelTwos')->get();

$models[0]->modelTwos[0]->modelThrees;
}
}

class EloquentStrictLoadingTestModel1 extends Model
{
public $table = 'test_model1';
public $timestamps = false;
protected $guarded = [];

public function modelTwos()
{
return $this->hasMany(EloquentStrictLoadingTestModel2::class, 'model_1_id');
}
}

class EloquentStrictLoadingTestModel2 extends Model
{
public $table = 'test_model2';
public $timestamps = false;
protected $guarded = [];

public function modelThrees()
{
return $this->hasMany(EloquentStrictLoadingTestModel3::class, 'model_2_id');
}
}

class EloquentStrictLoadingTestModel3 extends Model
{
public $table = 'test_model3';
public $timestamps = false;
protected $guarded = [];
}

0 comments on commit 12799b4

Please sign in to comment.