Laravel5 event 'deleting' doesn't work on Model::whereIn() - laravel

I have one to many relation on the user model, I have set an event when I delete the user will dell all Childs client.
on the resource, the Controller destroys method event 'deleting' method is work for normally.
But I create a mass massDestroy method using Model::whereIn() deleting event doesn't work.
Below is my related code, How can I fix it?
UsersController relate code
public function destroy(User $user)
{
$user->delete();
return back();
}
public function massDestroy(MassDestroyUserRequest $request)
{
if ($request->ajax()) {
User::whereIn('id', $request->get('ids'))->delete();
}
return response(null, Response::HTTP_NO_CONTENT);
}
User Model relate code
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use SoftDeletes, Notifiable;
public $table = 'users';
//skip
protected static function boot()
{
parent::boot();
self::deleting(function (User $user) {
$user->clients()->delete(); //doesn't work on Model::whereIn
});
}
public function clients()
{
return $this->hasMany(Client::class, 'user_id', 'id');
}
}
Client model relate code
public function user()
{
return $this->belongsTo(User::class ,'user_id', 'id');
}
PS* I have try to delete one by one( very ugly code ) as below is normally work.
UsersController
public function massDestroy(MassDestroyUserRequest $request)
{
if ($request->ajax()) {
$users = User::whereIn('id', $request->get('ids'))->get();
foreach ($users as $user ) {
$user ->delete();
}
}
return response(null, Response::HTTP_NO_CONTENT);
}

It is clearly stated in laravel documentation that if you need to perform mass operations then none of the events will be fired. You need to delete them one by one using foreach.
When executing a mass delete statement via Eloquent, the deleting and deleted model events will not be fired for the deleted models. This is because the models are never actually retrieved when executing the delete statement.
Please check note option in Deleting Models

You are using function get() to retrieve a collection of records and there is no method delete() on collection, either you delete by using first() function or delete them by looping the collection array.
I hope you got your answer

I have a trick for your problem
It is clearly stated in Laravel documentation Deleting Models that if you need to perform mass operations then none of the events will be fired.
When executing a mass delete statement via Eloquent, the deleting and deleted model events will not be fired for the deleted models. This is because the models are never actually retrieved when executing the delete statement.
My Trick is:
Use this code
$ids = User::whereIn('id', $request->get('ids'))->pluck('id');
User::destroy($ids);
instead of
//User::whereIn('id', $request->get('ids'))->delete();

Related

Laravel Activity Log won't work on Update and Delete

I'm using SPATIE laravel-activitylog I followed all the instructions but still, it only logs the Create function not update and delete while using it on a Modal
My Modal
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Traits\LogsActivity;
class z_education extends Model
{
//
use LogsActivity;
protected $fillable = [
'user_id',
'type',
'school_name',
'degree',
'isremoved',
];
protected static $logFillable = true;
}
My Controller
public function delete_user_education($id)
{
z_education::where('id', $id)->delete();
return back();
}
Your controller query is executed via the Query Builder, instead of an Eloquent Model. So there will be no model events to listen to.
Retrieve and delete the model itself to fire and log the events:
$model = z_education::findOrFail($id);
$model->delete();
return back();

Laravel - one-to-one relation through pivot table with eager load

I have this relationship
A Movement can have multiples steps
A Step can belongs to multiples Movements
So a had to create a pivot table and a belongsToMany relationship, but my pivot table have some extras columns, like finished and order
I want to have two relationships, one to get all steps from a movement and another one to get the current step from the movement (the last finished step)
I know how to get all steps
public function steps()
{
return $this->belongsToMany(MovementStep::class, 'movement_movement_steps')
->withPivot('order', 'finished')
->orderBy('pivot_order');
}
But how about the current step? I need this kind of relationship, but returning only one record and be able to eager load it cause I'm passing it to vue.js
public function current_step()
{
return $this->belongsToMany(MovementStep::class, 'movement_movement_steps')
->withPivot('order', 'finished')
->where('finished', true)
->orderBy('pivot_order', 'desc');
}
Notice, I'd like to do that without extras packages
alternative solution, but with extra package: Laravel hasOne through a pivot table (not the answer marked as correct, the answer from #cbaconnier)
A different approach from the answer provided by #mrhn is to create a custom relationship. Brent from Spatie did an excellent article about it
Although my answer will do the exact same queries than the one provided by staudenmeir's package it makes me realized that either you use the package, this answer or #mrhn answer, you may avoid the n+1 queries but you may still ends up will a large amount of hydrated models.
In this scenario, I don't think it's possible to avoid one or the other approach. The cache could be an answer though.
Since I'm not entirely sure about your schema, I will provide my solution using the users-photos example from my previous answer.
User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function photos()
{
return $this->belongsToMany(Photo::class);
}
public function latestPhoto()
{
return new \App\Relations\LatestPhotoRelation($this);
}
}
LastestPhotoRelation.php
<?php
namespace App\Relations;
use App\Models\User;
use App\Models\Photo;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class LatestPhotoRelation extends Relation
{
/** #var Photo|Builder */
protected $query;
/** #var User */
protected $user;
public function __construct(User $user)
{
parent::__construct(Photo::query(), $user);
}
/**
* #inheritDoc
*/
public function addConstraints()
{
$this->query
->join(
'user_photo',
'user_photo.photo_id',
'=',
'photos.id'
)->latest();
// if you have an ambiguous column name error you can use
// `->latest('movement_movement_steps.created_at');`
}
/**
* #inheritDoc
*/
public function addEagerConstraints(array $users)
{
$this->query
->whereIn(
'user_photo.user_id',
collect($users)->pluck('id')
);
}
/**
* #inheritDoc
*/
public function initRelation(array $users, $relation)
{
foreach ($users as $user) {
$user->setRelation(
$relation,
null
);
}
return $users;
}
/**
* #inheritDoc
*/
public function match(array $users, Collection $photos, $relation)
{
if ($photos->isEmpty()) {
return $users;
}
foreach ($users as $user) {
$user->setRelation(
$relation,
$photos->filter(function (Photo $photo) use ($user) {
return $photo->user_id === $user->id; // `user_id` came with the `join` on `user_photo`
})->first() // Photos are already DESC ordered from the query
);
}
return $users;
}
/**
* #inheritDoc
*/
public function getResults()
{
return $this->query->get();
}
}
Usage
$users = \App\Models\User::with('latestPhoto')->limit(5)->get();
The main difference from Brent's article, is that instead of using a Collection we are returning the latest Photo Model.
Laravel has a way to create getters and setters that act similar to columns in the database. These can perfectly solve your problem and you can append them to your serialization.
So instead your current_step is gonna be an accessor (getter). The syntax is getCurrentStepAttribute() for the function which will make it accessible on the current_step property. To avoid N + 1, eager load the steps when you retrieve the model(s) with the with('steps') method. Which is better than running it as a query, as it will execute N times always.
public function getCurrentStepAttribute() {
return $this->steps
->where('finished', true)
->sortByDesc('pivot_order')
->first();
}
Now you can use the append property on the Movement.php class, to include your Eloquent accessor.
protected $appends = ['current_step'];

How to always use withTrashed() for model Binding

In my app, I use soft delete on a lot of object, but I still want to access them in my app, just showing a special message that this item has been deleted and give the opportunity to restore it.
Currently I have to do this for all my route parametters in my RouteServiceProvider:
/**
* Define your route model bindings, pattern filters, etc.
*
* #return void
*/
public function boot()
{
parent::boot();
Route::bind('user', function ($value) {
return User::withTrashed()->find($value);
});
Route::bind('post', function ($value) {
return Post::withTrashed()->find($value);
});
[...]
}
Is there a quicker and better way to add the trashed Object to the model binding ?
Jerodev's answer didn't work for me. The SoftDeletingScope continued to filter out the deleted items. So I just overrode that scope and the SoftDeletes trait:
SoftDeletingWithDeletesScope.php:
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class SoftDeletingWithDeletesScope extends SoftDeletingScope
{
public function apply(Builder $builder, Model $model)
{
}
}
SoftDeletesWithDeleted.php:
namespace App\Models\Traits;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\Scopes\SoftDeletingWithDeletesScope;
trait SoftDeletesWithDeleted
{
use SoftDeletes;
public static function bootSoftDeletes()
{
static::addGlobalScope(new SoftDeletingWithDeletesScope);
}
}
This effectively just removes the filter while still allowing me to use all the rest of the SoftDeletingScope extensions.
Then in my model I replaced the SoftDeletes trait with my new SoftDeletesWithDeleted trait:
use App\Models\Traits\SoftDeletesWithDeleted;
class MyModel extends Model
{
use SoftDeletesWithDeleted;
For Laravel 5.6 to 7
You can follow this doc https://laravel.com/docs/5.6/scout#soft-deleting. And set the soft_delete option of the config/scout.php configuration file to true.
'soft_delete' => true,
For Laravel 8+
You can follow this doc https://laravel.com/docs/8.x/routing#implicit-soft-deleted-models. And append ->withTrashed() to the route that should accept trashed models:
Ex:
Route::get('/users/{user}', function (User $user) {
return $user->email;
})->withTrashed();
You can add a Global Scope to the models that have to be visible even when trashed.
For example:
class WithTrashedScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->withTrashed();
}
}
class User extends Model
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(new WithTrashedScope);
}
}
Update:
If you don't want to show the deleted objects you can still manually add ->whereNull('deleted_at') to your query.

withTrashed on hasManyThrough relation

How can withTrashed be applied on a hasManyThrough relation ?
$this->hasManyThrough('App\Message', 'App\Deal')->withTrashed();
returns
Call to undefined method Illuminate\Database\Query\Builder::withTrashed()
when i'm doing:
$messages = Auth::user()->messages()->with('deal')->orderBy('created_at', 'DESC')->get();`
Here is my Deal model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Deal extends Model
{
use SoftDeletes;
/* ... */
protected $dates = ['deleted_at'];
public function user() {
return $this->belongsTo('App\User');
}
public function messages() {
return $this->hasMany('App\Message'); // I've tried to put withTrashed() here, there is no error but it doesn't include soft deleting items.
}
}
To all those coming to this late, there is now a native way of doing this with Laravel.
$this->hasManyThrough('App\Message', 'App\Deal')->withTrashedParents();
This is not well documented but can be found in Illuminate\Database\Eloquent\Relations\HasManyThrough
The error is thrown because you are requesting a messages with deleted ones without using SoftDelete trait in Message model.
After I check the hasManyThrough relation code I found that there is no way to do it in this way, you should play around.
Ex:
get the deals of user with messages instead
$deals = Auth::user()->deals()->withTrashed()->with('messages')->get();
foreach($deals as $deal) {
//Do your logic here and you can access messages of deal with $deal->messages
}

Update Foreign Key with softDeletes()

Hi I've been looking for the similar solution but can't find anywhere. I don't even know if there is one. What I am trying to do is perform a softDelete on one of my model and want to update the user_id in the model with the the id of the user performing the action. I have tried using associate(), it doesn't throw any exception but does not work. I mean the delete is working but id is not updating
Here's what I've tried
public function postDelete(Request $request){
$appointment = \App\Models\emp_appointment::findOrFail($request->appid);
$user = $request->user();
$appointment->user()->associate($user);
$appointment->delete();
}
Here's my Appointment Model
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class emp_appointment extends Model{
use SoftDeletes;
protected $dates = ['deleted_at'];
public function employee(){
return $this->belongsTo('App\Models\employee','emp_id');
}
public function user(){
return $this->belongsTo('App\Models\tb_login','userid');
}
}
PS: There are no problems in the models and no errors anywhere. Only the problem is userid doesn't get updated in the appointment table.
Seems like the delete method isn't saving your change (association)
Try saving first then deleting
public function postDelete(Request $request){
$appointment = \App\Models\emp_appointment::findOrFail($request->appid);
$user = $request->user();
$appointment->user()->associate($user);
$appointment->save();
$appointment->delete();
}
EDIT
public function postDelete(Request $request){
$appointment = \App\Models\emp_appointment::findOrFail($request->appid);
$user = $request->user();
$appointment->user()->associate($user);
$appointment->deleted_at= date('Y-m-d H:i:s');
$appointment->save();
}

Resources