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] Eloquent Strict Loading #37334

Closed
wants to merge 8 commits into from
Closed
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
4 changes: 3 additions & 1 deletion src/Illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,9 @@ public function hydrate(array $items)
$instance = $this->newModelInstance();

return $instance->newCollection(array_map(function ($item) use ($instance) {
return $instance->newFromBuilder($item);
return tap($instance->newFromBuilder($item), function ($instance) {
$instance->strictLoading = optional($this->getConnection())->getConfig('strict_load');
});
}, $items));
}

Expand Down
5 changes: 5 additions & 0 deletions src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\JsonEncodingException;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\StrictLoadingViolationException;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection as BaseCollection;
Expand Down Expand Up @@ -426,6 +427,10 @@ protected function getAttributeFromArray($key)
*/
public function getRelationValue($key)
{
if ($this->strictLoading && ! $this->relationLoaded($key)) {

This comment was marked as resolved.

throw new StrictLoadingViolationException($this, $key);
}

// If the key already exists in the relationships array, it just means the
// relationship has already been loaded, so we'll just return it out of
// here because there is no need to query within the relations twice.
Expand Down
7 changes: 7 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 the lazy loading should be prevented.
*
* @var bool
*/
public $strictLoading = false;

/**
* The number of models to return for pagination.
*
Expand Down
39 changes: 39 additions & 0 deletions src/Illuminate/Database/StrictLoadingViolationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Illuminate\Database;

use RuntimeException;

class StrictLoadingViolationException 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("Trying to lazy load [{$relation}] in model [{$class}] is restricted.");

$this->model = $class;
$this->relation = $relation;
}
}
1 change: 1 addition & 0 deletions tests/Database/DatabaseEloquentBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ public function testGetMethodLoadsModelsAndHydratesEagerRelations()
$builder->shouldReceive('applyScopes')->andReturnSelf();
$builder->shouldReceive('getModels')->with(['foo'])->andReturn(['bar']);
$builder->shouldReceive('eagerLoadRelations')->with(['bar'])->andReturn(['bar', 'baz']);
$builder->shouldReceive('getConnection')->andReturnNull();
$builder->setModel($this->getMockModel());
$builder->getModel()->shouldReceive('newCollection')->with(['bar', 'baz'])->andReturn(new Collection(['bar', 'baz']));

Expand Down
136 changes: 136 additions & 0 deletions tests/Integration/Database/EloquentStrictLoadingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

namespace Illuminate\Tests\Integration\Database;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\StrictLoadingViolationException;
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');
});

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');
});
}

protected function getEnvironmentSetUp($app)
{
$app['config']->set('app.debug', 'true');

$app['config']->set('database.default', 'testbench');

$app['config']->set('database.connections.testbench', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
'strict_load' => true,
]);
}

public function testStrictModeThrowsAnExceptionOnLazyLoading()
{
$this->expectException(StrictLoadingViolationException::class);
$this->expectExceptionMessage('Trying to lazy load [modelTwos] in model [Illuminate\Tests\Integration\Database\EloquentStrictLoadingTestModel1] is restricted');

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

$models = EloquentStrictLoadingTestModel1::get();

$models[0]->modelTwos;
}

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(StrictLoadingViolationException::class);
$this->expectExceptionMessage('Trying to lazy load [modelThrees] in model [Illuminate\Tests\Integration\Database\EloquentStrictLoadingTestModel2] is restricted');

$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 = [];
}