Laravel parent model deletion doesn't fire up child model deleting event - laravel

I'm using laravel 5.4. I have a project model as parent and project_images model as child and here is the child model:
Schema::create('project_images', function (Blueprint $table) {
$table->increments('id');
$table->integer('project_id')->unsigned();
$table->foreign('project_id')->references('id')->on('projects')->onDelete('cascade');
$table->string('file', 100);
$table->timestamps();
});
so as expected whenever i delete a project all of its images records will be deleted as well. Then i created a deleting event in images model like this:
protected $events = [
'deleted' => ProjectImageDeleting::class
];
If i delete the image itself then this event also fires up but if i delete the project, then no.
My Question
How should i make this works that when i delete the parent, the child event also fires up? Am i missing something? (I already have event and listener and the code in listener works when i delete the image)
Update
I removed that line in migration and changed the project model to this:
namespace AliMHZ;
use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
public function images()
{
return $this->hasMany(ProjectImage::class);
}
protected static function boot()
{
parent::boot();
static::deleting(function($project) {
$project->images()->delete();
});
}
}
and this is my image model:
namespace AliMHZ;
use Illuminate\Database\Eloquent\Model;
use AliMHZ\Events\ProjectImageDeleting;
class ProjectImage extends Model
{
protected $events = [
'deleting' => ProjectImageDeleting::class
];
public function project()
{
return $this->belongsTo(Project::class);
}
}
and event:
namespace AliMHZ\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use AliMHZ\ProjectImage;
class ProjectImageDeleting
{
use Dispatchable,
InteractsWithSockets,
SerializesModels;
public $image;
public function __construct(ProjectImage $image)
{
$this->image = $image;
}
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}
and finally here is the listener:
namespace AliMHZ\Listeners;
use AliMHZ\Events\ProjectImageDeleting;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
class ProjectImageDeletingListener
{
public function __construct()
{
//
}
public function handle(ProjectImageDeleting $event)
{
\Log::info("File to be deleted: " . $event->image->file);
}
}
After these changes, when i delete project, all its images also will be deleted but the image event still doesn't fire up but if delete the image only then the event works. I'm testing this in laravel tinker and everytime i change any model i exit tinker and lunch it again.

When you are using the $table->foreign('project_id')->references('id')->on('projects')->onDelete('cascade'); the deletion is done on a database level, and as thus model events are not being triggered.
To have all the related model events trigger you would have to write code for this in the models manually.
<?php
class Project extends Eloquent
{
public function images()
{
return $this->has_many('Image');
}
protected static function boot() {
parent::boot();
static::deleting(function($project) {
// This will trigger the ProjectImageDeleting event.
$project->images()->delete();
});
}
}
Edited to answer edited part of question
Could you try the following
namespace AliMHZ;
use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
public function images()
{
return $this->hasMany(ProjectImage::class);
}
protected static function boot()
{
parent::boot();
static::deleting(function($project) {
$project->images()->get()->each(function ($image) {
$image->delete();
});
});
}
}
If you want to read more about why this is happening you can read about it on this issue on GitHub.

Related

Laravel API: How to make a default image in a table instead of NULL

I am currently working on having a default image in a table instead of null. I already have an API that will put an image in that specific column (web_banner_profile) which is a POST method and a DELETE method that will make that column NULL, all of which using postman. I want to know how I can put a default image on all of the webinars table in the web_banner_profile.
This is the Banner Upload Controller:
<?php
namespace App\Http\Controllers;
use App\Models\Banner;
use Illuminate\Http\Request;
// use Illuminate\Support\Facades\Validator;
class BannerUploadController extends Controller
{
public function FileUpload(Request $request, $id)
{
$uploaded_files = $request->file->store('public/uploads/');
$webinar = Banner::find($id);
$webinar->web_banner_profile = $request->file->hashName();
$results = $webinar->save();
if($results){
return ["result"=>"Image Added"];
}else{
return ["result"=>"Image Not Added"];
}
return ["result"=>"$uploaded_files"];
}
public function DeleteBanner($id)
{
$webinar = Banner::find($id);
if(is_null($webinar)){
return response()->json('Record not found!', 401);
}
$webinar->update(['web_banner_profile' => null]);
return response('Banner Deleted', 200);
}
}
This is the webinar table migration:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWebinarTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('webinar', function (Blueprint $table) {
$table->id();
$table->string('web_title');
$table->text('web_description');
$table->dateTime('web_start_date_time')->nullable();
$table->dateTime('web_end_date_time')->nullable();
$table->string('status')->nullable();
$table->string('remarks')->nullable();
$table->string('web_banner_profile')->nullable();
$table->bigInteger('created_by')->unsigned()->nullable();
$table->bigInteger('updated_by')->unsigned()->nullable();
$table->string('web_link')->nullable();
$table->timestamps();
});
Schema::table('webinar', function(Blueprint $table) {
$table->foreign('created_by')->references('id')->on('admins');
$table->foreign('updated_by')->references('id')->on('admins');
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::dropIfExists('webinar');
}
}
Any type of help/suggestion would be greatly appreciated. Thank you in advance!
I would consider using one of the Eloquent model events, specifically the created event. Something like the following:
class Webinar extends Model
{
protected static function booted()
{
static::created(function ($webinar) {
$webinar->update(['web_banner_profile' => 'your-image.jpg']);
});
}
}
Then whenever a new Webinar is created, the created event will be triggered and your default web_banner_profile value will be added to that record.
You don't have to hard code the value your-image.jpg, you could obtain it from a config of env file if you didn't want it in your code base to (arguably) make changing the value easier.

How to implement event/listeners with repository pattern in laravel 5.4

I can't make listeners trigger action update, create or delete when I user patter repository.
Addionally I have added my code in order to help my to solve my problem.
TicketController.php
namespace App\Http\Organizer\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Events\Contracts\IEvent;
use App\Entities\Event;
class TicketController extends Controller
{
protected $IEvent;
public function __construct( IEvent $IEvent )
{
$this->IEvent = $IEvent;
}
public function checkFutbolType ($activityId)
{
// I need to listen this action here
$event = $this->IEvent->update(21927, ['title'=>'new title']);
}
}
My RepoEvent.php:
<?php
namespace App\Http\Events\Repositories;
use App\Http\Events\Contracts\IEvent
;
class RepoEvent implements IEvent
{
protected $model;
public function __construct($model)
{
$this->model = $model;
}
public function update($activityId, $params)
{
return $this->model->where('id', $activityId)->update($params);
}
}
My AppServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Entities\Event;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
//event: creating
Event::creating(function (Event $event) {
return $event->creatingEvent();
});
//event: saving
Event::saving(function (Event $event) {
return $event->savingEvent();
});
//event: updating
Event::updating(function (Event $event) {
return $event->updatingEvent();
});
}
}
My interface IEvent.php:
<?php
namespace App\Http\Events\Contracts;
interface IEvent
{
public function update($activityId, $params);
}
My ServicesOrchestration.php:
<?php
namespace App\Http\Administration\Providers;
use App\Entities\Event;
use App\Http\Administration\Repositories\RepoEvent;
use Illuminate\Support\ServiceProvider;
class ServicesOrchestration extends ServiceProvider
{
public function boot()
{
}
public function register()
{
$this->app->bind('App\Http\Administration\Contracts\IEvent', function () {
return new RepoEvent(new Event());
});
}
}
My model Event.php
<?php
namespace App\Entities;
use Illuminate\Database\Eloquent\Model;
class Event extends Model
{
public function creatingUser() {
\Log::info('creating event');
}
public function savingUser() {
\Log::info('saving event');
}
public function updatingUser() {
\Log::info('updating event');
}
}
thanks in advance.thanks in advance.thanks in advance.thanks in advance.thanks in advance.thanks in advance
Here's the relevant snipped from the docs (scroll to mass updates):
When issuing a mass update via Eloquent, the saved and updated model events will not be fired for the updated models. This is because the models are never actually retrieved when issuing a mass update.
For your code to work you need to first retrieve the actual model instance like below:
public function update($activityId, $params)
{
$instance = $this->model->find($activityId);
$instance->fill($params);
$instance->save();
}
This will have an additional cost of doing two queries instead of one and only being able to update a single model at a time.
A sidenote: You're passing a model instance to the repository but what you actually want is to pass a query builder instance:
$this->app->bind('App\Http\Administration\Contracts\IEvent', function () {
return new RepoEvent(Event::query());
});

Laravel and Local Tunnel integration

My idea was to use Local Tunnel's subdomain feature to expose callback URI in a more convenient way. However, I believe that I could've achieved the same results in a simper way.
This is the solution with Laravel Valet:
In package.json I've added a script called shared
"scripts": {
...
"share": "lt --port 80 --subdomain blog --local-host blog.test"
}
In AppServiceProvider.php I've extended the UrlGenerator to avoid redirecting back to http://blog.test
<?php
namespace App\Providers;
use App\Services\LocalTunnelUrlGenerator;
use Blade;
use Illuminate\Http\Resources\Json\Resource;
use Illuminate\Routing\Router;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
(...)
}
public function register()
{
$this->enableLocalTunnel();
}
private function enableLocalTunnel()
{
if (!app()->environment('local') || !config('app.use_local_tunnel')) {
return;
}
$this->app->extend('url', function (UrlGenerator $defaultGenerator) {
/** #var Router $router */
$router = $this->app['router'];
$routes = $router->getRoutes();
return new LocalTunnelUrlGenerator($routes, $defaultGenerator->getRequest());
});
}
}
This is the the LocalTunnelUrlGenerator.php:
<?php
namespace App\Services;
use Illuminate\Http\Request;
use Illuminate\Routing\RouteCollection;
use Illuminate\Routing\UrlGenerator;
class LocalTunnelUrlGenerator extends UrlGenerator
{
public function __construct(RouteCollection $routes, Request $request)
{
parent::__construct($routes, $request);
}
public function formatRoot($scheme, $root = null)
{
return "https://blog.localtunnel.me";
}
}
Why all that? Because whenever the application call the redirect() method, we are sent back to http://blog.test.
Do I really need to extend the UrlGenerator to make it work?

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.

laravel 5.1 - trait boot not being called for model::update() function

I have created trait as follows on this page app/Traits/ModelEventThrower.php
namespace App\Traits;
use Input;
use Event;
use App\Events\ActivityLog;
use Illuminate\Database\Eloquent\Model;
//use Illuminate\Support\Facades\Event;
/**
* Class ModelEventThrower
* #package App\Traits
*
* Automatically throw Add, Update, Delete events of Model.
*/
trait ModelEventThrower {
/**
* Automatically boot with Model, and register Events handler.
*/
protected static function bootModelEventThrower()
{
foreach (static::getModelEvents() as $eventName) {
static::$eventName(function (Model $model) use ($eventName) {
try {
$reflect = new \ReflectionClass($model);
echo "here";exit;
} catch (\Exception $e) {
return true;
}
});
}
}
/**
* Set the default events to be recorded if the $recordEvents
* property does not exist on the model.
*
* #return array
*/
protected static function getModelEvents()
{
if (isset(static::$recordEvents)) {
return static::$recordEvents;
}
return [
'created',
'updated',
'deleted',
];
}
}
My City Model is something like this
namespace App;
use App\Traits\ModelEventThrower;
use App\Events\ActivityLog;
use Illuminate\Database\Eloquent\Model;
use Event;
class City extends Model
{
use ModelEventThrower;
//protected static $recordEvents = ['updated'];
...
}
My CitiesController is
namespace App\Http\Controllers\Admin;
use App\City;
use App\Country;
use Input;
use Validator;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
class CitiesController extends Controller
{
......
public function update(City $city,Request $request)
{
......
$city->where('id','=',$input['id'])->update($input);
Somehow, I dont see its calling the function written in trait file. When I tried to create $city->create($input); it echos "here" and stops execusion, but not doing same for update function , however I could successfully update the records.
Any suggestion/help will be highly appreciated.
I had a similar issue with Laravel. By adding a constructor in the model to call the boot() function of the parent Model, like so:
public function __construct()
{
parent::boot();
}
you can make sure that all the traits are booted. This solved it for me.

Resources