Implementing many to many Polymorphic relationship

Suppose, posts can have many photos then photos can relate to multiple posts also. Same goes for product table. That is same photos can be used by both posts and products table. This means it will be many to many relationship.

Hence unlike one to many polymorphic relationship instead of making polymorphic fields photoable_type and photoable_id in photos table itself we need to separate these fields to another table named photoable along with foreign id photo_id to associate/accommodate rows of many to many relationship for both products and posts table. 

That is if Photoable is a child model then Photo, Product and Post are parent model and both Product and Post model has multiple association to Photo model through Photoable modal instead of single association like in one to many polymorphic relationship. 

If we write the schema of this type of relationship it will be like below

photoables ( child table )
- id
- photo_id
- photoable_id
- photoable_type

photos ( parent table )
- id
- filename
- timestamps

posts  ( parent table )
- id
- title
- timestamps

products ( parent table )
- id
- title
- timestamps 

Now the process ( migration, model, controller, routes ) for the setup of this type relationship is mentioned below step by step 

Step 01: Migration setup for photos table will be like below 

public function up()
{
	Schema::create('photos', function (Blueprint $table) {
		$table->id();
		$table->string('filename');
		$table->timestamps();
	});
}


Step 02: Migration setup for photoables table will be like below

public function up()
{
	Schema::create('photoables', function (Blueprint $table) {
		$table->id();
		$table->foreignId('photo_id')->constrained();
		$table->integer('photoable_id')->unsigned();
		$table->string('photoable_type');          
		$table->timestamps();
	});
}


Step 03: Migration setup for posts table will be like below

public function up()
{
	Schema::create('posts', function (Blueprint $table) {
		$table->id();
		$table->foreignId('user_id')->constrained();
		$table->string('title');           
		$table->timestamps();
	});
}


Step 04: Migration setup for products table will be like below 

public function up()
{
	Schema::create('products', function (Blueprint $table) {
		$table->id();
		$table->foreignId('user_id')->constrained();
		$table->string('title');           
		$table->timestamps();
	});
}


Step 05: Model setup for Photo will be like below

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Photo extends Model
{
    use HasFactory;

    protected $fillable = ['filename'];

    public function posts() {
        return $this->morphedByMany( Post::class , 'photoable');
    }    
    
    public function products() {
        return $this->morphedByMany( Product::class , 'photoable');
    }   
    
} 


Step 06: Model setup for Post will be like below

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;

    protected $fillable = ['title', 'user_id'];

    public function photos()
    {
        return $this->morphToMany(Photo::class, 'photoable');
    }
        
}


Step 07: Model class for Product will be like below 

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasFactory;

    protected $fillable = ['title'];

    public function photos()
    {
        return $this->morphToMany(Photo::class, 'photoable');
    }       
}


Step 08: In PostController.php creating, updating, displaying and deleting rows using this relationship

function create() {
	//
	$images = ['1.jpg', '2.jpg', '3.jpg'];  
	
	$post = Post::create(['title' => 'test title', 'user_id' => 2, 'post_text' => 'demo des goes here']);
foreach( $images as $image ) { $photo = Photo::create([ 'filename' => $image ]);
                 // saving many photos for a post using p m to m relationship
                 $post->photos()->save($photo);

	}

	return redirect()->route('posts.index');
}

function index() {
	$posts = Post::with('photos')->get();
	dd($posts);
}

function edit(Post $post) {
	
	$post->update(['title' => 'test title 2']);
	$images = ['4.jpg', '5.jpg', '6.jpg'];

// detaching (photoables pivot table data) and deleting (photos table data)
// all associated photos for the post
$this->deletePhotosWithPost($post); foreach( $images as $image ) { $photo = Photo::create([ 'filename' => $image ]); $post->photos()->save($photo); } return redirect()->route('posts.index'); }

function delete(Post $post) {
	$post->load('photos'); //dd($post);

$this->deletePhotosWithPost($post); $post->delete(); return redirect()->route('posts.index'); }

function deletePhotosWithPost($post) { $post->photos->each(function ($photo) use ($post) { $post->photos()->detach($photo->id); $photo->delete(); }); }


Step 09: In routes/web.php we need to setup route of PostController

Route::resource('posts', PostController::class);
Route::get('/posts/delete/{post}', [PostController::class, 'delete']);

Now, if we were to changes in ProductController we need to do the same way mentioned above. 

However, in create() method to use the same photos we created in PostController above it will be like below
function create() {
    //
    $photos = Photo::get();

    $product = Product::create(['title' => 'test title' ]);

    foreach( $photos as $photo ) {
        $product->photos()->save($photo);
    }
    
    $products = Product::with('photos')->get();
    dd($products);

}

For the sake of simplicity we have used dummy image names in array instead of receiving actual multiple file inputs inside create() method in both PostController and ProductController. 

 In real production development these array will be populated with submission of form having multiple image files.

Related Posts