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

Commit

Permalink
Target binding: support BelongsToMany relationships (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalbaljet authored Dec 22, 2020
1 parent 2fc22e1 commit a7d9d2f
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 4 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

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

## 2.5.0 - 2020-12-22

- Support for `BelongsToMany`, `MorphMany`, and `MorphToMany` relationships (select element)

## 2.4.0 - 2020-12-11

- Support for Livewire modifiers

## 2.3.0 - 2020-12-01

- Support for PHP 8.0
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,20 @@ If you want a select element where multiple options can be selected, add the `mu
<x-form-select name="country_code" :options="$countries" multiple :default="['be', 'nl']" />
```

#### Using Eloquent relationships

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.

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.

```blade
<x-form>
@bind($video)
<x-form-select name="tags" :options="$tags" multiple many-relation />
@endbind
</x-form>
```

### Checkbox elements

Checkboxes have a default value of `1`, but you can customize it as well.
Expand Down
15 changes: 11 additions & 4 deletions src/Components/FormSelect.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace ProtoneMedia\LaravelFormComponents\Components;

use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

Expand All @@ -28,18 +29,24 @@ public function __construct(
$bind = null,
$default = null,
bool $multiple = false,
bool $showErrors = true
bool $showErrors = true,
bool $manyRelation = false
) {
$this->name = $name;
$this->label = $label;
$this->options = $options;
$this->name = $name;
$this->label = $label;
$this->options = $options;
$this->manyRelation = $manyRelation;

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

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

$this->selectedKey = old($inputName, $default);

if ($this->selectedKey instanceof Arrayable) {
$this->selectedKey = $this->selectedKey->toArray();
}
}

$this->multiple = $multiple;
Expand Down
49 changes: 49 additions & 0 deletions src/Components/HandlesBoundValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@

namespace ProtoneMedia\LaravelFormComponents\Components;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use ProtoneMedia\LaravelFormComponents\FormDataBinder;

trait HandlesBoundValues
{
/**
* Wether to retrieve the default value as a single
* attribute or as a collection from the database.
*
* @var boolean
*/
protected $manyRelation = false;

/**
* Get an instance of FormDataBinder.
*
Expand Down Expand Up @@ -41,6 +52,44 @@ private function getBoundValue($bind, string $name)

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

return $this->manyRelation
? $this->getAttachedKeysFromRelation($bind, $name)
: data_get($bind, $name);
}

/**
* Returns an array with the attached keys.
*
* @param mixed $bind
* @param string $name
* @return void
*/
private function getAttachedKeysFromRelation($bind, string $name): ?array
{
if (!$bind instanceof Model) {
return data_get($bind, $name);
}

$relation = $bind->{$name}();

if ($relation instanceof BelongsToMany) {
$relatedKeyName = $relation->getRelatedKeyName();

return $relation->getBaseQuery()
->get($relation->getRelated()->qualifyColumn($relatedKeyName))
->pluck($relatedKeyName)
->all();
}

if ($relation instanceof MorphMany) {
$parentKeyName = $relation->getLocalKeyName();

return $relation->getBaseQuery()
->get($relation->getQuery()->qualifyColumn($parentKeyName))
->pluck($parentKeyName)
->all();
}

return data_get($bind, $name);
}
}
30 changes: 30 additions & 0 deletions tests/Feature/InteractsWithDatabase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace ProtoneMedia\LaravelFormComponents\Tests\Feature;

use Illuminate\Database\Eloquent\Model;

trait InteractsWithDatabase
{
protected function setupDatabase()
{
Model::unguard();

$this->app['config']->set('database.default', 'sqlite');
$this->app['config']->set('database.connections.sqlite', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);

include_once __DIR__ . '/database/create_posts_table.php';
include_once __DIR__ . '/database/create_comments_table.php';
include_once __DIR__ . '/database/create_comment_post_table.php';
include_once __DIR__ . '/database/create_commentables_table.php';

(new \CreatePostsTable)->up();
(new \CreateCommentsTable)->up();
(new \CreateCommentPostTable)->up();
(new \CreateCommentablesTable)->up();
}
}
139 changes: 139 additions & 0 deletions tests/Feature/SelectRelationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php

namespace ProtoneMedia\LaravelFormComponents\Tests\Feature;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;
use ProtoneMedia\LaravelFormComponents\Tests\TestCase;

class PostBelongsToMany extends Model
{
protected $table = 'posts';

public function comments()
{
return $this->belongsToMany(Comment::class, 'comment_post', 'post_id', 'comment_id');
}
}

class PostMorphMany extends Model
{
protected $table = 'posts';

public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}

class PostMorphToMany extends Model
{
protected $table = 'posts';

public function comments()
{
return $this->morphToMany(Comment::class, 'commentable');
}
}

class Comment extends Model
{
}

class SelectRelationTest extends TestCase
{
use InteractsWithDatabase;

/** @test */
public function it_handles_belongs_to_many_relationships()
{
$this->setupDatabase();

$post = PostBelongsToMany::create(['content' => 'Content']);

$commentA = Comment::create(['content' => 'Content A']);
$commentB = Comment::create(['content' => 'Content B']);
$commentC = Comment::create(['content' => 'Content C']);

$post->comments()->sync([$commentA->getKey(), $commentC->getKey()]);

$options = Comment::get()->pluck('content', 'id');

Route::get('select-relation', function () use ($post, $options) {
return view('select-relation')
->with('post', $post)
->with('options', $options);
})->middleware('web');

DB::enableQueryLog();

$this->visit('/select-relation')
->seeElement('option[value="' . $commentA->getKey() . '"]:selected')
->seeElement('option[value="' . $commentB->getKey() . '"]:not(:selected)')
->seeElement('option[value="' . $commentC->getKey() . '"]:selected');

// make sure we cache the result for each option element
$this->assertCount(1, DB::getQueryLog());
}

/** @test */
public function it_handles_morph_many_relationships()
{
$this->setupDatabase();

$post = PostMorphMany::create(['content' => 'Content']);

$commentA = $post->comments()->create(['content' => 'Content A']);
$commentB = Comment::create(['content' => 'Content B']);
$commentC = $post->comments()->create(['content' => 'Content C']);

$options = Comment::get()->pluck('content', 'id');

Route::get('select-relation', function () use ($post, $options) {
return view('select-relation')
->with('post', $post)
->with('options', $options);
})->middleware('web');

DB::enableQueryLog();

$this->visit('/select-relation')
->seeElement('option[value="' . $commentA->getKey() . '"]:selected')
->seeElement('option[value="' . $commentB->getKey() . '"]:not(:selected)')
->seeElement('option[value="' . $commentC->getKey() . '"]:selected');

// make sure we cache the result for each option element
$this->assertCount(1, DB::getQueryLog());
}

/** @test */
public function it_handles_morph_to_many_relationships()
{
$this->setupDatabase();

$post = PostMorphToMany::create(['content' => 'Content']);

$commentA = $post->comments()->create(['content' => 'Content A']);
$commentB = Comment::create(['content' => 'Content B']);
$commentC = $post->comments()->create(['content' => 'Content C']);

$options = Comment::get()->pluck('content', 'id');

Route::get('select-relation', function () use ($post, $options) {
return view('select-relation')
->with('post', $post)
->with('options', $options);
})->middleware('web');

DB::enableQueryLog();

$this->visit('/select-relation')
->seeElement('option[value="' . $commentA->getKey() . '"]:selected')
->seeElement('option[value="' . $commentB->getKey() . '"]:not(:selected)')
->seeElement('option[value="' . $commentC->getKey() . '"]:selected');

// make sure we cache the result for each option element
$this->assertCount(1, DB::getQueryLog());
}
}
30 changes: 30 additions & 0 deletions tests/Feature/database/create_comment_post_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCommentPostTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('comment_post', function (Blueprint $table) {
$table->unsignedBigInteger('post_id');
$table->unsignedBigInteger('comment_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('comment_post');
}
}
31 changes: 31 additions & 0 deletions tests/Feature/database/create_commentables_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateCommentablesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('commentables', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('comment_id');
$table->morphs('commentable');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('comments');
}
}
Loading

0 comments on commit a7d9d2f

Please sign in to comment.