How to use model events with query builder in laravel - events

I'm using model events such as static::saving, static::saved, etc in my models' static function boot method, and that works great when users save new posts, but when I do something like this:
$post::where('id', $post_id)->update(array('published'=>1));
Updating in this way does not run those model events. My current solution is to just not use this method of updating and instead do:
$post = Post::find($post_id);
$post->published = 1;
$post->save();
But is there any way to make the model events work with the first example using query builder?

Model events will not work with a query builder at all.
One option is to use Event listener for illuminate.query from /Illuminate/Database/Connection.php. But this will work only for saved, updated and deleted. And requires a bit of work, involving processing the queries and looking for SQL clauses, not to mention the DB portability issues this way.
Second option, which you do not want, is Eloquent. You should still consider it, because you already have the events defined. This way you can use also events ending with -ing.

Related

Laravel - trouble with Eloquent Collection methods such as last()

I have a variable $courses that in my debugger is shown as type App/Courses
In the model, there is a one to many relation and I retrieve these related items in the model and then access as $courses->relateditems
In the debugger, $courses->relateditems is shown as type Illuminate/Database/Eloquent/Collection
Ok, all makes sense.
I want to get last item in the $courses->relateditems collection. So I try
$courses->relateditems->last()->startdate
But this is not returning the value that I know exists. And when I evaluate the expression $courses->relateditems->last() in the debugger I get this in my laravel.log:
Symfony\Component\Debug\Exception\FatalErrorException: Cannot access self:: when no class scope is active in /app/Courses.php:68
I am just not sure what is going on. I know I can use DB queries to just get the data I need, but I have a model event triggering a function and that function receives the $courses object/model (or however we name it) and I am just trying to get this working as above.
Ideas on what I am doing wrong?
thanks,
Brian
The issue here based on the error you have at the bottom of your post is a mistake in your code.
However, I think you've misunderstood how Laravel relationships work. They essentially come in two forms on a model, that of a property and that of a method.
relateditems Is a magic property that will return the result of a simple select on the relationship if one has already been performed. If one hasn't been performed it will perform one and then return it, known as eager loading.
relateditems() Is a method that returns a query builder instance for that relationship.
By calling relateditems->last() you're loading ALL related items from the database and then getting the last, which is less than optimal.
The best thing for you to do is this:
$course->relateditems()->orderBy('id', 'desc')->first();
This will get the first item return, however, since we've ordered it by id in descending order, it'll be reversed from what could be considered its default ordering. Feel free to change the id to whatever you want.

What to store in a notification?

I'm currently utilizing Laravels notification package a bit more, but the following bothers me for weeks now: What should I really store in notifications?
Sometimes notifications are related to specific models, but not always.
Examples: Your blog post was published. or An error occurred while doing something. The entry was deleted.
Sometimes these models have relationships like Post → Category and the message should look like: Your blog post in the category "A Category" was published.
Now the questions:
Should I save a related model completely (eg. Category)? This would make accessing it later easier, but it's also a source for inconsistency. Or should I simply save the category ID? Only saving the ID means that I can reference the current data, but what happens if the category gets deleted? Then the notification cannot be rendered. Also I would need to also query the related models for this notification everytime.
Should I save the full message or only the data and compose the message on the client? (App, SPA Web-Frontend...). What about localization then?
What is a best practice for future scaling and also for extending existing notifications in the future?
So you propose to either go for:
1. Save notifications including all data required to display it
OR
2. Save notifications with just references so it can render message later on
So let's consider the advantages and drawbacks of both options.
Option 1: saving including all data
If a related model is deleted, the notification message can still be rendered as before (as you mentioned)
If a related model is changed (e.g. category title is changed), the notification message does not change
If you want to change a notification later on to include additional fields from related models, you won't have those fields available
Option 2: saving including just references
If a related model is deleted, the notification can not be rendered (as you mentioned). I would however argue that the notification wouldn't make much sense in this case.
If a related model is changed (e.g. category title is changed), the notifciation message changes with it
If you want to change a notification later on to include additional fields from related models, you will have those fields available
Additionaly if you were to serialize the notifications in the database you won't be able to deserialize them if you changed the model for it later on (e.g. a field is deleted).
Implementation of option 2
In order to go for option 2 additional database load can't really be avoided.
Easy way
The easiest way would be to resolve the relationships in the notification would be to query the relationships during the rendering of the notifications array, this however will cause the system to an additional query for each relationship.
NotificationController.php
$user = App\User::find(1);
foreach ($user->notifications as $notification) {
echo $notification->type;
}
MyNotification.php
public function toArray($notifiable)
{
$someRelatedModel = Model::find($this->someRelatedModel_id);
return [
'invoice_id' => $this->invoice->id,
'amount' => $this->invoice->amount,
'relatedModelData' => $someRelatedModel->data,
];
}
Nicer way
The better solution would be to adjust the query currently used for retrieving the notifications so it will include the relationships on the initial load.
NotificationController.php
$notification = App\Notification::byUserId(1)->with('someRelatedModel);
See eager loading for more on this.
Tl;dr Considering the points above I'd go with option 2; only keep references to models you'll need when rendering the notification.

Assert model was not made searchable

I'm building a system to manage some articles for my company using Laravel and Laravel Scout with Algolia as the search backend.
One of the requirements states that whenever something in an article is changed, a backup is kept so we can prove that a certain information was displayed at a specific time.
I've implemented that by cloning the existing article with all its relationships before updating it. Here is the method on the Article model:
public function clone(array $relations = null, array $except = null) {
if($relations) {
$this->load($relations);
}
$replica = $this->replicate($except);
$replica->save();
$syncRelations = collect($this->relations)->only($relations);
foreach($syncRelations as $relation => $models) {
$replica->{$relation}()->sync($models);
}
return $replica;
}
The problem is the $replica->save() line. I need to save the model first, in order for it to have an ID when syncing the relationships.
But: The only thing preventing scout from indexing the model is if the model has its archived_at field set to any non-null value. But since this is a clone of the original model, this field is set to null as expected, and is only changed after the cloning procedure is done.
The problem: Scout is syncing the cloned model to Algolia, so I have duplicates there. I know how to solve this, by wrapping the clone call into the withoutSyncingToSearch (https://laravel.com/docs/5.6/scout#pausing-indexing) callback.
But since this is rather important and the bug is already out there, I want to have a unit test backing me up that it was indeed not synced to Algolia.
I don't have any idea how to test this though and searching for a way to test Scout only leads to answers that tell me not to test Scout, but rather that my model can be indexed etc.
The question: How do I create a Unittest that proves that the cloned model wasn't synced to Algolia?
At the moment I'm thinking about creating a custom Scout driver for testing, but it seems to be a total overkill for testing one single function.

Laravel Eloquent Events Implementation

I am updating my model instance or inserting a new one like this:
$model = Model::updateOrCreate([id' => $request['id']],
$model_to_update_array);
I want to execute some code only when existing model instance ('tourist') was updated (and NOT when a new one was created or nothing changes).
I've read https://laravel.com/docs/5.4/eloquent#events about Eloquent events and it seems to me that I need to use updated or updating event. As I understand these events are 'built-in' in Laravel, so I don't have to use a lot of stuff from here: https://laravel.com/docs/5.4/events
I haven't found a tutorial showing how to implement Eloquent events. Since I am new to events conception at all, it's hard for me to understand how to use them. Can anyone drop a link to a good tutorial about Eloquent events (not events in general, but Eloqeunt events in particular) or maybe it can be shortly explained here?
Thank you in advance!
The easiest way to add Eloquent event for a particular model is to overwrite its boot() method:
protected static function boot()
{
parent::boot();
static::updating(function ($model) {
});
}
When you put this in your model the anonymous function will run every time when the model is being updated. Please note that there is a difference between calling static::updating() and static::updated() depending on when you want to execute your code.
#TheFallen gave a great answer to this problem in another thread on StackOverflow, please read if you are interested in thoroughly explained solution:
Laravel Eloquent Events - implement to save model if Updated

Displaying Data from an Eloquent collection with Relations

I'm trying to get data from an Eloquent query as follows:
$submission->find($id)->first()->with('user', 'clientform')->get();
I'm sending the submission to the view and attempting to access properties from the user model like so:
{{ $submission->clientform->name }}
However, Laravel is throwing the following error:
Undefined property: Illuminate\Database\Eloquent\Collection::$clientform
What am I doing wrong with the way my query is formatted?
You're overdoing it!
Let's break it down:
$submission->find($id);
Will return an instance of the model which corresponds to the entry having the primary key of $id.
$submission->find($id)->first();
This is an unncessary repetition; the find method already gives you a single entry, no need to call first on it.
$submission->find($id)->first()->with('user', 'clientform');
This is where you start going the wrong way; when calling with on a model, Laravel will transform that model into a query builder again, even though it was already resolved by calling find.
$submission->find($id)->first()->with('user', 'clientform')->get();
Finally, the get method resolves the builder into a Illuminate\Support\Collection, regardless of the number of entries found (i.e. it could be a Collection of only one item). It's worth noting, though, that your query will most likely have been fully reset by calling with. It would be the same as instantiating a fresh Submission model and building your query with it. That means you're probably using a collection of all your submission entries, not just the one with $id.
Long story short, this is what you want:
$submission->with('user', 'clientform')->find($id);
It will fetch the matching $id, with the user and clientform relations eager-loaded.

Resources