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

[8.x] Add eloquent strict loading mode #37363

Merged
merged 7 commits into from
May 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1796,6 +1821,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 = [];
}