Automatically deleting NESTED related rows in Laravel 5.4 (Eloquent ORM) - php

Let's assume I have a Book. This Book has Chapters and those Chapters in this Book have Subchapters.
So I have three models:
Book > Chapter > Subchapter
When I delete the Book ($book->delete();), I also want to delete the related Chapters of this Book and also the related Subchapters of all the Chapters from the Book.
Here (Automatically deleting related rows in Laravel (Eloquent ORM)) I found out about Eloquent Events. Whenever a Book is deleted, before that, the Chapter gets deleted because we hook in:
class Book extends Eloquent
{
public function chapters()
{
return $this->has_many('Chapter');
}
protected static function boot() {
parent::boot();
static::deleting(function($book) {
$book->chapters()->delete();
});
}
}
So I thought, I only have to implement the same code in my Chapter-Model, only exchanging "Book" with "Chapter" and "Chapter" with "Subchapter":
class Chapter extends Eloquent
{
public function subChapters()
{
return $this->has_many('SubChapter');
}
protected static function boot() {
parent::boot();
static::deleting(function($chapter) {
$chapter->subChapters()->delete();
});
}
}
This works fine when I delete a Chapter. All the Subchapters will also be deleted.
When I delete the Book it works fine with the Chapters. All the Chapters will also be deleted.
However it only deletes Chapters when I delete the Book. It does not delete the related Subchapters of the deleted Chapters.
Can anybody help me?

That's because when you delete multiple objects at the same time it doesn't trigger the boot deleting function for each model so you should loop through the objects and delete them one by one:
class Book extends Eloquent
{
public function chapters()
{
return $this->has_many(Chapter::class);
}
protected static function boot() {
parent::boot();
static::deleting(function($book) {
foreach($book->chapters as $chapter){
$chapter->delete();
}
});
}
}
/********************************/
class Chapter extends Eloquent
{
public function subChapters()
{
return $this->has_many(SubChapter::class);
}
protected static function boot() {
parent::boot();
static::deleting(function($chapter) {
foreach($chapter->subChapters as $subChapter){
$subChapter->delete();
}
});
}
}
However my recommendation is to set cascading foreign key relation between the tables so the DBMS is going to delete the related rows automatically, the sample code below shows you how to do it in migration files:
Schema::create('chapters', function (Blueprint $table) {
$table->increments('id');
$table->integer('book_id')->unsigned();
$table->string('title');
$table->timestamps();
$table->foreign('book_id')
->references('id')
->on('books')
->onDelete('cascade')
->onUpdate('cascade');
});
Do the same for subChapters.
Hope It helps...

This is because $book->chapters() will just return an instance of Builder. When you call delete() on the builder it will simply just run the query needed to delete those models and not new them up and then delete them which will mean that the deleting event on the Chapter will never be fired.
To get around this you can do something like:
protected static function boot() {
parent::boot();
static::deleting(function($book) {
$book->chapters->each(function ($chapter) {
$chapter->delete();
});
});
}
Hope this helps!

The correct way to do that is to use onDelete('cascade'). In the chapters table migration:
$table->foreign('book_id')
->references('id')
->on('books')
->onDelete('cascade');
In the subchapters migration:
$table->foreign('chapter_id')
->references('id')
->on('chapters')
->onDelete('cascade');
In this case, you don't need to write any code and all chapters and subchapters will be deleted automatically.

Related

Associating multiple single instances of the same model in Laravel

I am working on a project in which there are events, which each relate to two single forms on two separate relations – booking and survey. These forms are identically constructed, making it seem unnecessary to use two entirely distinct form models – I instead wanted to use a polymorphic relation, but it appears that isn't possible.
What is the appropriate way to structure this relationship?
Events have one or no booking form
Events have one or no survey form
Forms are a separate, single table
What I have tried:
Polymorphic relationship: Not compatible with two relations to the same model.
Has one relationship: This used a booking_id and survey_id but refused to set either of these fields.
Has many relationship with a type field: Made it difficult to easily save the forms, as it wasn't possible to save to the single relationship. There was also no restriction on the number of forms.
class Event extends Model
{
public function booking()
{
return $this->hasOne(Form::class, 'id', 'booking_form_id');
}
public function survey()
{
return $this->hasOne(Form::class, 'id', 'survey_form_id');
}
}
...
class Form extends Model
{
public function event()
{
return $this->belongsTo(Event::class);
}
}
...
$event = new Event;
$event->name = 'Event';
$event->save();
$booking = new Form;
$booking->name = 'booking';
$event->booking()->save($booking);
$survey = new Form;
$survey->name = 'survey';
$event->survey()->save($survey);
...
Schema::create('events', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->unsignedInteger('booking_form_id')->nullable()->index();
$table->unsignedInteger('survey_form_id')->nullable()->index();
$table->timestamps();
});
Schema::create('forms', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});
What would be preferable:
Using a polymorphic relationship which would allow forms to be used in other parts of the application.
Using multiple hasOne relationships to limit the number of forms to one for each type.
I think you got your param order wrong. It's hasOne($related, $foreignKey, $localKey)
class Event extends Model
{
/* if you haven't changed the default primary keys, $localKey should be equal to 'id' */
public function booking()
{
return $this->belongsTo(Form::class, 'booking_form_id');
}
public function survey()
{
return $this->belongsTo(Form::class, 'survey_form_id');
}
}
class Form extends Model
{
public function booking_event()
{
return $this->hasOne(Event::class, 'booking_form_id');
}
public function survey_event()
{
return $this->hasOne(Event::class, 'survey_form_id');
}
}
Now there's 2 ways you can go about this.
If a Form can belong to both kind of events, you need to return a collection when accessing $form->event.
If a Form can belong to only one kind of event, you need to guess which kind and return the model when accessing $form->event.
# Form model
# 1. can be achieved using an accessor. Cannot be eager loaded but can be appended with the $appends Model property
public function getEventsAttribute()
{
return collect([$this->booking_event, $this->survey_event]);
}
# Form model
# 2. can be achieved using a relationship that guesses which relation it should return. Since it returns a relationship, it can be eager loaded.
public function event()
{
return ($this->booking_event()->count() != 0) ? $this->booking_event() : $this->survey_event();
}

Im unable to delete the records present int he Pivot Table

I have five tables:
Post
category
Tags
category_post
post_tag
The problem that I am getting is that if I delete the post then it should also delete all the relations of that post in all the tables where it is related. But the system is performing the total opposite it is only deleting the post in the post table.
I found a solution which was
$table->engine='InnoDB'
but my problem still remains the same
This is my Migration for the Category_post Pivot Table
public function up()
{
Schema::create('post_tag', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->integer('post_id')->index()->unsigned();
$table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
$table->integer('tag_id')->index()->unsigned();
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
$table->timestamps();
});
}
This is what I am doing in the controller
public function destroy(Post $post)
{
$post=Post::find($post->id);
$post->delete();
return redirect('admin/post')->with('message','Deleted Sucessfully');
}
I also Tried this
public function destroy(Post $post)
{
$post=Post::find($post->id);
$post->categories()->delete();
$post->tags()->delete();
$post->delete();
return redirect('admin/post')->with('message','Deleted Sucessfully');
}
But got the same results
When using pivot tables for ManyToMany relationships in Laravel, you should detach the associated tags and categories with the Post model instead of deleting them as per the docs
Besides, your controller code is deleting the tags and categories models and not the association which would corrupt any other posts that are attached to those tags and categories.
Here's an example of the correct way to do it
In your tags migrations
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('tags', function (Blueprint $table) {
$table->bigIncrements('id');
// Any other columns goes here
$table->timestamps();
});
Schema::create('post_tag', function (Blueprint $table) {
$table->bigInteger('post_id');
$table->bigInteger('tag_id');
// ensures a specific post can be associated a specific tag only once
$table->primary(['post_id', 'tag_id']);
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::dropIfExists('post_tag');
Schema::dropIfExists('tags');
}
Do the same thing for categories migration
Specify the ManyToMany relationship in your Eloquent model like so
class Post extends Model
{
public function tags()
{
return $this->belongsToMany('App\Tag');
}
public function categories()
{
return $this->belongsToMany('App\Category');
}
}
Now when associating tags/categories with a post use the attach method
$post = Post::create([]); // this is only sample code, fill your data as usual
$tag = Tag::create([]);
$category = Category::create([]);
// You can either attach by the model itself or ID
$post->tags()->attach($tag);
$post->categories()->attach($category);
And finally when destroying the Post model, just deassociate the relationship with the tags and categories instead of deleting them using the detach method like so
public function destroy(Post $post)
{
$post->categories()->detach();
$post->tags()->detach();
$post->delete();
return redirect('admin/post')->with('message','Deleted Sucessfully');
}

Find all posts associated with a tag in laravel

I am building a blog with laravel where a post has many tags. I want to filter all post by a tag. means if I click on "PHP" tag I want to get all associated post.
Here is my code
I have two table first for tags and a second table for the link with posts
tag_table
public function up()
{
Schema::create('tags', function (Blueprint $table) {
$table->increments('id');
$table->string('tags');
$table->timestamps();
});
}
relation tag table
public function up()
{
Schema::create('article_tag', function (Blueprint $table) {
$table->increments('id');
$table->integer('article_id')->unsigned();
$table->foreign('article_id')->references('id')->on('articles');
$table->integer('tag_id')->unsigned();
$table->foreign('tag_id')->references('id')->on('tags');
});
}
Article Model
class Article extends Model
{
public function tags()
{
return $this->belongsToMany('App\Tag');
}
}
tag model
class Tag extends Model
{
public function articles()
{
return $this->belongsToMany('App\Article');
}
}
Tag Controller
public function show($id,$name)
{
//here I received tag id and name.
$list->with('articles')->get();
return view('articles.tagshow')->withList($list);
}
Eloquent offers the whereHas method that lets you filter on attributes of related models. In order to filter articles by the name of their associated tags, you should do the following:
$articles = Article::whereHas('tags', function($query) use ($tagName) {
$query->whereName($tagName);
})->get();
However, in your case it should be even simpler, because you already have the tag ID in your controller, so you can simply get a tag model by ID and then return related articles:
public function show($id,$name) {
return Tag::findOrFail($id)->articles;
}
Check the docs on querying relations for more details: https://laravel.com/docs/5.6/eloquent-relationships#querying-relations

Store Comments in DB

I am looking for a solution for storing comments in the database, but it is not difficult at all:
In one table wants to write comments from several modules on the website.
I am currently creating a table using code 'comments table':
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->integer('module_id')->unsigned();
$table->integer('parent_id')->unsigned();
$table->text('body');
$table->timestamps();
});
}
Comments modules table:
public function up()
{
//
Schema::create('comment_module',function (Blueprint $table){
$table->increments('id');
$table->string('title',190);
$table->string('name',190)->unique();
$table->text('body');
$table->timestamps();
});
}
for now everything is okay, but i have problem with select all comments for each blog post,gallery, etc..
blog, gallery - name of modules.
code for Map.php model
public function comments(){
return $this->hasMany(Comment::class,'module_id');
}
CommentModule.php model
public function comments(){
return $this->hasMany(Comment::class,'module_id');
}
Comment.php
public function module(){
return $this->belongsTo(CommentModule::class);
}
and now how to pass a 'mmodule_id' ?
normal use with any relationships for one table will be like that:
$map->comments->body . . etc.
but for that construction don`t work, yes of course i can use raw query and use join, right ?
Is any option to use a Eloquent?
IMHO for what I understood you want to attach comments to more than one Eloquent Model. There is a clean example in the laravel docs with Polymorphic Relations
As summary you have to add two fields on the comments table: commentable_id(integer) and commentable_type (string).
After that you declare the relation on the Comment model:
public function commentable()
{
return $this->morphTo();
}
And you can use the comments() relation in each model you want to attach comments, i.e.:
class Post extends Model
{
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
class Map extends Model
{
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
Now you can retrieve comments as usual:
$post->comments;
To attach a new comment to a parent:
$post->comments()->create([array of comment attributes]);

Laravel 5.1 Multiple Many to Many Relationship on the Same Model

I seen to of got tangled in Laravel's ORM with the following:
Scenerio: All Users have a Watchlist, the Watchlist contains other Users.
I can't seem the get the relationships to work correctly as they are cyclical, so far I have the following:
class UserWatchlist extends Model
{
protected $table = 'UserWatchlist';
public function Owner() {
return $this->belongsTo('App\User');
}
public function WatchedUsers() {
return $this->hasMany('App\User');
}
}
Schema::create('UserWatchlist', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('Users')->onDelete('cascade');
$table->integer('watched_id')->unsigned();
$table->foreign('watched_id')->references('id')->on('Users')->onDelete('cascade');
$table->timestamps();
});
class User extends Model
{
public function Watchlist() {
return $this->hasOne('App\UserWatchlist');
}
public function WatchedBy() {
return $this->belongsToMany('App\UserWatchlist');
}
}
It is not pulling through the correct in formation i'm expecting. Am I missing something fundamental?
Since UserWatchlist is a pivot table, i suppose you are facing a many to many relationship with both the elements of the relation being the same model (User)
If that is the case, you should not build a model for the pivot table UserWatchlist but all you have to do is to set the relation between the users through the pivot table:
class User extends Model
{
//get all the Users this user is watching
public function Watchlist()
{
return $this->belongsToMany('User', 'UserWatchlist', 'user_id', 'watched_id' );
}
//get all the Users this user is watched by
public function WatchedBy()
{
return $this->belongsToMany('User', 'UserWatchlist', 'watched_id', 'user_id' );
}
}
Check here for more info on many-to-many relationship

Categories