Skip to content

Commit 066b740

Browse files
[11.x] Add pending attributes (#53720)
* Add pending attributes * Support withAttributes on many-to-many * Test withAttributes querying on many-to-many * Test withAttributes on polymorphic many-to-many * Make withAttributes qualify columns * Fix CS * Fix CS * formatting * formatting * format comment --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 3c71a81 commit 066b740

File tree

7 files changed

+644
-0
lines changed

7 files changed

+644
-0
lines changed

src/Illuminate/Database/Eloquent/Builder.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ class Builder implements BuilderContract
5353
*/
5454
protected $model;
5555

56+
/**
57+
* The attributes that should be added to new models created by this builder.
58+
*
59+
* @var array
60+
*/
61+
public $pendingAttributes = [];
62+
5663
/**
5764
* The relationships that should be eager loaded.
5865
*
@@ -1622,6 +1629,8 @@ public function withOnly($relations)
16221629
*/
16231630
public function newModelInstance($attributes = [])
16241631
{
1632+
$attributes = array_merge($this->pendingAttributes, $attributes);
1633+
16251634
return $this->model->newInstance($attributes)->setConnection(
16261635
$this->query->getConnection()->getName()
16271636
);
@@ -1782,6 +1791,30 @@ protected function addNestedWiths($name, $results)
17821791
return $results;
17831792
}
17841793

1794+
/**
1795+
* Specify attributes that should be added to any new models created by this builder.
1796+
*
1797+
* The given key / value pairs will also be added as where conditions to the query.
1798+
*
1799+
* @param \Illuminate\Contracts\Database\Query\Expression|array|string $attributes
1800+
* @param mixed $value
1801+
* @return $this
1802+
*/
1803+
public function withAttributes(Expression|array|string $attributes, $value = null)
1804+
{
1805+
if (! is_array($attributes)) {
1806+
$attributes = [$attributes => $value];
1807+
}
1808+
1809+
foreach ($attributes as $column => $value) {
1810+
$this->where($this->qualifyColumn($column), $value);
1811+
}
1812+
1813+
$this->pendingAttributes = array_merge($this->pendingAttributes, $attributes);
1814+
1815+
return $this;
1816+
}
1817+
17851818
/**
17861819
* Apply query-time casts to the model instance.
17871820
*

src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,8 @@ public function saveManyQuietly($models, array $pivotAttributes = [])
13511351
*/
13521352
public function create(array $attributes = [], array $joining = [], $touch = true)
13531353
{
1354+
$attributes = array_merge($this->getQuery()->pendingAttributes, $attributes);
1355+
13541356
$instance = $this->related->newInstance($attributes);
13551357

13561358
// Once we save the related model, we need to attach it to the base model via

src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,12 @@ protected function setForeignAttributesForCreate(Model $model)
447447
{
448448
$model->setAttribute($this->getForeignKeyName(), $this->getParentKey());
449449

450+
foreach ($this->getQuery()->pendingAttributes as $key => $value) {
451+
if (! $model->hasAttribute($key)) {
452+
$model->setAttribute($key, $value);
453+
}
454+
}
455+
450456
$this->applyInverseRelationToModel($model);
451457
}
452458

src/Illuminate/Database/Eloquent/Relations/MorphOneOrMany.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ protected function setForeignAttributesForCreate(Model $model)
9696

9797
$model->{$this->getMorphType()} = $this->morphClass;
9898

99+
foreach ($this->getQuery()->pendingAttributes as $key => $value) {
100+
if (! $model->hasAttribute($key)) {
101+
$model->setAttribute($key, $value);
102+
}
103+
}
104+
99105
$this->applyInverseRelationToModel($model);
100106
}
101107

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Database;
4+
5+
use Illuminate\Database\Capsule\Manager as DB;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
8+
use Illuminate\Database\Eloquent\Relations\MorphToMany;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class DatabaseEloquentBelongsToManyWithAttributesTest extends TestCase
12+
{
13+
protected function setUp(): void
14+
{
15+
$db = new DB;
16+
17+
$db->addConnection([
18+
'driver' => 'sqlite',
19+
'database' => ':memory:',
20+
]);
21+
$db->bootEloquent();
22+
$db->setAsGlobal();
23+
$this->createSchema();
24+
}
25+
26+
public function testCreatesWithAttributesAndPivotValues(): void
27+
{
28+
$post = ManyToManyWithAttributesPost::create();
29+
$tag = $post->metaTags()->create(['name' => 'long article']);
30+
31+
$this->assertSame('long article', $tag->name);
32+
$this->assertTrue($tag->visible);
33+
34+
$pivot = DB::table('with_attributes_pivot')->first();
35+
$this->assertSame('meta', $pivot->type);
36+
$this->assertSame($post->id, $pivot->post_id);
37+
$this->assertSame($tag->id, $pivot->tag_id);
38+
}
39+
40+
public function testQueriesWithAttributesAndPivotValues(): void
41+
{
42+
$post = new ManyToManyWithAttributesPost(['id' => 2]);
43+
$wheres = $post->metaTags()->toBase()->wheres;
44+
45+
$this->assertContains([
46+
'type' => 'Basic',
47+
'column' => 'with_attributes_tags.visible',
48+
'operator' => '=',
49+
'value' => true,
50+
'boolean' => 'and',
51+
], $wheres);
52+
53+
$this->assertContains([
54+
'type' => 'Basic',
55+
'column' => 'with_attributes_pivot.type',
56+
'operator' => '=',
57+
'value' => 'meta',
58+
'boolean' => 'and',
59+
], $wheres);
60+
}
61+
62+
public function testMorphToManyWithAttributes(): void
63+
{
64+
$post = new ManyToManyWithAttributesPost(['id' => 2]);
65+
$wheres = $post->morphedTags()->toBase()->wheres;
66+
67+
$this->assertContains([
68+
'type' => 'Basic',
69+
'column' => 'with_attributes_tags.visible',
70+
'operator' => '=',
71+
'value' => true,
72+
'boolean' => 'and',
73+
], $wheres);
74+
75+
$this->assertContains([
76+
'type' => 'Basic',
77+
'column' => 'with_attributes_taggables.type',
78+
'operator' => '=',
79+
'value' => 'meta',
80+
'boolean' => 'and',
81+
], $wheres);
82+
83+
$this->assertContains([
84+
'type' => 'Basic',
85+
'column' => 'with_attributes_taggables.taggable_type',
86+
'operator' => '=',
87+
'value' => ManyToManyWithAttributesPost::class,
88+
'boolean' => 'and',
89+
], $wheres);
90+
91+
$this->assertContains([
92+
'type' => 'Basic',
93+
'column' => 'with_attributes_taggables.taggable_id',
94+
'operator' => '=',
95+
'value' => 2,
96+
'boolean' => 'and',
97+
], $wheres);
98+
99+
$tag = $post->morphedTags()->create(['name' => 'new tag']);
100+
101+
$this->assertTrue($tag->visible);
102+
$this->assertSame('new tag', $tag->name);
103+
$this->assertSame($tag->id, $post->morphedTags()->first()->id);
104+
}
105+
106+
public function testMorphedByManyWithAttributes(): void
107+
{
108+
$tag = new ManyToManyWithAttributesTag(['id' => 4]);
109+
$wheres = $tag->morphedPosts()->toBase()->wheres;
110+
111+
$this->assertContains([
112+
'type' => 'Basic',
113+
'column' => 'with_attributes_posts.title',
114+
'operator' => '=',
115+
'value' => 'Title!',
116+
'boolean' => 'and',
117+
], $wheres);
118+
119+
$this->assertContains([
120+
'type' => 'Basic',
121+
'column' => 'with_attributes_taggables.type',
122+
'operator' => '=',
123+
'value' => 'meta',
124+
'boolean' => 'and',
125+
], $wheres);
126+
127+
$this->assertContains([
128+
'type' => 'Basic',
129+
'column' => 'with_attributes_taggables.taggable_type',
130+
'operator' => '=',
131+
'value' => ManyToManyWithAttributesPost::class,
132+
'boolean' => 'and',
133+
], $wheres);
134+
135+
$this->assertContains([
136+
'type' => 'Basic',
137+
'column' => 'with_attributes_taggables.tag_id',
138+
'operator' => '=',
139+
'value' => 4,
140+
'boolean' => 'and',
141+
], $wheres);
142+
143+
$post = $tag->morphedPosts()->create();
144+
$this->assertSame('Title!', $post->title);
145+
$this->assertSame($post->id, $tag->morphedPosts()->first()->id);
146+
}
147+
148+
protected function createSchema()
149+
{
150+
$this->schema()->create('with_attributes_posts', function ($table) {
151+
$table->increments('id');
152+
$table->string('title')->nullable();
153+
$table->timestamps();
154+
});
155+
156+
$this->schema()->create('with_attributes_tags', function ($table) {
157+
$table->increments('id');
158+
$table->string('name');
159+
$table->boolean('visible')->nullable();
160+
$table->timestamps();
161+
});
162+
163+
$this->schema()->create('with_attributes_pivot', function ($table) {
164+
$table->integer('post_id');
165+
$table->integer('tag_id');
166+
$table->string('type');
167+
});
168+
169+
$this->schema()->create('with_attributes_taggables', function ($table) {
170+
$table->integer('tag_id');
171+
$table->integer('taggable_id');
172+
$table->string('taggable_type');
173+
$table->string('type');
174+
});
175+
}
176+
177+
/**
178+
* Tear down the database schema.
179+
*
180+
* @return void
181+
*/
182+
protected function tearDown(): void
183+
{
184+
$this->schema()->drop('with_attributes_posts');
185+
$this->schema()->drop('with_attributes_tags');
186+
$this->schema()->drop('with_attributes_pivot');
187+
}
188+
189+
/**
190+
* Get a database connection instance.
191+
*
192+
* @return \Illuminate\Database\Connection
193+
*/
194+
protected function connection($connection = 'default')
195+
{
196+
return Model::getConnectionResolver()->connection($connection);
197+
}
198+
199+
/**
200+
* Get a schema builder instance.
201+
*
202+
* @return \Illuminate\Database\Schema\Builder
203+
*/
204+
protected function schema($connection = 'default')
205+
{
206+
return $this->connection($connection)->getSchemaBuilder();
207+
}
208+
}
209+
210+
class ManyToManyWithAttributesPost extends Model
211+
{
212+
protected $guarded = [];
213+
protected $table = 'with_attributes_posts';
214+
215+
public function tags(): BelongsToMany
216+
{
217+
return $this->belongsToMany(
218+
ManyToManyWithAttributesTag::class,
219+
'with_attributes_pivot',
220+
'tag_id',
221+
'post_id',
222+
);
223+
}
224+
225+
public function metaTags(): BelongsToMany
226+
{
227+
return $this->tags()
228+
->withAttributes('visible', true)
229+
->withPivotValue('type', 'meta');
230+
}
231+
232+
public function morphedTags(): MorphToMany
233+
{
234+
return $this
235+
->morphToMany(
236+
ManyToManyWithAttributesTag::class,
237+
'taggable',
238+
'with_attributes_taggables',
239+
relatedPivotKey: 'tag_id'
240+
)
241+
->withAttributes('visible', true)
242+
->withPivotValue('type', 'meta');
243+
}
244+
}
245+
246+
class ManyToManyWithAttributesTag extends Model
247+
{
248+
protected $guarded = [];
249+
protected $table = 'with_attributes_tags';
250+
251+
public function morphedPosts(): MorphToMany
252+
{
253+
return $this
254+
->morphedByMany(
255+
ManyToManyWithAttributesPost::class,
256+
'taggable',
257+
'with_attributes_taggables',
258+
'tag_id',
259+
)
260+
->withAttributes('title', 'Title!')
261+
->withPivotValue('type', 'meta');
262+
}
263+
}

0 commit comments

Comments
 (0)