laravel hook into Eloquent pre and post save for every model - php

I'm new to Laravel and ORM's in general. How could i hook into Eloquent to fire code before and after a save of any model? I know i can do the following for specific models but i'm looking at figuring out how to do this for every model.
class Page extends Eloquent {
public function save()
{
// before save code
parent::save();
// after save code
}
}

Using laravel models own life cycle events may solve this easy
/**
* model life cycle event listeners
*/
public static function boot(){
parent::boot();
static::creating(function ($instance){
//
});
static::created(function ($instance){
//
});
}

There's even a better way of accomplishing this! Create an observer for, lets say a model called House:
class HouseObserver {
public function saving(House $house) {
// Code before save
}
public function saved(House $house) {
// Code after save
}
}
Now register the observer with the House model by adding the line House::observe(new HouseObserver) somewhere. The line can be added in the boot method of the model:
class House extends Eloquent {
// Lots of model code
public static function boot() {
parent::boot();
self::observe(new HouseObserver);
}
}
More info can be found here.

You can create a BaseModel class that extends eloquent and then have all your models extend BaseModel. Here's an example:
abstract class Elegant extends Eloquent{
/* Save ****************************/
public function preNew() {}
public function postNew() {}
public function preSave() { return true; }
public function postSave() {}
public function save($validate=true, $preSave=null, $postSave=null)
{
$newRecord = !$this->exists;
if ($validate)
if (!$this->valid()) return false;
if($newRecord)
$this->preNew();
$before = is_null($preSave) ? $this->preSave() : $preSave($this);
// check before & valid, then pass to parent
$success = ($before) ? parent::save() : false;
if ($success)
is_null($postSave) ? $this->postSave() : $postSave($this);
if($newRecord)
$this->postNew();
return $success;
}
public function onForceSave(){}
public function forceSave($validate=true, $rules=array(), $messages=array(), $onForceSave=null)
{
if ($validate)
$this->valid($rules, $messages);
$before = is_null($onForceSave) ? $this->onForceSave() : $onForceSave($this); // execute onForceSave
return $before ? parent::save() : false; // save regardless of the result of validation
}
/** Soft Delete ****************************/
public function preSoftDelete() { return true; }
public function postSoftDelete() { }
public function softDelete($val = true, $preSoftDelete=null, $postSoftDelete=null)
{
if ($this->exists)
{
$before = is_null($preSoftDelete) ? $this->preSoftDelete() : $preSoftDelete($this);
$success = null;
if($before) {
$this->set_attribute(static::$softDelete, $val);
$success = $this->save(false);
}
else
$success = false;
if ($success)
{
is_null($postSoftDelete) ? $this->postSoftDelete() : $postSoftDelete($this);
}
return $success;
}
}
/** Hard Delete ****************************/
public function preDelete() { return true;}
public function postDelete(){}
public function delete( $preDelete=null, $postDelete=null)
{
if ($this->exists)
{
$before = is_null($preDelete) ? $this->preDelete() : $preDelete($this);
$success = ($before) ? parent::delete() : false;
if ($success)
{
is_null($postDelete) ? $this->postDelete() : $postDelete($this);
}
return $success;
}
}
}

Related

Laravel 5.2 Nested Eloquent relationships

I have a User which is of type Player and has several Equipments
I want to request a piece of Equipment and see if the User is it's owner before returning it to the user. If they do not own it they will get an unauthorized response
Here are the relationships I have for the models:
App\User.php
class User extends Authenticatable
{
protected $table = 'user';
public function player()
{
return $this->hasOne(Player::class);
}
}
App\Player.php
class Player extends Model
{
protected $table = 'player';
public function equipment()
{
return $this->hasMany(Equipment::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
App\Equipment.php
class Equipment extends Model
{
protected $table = 'equipement';
public function player()
{
return $this->belongsTo(Player::class);
}
}
EquipmentController.php
With my attempt which is working... just very ugly.
class EquipmentController extends Controller
{
public function show($id)
{
$equipment = Equipment::find($id);
if ( ! $equipment ) {
return 'Equipment does not exist');
}
// my attempt
$test = Equipment::with('player.user')->findOrFail($id);
if ($test->toArray()['player']['user']['id'] != Auth::user()->id){
return 'Unauthorized';
}
//
return $equipment;
}
}
Is there a neater way to do this?
I want something readable in the controller like:
if(!$equipment->ownedBy(Auth::user())){
return 'Unauthorized';
}
Or something similarly as readable.
And once the relationship is found, I'm not sure where the logic should be placed. Should it be in the Equipment model?
Any help would be much appreciated!
In your Equipment model:
public function authorized()
{
return ($this->player->user->id == auth()->user()->id())
}
Then from your controller, try:
$equipment->authorized() //returns true or false

Eager loading on polymorphic relations

class Admin {
public function user()
{
return $this->morphOne('App\Models\User', 'humanable');
}
public function master()
{
return $this->hasOne('App\Models\Master');
}
}
class Master {
public function admin()
{
return $this->hasOne('App\Models\Admin');
}
}
class User {
public function humanable()
{
return $this->morphTo();
}
public function images()
{
return $this->hasOne('\App\Models\Image');
}
}
class Image {
public function user()
{
return $this->belongsTo('\App\Models\User');
}
}
Now if I dump this:
return \App\Models\Admin::where('id',1)->with(array('user.images','master'))->first();
I get the perfect result one master, one user and one image record.
But if I do this
return $user = \App\Models\User::where('id',1)->with(array('humanable','humanable.master'))->first();
I only get one Admin record, the query get * from masters doesn't even run.
Any idea what I'm doing wrong, I'm sure this is possible.
If I remember correctly Laravel has lots of pitfall. You can try to use the protected $with var in Admin model instead of query builder with function.
class Admin {
protected $with = ['master'];
public function user() {
return $this->morphOne('App\Models\User', 'humanable');
}
public function master() {
return $this->hasOne('App\Models\Master');
}
}
In query builder, only need to include humanable. Now you should see master inside the humanable object.
return $user = \App\Models\User::where('id',1)->with('humanable')->first();
Hope this help.

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