Skip to content
This repository was archived by the owner on Jan 2, 2024. It is now read-only.

Commit a7d9d2f

Browse files
authored
Target binding: support BelongsToMany relationships (#34)
1 parent 2fc22e1 commit a7d9d2f

12 files changed

+413
-4
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to `laravel-form-components` will be documented in this file
44

5+
## 2.5.0 - 2020-12-22
6+
7+
- Support for `BelongsToMany`, `MorphMany`, and `MorphToMany` relationships (select element)
8+
9+
## 2.4.0 - 2020-12-11
10+
11+
- Support for Livewire modifiers
12+
513
## 2.3.0 - 2020-12-01
614

715
- Support for PHP 8.0

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,20 @@ If you want a select element where multiple options can be selected, add the `mu
280280
<x-form-select name="country_code" :options="$countries" multiple :default="['be', 'nl']" />
281281
```
282282

283+
#### Using Eloquent relationships
284+
285+
This package has built-in support for `BelongsToMany`, `MorphMany`, and `MorphToMany` relationships. To utilize this feature, you must add both the `multiple` and `many-relation` attribute to the select element.
286+
287+
In the example below, you can attach one or more tags to the bound video. By using the `many-relation` attribute, it will correctly retrieve the selected options (attached tags) from the database.
288+
289+
```blade
290+
<x-form>
291+
@bind($video)
292+
<x-form-select name="tags" :options="$tags" multiple many-relation />
293+
@endbind
294+
</x-form>
295+
```
296+
283297
### Checkbox elements
284298

285299
Checkboxes have a default value of `1`, but you can customize it as well.

src/Components/FormSelect.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace ProtoneMedia\LaravelFormComponents\Components;
44

5+
use Illuminate\Contracts\Support\Arrayable;
56
use Illuminate\Support\Arr;
67
use Illuminate\Support\Str;
78

@@ -28,18 +29,24 @@ public function __construct(
2829
$bind = null,
2930
$default = null,
3031
bool $multiple = false,
31-
bool $showErrors = true
32+
bool $showErrors = true,
33+
bool $manyRelation = false
3234
) {
33-
$this->name = $name;
34-
$this->label = $label;
35-
$this->options = $options;
35+
$this->name = $name;
36+
$this->label = $label;
37+
$this->options = $options;
38+
$this->manyRelation = $manyRelation;
3639

3740
if ($this->isNotWired()) {
3841
$inputName = Str::before($name, '[]');
3942

4043
$default = $this->getBoundValue($bind, $inputName) ?: $default;
4144

4245
$this->selectedKey = old($inputName, $default);
46+
47+
if ($this->selectedKey instanceof Arrayable) {
48+
$this->selectedKey = $this->selectedKey->toArray();
49+
}
4350
}
4451

4552
$this->multiple = $multiple;

src/Components/HandlesBoundValues.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,21 @@
22

33
namespace ProtoneMedia\LaravelFormComponents\Components;
44

5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
7+
use Illuminate\Database\Eloquent\Relations\MorphMany;
58
use ProtoneMedia\LaravelFormComponents\FormDataBinder;
69

710
trait HandlesBoundValues
811
{
12+
/**
13+
* Wether to retrieve the default value as a single
14+
* attribute or as a collection from the database.
15+
*
16+
* @var boolean
17+
*/
18+
protected $manyRelation = false;
19+
920
/**
1021
* Get an instance of FormDataBinder.
1122
*
@@ -41,6 +52,44 @@ private function getBoundValue($bind, string $name)
4152

4253
$bind = $bind ?: $this->getBoundTarget();
4354

55+
return $this->manyRelation
56+
? $this->getAttachedKeysFromRelation($bind, $name)
57+
: data_get($bind, $name);
58+
}
59+
60+
/**
61+
* Returns an array with the attached keys.
62+
*
63+
* @param mixed $bind
64+
* @param string $name
65+
* @return void
66+
*/
67+
private function getAttachedKeysFromRelation($bind, string $name): ?array
68+
{
69+
if (!$bind instanceof Model) {
70+
return data_get($bind, $name);
71+
}
72+
73+
$relation = $bind->{$name}();
74+
75+
if ($relation instanceof BelongsToMany) {
76+
$relatedKeyName = $relation->getRelatedKeyName();
77+
78+
return $relation->getBaseQuery()
79+
->get($relation->getRelated()->qualifyColumn($relatedKeyName))
80+
->pluck($relatedKeyName)
81+
->all();
82+
}
83+
84+
if ($relation instanceof MorphMany) {
85+
$parentKeyName = $relation->getLocalKeyName();
86+
87+
return $relation->getBaseQuery()
88+
->get($relation->getQuery()->qualifyColumn($parentKeyName))
89+
->pluck($parentKeyName)
90+
->all();
91+
}
92+
4493
return data_get($bind, $name);
4594
}
4695
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace ProtoneMedia\LaravelFormComponents\Tests\Feature;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
trait InteractsWithDatabase
8+
{
9+
protected function setupDatabase()
10+
{
11+
Model::unguard();
12+
13+
$this->app['config']->set('database.default', 'sqlite');
14+
$this->app['config']->set('database.connections.sqlite', [
15+
'driver' => 'sqlite',
16+
'database' => ':memory:',
17+
'prefix' => '',
18+
]);
19+
20+
include_once __DIR__ . '/database/create_posts_table.php';
21+
include_once __DIR__ . '/database/create_comments_table.php';
22+
include_once __DIR__ . '/database/create_comment_post_table.php';
23+
include_once __DIR__ . '/database/create_commentables_table.php';
24+
25+
(new \CreatePostsTable)->up();
26+
(new \CreateCommentsTable)->up();
27+
(new \CreateCommentPostTable)->up();
28+
(new \CreateCommentablesTable)->up();
29+
}
30+
}

tests/Feature/SelectRelationTest.php

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
namespace ProtoneMedia\LaravelFormComponents\Tests\Feature;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Support\Facades\DB;
7+
use Illuminate\Support\Facades\Route;
8+
use ProtoneMedia\LaravelFormComponents\Tests\TestCase;
9+
10+
class PostBelongsToMany extends Model
11+
{
12+
protected $table = 'posts';
13+
14+
public function comments()
15+
{
16+
return $this->belongsToMany(Comment::class, 'comment_post', 'post_id', 'comment_id');
17+
}
18+
}
19+
20+
class PostMorphMany extends Model
21+
{
22+
protected $table = 'posts';
23+
24+
public function comments()
25+
{
26+
return $this->morphMany(Comment::class, 'commentable');
27+
}
28+
}
29+
30+
class PostMorphToMany extends Model
31+
{
32+
protected $table = 'posts';
33+
34+
public function comments()
35+
{
36+
return $this->morphToMany(Comment::class, 'commentable');
37+
}
38+
}
39+
40+
class Comment extends Model
41+
{
42+
}
43+
44+
class SelectRelationTest extends TestCase
45+
{
46+
use InteractsWithDatabase;
47+
48+
/** @test */
49+
public function it_handles_belongs_to_many_relationships()
50+
{
51+
$this->setupDatabase();
52+
53+
$post = PostBelongsToMany::create(['content' => 'Content']);
54+
55+
$commentA = Comment::create(['content' => 'Content A']);
56+
$commentB = Comment::create(['content' => 'Content B']);
57+
$commentC = Comment::create(['content' => 'Content C']);
58+
59+
$post->comments()->sync([$commentA->getKey(), $commentC->getKey()]);
60+
61+
$options = Comment::get()->pluck('content', 'id');
62+
63+
Route::get('select-relation', function () use ($post, $options) {
64+
return view('select-relation')
65+
->with('post', $post)
66+
->with('options', $options);
67+
})->middleware('web');
68+
69+
DB::enableQueryLog();
70+
71+
$this->visit('/select-relation')
72+
->seeElement('option[value="' . $commentA->getKey() . '"]:selected')
73+
->seeElement('option[value="' . $commentB->getKey() . '"]:not(:selected)')
74+
->seeElement('option[value="' . $commentC->getKey() . '"]:selected');
75+
76+
// make sure we cache the result for each option element
77+
$this->assertCount(1, DB::getQueryLog());
78+
}
79+
80+
/** @test */
81+
public function it_handles_morph_many_relationships()
82+
{
83+
$this->setupDatabase();
84+
85+
$post = PostMorphMany::create(['content' => 'Content']);
86+
87+
$commentA = $post->comments()->create(['content' => 'Content A']);
88+
$commentB = Comment::create(['content' => 'Content B']);
89+
$commentC = $post->comments()->create(['content' => 'Content C']);
90+
91+
$options = Comment::get()->pluck('content', 'id');
92+
93+
Route::get('select-relation', function () use ($post, $options) {
94+
return view('select-relation')
95+
->with('post', $post)
96+
->with('options', $options);
97+
})->middleware('web');
98+
99+
DB::enableQueryLog();
100+
101+
$this->visit('/select-relation')
102+
->seeElement('option[value="' . $commentA->getKey() . '"]:selected')
103+
->seeElement('option[value="' . $commentB->getKey() . '"]:not(:selected)')
104+
->seeElement('option[value="' . $commentC->getKey() . '"]:selected');
105+
106+
// make sure we cache the result for each option element
107+
$this->assertCount(1, DB::getQueryLog());
108+
}
109+
110+
/** @test */
111+
public function it_handles_morph_to_many_relationships()
112+
{
113+
$this->setupDatabase();
114+
115+
$post = PostMorphToMany::create(['content' => 'Content']);
116+
117+
$commentA = $post->comments()->create(['content' => 'Content A']);
118+
$commentB = Comment::create(['content' => 'Content B']);
119+
$commentC = $post->comments()->create(['content' => 'Content C']);
120+
121+
$options = Comment::get()->pluck('content', 'id');
122+
123+
Route::get('select-relation', function () use ($post, $options) {
124+
return view('select-relation')
125+
->with('post', $post)
126+
->with('options', $options);
127+
})->middleware('web');
128+
129+
DB::enableQueryLog();
130+
131+
$this->visit('/select-relation')
132+
->seeElement('option[value="' . $commentA->getKey() . '"]:selected')
133+
->seeElement('option[value="' . $commentB->getKey() . '"]:not(:selected)')
134+
->seeElement('option[value="' . $commentC->getKey() . '"]:selected');
135+
136+
// make sure we cache the result for each option element
137+
$this->assertCount(1, DB::getQueryLog());
138+
}
139+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
class CreateCommentPostTable extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*
12+
* @return void
13+
*/
14+
public function up()
15+
{
16+
Schema::create('comment_post', function (Blueprint $table) {
17+
$table->unsignedBigInteger('post_id');
18+
$table->unsignedBigInteger('comment_id');
19+
});
20+
}
21+
/**
22+
* Reverse the migrations.
23+
*
24+
* @return void
25+
*/
26+
public function down()
27+
{
28+
Schema::dropIfExists('comment_post');
29+
}
30+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
class CreateCommentablesTable extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*
12+
* @return void
13+
*/
14+
public function up()
15+
{
16+
Schema::create('commentables', function (Blueprint $table) {
17+
$table->bigIncrements('id');
18+
$table->unsignedBigInteger('comment_id');
19+
$table->morphs('commentable');
20+
});
21+
}
22+
/**
23+
* Reverse the migrations.
24+
*
25+
* @return void
26+
*/
27+
public function down()
28+
{
29+
Schema::dropIfExists('comments');
30+
}
31+
}

0 commit comments

Comments
 (0)