I have two models, Show and Episode, with a one to many relationship. I have an Observer for each model to watch when they are deleted and do some tasks. If my ShowObserver looks like this everything works properly and cascades down, the EpisodeObserver fires its deleting() method for each Episode that is deleted along with the show:
<?php
/**
* Listen to the Show deleting event.
*
* #param Show $show
* #return void
*/
public function deleting(Show $show)
{
if ($show->isForceDeleting()) {
foreach ($show->episodes()->onlyTrashed()->get() as $episode) {
$episode->forceDelete();
}
} else {
$show->episodes()->delete();
}
}
However, if I change it to looks like this the EpisodeObserver#deleting() methods never fire even though the Episodes do get forceDeleted:
<?php
/**
* Listen to the Show deleting event.
*
* #param Show $show
* #return void
*/
public function deleting(Show $show)
{
if ($show->isForceDeleting()) {
$show->episodes()->onlyTrashed()->forceDelete();
} else {
$show->episodes()->delete();
}
}
Is there something about $show->episodes()->onlyTrashed()->forceDelete(); that is incorrect, or is this potentially a bug?
Check out the documentation (on the red warning block): https://laravel.com/docs/5.3/eloquent#deleting-models
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.
This is also same to update call.
So if you need to fire the events, you have no choice but to delete it one by one, or fire your own custom event if performance is critical.
Related
I have a method that needs to pull in information from three related models. I have a solution that works but I'm afraid that I'm still running into the N+1 query problem (also looking for solutions on how I can check if I'm eager loading correctly).
The three models are Challenge, Entrant, User.
Challenge Model contains:
/**
* Retrieves the Entrants object associated to the Challenge
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entrants()
{
return $this->hasMany('App\Entrant');
}
Entrant Model contains:
/**
* Retrieves the Challenge object associated to the Entrant
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function challenge()
{
return $this->belongsTo('App\Challenge', 'challenge_id');
}
/**
* Retrieves the User object associated to the Entrant
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\User', 'user_id');
}
and User model contains:
/**
* Retrieves the Entrants object associated to the User
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entrants()
{
return $this->hasMany('App\Entrant');
}
The method I am trying to use eager loading looks like this:
/**
* Returns an array of currently running challenges
* with associated entrants and associated users
* #return array
*/
public function liveChallenges()
{
$currentDate = Carbon::now();
$challenges = Challenge::where('end_date', '>', $currentDate)
->with('entrants.user')
->where('start_date', '<', $currentDate)
->where('active', '1')
->get();
$challengesObject = [];
foreach ($challenges as $challenge) {
$entrants = $challenge->entrants->load('user')->sortByDesc('current_total_amount')->all();
$entrantsObject = [];
foreach ($entrants as $entrant) {
$user = $entrant->user;
$entrantsObject[] = [
'entrant' => $entrant,
'user' => $user
];
}
$challengesObject[] = [
'challenge' => $challenge,
'entrants' => $entrantsObject
];
}
return $challengesObject;
}
I feel like I followed what the documentation recommended: https://laravel.com/docs/5.5/eloquent-relationships#eager-loading
but not to sure how to check to make sure I'm not making N+1 queries opposed to just 2. Any tips or suggestions to the code are welcome, along with methods to check that eager loading is working correctly.
Use Laravel Debugbar to check queries your Laravel application is creating for each request.
Your Eloquent query should generate just 3 raw SQL queries and you need to make sure this line doesn't generate N additional queries:
$entrants = $challenge->entrants->load('user')->sortByDesc('current_total_amount')->all()
when you do ->with('entrants.user') it loads both the entrants and the user once you get to ->get(). When you do ->load('user') it runs another query to get the user. but you don't need to do this since you already pulled it when you ran ->with('entrants.user').
If you use ->loadMissing('user') instead of ->load('user') it should prevent the redundant call.
But, if you leverage Collection methods you can get away with just running the 1 query at the beginning where you declared $challenges:
foreach ($challenges as $challenge) {
// at this point, $challenge->entrants is a Collection because you already eager-loaded it
$entrants = $challenge->entrants->sortByDesc('current_total_amount');
// etc...
You don't need to use ->load('user') because $challenge->entrants is already populated with entrants and the related users. so you can just leverage the Collection method ->sortByDesc() to sort the list in php.
also, You don't need to run ->all() because that would convert it into an array of models (you can keep it as a collection of models and still foreach it).
Using Laravel 5.4
I have a Job model which has a enum field on it with different statuses. These statuses change in many different places. I made a JobHistory model and migration which tracks those changes .
On my Job model i define the new laravel 5.4 way of tracking eloquent events:
/**
* The events that should be fired for eloquent actions
*
* #var array
*/
protected $events = [
'updating' => JobChangedStatus::class
];
Status changes are done like this:
/**
* Change the job status
*
* #param $status
*/
public function changeStatus($status)
{
$this->update([
'status' => $status,
]);
}
EventServiceProvider:
'App\Events\Jobs\JobChangedStatus' => [
'App\Listeners\Jobs\CreateJobHistory',
],
CreateJobHistory Listener:
$job = $event->job;
$jobHistory = new JobHistory();
$jobHistory->old_status = $job->getOriginal('status');
$jobHistory->new_status = $job->status;
$jobHistory->job()->associate($job);
$jobHistory->executor()->associate(Auth::user());
$jobHistory->save();
When i change my job status from e.g New to In_progress
My JobHistory table will look like this:
So the new_status and the old_status both give the old value. I tried using $job->getDirty() but when i print it it just gives back a empty array.
What am i doing wrong?
Usually I would achieve this inside an Observer. It feels a little awkward to see the events/listener setup like that.
In your AppServiceProvider.php:
public function boot()
{
App\Job::observe(App\Observers\JobObserver::class);
}
And then in App\Observers\JobObserver.php:
use App\Job;
use App\JobHistory;
class JobObserver{
public function updating(Job $job)
{
$jobHistory = new JobHistory();
$jobHistory->old_status = $job->getOriginal('status');
$jobHistory->new_status = $job->status;
$jobHistory->job()->associate($job);
$jobHistory->executor()->associate(Auth::user());
$jobHistory->save();
}
}
For eloquent models it makes the most sense (just my opinion) to use observers. The events/listeners model I use for listening to mail events, job events, possibly notifications, etc.
The other reason this may help you in your situation is if the event is being queued instead of ran synchronously, you will end up with a race condition and most cases the model will have been saved and that is why getDirty() has no keys. With observers, the operations are always ran synchronously and you will not run into a timing issue.
I have an event called UserWasRegistered also I have a listener called UserWasRegistered from there I intned to develop job commands called:
EmailRegistrationConfirmation
NotifyAdminsNewRegistration
CreateNewBillingAccount
All these jobs would be executed within the UserWasRegistered event listener class.
Is this the correct approach or should i just have multiple listeners for UserWasRegistered? I feel using the jobs approach enabled me to call those "jobs" from other areas in my application at different times. e.g. calling CreateNewBillingAccount might be called if a user changed their details...?
I recommend to change the listener names that's more explicit about what's happening, so I'd avoid directly pairing listeners with events.
We're using an anemic event/listener approach, so listeners pass the actual task to "doers" (jobs, services, you name it).
This example is taken from a real system:
app/Providers/EventServiceProvider.php:
OrderWasPaid::class => [
ProvideAccessToProduct::class,
StartSubscription::class,
SendOrderPaidNotification::class,
ProcessPendingShipment::class,
LogOrderPayment::class
],
StartSubscription listeners:
namespace App\Modules\Subscription\Listeners;
use App\Modules\Order\Contracts\OrderEventInterface;
use App\Modules\Subscription\Services\SubscriptionCreator;
class StartSubscription
{
/**
* #var SubscriptionCreator
*/
private $subscriptionCreator;
/**
* StartSubscription constructor.
*
* #param SubscriptionCreator $subscriptionCreator
*/
public function __construct(SubscriptionCreator $subscriptionCreator)
{
$this->subscriptionCreator = $subscriptionCreator;
}
/**
* Creates the subscription if the order is a subscription order.
*
* #param OrderEventInterface $event
*/
public function handle(OrderEventInterface $event)
{
$order = $event->getOrder();
if (!$order->isSubscription()) {
return;
}
$this->subscriptionCreator->createFromOrder($order);
}
}
This way you can invoke jobs/services (SubscriptionCreator in this example) in other areas of your application.
It's also possible to bind the listener to other events as well, other than OrderWasPaid.
I am new to Magento. I want to build an observer which on cancellation of an order will perform a query to my database and will decide whether the order is cancellable or not (This is decided on the basis of a certain state.). If it can't be cancelled, then it should break the cancel event and display a message that the order cannot be cancelled.
Which event I should choose, order_cancel_after or sales_order_item_cancel, and how can I break out of this event in between?
Thanks in advance. :)
There is no general answer to this, it depends on the context where the event is triggered and what happens there afterwards.
The events don't have an interface to "stop" them and they are not tied to the actual "event" (i.e. order cancellation) other than by name.
So you will have to look at the code of Mage_Sales_Model_Order_Item where sales_order_item_cancel gets triggered (order_cancel_after is obviously the wrong place to look because at that point the order is already cancelled):
/**
* Cancel order item
*
* #return Mage_Sales_Model_Order_Item
*/
public function cancel()
{
if ($this->getStatusId() !== self::STATUS_CANCELED) {
Mage::dispatchEvent('sales_order_item_cancel', array('item'=>$this));
$this->setQtyCanceled($this->getQtyToCancel());
$this->setTaxCanceled($this->getTaxCanceled() + $this->getBaseTaxAmount() * $this->getQtyCanceled() / $this->getQtyOrdered());
$this->setHiddenTaxCanceled($this->getHiddenTaxCanceled() + $this->getHiddenTaxAmount() * $this->getQtyCanceled() / $this->getQtyOrdered());
}
return $this;
}
You see that there is no additional check after the event was dispatched, but it would be possible to set the qty_to_cancel attributes to 0 to uneffect the cancelling.
Your observer method:
public function salesOrderItemCancel(Varien_Event_Observer $observer)
{
$item = $observer->getEvent()->getItem();
if (!$this->_isCancellable($item->getOrder())) {
$item->setQtyToCancel(0);
$this->_showErrorMessage();
}
}
Note that you don't have to set tax_canceled or hidden_tax_canceled because they depend on qty_canceled and thus will stay 0.
I need to save some cms pages and delete others in a single transaction.
So, how to I make this:
$page1->save();
$page2->delete();
A single transaction? For reference, both $page1 and $page2 come from Mage::getModel('cms/page'). Also, I found an excellent answer here that tells me how to do two saves in a transaction, but not how to do both a save and delete. How can it be done?
If you must do this in a single transaction, just call isDeleted(true) on those items which you wish to be deleted:
//Build out previous items, then for each which should be deleted...
$page2->isDeleted(true);
$transaction = Mage::getModel('core/resource_transaction');
$transaction->addObject($page1)
$transaction->addObject($page2)
//$transaction->addObject(...) etc...
$transaction->save();
Thought I should add an explanation (from Mage_Core_Model_Abstract::save() [link]):
/**
* Save object data
*
* #return Mage_Core_Model_Abstract
*/
public function save()
{
/**
* Direct deleted items to delete method
*/
if ($this->isDeleted()) {
return $this->delete();
}
// ...
}