diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 642ed2ec40e8..a87af33fa5e6 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -19,6 +19,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; +use Illuminate\Support\ValidatedInput; use ReflectionClass; use ReflectionMethod; @@ -151,10 +152,10 @@ public function __construct(QueryBuilder $query) /** * Create and return an un-saved model instance. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return \Illuminate\Database\Eloquent\Model|static */ - public function make(array $attributes = []) + public function make(array|ValidatedInput $attributes = []) { return $this->newModelInstance($attributes); } @@ -1017,10 +1018,10 @@ protected function ensureOrderForCursorPagination($shouldReverse = false) /** * Save a new model and return the instance. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return \Illuminate\Database\Eloquent\Model|$this */ - public function create(array $attributes = []) + public function create(array|ValidatedInput $attributes = []) { return tap($this->newModelInstance($attributes), function ($instance) { $instance->save(); @@ -1030,10 +1031,10 @@ public function create(array $attributes = []) /** * Save a new model and return the instance. Allow mass-assignment. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return \Illuminate\Database\Eloquent\Model|$this */ - public function forceCreate(array $attributes) + public function forceCreate(array|ValidatedInput $attributes) { return $this->model->unguarded(function () use ($attributes) { return $this->newModelInstance()->create($attributes); @@ -1043,10 +1044,10 @@ public function forceCreate(array $attributes) /** * Save a new model instance with mass assignment without raising model events. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return \Illuminate\Database\Eloquent\Model|$this */ - public function forceCreateQuietly(array $attributes = []) + public function forceCreateQuietly(array|ValidatedInput $attributes = []) { return Model::withoutEvents(fn () => $this->forceCreate($attributes)); } diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index d93193e12536..a66a99eccacc 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -20,6 +20,7 @@ use Illuminate\Support\Collection as BaseCollection; use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; +use Illuminate\Support\ValidatedInput; use JsonSerializable; use LogicException; use Stringable; @@ -228,10 +229,10 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt /** * Create a new Eloquent model instance. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return void */ - public function __construct(array $attributes = []) + public function __construct(array|ValidatedInput $attributes = []) { $this->bootIfNotBooted(); @@ -501,15 +502,19 @@ public static function withoutBroadcasting(callable $callback) /** * Fill the model with an array of attributes. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return $this * * @throws \Illuminate\Database\Eloquent\MassAssignmentException */ - public function fill(array $attributes) + public function fill(array|ValidatedInput $attributes) { $totallyGuarded = $this->totallyGuarded(); + if ($attributes instanceof ValidatedInput) { + $attributes = $attributes->all(); + } + $fillable = $this->fillableFromArray($attributes); foreach ($fillable as $key => $value) { @@ -551,10 +556,10 @@ public function fill(array $attributes) /** * Fill the model with an array of attributes. Force mass assignment. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return $this */ - public function forceFill(array $attributes) + public function forceFill(array|ValidatedInput $attributes) { return static::unguarded(fn () => $this->fill($attributes)); } @@ -590,11 +595,11 @@ public function qualifyColumns($columns) /** * Create a new instance of the given model. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @param bool $exists * @return static */ - public function newInstance($attributes = [], $exists = false) + public function newInstance(array|ValidatedInput $attributes = [], $exists = false) { // This method just provides a convenient way for us to generate fresh model // instances of this current model. It is particularly useful during the @@ -611,7 +616,7 @@ public function newInstance($attributes = [], $exists = false) $model->mergeCasts($this->casts); - $model->fill((array) $attributes); + $model->fill($attributes); return $model; } @@ -982,11 +987,11 @@ protected function incrementOrDecrement($column, $amount, $extra, $method) /** * Update the model in the database. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @param array $options * @return bool */ - public function update(array $attributes = [], array $options = []) + public function update(array|ValidatedInput $attributes = [], array $options = []) { if (! $this->exists) { return false; @@ -998,13 +1003,13 @@ public function update(array $attributes = [], array $options = []) /** * Update the model in the database within a transaction. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @param array $options * @return bool * * @throws \Throwable */ - public function updateOrFail(array $attributes = [], array $options = []) + public function updateOrFail(array|ValidatedInput $attributes = [], array $options = []) { if (! $this->exists) { return false; @@ -1016,11 +1021,11 @@ public function updateOrFail(array $attributes = [], array $options = []) /** * Update the model in the database without raising any events. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @param array $options * @return bool */ - public function updateQuietly(array $attributes = [], array $options = []) + public function updateQuietly(array|ValidatedInput $attributes = [], array $options = []) { if (! $this->exists) { return false; diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index 9fe0d301918b..1822a3e12395 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -14,6 +14,7 @@ use Illuminate\Database\Query\Grammars\MySqlGrammar; use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Support\Str; +use Illuminate\Support\ValidatedInput; use InvalidArgumentException; class BelongsToMany extends Relation @@ -1273,12 +1274,12 @@ public function saveManyQuietly($models, array $pivotAttributes = []) /** * Create a new instance of the related model. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @param array $joining * @param bool $touch * @return \Illuminate\Database\Eloquent\Model */ - public function create(array $attributes = [], array $joining = [], $touch = true) + public function create(array|ValidatedInput $attributes = [], array $joining = [], $touch = true) { $instance = $this->related->newInstance($attributes); diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php index e1d295d86be4..18b98ad43b68 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; use Illuminate\Database\UniqueConstraintViolationException; +use Illuminate\Support\ValidatedInput; abstract class HasOneOrMany extends Relation { @@ -46,10 +47,10 @@ public function __construct(Builder $query, Model $parent, $foreignKey, $localKe /** * Create and return an un-saved instance of the related model. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return \Illuminate\Database\Eloquent\Model */ - public function make(array $attributes = []) + public function make(array|ValidatedInput $attributes = []) { return tap($this->related->newInstance($attributes), function ($instance) { $this->setForeignAttributesForCreate($instance); @@ -331,10 +332,10 @@ public function saveManyQuietly($models) /** * Create a new instance of the related model. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return \Illuminate\Database\Eloquent\Model */ - public function create(array $attributes = []) + public function create(array|ValidatedInput $attributes = []) { return tap($this->related->newInstance($attributes), function ($instance) { $this->setForeignAttributesForCreate($instance); @@ -346,10 +347,10 @@ public function create(array $attributes = []) /** * Create a new instance of the related model without raising any events to the parent model. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return \Illuminate\Database\Eloquent\Model */ - public function createQuietly(array $attributes = []) + public function createQuietly(array|ValidatedInput $attributes = []) { return Model::withoutEvents(fn () => $this->create($attributes)); } @@ -357,10 +358,10 @@ public function createQuietly(array $attributes = []) /** * Create a new instance of the related model. Allow mass-assignment. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return \Illuminate\Database\Eloquent\Model */ - public function forceCreate(array $attributes = []) + public function forceCreate(array|ValidatedInput $attributes = []) { $attributes[$this->getForeignKeyName()] = $this->getParentKey(); @@ -370,10 +371,10 @@ public function forceCreate(array $attributes = []) /** * Create a new instance of the related model with mass assignment without raising model events. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return \Illuminate\Database\Eloquent\Model */ - public function forceCreateQuietly(array $attributes = []) + public function forceCreateQuietly(array|ValidatedInput $attributes = []) { return Model::withoutEvents(fn () => $this->forceCreate($attributes)); } diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphMany.php index 3636f25d06c2..5b886f8eb854 100755 --- a/src/Illuminate/Database/Eloquent/Relations/MorphMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphMany.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\ValidatedInput; class MorphMany extends MorphOneOrMany { @@ -67,10 +68,10 @@ public function match(array $models, Collection $results, $relation) /** * Create a new instance of the related model. Allow mass-assignment. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return \Illuminate\Database\Eloquent\Model */ - public function forceCreate(array $attributes = []) + public function forceCreate(array|ValidatedInput $attributes = []) { $attributes[$this->getMorphType()] = $this->morphClass; @@ -80,10 +81,10 @@ public function forceCreate(array $attributes = []) /** * Create a new instance of the related model with mass assignment without raising model events. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return \Illuminate\Database\Eloquent\Model */ - public function forceCreateQuietly(array $attributes = []) + public function forceCreateQuietly(array|ValidatedInput $attributes = []) { return Model::withoutEvents(fn () => $this->forceCreate($attributes)); } diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php index 3cfec895548d..4187d3b273c2 100755 --- a/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\ValidatedInput; abstract class MorphOneOrMany extends HasOneOrMany { @@ -70,10 +71,10 @@ public function addEagerConstraints(array $models) /** * Create a new instance of the related model. Allow mass-assignment. * - * @param array $attributes + * @param array|ValidatedInput $attributes * @return \Illuminate\Database\Eloquent\Model */ - public function forceCreate(array $attributes = []) + public function forceCreate(array|ValidatedInput $attributes = []) { $attributes[$this->getForeignKeyName()] = $this->getParentKey(); $attributes[$this->getMorphType()] = $this->morphClass; diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index cbfe8f74b1f9..343e20477d55 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -44,6 +44,7 @@ use Illuminate\Support\InteractsWithTime; use Illuminate\Support\Str; use Illuminate\Support\Stringable; +use Illuminate\Support\ValidatedInput; use InvalidArgumentException; use LogicException; use Mockery as m; @@ -625,6 +626,18 @@ public function testMakeMethodDoesNotSaveNewModel() $this->assertSame('taylor', $model->name); } + public function testMakeMethodAllowsSafeObject() + { + $model = EloquentModelSaveStub::make(new ValidatedInput(['name' => 'taylor'])); + $this->assertSame('taylor', $model->name); + } + + public function testConstructorAllowsSafeObject() + { + $model = new EloquentModelSaveStub(new ValidatedInput(['name' => 'taylor'])); + $this->assertSame('taylor', $model->name); + } + public function testForceCreateMethodSavesNewModelWithGuardedAttributes() { $_SERVER['__eloquent.saved'] = false; @@ -1434,6 +1447,14 @@ public function testFillable() $this->assertSame('bar', $model->age); } + public function testFillingWithSafeObject() + { + $model = new EloquentModelStub; + $model->fill(new ValidatedInput(['name' => 'foo', 'age' => 'bar'])); + $this->assertSame('foo', $model->name); + $this->assertSame('bar', $model->age); + } + public function testQualifyColumn() { $model = new EloquentModelStub; diff --git a/tests/Integration/Database/EloquentModelTest.php b/tests/Integration/Database/EloquentModelTest.php index d4ad6c207437..c97d9193a839 100644 --- a/tests/Integration/Database/EloquentModelTest.php +++ b/tests/Integration/Database/EloquentModelTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; +use Illuminate\Support\ValidatedInput; class EloquentModelTest extends DatabaseTestCase { @@ -126,6 +127,51 @@ public function testInsertRecordWithReservedWordFieldName() 'analyze' => true, ]); } + + public function testCreateModelWithSafeObject() + { + Schema::create('actions', function (Blueprint $table) { + $table->id(); + $table->string('label'); + }); + + $model = new class extends Model + { + protected $table = 'actions'; + protected $fillable = ['label']; + public $timestamps = false; + }; + + $model->newInstance()->create(new ValidatedInput([ + 'label' => 'test', + ])); + + $this->assertDatabaseHas('actions', [ + 'label' => 'test', + ]); + } + + public function testCreateModelWithSafeObjectThrowsExceptionForUnknownColumn() + { + Schema::create('actions', function (Blueprint $table) { + $table->id(); + $table->string('label'); + }); + + $model = new class extends Model + { + protected $table = 'actions'; + protected $fillable = []; + public $timestamps = false; + }; + + $this->expectException(\Illuminate\Database\QueryException::class); + + $model->newInstance()->forceCreate(new ValidatedInput([ + 'label' => 'test', + 'unknown' => 'column', + ])); + } } class TestModel1 extends Model diff --git a/tests/Integration/Database/EloquentUpdateTest.php b/tests/Integration/Database/EloquentUpdateTest.php index 68fdc26993a2..97ed69f4ee24 100644 --- a/tests/Integration/Database/EloquentUpdateTest.php +++ b/tests/Integration/Database/EloquentUpdateTest.php @@ -7,6 +7,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; +use Illuminate\Support\ValidatedInput; class EloquentUpdateTest extends DatabaseTestCase { @@ -46,6 +47,19 @@ public function testBasicUpdate() $this->assertCount(0, TestUpdateModel1::all()); } + public function testUpdateAllowsSafeObject() + { + $model = TestUpdateModel1::create([ + 'name' => Str::random(), + ]); + + $model->update(new ValidatedInput(['title' => 'Developer'])); + + $model->refresh(); + + $this->assertSame('Developer', $model->title); + } + public function testUpdateWithLimitsAndOrders() { if ($this->driver === 'sqlsrv') {