From d651207f3d526cef7716be9544d488abc666ca8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rube=CC=81n=20Robles?= Date: Thu, 23 May 2024 21:02:04 +0200 Subject: [PATCH] add morph model binding tests --- src/Attributes/BindModel.php | 12 +++- testbench.yaml | 3 +- tests/Integration/DataTransferObjectTest.php | 66 +++++++++++++++++++ .../app/DataTransferObjects/UpdateTagData.php | 10 ++- .../Http/Requests/TagUpdateFormRequest.php | 2 + workbench/app/Models/Film.php | 33 ++++++++++ workbench/app/Models/Post.php | 6 +- workbench/app/Models/Tag.php | 11 +++- .../Providers/WorkbenchServiceProvider.php | 9 ++- workbench/database/factories/FilmFactory.php | 36 ++++++++++ .../2022_05_31_163139_create_tags_table.php | 4 +- .../2022_05_32_163139_create_films_table.php | 36 ++++++++++ workbench/routes/api.php | 8 +-- 13 files changed, 214 insertions(+), 22 deletions(-) create mode 100644 workbench/app/Models/Film.php create mode 100644 workbench/database/factories/FilmFactory.php create mode 100644 workbench/database/migrations/2022_05_32_163139_create_films_table.php diff --git a/src/Attributes/BindModel.php b/src/Attributes/BindModel.php index 29e8bff..108f2ca 100644 --- a/src/Attributes/BindModel.php +++ b/src/Attributes/BindModel.php @@ -83,7 +83,7 @@ public function getMorphModel(string $fromPropertyKey, array $properties, array { $morphTypePropertyKey = $this->getMorphPropertyTypeKey($fromPropertyKey); - $types = array_filter(explode(',', $properties[$morphTypePropertyKey] ?? '')); + $types = array_filter(array_map('trim', explode(',', $properties[$morphTypePropertyKey] ?? ''))); if (count($types) === 0) { throw new Exception('Morph type must be specified to be able to bind a model from a morph.'); @@ -98,12 +98,18 @@ public function getMorphModel(string $fromPropertyKey, array $properties, array ); if (count($modelModelClass) === 0 && count($propertyTypeClasses) > 0) { - $modelModelClass = array_filter($propertyTypeClasses, fn (string $class) => (new $class())->getMorphClass() === $type); + var_dump($propertyTypeClasses); + var_dump($morphMap); + var_dump($types); + $modelModelClass = array_filter( + $propertyTypeClasses, + fn (string $class) => in_array((new $class())->getMorphClass(), $types) + ); $modelModelClass = reset($modelModelClass); } - if (count($modelModelClass) === 0) { + if (! $modelModelClass || count($modelModelClass) === 0) { throw new Exception('Morph type not found on relation map or within types.'); } diff --git a/testbench.yaml b/testbench.yaml index d291573..43d5d01 100644 --- a/testbench.yaml +++ b/testbench.yaml @@ -1,5 +1,6 @@ providers: - OpenSoutheners\LaravelDto\ServiceProvider + - Workbench\App\Providers\WorkbenchServiceProvider migrations: - workbench/database/migrations @@ -8,7 +9,7 @@ migrations: # - Workbench\Database\Seeders\DatabaseSeeder workbench: - start: '/' + start: "/" install: true discovers: web: true diff --git a/tests/Integration/DataTransferObjectTest.php b/tests/Integration/DataTransferObjectTest.php index e00a9c9..cba3e6d 100644 --- a/tests/Integration/DataTransferObjectTest.php +++ b/tests/Integration/DataTransferObjectTest.php @@ -13,9 +13,12 @@ use Workbench\App\DataTransferObjects\UpdatePostData; use Workbench\App\DataTransferObjects\UpdatePostWithDefaultData; use Workbench\App\Enums\PostStatus; +use Workbench\App\Models\Film; use Workbench\App\Models\Post; use Workbench\App\Models\User; +use Workbench\Database\Factories\FilmFactory; use Workbench\Database\Factories\PostFactory; +use Workbench\Database\Factories\TagFactory; class DataTransferObjectTest extends TestCase { @@ -197,6 +200,69 @@ public function testDataTransferObjectWithDefaultValueAttributeGetsBoundWhenOneI ]); } + public function testDataTransferObjectWithMorphsGetsModelsBoundOfEachTypeSent() + { + $user = User::create([ + 'email' => 'ruben@hello.com', + 'password' => '1234', + 'name' => 'Ruben', + ]); + + $this->actingAs($user); + + $horrorTag = TagFactory::new()->create([ + 'name' => 'Horror', + 'slug' => 'horror', + ]); + + $fooBarPost = PostFactory::new()->create([ + 'title' => 'Foo bar', + 'slug' => 'foo-bar', + ]); + + $helloWorldPost = PostFactory::new()->create([ + 'title' => 'Hello world', + 'slug' => 'hello-world', + ]); + + $myFilm = FilmFactory::new()->create([ + 'title' => 'My Film', + 'slug' => 'my-film', + 'year' => 1997 + ]); + + $response = $this->patchJson('tags/1', [ + 'name' => 'Scary', + 'taggable' => '1, 1, 2', + 'taggable_type' => 'film, post', + ]); + + $response->assertSuccessful(); + + $response->assertJsonCount(3, 'data.taggable'); + + $response->assertJsonFragment([ + "id" => 1, + "title" => "My Film", + "year" => "1997", + "about" => null + ]); + + $response->assertJsonFragment([ + "id" => 1, + "title" => "Foo bar", + "slug" => "foo-bar", + "status" => "published" + ]); + + $response->assertJsonFragment([ + "id" => 2, + "title" => "Hello world", + "slug" => "hello-world", + "status" => "published" + ]); + } + public function testNestedDataTransferObjectsGetsTheNestedAsObjectInstance() { $this->markTestIncomplete('Need to create nested actions/DTOs'); diff --git a/workbench/app/DataTransferObjects/UpdateTagData.php b/workbench/app/DataTransferObjects/UpdateTagData.php index 75c053f..51a0fca 100644 --- a/workbench/app/DataTransferObjects/UpdateTagData.php +++ b/workbench/app/DataTransferObjects/UpdateTagData.php @@ -3,22 +3,28 @@ namespace Workbench\App\DataTransferObjects; use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Support\Collection; use OpenSoutheners\LaravelDto\Attributes\BindModel; use OpenSoutheners\LaravelDto\Attributes\WithDefaultValue; use OpenSoutheners\LaravelDto\Contracts\ValidatedDataTransferObject; use OpenSoutheners\LaravelDto\DataTransferObject; use Workbench\App\Http\Requests\TagUpdateFormRequest; +use Workbench\App\Models\Film; use Workbench\App\Models\Post; use Workbench\App\Models\Tag; use Workbench\App\Models\User; class UpdateTagData extends DataTransferObject implements ValidatedDataTransferObject { + /** + * @param \Illuminate\Support\Collection<\Workbench\App\Models\Post|\Workbench\App\Models\Film> $taggable + */ public function __construct( #[BindModel] public Tag $tag, - #[BindModel] - public ?Post $post, + #[BindModel([Post::class => 'slug', Film::class])] + public Collection $taggable, + public array $taggableType, public string $name, #[WithDefaultValue(Authenticatable::class)] public User $authUser diff --git a/workbench/app/Http/Requests/TagUpdateFormRequest.php b/workbench/app/Http/Requests/TagUpdateFormRequest.php index 40878c6..add0f18 100644 --- a/workbench/app/Http/Requests/TagUpdateFormRequest.php +++ b/workbench/app/Http/Requests/TagUpdateFormRequest.php @@ -15,6 +15,8 @@ public function rules() { return [ 'name' => ['required', 'string'], + 'taggable' => ['required', 'string'], + 'taggable_type' => ['required', 'string'], ]; } } diff --git a/workbench/app/Models/Film.php b/workbench/app/Models/Film.php new file mode 100644 index 0000000..5c50528 --- /dev/null +++ b/workbench/app/Models/Film.php @@ -0,0 +1,33 @@ +|bool + */ + protected $guarded = []; + + /** + * The attributes that should be visible in serialization. + * + * @var array + */ + protected $visible = [ + 'id', 'title', 'year', 'about', + ]; + + public function tags(): MorphToMany + { + return $this->morphToMany(Tag::class, 'taggable'); + } +} diff --git a/workbench/app/Models/Post.php b/workbench/app/Models/Post.php index 2fbc1f1..3698e16 100644 --- a/workbench/app/Models/Post.php +++ b/workbench/app/Models/Post.php @@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\MorphToMany; class Post extends Model { @@ -26,8 +26,8 @@ class Post extends Model 'id', 'title', 'slug', 'status', 'tags', ]; - public function tags(): BelongsToMany + public function tags(): MorphToMany { - return $this->belongsToMany(Tag::class); + return $this->morphToMany(Tag::class, 'taggable'); } } diff --git a/workbench/app/Models/Tag.php b/workbench/app/Models/Tag.php index f442317..92809cf 100644 --- a/workbench/app/Models/Tag.php +++ b/workbench/app/Models/Tag.php @@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphToMany; class Tag extends Model { @@ -26,8 +26,13 @@ class Tag extends Model 'id', 'name', 'slug', ]; - public function post(): BelongsTo + public function posts(): MorphToMany { - return $this->belongsTo(Post::class); + return $this->morphedByMany(Post::class, 'taggable'); + } + + public function films(): MorphToMany + { + return $this->morphedByMany(Post::class, 'taggable'); } } diff --git a/workbench/app/Providers/WorkbenchServiceProvider.php b/workbench/app/Providers/WorkbenchServiceProvider.php index e8cec9c..44b08f3 100644 --- a/workbench/app/Providers/WorkbenchServiceProvider.php +++ b/workbench/app/Providers/WorkbenchServiceProvider.php @@ -2,7 +2,9 @@ namespace Workbench\App\Providers; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\ServiceProvider; +use Workbench\App\Models; class WorkbenchServiceProvider extends ServiceProvider { @@ -19,6 +21,11 @@ public function register(): void */ public function boot(): void { - // + Relation::enforceMorphMap([ + 'post' => Models\Post::class, + 'tag' => Models\Tag::class, + 'film' => Models\Film::class, + 'user' => Models\User::class, + ]); } } diff --git a/workbench/database/factories/FilmFactory.php b/workbench/database/factories/FilmFactory.php new file mode 100644 index 0000000..77c86d8 --- /dev/null +++ b/workbench/database/factories/FilmFactory.php @@ -0,0 +1,36 @@ + + */ +class FilmFactory extends Factory +{ + /** + * The name of the factory's corresponding model. + * + * @var string + */ + protected $model = Film::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + $name = $this->faker->unique()->words(2, true); + + return [ + 'title' => $name, + 'slug' => Str::slug($name), + 'year' => $this->faker->year(), + ]; + } +} diff --git a/workbench/database/migrations/2022_05_31_163139_create_tags_table.php b/workbench/database/migrations/2022_05_31_163139_create_tags_table.php index 17a5df8..63de204 100644 --- a/workbench/database/migrations/2022_05_31_163139_create_tags_table.php +++ b/workbench/database/migrations/2022_05_31_163139_create_tags_table.php @@ -22,10 +22,10 @@ public function up() $table->timestamps(); }); - Schema::create('post_tag', function (Blueprint $table) { + Schema::create('taggables', function (Blueprint $table) { $table->id(); - $table->foreignIdFor(Post::class); $table->foreignIdFor(Tag::class); + $table->morphs('taggable'); }); } diff --git a/workbench/database/migrations/2022_05_32_163139_create_films_table.php b/workbench/database/migrations/2022_05_32_163139_create_films_table.php new file mode 100644 index 0000000..c6a4910 --- /dev/null +++ b/workbench/database/migrations/2022_05_32_163139_create_films_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('title'); + $table->string('slug')->unique(); + $table->string('year')->index(); + $table->text('about')->nullable(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('films'); + } +}; diff --git a/workbench/routes/api.php b/workbench/routes/api.php index 45a9349..25c3438 100644 --- a/workbench/routes/api.php +++ b/workbench/routes/api.php @@ -20,14 +20,8 @@ return response()->json($data->toArray()); })->middleware('api'); -Route::patch('post/{post}/tags', function (UpdatePostWithTags $data) { - $tagsData = $data->tags->map(fn ($tag) => UpdateTagData::fromArray([ - 'post' => $data->post, - 'tag' => $tag, - ])); - +Route::patch('tags/{tag}', function (UpdateTagData $data) { return response()->json([ - 'tagsData' => $tagsData->toArray(), 'data' => $data->toArray(), ]); })->middleware('api');