diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index f9415a9613a1..70eb0a067f63 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -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)); } diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index af89e47e0a28..6db255e1ff59 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -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; @@ -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); } /** diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index bc212651496e..94c5bf807d71 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -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. * @@ -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. * @@ -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. * @@ -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. * diff --git a/src/Illuminate/Database/LazyLoadingViolationException.php b/src/Illuminate/Database/LazyLoadingViolationException.php new file mode 100644 index 000000000000..1bcd40c95a36 --- /dev/null +++ b/src/Illuminate/Database/LazyLoadingViolationException.php @@ -0,0 +1,39 @@ +model = $class; + $this->relation = $relation; + } +} diff --git a/tests/Database/DatabaseEloquentBelongsToManyWithCastedAttributesTest.php b/tests/Database/DatabaseEloquentBelongsToManyWithCastedAttributesTest.php index 0e18b5b933af..75d16302b472 100644 --- a/tests/Database/DatabaseEloquentBelongsToManyWithCastedAttributesTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManyWithCastedAttributesTest.php @@ -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) [ diff --git a/tests/Integration/Database/EloquentStrictLoadingTest.php b/tests/Integration/Database/EloquentStrictLoadingTest.php new file mode 100644 index 000000000000..53e70657e367 --- /dev/null +++ b/tests/Integration/Database/EloquentStrictLoadingTest.php @@ -0,0 +1,134 @@ +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 = []; +}