Eloquent: hook into 'saving' event on both the model and his parent - php

Having this parent:
class BaseModel extends Eloquent {
protected static $rules = [];
public static function boot() {
parent::boot();
static::saving(function($model) {
return $model->validate(); // <- this executes
});
}
}
How can I be able to still do the same on the child model?
class Car extends BaseModel {
protected static $rules = [];
public static function boot() {
parent::boot();
static::saving(function($model) {
$model->doStuff(); // <- this doesn't execute
});
}
}
The saving() in the child only executes if I remove the saving() on the parent. I need both!

I found out the solution and it's actually pretty simple.
Here is the behavior of the *ing Eloquent events, depending on the return type:
return null or no return: the model will be saved or the next saving callback will be executed
return true: the model will be saved but the next saving callback will NOT be executed
return false: the model will not be saved and the next saving callback will not be executed
So, the solution for this problem will be simply:
class BaseModel extends Eloquent {
protected static $rules = [];
public static function boot() {
parent::boot();
static::saving(function($model) {
if(!$model->validate())
return false; // only return false if validate() fails, otherwise don't return anything
});
}
}

Related

Delete hasManyThrough relationship rows using Laravel's Eloquent

I have three models, Advertiser, PtcAd, and PtcCampaign. When deleting a Advertiser I want to delete all related PtcAds and PtcCampaigns. The Advertiser has many PtcCampaigns through PtcAds.
Advertiser Model
use SoftDeletes;
protected $dates = ['deleted_at'];
public function ptcAds()
{
return $this->hasMany('App\PtcAd');
}
public function ptcCampaigns()
{
return $this->hasManyThrough('App\PtcCampaign', 'App\PtcAd');
}
public function delete()
{
$this->ptcAds()->delete();
// I'VE TRIED WITH AND WITHOUT THIS
$this->ptcCampaigns()->delete();
return parent::delete();
}
PtcAd Model
use SoftDeletes;
protected $fillable = ['advertiser_id', 'title'];
protected $dates = ['deleted_at'];
public function advertiser()
{
return $this->belongsTo('App\Advertiser');
}
public function ptcCampaigns()
{
return $this->hasMany('App\ptcCampaign');
}
public function delete()
{
$this->ptcCampaigns()->delete();
return parent::delete();
}
PtcCampaign Model
use SoftDeletes;
public $timestamps = false;
protected $fillable = ['ptc_ad_id', 'clicks'];
protected $dates = ['paused_at', 'deleted_at'];
public function ptcAd()
{
return $this->belongsTo('App\PtcAd');
}
My tests:
public function test_delete_advertiser()
{
$advertiser = factory(Advertiser::class)->create();
$ptcAd = factory(PtcAd::class)->create(['advertiser_id' => $advertiser->id]);
$ptcCampaign = factory(PtcCampaign::class)->create(['ptc_ad_id' => $ptcAd->id]);
$this->assertTrue($advertiser->delete());
$this->assertFalse(Advertiser::all()->contains($advertiser));
$this->assertFalse(PtcAd::all()->contains($ptcAd));
// THE FOLLOWING TEST DOESN'T WORK!
$this->assertFalse(PtcCampaign::all()->contains($ptcCampaign));
}
// ALL OF THE FOLLOWING TESTS WORK!
public function test_delete_ad()
{
$ptcAd = factory(PtcAd::class)->create();
$ptcCampaign = factory(PtcCampaign::class)->create(['ptc_ad_id' => $ptcAd->id]);
$this->assertTrue($ptcAd->delete());
$this->assertFalse(PtcAd::all()->contains($ptcAd));
$this->assertFalse(PtcCampaign::all()->contains($ptcCampaign));
}
The $this->assertFalse(PtcCampaign::all()->contains($ptcCampaign)) in the test_delete_advertiser() test fails, why?
I have more tests to make sure all the relationships work so I really don't know what could possibly be wrong. My next attempt would be to make foreach in the Advertiser's delete() method but maybe there's something simpler and I want to understand why this doesn't work.
It looks the problem is with the sequence of delete statement.
Try by changing the sequence like below:
public function delete()
{
$this->ptcCampaigns()->delete();
$this->ptcAds()->delete();
return parent::delete();
}
You can use Laravel's Model Events (deleting) to delete related models like this:
class Advertiser extends Eloquent
{
public function ptcAds()
{
return $this->hasMany('PtcAd');
}
// this is a recommended way to declare event handlers
protected static function boot() {
parent::boot();
static::deleting(function($adv) { // before delete() method call this
$adv->ptcAds()->delete();
// do the rest of the cleanup...
});
}
}
// Same for PtcCompaigns
class PtcAd extends Eloquent
{
public function ptcCompaigns()
{
return $this->hasMany('PtcCompaigns');
}
// this is a recommended way to declare event handlers
protected static function boot() {
parent::boot();
static::deleting(function($ptc_ad) { // before delete() method call this
$ptc_ad->ptcCompaigns()->delete();
// do the rest of the cleanup...
});
}
}
Hope this helps!

eloquent boot not firing only for one class

I'm using eloquent for a project and I would like to delete articles. The problem is that the database is complicated. There are articles, that have article_lvl1 and article_lvl1 have article_lvl2. When I delete an article the event functions work fine :
<?php
namespace app\model;
class Article extends \Illuminate\Database\Eloquent\Model
{
public static function boot()
{
parent::boot();
// cause a delete of a product to cascade to children so they are also deleted
static::deleting(function($article)
{
$article->ArticleNiveau1()->delete();
$article->ArticleNiveau2()->delete();
});
}
public function ArticleNiveau1()
{
return $this->hasMany('app\model\ArticleNiveau1', 'id_article');
}
public function ArticleNiveau2()
{
return $this->hasMany('app\model\ArticleNiveau2', 'id_article');
}
protected $table = 'Article';
public $timestamps = false;
protected $primaryKey = 'id_article';
}
But an article can also have article_Informations, which can have article_Date. My problem is that everything in the boot functions executes fine on delete, but not the article_Date. Here is the code :
<?php
namespace app\model;
class Article_Informations extends \Illuminate\Database\Eloquent\Model
{
public function Article()
{
return $this->belongsTo('app\model\Article', 'id_article');
}
public function ArticleNiveau1()
{
return $this->belongsTo('app\model\ArticleNiveau1', 'id_nv1');
}
public function ArticleNiveau2()
{
return $this->belongsTo('app\model\ArticleNiveau2', 'id_nv2');
}
public function Article_Date()
{
return $this->hasMany('app\model\Article_Date', 'id_info');
}
public static function boot()
{
parent::boot();
// cause a delete of a product to cascade to children so they are also deleted
static::deleting(function($info)
{
$info->Article_Date()->delete();
});
}
protected $table = 'Article_Informations';
public $timestamps = false;
protected $primaryKey = 'id_info';
}
I don't know why, it's the only event that is not fired on delete.
Can someone explain me ?

Laravel Model Events don't fire

I have a model with this code:
<?php
use Illuminate\Database\Eloquent\SoftDeletingTrait;
class Intervention extends Eloquent {
use SoftDeletingTrait;
protected $fillable = array('start_date','stove_id','description','operation_mode','store_id','user_id','intervention_status_id','code');
public function operations()
{
return $this->hasMany('InterventionOperation');
}
public function store()
{
return $this->belongsTo('Store');
}
public function stove()
{
return $this->belongsTo('Stove');
}
public function user()
{
return $this->belongsTo('User');
}
public function statues()
{
return $this->hasMany('InterventionStatus');
}
then the boot
public static function boot()
{
parent::boot();
static::creating(function($intervention)
{
exit("creating");
});
static::created(function($intervention){
exit("created");
});
static::updating(function($intervention)
{
exit("updating");
});
}
the controller:
$intervention = new \Intervention(\Input::all());
$status = \Status::find(\Input::get('status')['id']);
$interventionStatus = new \InterventionStatus();
$interventionStatus->change_status_date = new \DateTime();
$interventionStatus->status()->associate($status);
$interventionStatus->description = "";
$user = \Auth::user();
$store = $user->store;
$intervention->store()->associate($store);
$intervention->user()->associate($user);
$intervention->request_date = new \DateTime();
$intervention->save();
...
but when save model, creating callback is not call.
I have try put exit("test") after parent::boot(); and exit is triggered.
If I put event's code in app/start/global.php it work.
I have try use the code in another model and work.
I do not know why it does not work.
Resolved:
I recreated the database and now everything works. Probably, in the various attempts to save, some relationship was skipped.
Thank you all for the help!
I think this has something to with the namespaces and registering the correct class in the event. Let's hack the source code a bit :)
In: /vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php
Add:
public function getAllEvents()
{
return array_keys($this->listeners);
}
And call/dump Event::getAllEvents();
Try this for both cases (boot in the model and in the global.php) and compare.

Laravel model object chaining

For some reason, I cannot chain model objects. I'm trying to eager load 'Location' for an 'Order' and would prefer the logic to be contained in the models themselves. But past one chain, it does not work.
class Order extends Eloquent {
protected $table = 'orders';
public function customer() {
return $this->belongsTo('Customer');
public function location() {
return $this->customer()->location(); // this does not work
}
}
class Customer extends Eloquent {
protected $table = 'customers';
public function user() {
return $this->belongsTo('User');
}
public function orders() {
return $this->hasMany('Order');
}
public function location() {
return $this->user()->location();
// return $this->user(); // WORKS!!
}
}
class User extends Eloquent {
protected $table = 'users';
public function locations() {
return $this->hasMany('Location');
}
public function location() {
return $this->locations()->first();
}
}
I eventually want to do this:
class ChefController extends BaseController {
public function get_orders() {
$chef = $this->get_user_chef(); // this already works
return $chef->orders()->with('location')->get(); // does not work
}
}
Try to reference relation (user table) by adding user_id as second argument, like this:
public function user() {
return $this->belongsTo('User',"user_id");
}
Maybe you called that id field different, but you know what I mean.

Laravel model event saving is not firing

I'm trying to simulate what Ardent package is doing. Which is validating a model right before saving.
I've created this BaseModel (According to Laravel Testing decoded book). And added this code :
class BaseModel extends Eloquent {
protected static $rules = [];
public $errors = [];
public function validate(){
$v = Validator::make($this->attributes, static::$rules);
if($v->passes()) {
return true;
}
$this->errors = $v->messages();
return false;
}
public static function boot(){
parent::boot();
static::saving(function($model){
if($model->validate() === true){
foreach ($model->attributes as $key => $value) {
if(preg_match("/[a-zA-Z]+_confirmation/", $key)){
array_splice($model->attributes, array_search($key, array_keys($model->attributes)), 1);
}
}
echo "test"; //This is for debugging if this event is fired or not
return true;
} else {
return false;
}
});
}
}
Now, this is my Post model :
class Post extends BaseModel {
public static $rules = array(
'body' => 'required',
'user_id' => 'required',
);
}
In this test i'm expecting it to fail. Instead, it passes ! , $post->save() returns true !
class PostTest extends TestCase {
public function testSavingPost(){
$post = new Post();
$this->assertFalse($post->save());
}
}
When i tried to throw an echo statement inside the saving event. It didn't appear, So i understand that my defined saving event is not invoked. I don't know why.
check out this discussion: https://github.com/laravel/framework/issues/1181
you'll probably need to re-register your events in your tests.
class PostTest extends TestCase {
public function setUp()
{
parent::setUp();
// add this to remove all event listeners
Post::flushEventListeners();
// reboot the static to reattach listeners
Post::boot();
}
public function testSavingPost(){
$post = new Post();
$this->assertFalse($post->save());
}
}
Or, better yet, you should extract the event registration functionality out of the boot function into a public static method:
class Post extends Model {
protected static boot()
{
parent::boot();
static::registerEventListeners();
}
protected static registerEventListeners()
{
static::saving(...);
static::creating(...);
...etc.
}
}
And then call Post::flushEventListeners(); Post::registerEventListeners(); in the setUp() test method.
The saving event looks fine for me. The validation fails, so $post->save() returns false. Your test passes because you expect $post->save() to be false (assertFalse), which in this case is correct.
Try these tests instead.
public function testSavingInvalidPost() {
$post = new Post();
$this->assertFalse($post->save());
}
public function testSavingValidPost() {
$post = new Post();
$post->body = 'Content';
$post->user_id = 1;
$this->assertTrue($post->save());
}

Categories