Laravel 5.6 not saving field in `created` model event - php

I am trying to save a field from the created model event but for some reason the stripe_coupon_id column is never being saved. The created event does run as I've tested by trying a dd inside it and it does fire the event but just does not save that column.
class DiscountRate extends Model
{
public $table = "discount_rates";
public $primaryKey = "id";
public $timestamps = true;
public $fillable = [
'id',
'name',
'rate',
'active',
'stripe_coupon_id'
];
public static function boot()
{
parent::boot();
self::created(function ($discountRate) {
$coupon_id = str_slug($discountRate->name);
$discountRate->stripe_coupon_id = $coupon_id;
});
}
}
In my controller I simply call a service function which calls the default Laravel model create function:
public function store(DiscountRateCreateRequest $request)
{
$result = $this->service->create($request->except('_token'));
if ($result) {
return redirect(route('discount_rates.edit', ['id' => $result->id]))->with('message', 'Successfully created');
}
}
discount_rates table:

The created event is triggered after your model is created. In this case, you need to to call $discountRate->save() in the end in order to update the model which you just created.
As alternative, you can use creating event instead. In this case you don't need to call save() in the end because the model is not yet saved in your database.
A big difference in creating event is that the model does not have an id yet if you use auto-incrementing which is default behavior.
More info about events you can find here.

you have to set stripe_coupon_id before creating. So replace static::creating instead of self::created in boot method of DiscountRate model.

Related

Laravel Policy and Show Method with View Method logical Problem

I am using Policys and want to be sure that I am prevent data to be shown of other users.
In every Table I have the column 'user_id' and check if the current logged in user with his id the same with the data and his user_id.
In this specific case I have a table of Objects and Objektverwaltung where the objekt_id is given as foreign key.
I want to use my policy to be sure that just the data for the given object was shown in objektverwaltung where the foreign key 'objekt_id' is given.
ObjektVerwaltung Controller with the show method:
public function show($objektvwId) {
$objektId = ObjektVerwaltung::with('Objekt')->find($objektvwId);
$this->authorize('view', $objektId);
$objekte = ObjektVerwaltung::where('objekt_id',$objektvwId)->get();
return view('objekte.verwaltung', compact('objekte'));
}
Policy:
public function view(User $user, ObjektVerwaltung $objektVerwaltung)
{
return $objektVerwaltung->user_id === $user->id;
}
Models:
class ObjektVerwaltung extends Model
{
use HasFactory;
protected $table = 'objekte_verwaltungens';
protected $fillable = ['user_id','objekt_id','key', 'value'];
public function Objekt() {
return $this->belongsTo(Objekt::class);
}
}
class Objekt extends Model
{
use HasFactory;
protected $table = 'objekts';
protected $fillable = ['name','strasse', 'hausnummer', 'plz', 'ort', 'user_id'];
public function Mieter() {
return $this->hasMany(Mieter::class);
}
public function Company() {
return $this->belongTo(Company::class);
}
public function Objektverwaltung() {
return $this->hasMany(ObjektVerwaltung::class);
}
}
I learned that I can easily use find() as method for the Models to validate data. But in this specific case I have to check for the objekt_id (foreign key in objektverwaltung) and not for the ID and because of that I cant use find(). But if I use where or another method I cant use my policy and always getting unauthorized.
I tried to use the with method on the model but maybe there is a better way to my problem. I strongly believe.
Thanks!
This could be solution, but I am getting always "Unauthorized" and do not get to the policy: $objekt= ObjektVerwaltung::where('objekt_id', $objektId)->get(); $this->authorize('view', $objekt);
I solved this issue. I had to use my ObjektPolicy, because I am using the objekt_id Key.

doesn't have a default value error with set mutator And update observer does not working

In saving a model, I have an error thats the slug attribute doesn't have a default value.
I had created a setSlugAttribute mutators but it gived me the error again.
//Controller save method inside
* * *
Task::create($request->all());
* * *
//Task model
public function setSlugAttribute(){
$this->attributes['slug'] = Str::slug($this->title, '-');
}
How can I fix it? It does fix by using observe(saving), doesn't it? Another idea?
I have created an TaskObserver and I set it in ServiceProvider.
In the observer, updated(Task $task) and updating(Task $task) methods didn't work !
But the created method works.
//Update method inside:
$array = $request->all(['title', 'description', 'deadline', 'budget','guest_token']);
$task = Task::where('id',$request->id)->update($array);
//I am waiting working of udate observer but it don't
//TaskObserver
public function updating(Task $task)
{
dd("updating");
}
public function updated(Task $task)
{
dd("updated");
}
I have solved this problem by doing the following:
//when I assigned it to a variable and then calling update method, it worked
$task = Task::where('id',$request->id)->first();
$update = $task->update($request->all(['title', 'description', 'deadline', 'budget','guest_token']));
So every time a Task is created or updated you want the slug column to be auto-populated from title column.
Accessors are not good for this. What you want is observers. For observers you have two choice: closure based or class based. Considering your use case is not too complex, I'd choose closure based observers.
You need two events creating and saving to handle it when the model is first creating, and when model is updating. So your task model should look like this:
<?php
class Task extends Model
{
protected static function creating()
{
static::creating(function ($task) {
$task->slug = Str::slug($task->title, '-');
});
static::saving(function ($task) {
$task->slug = Str::slug($task->title, '-');
});
}
}
This should do the trick.

How to pass parameter to a laravel elequent model's event observer

I have a model in laravel and I want to do something after the first time which an object of my model is created. the simplest way is to add a static boot method inside my model's class like the code below:
class modelName extends Model
{
public static function boot()
{
parent::boot();
self::created(function ($model) {
//the model created for the first time and saved
//do something
//code here
});
}
}
so far so good! the problem is: the ONLY parameter that created method accepts is the model object itself(according to the documentation) :
Each of these methods receives the model as their only argument.
https://laravel.com/docs/5.5/eloquent#events
I need more arguments to work with after model creation. how can I do that?
Or is there any other way to do something while it's guaranteed that the model has been created?
laravel version is 5.5.
You're close. What I would probably do would be to dispatch an event right after you actually create the model in your controller. Something like this.
class WhateverController
{
public function create()
{
$model = Whatever::create($request->all());
$anotherModel = Another::findOrFail($request->another_id);
if (!$model) {
// The model was not created.
return response()->json(null, 500);
}
event(new WhateverEvent($model, $anotherModel));
}
}
I solved the issue using static property in eloquent model class:
class modelName extends Model
{
public static $extraArguments;
public function __construct(array $attributes = [],$data = [])
{
parent::__construct($attributes);
self::$extraArguments = $data ;
public static function boot()
{
parent::boot();
self::created(function ($model) {
//the model created for the first time and saved
//do something
//code here
self::$extraArguments; // is available in here
});
}
}
It works! but I don't know if it may cause any other misbehavior in the application.
Using laravel events is also a better and cleaner way to do that in SOME cases.but the problem with event solution is you can't know if the model has been created for sure and it's time to call the event or it's still in creating status ( and not created status).

Fire Laravel events on touch

this is my scenario:
I'm using Laravel 5.5.x.
I have two models, linked in one to many way.
class Artwork extends Model
{
//It has timestamps
protected $table = 'artworks';
protected $fillable = [
'artwork_category_id'
];
public function artworkCategory()
{
return $this->belongsTo('App\Models\ArtworkCategory');
}
}
class ArtworkCategory extends Model
{
use SoftDeletes;
protected $touches = ["artworks"];
/**
* #var string
*/
protected $table = 'artwork_categories';
protected $fillable = [
'category_name',
'acronym',
'deleted_at'
];
public function artworks()
{
return $this->hasMany('App\Models\Artwork');
}
}
Touch works correctly, so when I update an artwork category the related artworks updated_at field is updated.
But I need to listen the "touch" event on each artwork.
I've tried inserting "updated" listener on boot method in AppServiceProvider, but it is not fired.
Artwork::updated(function ($model){
\Log::debug("HERE I AM");
});
I've tried using an observer, but no luck.
class ArtworkObserver
{
public function updated(Artwork $artwork)
{
dd($artwork);
}
}
Boot method in AppServiceProvider:
Artwork::observe(ArtworkObserver::class)
Question:
Could somebody show me the right way to do it? Or tell me where am I wrong?
I was not good enough to find an example that helps me how to do it.
Update
I need to achieve this because I have to "fire" Scout to save updated data on Elasticsearch on Artwork index.
Most probably $touches uses mass update, and if you check Events section of Eloquent you'll find following:
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.
The best that I can think of is that, you update Artworks manually (instead of $touches) when a ArtworkCategory is updated:
class ArtworkCategory extends Model
{
use SoftDeletes;
protected $fillable = [
'category_name',
'acronym',
'deleted_at'
];
public function artworks()
{
return $this->hasMany('App\Models\Artwork');
}
public static function boot()
{
parent::boot();
static::updated(function($artworkCategory)
{
$artworkCategory->artworks->each(function($artwork) {
$artwork->setUpdatedAt($artwork->freshTimestamp());
$artwork->save(); /// Will trigger updated on the artwork model
});
});
}
}

Laravel observer for pivot table

I've got a observer that has a update method:
ObserverServiceProvider.php
public function boot()
{
Relation::observe(RelationObserver::class);
}
RelationObserver.php
public function updated(Relation $relation)
{
$this->cache->tags(Relation::class)->flush();
}
So when I update a relation in my controller:
public function update(Request $request, Relation $relation)
{
$relation->update($request->all()));
return back();
}
Everything is working as expected. But now I've got a pivot table. A relation belongsToMany products.
So now my controller method looks like this:
public function update(Request $request, Relation $relation)
{
if(empty($request->products)) {
$relation->products()->detach();
} else {
$relation->products()->sync(collect($request->products)->pluck('id'));
}
$relation->update($request->all());
return back();
}
The problem is that the observer is not triggered anymore if I only add or remove products.
How can I trigger the observer when the pivot table updates aswel?
Thanks
As you already know, Laravel doesn't actually retrieve the models nor call save/update on any of the models when calling sync() thus no event's are created by default. But I came up with some alternative solutions for your problem.
1 - To add some extra functionality to the sync() method:
If you dive deeper into the belongsToMany functionality you will see that it tries to guess some of the variable names and returns a BelongsToMany object. Easiest way would be to make your relationship function to simply return a custom BelongsToMany object yourself:
public function products() {
// Product::class is by default the 1. argument in ->belongsToMany calll
$instance = $this->newRelatedInstance(Product::class);
return new BelongsToManySpecial(
$instance->newQuery(),
$this,
$this->joiningTable(Product::class), // By default the 2. argument
$this->getForeignKey(), // By default the 3. argument
$instance->getForeignKey(), // By default the 4. argument
null // By default the 5. argument
);
}
Or alternatively copy the whole function, rename it and make it return the BelongsToManySpecial class. Or omit all the variables and perhaps simply return new BelongsToManyProducts class and resolve all the BelongsToMany varialbes in the __construct... I think you got the idea.
Make the BelongsToManySpecial class extend the original BelongsToMany class and write a sync function to the BelongsToManySpecial class.
public function sync($ids, $detaching = true) {
// Call the parent class for default functionality
$changes = parent::sync($ids, $detaching);
// $changes = [ 'attached' => [...], 'detached' => [...], 'updated' => [...] ]
// Add your functionality
// Here you have access to everything the BelongsToMany function has access and also know what changes the sync function made.
// Return the original response
return $changes
}
Alternatively override the detach and attachNew functions for similar results.
protected function attachNew(array $records, array $current, $touch = true) {
$result = parent::attachNew($records, $current, $touch);
// Your functionality
return $result;
}
public function detach($ids = null, $touch = true)
$result = parent::detach($ids, $touch);
// Your functionality
return $result;
}
If you want to dig deeper and want to understand what's going on under the hood then analyze the Illuminate\Database\Eloquent\Concerns\HasRelationship trait - specifically the belongsToMany relationship function and the BelongsToMany class itself.
2 - Create a trait called BelongsToManySyncEvents which doesn't do much more than returns your special BelongsToMany class
trait BelongsToManySyncEvents {
public function belongsToMany($related, $table = null, $foreignKey = null, $relatedKey = null, $relation = null) {
if (is_null($relation)) {
$relation = $this->guessBelongsToManyRelation();
}
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$relatedKey = $relatedKey ?: $instance->getForeignKey();
if (is_null($table)) {
$table = $this->joiningTable($related);
}
return new BelongsToManyWithSyncEvents(
$instance->newQuery(), $this, $table, $foreignKey, $relatedKey, $relation
);
}
}
Create the BelongsToManyWithSyncEvents class:
class BelongsToManyWithSyncEvents extends BelongsToMany {
public function sync($ids, $detaching = true) {
$changes = parent::sync($ids, $detaching);
// Do your own magic. For example using these variables if needed:
// $this->get() - returns an array of objects given with the sync method
// $this->parent - Object they got attached to
// Maybe call some function on the parent if it exists?
return $changes;
}
}
Now add the trait to your class.
3 - Combine the previous solutions and add this functionality to every Model that you have in a BaseModel class etc. For examples make them check and call some method in case it is defined...
$functionName = 'on' . $this->foreignKey . 'Sync';
if(method_exists($this->parent), $functionName) {
$this->parent->$functionName($changes);
}
4 - Create a service
Inside that service create a function that you must always call instead of the default sync(). Perhaps call it something attachAndDetachProducts(...) and add your events or functionality
As I didn't have that much information about your classes and relationships you can probably choose better class names than I provided. But if your use case for now is simply to clear cache then I think you can make use of some of the provided solutions.
When I search about this topic, it came as the first result.
However, for newer Laravel version you can just make a "Pivot" model class for that.
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class PostTag extends Pivot
{
protected $table = 'post_tag';
public $timestamps = null;
}
For the related model
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)->using(PostTag::class);
}
and you have to put your declare your observer in EventServiceProvider
as stated in Laravel Docs
PostTag::observe(PostTagObserver::class);
Reference: Observe pivot tables in Laravel
Just add:
public $afterCommit = true;
at the beginning of the observer class.. It will wait until the transactions are done, then performs your sync which should then work fine..
Please check Laravel's documentation for that.
It seems this solutions was just added in Laravel 8.

Categories