Laravel 5 model deleting event ignoring attributes when soft deleting - laravel-5

Something I find puzzling in Laravel 5 is how it handles the the deleting event and any changed attributes. Basically a soft delete is just an update of the deleted_at column on the table. I was trying to be clever and also include the user ID of the user making the delete. Problem is the deleting method ignores the changed attributes when passing them to the query builder.
Take this model for example.
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model {
use SoftDeletes;
protected $table = 'users';
protected $fillable = [
'user_name', 'created_at', 'created_by', 'updated_at', 'updated_by', 'deleted_at'
];
public static function boot() {
parent::boot();
// This works and updates my updated_by column
User::updating(function($user) {
$user->updated_by = 1;
});
// This is being ignored and does not update the updated_by column
User::deleting(function($user) {
$user->updated_by = 1;
});
}
}
I have traced the create, update and delete model methods. In every case my event listeners are being picked up and processed.
In the create and update save method though, there is a difference. Both of these methods call the getDirty() method which looks for changed attributes and passes the array to the query builder. This is why that insert or update event works.
The delete method however does not do this check and therefore does not pass those attributes to the builder. Although if you look at the stack during the entire oprtation, $this->model['attributes'] has my changed updated_at attribute! The builder just never uses it.
So I guess my question without re-writing the source how would you go about injecting an attribute into the delete method when using soft deletes. Like in my model example.
The end goal here is to update the updated_by field while doing a soft delete. I am trying to avoid first doing an update of the table then doing a delete.

Just override the method runSoftDelete inside your model
class User extends Model
{
protected function runSoftDelete()
{
$query = $this->newQuery()->where($this->getKeyName(), $this->getKey());
$this->{$this->getDeletedAtColumn()} = $time = $this->freshTimestamp();
$query->update(array($this->getDeletedAtColumn() => $this->fromDateTime($time), 'updated_by' => 1));
}
}

I think the problem is the updating event calls the save() function after the event, so any changes you do will be included in the save() call.
The delete event probably does not call the same save() function - so your changes are being lost.
Try to force the save yourself and see if that works?
User::deleting(function($user) {
$user->updated_by = 1;
$user->save();
});

Related

Laravel getAttribute() on eloquent?

so i just wondered, if something like this is possible, since my code does not work.
protected $appends = ['position_name'];
public function getPositionNameAttribute()
{
return $this->belongsTo('App\EmployeePosition', 'employee_position_id')->name;
}
Can I append the name of Eloquen relationship model?
edit: so far, i am using this:
foreach ($employees as $e) {
$e->position_name = $e->position->name;
}
So, I needed to use the relation defined before.
protected $appends = ['position_name'];
public function position()
{
return $this->belongsTo('App\EmployeePosition', 'employee_position_id');
}
public function getPositionNameAttribute()
{
return $this->position->name;
}
Based on your comments i'd suggest to use the laravel default solution for your problems API resrouces
eg
class EmployeeResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'position_name' => $this->position->name,
];
}
}
note: using the with as other people suggested to preload information can increase performance by reducing the amount of queries, if you are returning a collection of employees.
Creating an accessor that looks up a value in another model and appending this field by using $appends is bad practice and will lead to n+1 queries whenever you fetch your Employee model. You should avoid doing this and just use $employee->position->name.
You should also make sure to use Employee::with('position') when you need to show the position name, so that the position model is fetched in a single query.
If the position name is something that you need in all your Employee queries, then you can set the Employee to always eager load the position by defining the following inside your Employee model:
/**
* The relationships that should always be loaded.
*
* #var array
*/
protected $with = ['position'];
I think you can just create a model with position names and reference it to the position id in the other mode by using eloquent relationships.

How to delete the first row

I have some trouble with Laravel SQL builder. I want to delete the first row. I am using the below code:
DB::table('ahah')->first()->delete().
In the controller, I've already imported DB class using the statement use Illuminate\Support\Facades\DB. Can you tell me what's the problem and how to fix it?
It doesn't work because DB::table('ahah')->first() is returning a stdClass object. And of course, stdClass has no delete() method.
So you will need to get the first item of your table with SQL statements:
// Supposing your primary key column is called 'id'
DB::table('ahah')
->orderBy('id')
->limit(1)
->delete();
If are working in Laravel then it's better to use Eloquent ORM by creating Model.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
// Code Stuff
}
fetch the first records and delete
$user = \App\Models\User::orderBy('id')->first()->delete();
ModelName: Change with your model
$item = ModelName::orderBy('id', 'ASC')->first();
$item->delete();
Use the oldest method for getting the first record:
$ahah_first = \DB::table('ahah')->oldest()->first();
\DB::table('ahah')->where('id', $ahah_first->id)->delete();
As others answered, your requirement should be fulfilled as you described in your question.
I'm just adding a cleaner technique to delete the FIRST row of the table. Create the model (made using Eloquent), then add a scope method which will return the first row of the table and then you just need to call delete() on the query builder.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class FooBarModel extends Model
{
protected $table = 'foo_bar';
/**
* Scope a query to retrieve first row.
*
* #param \Illuminate\Database\Eloquent\Builder $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public function scopeFirstRow($query)
{
return $query->orderBy('id')->first(); // Assuming id is the name of primary key column in the table
}
}
Then you may delete the first row as:
FooBarModel::firstRow()->delete(); // This will delete the very first available row every time it is called
However, handling the exceptions is always a good practice. Here if no row exists in the table, then calling the above statement will raise an exception as the query builder object will return null. So better first check if the result is not null then only call delete() method.
$firstRow = FooBarModel::firstRow();
if (!is_null($firstRow)) {
$firstRow->delete();
}
Similarly, you can create other scopes as well to delete the last row, last to the last row, etc.
If you want to go for a more cleaner way than this then you can just write a method in the Model itself and write all code to delete there.
/**
* #return bool
*/
public static function deleteFirstRow()
{
$firstRow = self::firstRow();
if (!is_null($firstRow)) {
$firstRow->delete();
return true;
}
return false;
}
Then call deleteFirstRow() to delete first row.
FooBarModel::deleteFirstRow();
Hope this helps.

How to protect Laravel model properties

When working with other frameworks, or pure-PHP, I protect my model properties. I then create public getters and setters where required, and proxy to them using __get() and __set(). This helps me sleep at night.
Recently I started using Laravel and I am surprised at how 'unprotected' the Eloquent models are. I understand that I can use the $guarded and $fillable properties to control mass assignment, but that still leaves a lot of room for accidental access.
For example, my model has a status property. It has a default value set on model creation, and should only be modified when $model->activate() or $model->deactivate() is called. But by default, Laravel allows developers to modify it directly. As far as I can see, the only way to prevent this is to create a setter, and throw an exception if it is called.
Am I missing something? Perhaps I just need to relax? What's the best way to build Eloquent models that are secure by default?
You can override __get and __set method. You need to define an array protectedProperties and a boolean variable protectedChecks so you can control the model fields.
protected $protectedChecks = true;
protected $protectedProperties = [ 'status' ];
protected $fillable = ['status'];
public function __get($key)
{
return (in_array($key, $this->fillable) && !in_array($key, $this->protectedProperties)) ? $this->attributes[$key] : null;
}
public function __set($key, $value)
{
if(!$this->protectedChecks || !in_array($key, $this->protectedProperties))
return parent::__set($key, $value);
trigger_error('Protected Field');
}
public function activate()
{
$this->protectedChecks = false;
$this->status = 1;
$this->save(); // this is optional if you want to save the model immediately
$this->protectedChecks = true;
}
If you want to use every model you should write something like above in BaseModel.
You may try:
<?php
class User extends Eloquent {
protected $hidden = array('password', 'token');
}
For what i see you probabely coming from symfony or other system that uses Mapping as a base to deal with database layer. Forget what you have done there as Eloquent uses Active Records an is different.
Best way is this: Eloquent: Accessors & Mutators
Alo check in laracast there is explanation how to do it in old php fashion way using absolute properties.

Laravel 5 - Inserting all POST data into database?

I'm using Laravel 5 and I created a resource controller, setup routing and setup my database model.
My question is: when I do a POST, my store method is called on the controller but how can I take the request data and insert a new entry in the db without having to explicitly set every field?
Here's my code:
public function store(Request $request)
{
$data = $request->all(); // this grabs all my request data - great!
$user = new User;
$user-> ??? // insert ALL posted data
$user->save();
}
I understand that I can do...
$user->name = $request->name;
...for every field. But can someone tell me how to insert everything?
I realize I can probably do a foreach but I'm wondering if there's something better.
There is method fill in every model https://laravel.com/api/5.3/Illuminate/Database/Eloquent/Model.html#method_fill:
$user->fill($request->all());
Eventually you can use create:
\User::create($request->all());
This above is called mass asignment and there is section about this in Eloquents documentation https://laravel.com/docs/5.1/eloquent
You need to define which keys might be set:
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
// The attributes that are mass assignable
protected $fillable = ['name', 'login', 'birthdate'];
}

How to use a protected property in an Eloquent model without Laravel trying to save it to the database

In one of my models, I have an attribute named "slug". When the slug is changed, I need to record the original slug before updating it to the new one, so my model has a protected property "originalSlug". Before saving the model, I do something like this in my model:
protected $originalSlug;
public function customSave($newSlug){
$this->originalSlug = $this->slug;
$this->slug = $newSlug;
return $this->save();
}
Then I have an event that does other tasks using that originalSlug after a successful save.
The problem is Laravel is trying to save the originalSlug to the database though it isn't actually an attribute and doesn't have a database column. So it fails with the "Column not found" error.
What could I do to get Laravel to ignore that originalSlug property, or is there a better way I should be doing this?
If you want Eloquent to ignore a property, it needs to be accessible to set, otherwise __set will be called and Eloquent will treat it as an attribute.
You can alternatively use mutator for this.
So here's what you need:
public $originalSlug;
public function customSave($newSlug){
$this->originalSlug = $this->slug;
$this->slug = $newSlug;
return $this->save();
}
or:
protected $originalSlug;
public function customSave($newSlug){
$this->originalSlug = $this->slug;
$this->slug = $newSlug;
return $this->save();
}
public function setOriginalSlugAttribute($value)
{
$this->originalSlug = $value;
}
Then Eloquent will not set an originalSlug attribute , so it won't be saved to the db.
You can do that with events, like suggested in the comments, and I would suggest this way too.

Resources